Import maildir-utils_1.8.14.orig.tar.gz
authorMartin <debacle@debian.org>
Tue, 7 Feb 2023 22:53:40 +0000 (22:53 +0000)
committerMartin <debacle@debian.org>
Tue, 7 Feb 2023 22:53:40 +0000 (22:53 +0000)
[dgit import orig maildir-utils_1.8.14.orig.tar.gz]

334 files changed:
.editorconfig [new file with mode: 0644]
.github/ISSUE_TEMPLATE/feature-request.md [new file with mode: 0644]
.github/ISSUE_TEMPLATE/guile.md [new file with mode: 0644]
.github/ISSUE_TEMPLATE/misc.md [new file with mode: 0644]
.github/ISSUE_TEMPLATE/mu-bug-report.md [new file with mode: 0644]
.github/ISSUE_TEMPLATE/mu4e-bug-report.md [new file with mode: 0644]
.github/issue_template.md [new file with mode: 0644]
.github/workflows/build-and-test.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.mailmap [new file with mode: 0644]
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
Makefile.meson [new file with mode: 0644]
NEWS [new file with mode: 0644]
NEWS.org [new file with mode: 0644]
README.org [new file with mode: 0644]
TODO [new file with mode: 0644]
autogen.sh [new file with mode: 0755]
build-aux/config.rpath [new file with mode: 0644]
build-aux/meson-install-info.sh [new file with mode: 0644]
configure.ac [new file with mode: 0644]
contrib/Makefile.am [new file with mode: 0644]
contrib/mu-completion.zsh [new file with mode: 0644]
contrib/mu-sexp-convert [new file with mode: 0755]
contrib/mu.spec [new file with mode: 0644]
gtest.mk [new file with mode: 0644]
guile/Makefile.am [new file with mode: 0644]
guile/compile-scm.in [new file with mode: 0644]
guile/examples/Makefile.am [new file with mode: 0644]
guile/examples/contacts-export [new file with mode: 0755]
guile/examples/msg-graphs [new file with mode: 0755]
guile/examples/mu-biff [new file with mode: 0755]
guile/examples/org2mu4e [new file with mode: 0755]
guile/fdl.texi [new file with mode: 0644]
guile/meson.build [new file with mode: 0644]
guile/mu-guile-message.cc [new file with mode: 0644]
guile/mu-guile-message.hh [new file with mode: 0644]
guile/mu-guile-message.x [new file with mode: 0644]
guile/mu-guile.cc [new file with mode: 0644]
guile/mu-guile.hh [new file with mode: 0644]
guile/mu-guile.texi [new file with mode: 0644]
guile/mu-guile.x [new file with mode: 0644]
guile/mu.scm [new file with mode: 0644]
guile/mu/Makefile.am [new file with mode: 0644]
guile/mu/README [new file with mode: 0644]
guile/mu/contact.scm [new file with mode: 0644]
guile/mu/message.scm [new file with mode: 0644]
guile/mu/part.scm [new file with mode: 0644]
guile/mu/plot.scm [new file with mode: 0644]
guile/mu/script.scm [new file with mode: 0644]
guile/mu/stats.scm [new file with mode: 0644]
guile/scripts/Makefile.am [new file with mode: 0644]
guile/scripts/find-dups.scm [new file with mode: 0755]
guile/scripts/msgs-count.scm [new file with mode: 0755]
guile/scripts/msgs-per-day.scm [new file with mode: 0755]
guile/scripts/msgs-per-hour.scm [new file with mode: 0755]
guile/scripts/msgs-per-month.scm [new file with mode: 0755]
guile/scripts/msgs-per-year-month.scm [new file with mode: 0755]
guile/scripts/msgs-per-year.scm [new file with mode: 0755]
guile/tests/meson.build [new file with mode: 0644]
guile/tests/test-mu-guile.cc [new file with mode: 0644]
guile/tests/test-mu-guile.scm [new file with mode: 0755]
lib/Makefile.am [new file with mode: 0644]
lib/doxyfile.in [new file with mode: 0644]
lib/index/Makefile.am [new file with mode: 0644]
lib/index/meson.build [new file with mode: 0644]
lib/index/mu-indexer.cc [new file with mode: 0644]
lib/index/mu-indexer.hh [new file with mode: 0644]
lib/index/mu-scanner.cc [new file with mode: 0644]
lib/index/mu-scanner.hh [new file with mode: 0644]
lib/index/test-scanner.cc [new file with mode: 0644]
lib/meson.build [new file with mode: 0644]
lib/message/Makefile.am [new file with mode: 0644]
lib/message/meson.build [new file with mode: 0644]
lib/message/mu-contact.cc [new file with mode: 0644]
lib/message/mu-contact.hh [new file with mode: 0644]
lib/message/mu-document.cc [new file with mode: 0644]
lib/message/mu-document.hh [new file with mode: 0644]
lib/message/mu-fields.cc [new file with mode: 0644]
lib/message/mu-fields.hh [new file with mode: 0644]
lib/message/mu-flags.cc [new file with mode: 0644]
lib/message/mu-flags.hh [new file with mode: 0644]
lib/message/mu-message-file.cc [new file with mode: 0644]
lib/message/mu-message-file.hh [new file with mode: 0644]
lib/message/mu-message-part.cc [new file with mode: 0644]
lib/message/mu-message-part.hh [new file with mode: 0644]
lib/message/mu-message.cc [new file with mode: 0644]
lib/message/mu-message.hh [new file with mode: 0644]
lib/message/mu-mime-object.cc [new file with mode: 0644]
lib/message/mu-mime-object.hh [new file with mode: 0644]
lib/message/mu-priority.cc [new file with mode: 0644]
lib/message/mu-priority.hh [new file with mode: 0644]
lib/message/test-mu-message.cc [new file with mode: 0644]
lib/mu-bookmarks.cc [new file with mode: 0644]
lib/mu-bookmarks.hh [new file with mode: 0644]
lib/mu-contacts-cache.cc [new file with mode: 0644]
lib/mu-contacts-cache.hh [new file with mode: 0644]
lib/mu-maildir.cc [new file with mode: 0644]
lib/mu-maildir.hh [new file with mode: 0644]
lib/mu-parser.cc [new file with mode: 0644]
lib/mu-parser.hh [new file with mode: 0644]
lib/mu-query-match-deciders.cc [new file with mode: 0644]
lib/mu-query-match-deciders.hh [new file with mode: 0644]
lib/mu-query-results.hh [new file with mode: 0644]
lib/mu-query-threads.cc [new file with mode: 0644]
lib/mu-query-threads.hh [new file with mode: 0644]
lib/mu-query.cc [new file with mode: 0644]
lib/mu-query.hh [new file with mode: 0644]
lib/mu-runtime.cc [new file with mode: 0644]
lib/mu-runtime.hh [new file with mode: 0644]
lib/mu-script.cc [new file with mode: 0644]
lib/mu-script.hh [new file with mode: 0644]
lib/mu-server.cc [new file with mode: 0644]
lib/mu-server.hh [new file with mode: 0644]
lib/mu-store.cc [new file with mode: 0644]
lib/mu-store.hh [new file with mode: 0644]
lib/mu-tokenizer.cc [new file with mode: 0644]
lib/mu-tokenizer.hh [new file with mode: 0644]
lib/mu-tree.hh [new file with mode: 0644]
lib/mu-xapian.cc [new file with mode: 0644]
lib/mu-xapian.hh [new file with mode: 0644]
lib/tests/bench-indexer.cc [new file with mode: 0644]
lib/tests/cjk/cur/test1 [new file with mode: 0644]
lib/tests/cjk/cur/test2 [new file with mode: 0644]
lib/tests/cjk/cur/test3 [new file with mode: 0644]
lib/tests/cjk/cur/test4 [new file with mode: 0644]
lib/tests/meson.build [new file with mode: 0644]
lib/tests/test-indexer.cc [new file with mode: 0644]
lib/tests/test-mu-container.cc [new file with mode: 0644]
lib/tests/test-mu-maildir.cc [new file with mode: 0644]
lib/tests/test-mu-msg-fields.cc [new file with mode: 0644]
lib/tests/test-mu-msg.cc [new file with mode: 0644]
lib/tests/test-mu-store-query.cc [new file with mode: 0644]
lib/tests/test-mu-store.cc [new file with mode: 0644]
lib/tests/test-parser.cc [new file with mode: 0644]
lib/tests/test-query.cc [new file with mode: 0644]
lib/tests/test-tokenizer.cc [new file with mode: 0644]
lib/tests/testdir/cur/1220863042.12663_1.mindcrime!2,S [new file with mode: 0644]
lib/tests/testdir/cur/1220863060.12663_3.mindcrime!2,S [new file with mode: 0644]
lib/tests/testdir/cur/1220863087.12663_15.mindcrime!2,PS [new file with mode: 0644]
lib/tests/testdir/cur/1220863087.12663_19.mindcrime!2,S [new file with mode: 0644]
lib/tests/testdir/cur/1220863087.12663_5.mindcrime!2,S [new file with mode: 0644]
lib/tests/testdir/cur/1220863087.12663_7.mindcrime!2,RS [new file with mode: 0644]
lib/tests/testdir/cur/1252168370_3.14675.cthulhu!2,S [new file with mode: 0644]
lib/tests/testdir/cur/1283599333.1840_11.cthulhu!2, [new file with mode: 0644]
lib/tests/testdir/cur/1305664394.2171_402.cthulhu!2, [new file with mode: 0644]
lib/tests/testdir/cur/encrypted!2,S [new file with mode: 0644]
lib/tests/testdir/cur/multimime!2,FS [new file with mode: 0644]
lib/tests/testdir/cur/multirecip!2,S [new file with mode: 0644]
lib/tests/testdir/cur/signed!2,S [new file with mode: 0644]
lib/tests/testdir/cur/signed-encrypted!2,S [new file with mode: 0644]
lib/tests/testdir/cur/special!2,Sabc [new file with mode: 0644]
lib/tests/testdir/new/1220863087.12663_21.mindcrime [new file with mode: 0644]
lib/tests/testdir/new/1220863087.12663_23.mindcrime [new file with mode: 0644]
lib/tests/testdir/new/1220863087.12663_25.mindcrime [new file with mode: 0644]
lib/tests/testdir/new/1220863087.12663_9.mindcrime [new file with mode: 0644]
lib/tests/testdir/tmp/1220863087.12663.ignore [new file with mode: 0644]
lib/tests/testdir2/Foo/cur/arto.eml [new file with mode: 0644]
lib/tests/testdir2/Foo/cur/fraiche.eml [new file with mode: 0644]
lib/tests/testdir2/Foo/cur/mail5 [new file with mode: 0644]
lib/tests/testdir2/Foo/new/.noindex [new file with mode: 0644]
lib/tests/testdir2/Foo/tmp/.noindex [new file with mode: 0644]
lib/tests/testdir2/bar/cur/181736.eml [new file with mode: 0644]
lib/tests/testdir2/bar/cur/mail1 [new file with mode: 0644]
lib/tests/testdir2/bar/cur/mail2 [new file with mode: 0644]
lib/tests/testdir2/bar/cur/mail3 [new file with mode: 0644]
lib/tests/testdir2/bar/cur/mail4 [new file with mode: 0644]
lib/tests/testdir2/bar/cur/mail5 [new file with mode: 0644]
lib/tests/testdir2/bar/cur/mail6 [new file with mode: 0644]
lib/tests/testdir2/bar/new/.noindex [new file with mode: 0644]
lib/tests/testdir2/bar/tmp/.noindex [new file with mode: 0644]
lib/tests/testdir2/wom_bat/cur/atomic [new file with mode: 0644]
lib/tests/testdir2/wom_bat/cur/rfc822.1 [new file with mode: 0644]
lib/tests/testdir2/wom_bat/cur/rfc822.2 [new file with mode: 0644]
lib/tests/testdir4/1220863042.12663_1.mindcrime!2,S [new file with mode: 0644]
lib/tests/testdir4/1220863087.12663_19.mindcrime!2,S [new file with mode: 0644]
lib/tests/testdir4/1252168370_3.14675.cthulhu!2,S [new file with mode: 0644]
lib/tests/testdir4/1283599333.1840_11.cthulhu!2, [new file with mode: 0644]
lib/tests/testdir4/1305664394.2171_402.cthulhu!2, [new file with mode: 0644]
lib/tests/testdir4/181736.eml [new file with mode: 0644]
lib/tests/testdir4/encrypted!2,S [new file with mode: 0644]
lib/tests/testdir4/mail1 [new file with mode: 0644]
lib/tests/testdir4/mail5 [new file with mode: 0644]
lib/tests/testdir4/multimime!2,FS [new file with mode: 0644]
lib/tests/testdir4/signed!2,S [new file with mode: 0644]
lib/tests/testdir4/signed-bad!2,S [new file with mode: 0644]
lib/tests/testdir4/signed-encrypted!2,S [new file with mode: 0644]
lib/tests/testdir4/special!2,Sabc [new file with mode: 0644]
lib/thirdparty/Makefile.am [new file with mode: 0644]
lib/thirdparty/expected.hpp [new file with mode: 0644]
lib/thirdparty/optional.hpp [new file with mode: 0644]
lib/thirdparty/tabulate.hpp [new file with mode: 0644]
lib/tokenize.cc [new file with mode: 0644]
lib/utils/Makefile.am [new file with mode: 0644]
lib/utils/meson.build [new file with mode: 0644]
lib/utils/mu-async-queue.hh [new file with mode: 0644]
lib/utils/mu-command-parser.cc [new file with mode: 0644]
lib/utils/mu-command-parser.hh [new file with mode: 0644]
lib/utils/mu-error.hh [new file with mode: 0644]
lib/utils/mu-logger.cc [new file with mode: 0644]
lib/utils/mu-logger.hh [new file with mode: 0644]
lib/utils/mu-option.cc [new file with mode: 0644]
lib/utils/mu-option.hh [new file with mode: 0644]
lib/utils/mu-readline.cc [new file with mode: 0644]
lib/utils/mu-readline.hh [new file with mode: 0644]
lib/utils/mu-result.hh [new file with mode: 0644]
lib/utils/mu-sexp.cc [new file with mode: 0644]
lib/utils/mu-sexp.hh [new file with mode: 0644]
lib/utils/mu-test-utils.cc [new file with mode: 0644]
lib/utils/mu-test-utils.hh [new file with mode: 0644]
lib/utils/mu-util.c [new file with mode: 0644]
lib/utils/mu-util.h [new file with mode: 0644]
lib/utils/mu-utils-format.hh [new file with mode: 0644]
lib/utils/mu-utils.cc [new file with mode: 0644]
lib/utils/mu-utils.hh [new file with mode: 0644]
lib/utils/mu-xapian-utils.hh [new file with mode: 0644]
lib/utils/tests/meson.build [new file with mode: 0644]
lib/utils/tests/test-command-parser.cc [new file with mode: 0644]
lib/utils/tests/test-mu-str.c [new file with mode: 0644]
lib/utils/tests/test-mu-util.c [new file with mode: 0644]
lib/utils/tests/test-option.cc [new file with mode: 0644]
lib/utils/tests/test-sexp.cc [new file with mode: 0644]
lib/utils/tests/test-utils.cc [new file with mode: 0644]
m4/Makefile.am [new file with mode: 0644]
m4/ax_ac_append_to_file.m4 [new file with mode: 0644]
m4/ax_ac_print_to_file.m4 [new file with mode: 0644]
m4/ax_add_am_macro_static.m4 [new file with mode: 0644]
m4/ax_am_macros_static.m4 [new file with mode: 0644]
m4/ax_append_compile_flags.m4 [new file with mode: 0644]
m4/ax_append_flag.m4 [new file with mode: 0644]
m4/ax_append_link_flags.m4 [new file with mode: 0644]
m4/ax_check_compile_flag.m4 [new file with mode: 0644]
m4/ax_check_enable_debug.m4 [new file with mode: 0644]
m4/ax_check_gnu_make.m4 [new file with mode: 0644]
m4/ax_check_link_flag.m4 [new file with mode: 0644]
m4/ax_code_coverage.m4 [new file with mode: 0644]
m4/ax_compiler_flags.m4 [new file with mode: 0644]
m4/ax_compiler_flags_cflags.m4 [new file with mode: 0644]
m4/ax_compiler_flags_cxxflags.m4 [new file with mode: 0644]
m4/ax_compiler_flags_gir.m4 [new file with mode: 0644]
m4/ax_compiler_flags_ldflags.m4 [new file with mode: 0644]
m4/ax_cxx_compile_stdcxx.m4 [new file with mode: 0644]
m4/ax_cxx_compile_stdcxx_17.m4 [new file with mode: 0644]
m4/ax_file_escapes.m4 [new file with mode: 0644]
m4/ax_is_release.m4 [new file with mode: 0644]
m4/ax_lib_readline.m4 [new file with mode: 0644]
m4/ax_require_defined.m4 [new file with mode: 0644]
m4/ax_valgrind_check.m4 [new file with mode: 0644]
m4/guile.m4 [new file with mode: 0644]
m4/host-cpu-c-abi.m4 [new file with mode: 0644]
m4/lib-ld.m4 [new file with mode: 0644]
m4/lib-link.m4 [new file with mode: 0644]
m4/lib-prefix.m4 [new file with mode: 0644]
man/Makefile.am [new file with mode: 0644]
man/meson.build [new file with mode: 0644]
man/mu-add.1 [new file with mode: 0644]
man/mu-bookmarks.5 [new file with mode: 0644]
man/mu-cfind.1 [new file with mode: 0644]
man/mu-easy.1 [new file with mode: 0644]
man/mu-extract.1 [new file with mode: 0644]
man/mu-fields.1 [new file with mode: 0644]
man/mu-find.1 [new file with mode: 0644]
man/mu-help.1 [new file with mode: 0644]
man/mu-index.1 [new file with mode: 0644]
man/mu-info.1 [new file with mode: 0644]
man/mu-init.1 [new file with mode: 0644]
man/mu-mkdir.1 [new file with mode: 0644]
man/mu-query.7 [new file with mode: 0644]
man/mu-remove.1 [new file with mode: 0644]
man/mu-script.1 [new file with mode: 0644]
man/mu-server.1 [new file with mode: 0644]
man/mu-verify.1 [new file with mode: 0644]
man/mu-view.1 [new file with mode: 0644]
man/mu.1 [new file with mode: 0644]
meson.build [new file with mode: 0644]
meson_options.txt [new file with mode: 0644]
mu/Makefile.am [new file with mode: 0644]
mu/meson.build [new file with mode: 0644]
mu/mu-cmd-cfind.cc [new file with mode: 0644]
mu/mu-cmd-extract.cc [new file with mode: 0644]
mu/mu-cmd-fields.cc [new file with mode: 0644]
mu/mu-cmd-find.cc [new file with mode: 0644]
mu/mu-cmd-index.cc [new file with mode: 0644]
mu/mu-cmd-script.cc [new file with mode: 0644]
mu/mu-cmd-server.cc [new file with mode: 0644]
mu/mu-cmd.cc [new file with mode: 0644]
mu/mu-cmd.hh [new file with mode: 0644]
mu/mu-config.cc [new file with mode: 0644]
mu/mu-config.hh [new file with mode: 0644]
mu/mu-help-strings.awk [new file with mode: 0644]
mu/mu-help-strings.txt [new file with mode: 0644]
mu/mu-memcheck.in [new file with mode: 0644]
mu/mu.cc [new file with mode: 0644]
mu/tests/gmime-test.c [new file with mode: 0644]
mu/tests/meson.build [new file with mode: 0644]
mu/tests/test-mu-cmd-cfind.cc [new file with mode: 0644]
mu/tests/test-mu-cmd.cc [new file with mode: 0644]
mu/tests/test-mu-query.cc [new file with mode: 0644]
mu4e/Makefile.am [new file with mode: 0644]
mu4e/TODO [new file with mode: 0644]
mu4e/fdl.texi [new file with mode: 0644]
mu4e/meson.build [new file with mode: 0644]
mu4e/mu4e-about.org [new file with mode: 0644]
mu4e/mu4e-actions.el [new file with mode: 0644]
mu4e/mu4e-bookmarks.el [new file with mode: 0644]
mu4e/mu4e-compose.el [new file with mode: 0644]
mu4e/mu4e-config.el.in [new file with mode: 0644]
mu4e/mu4e-contacts.el [new file with mode: 0644]
mu4e/mu4e-context.el [new file with mode: 0644]
mu4e/mu4e-contrib.el [new file with mode: 0644]
mu4e/mu4e-draft.el [new file with mode: 0644]
mu4e/mu4e-folders.el [new file with mode: 0644]
mu4e/mu4e-headers.el [new file with mode: 0644]
mu4e/mu4e-helpers.el [new file with mode: 0644]
mu4e/mu4e-icalendar.el [new file with mode: 0644]
mu4e/mu4e-lists.el [new file with mode: 0644]
mu4e/mu4e-main.el [new file with mode: 0644]
mu4e/mu4e-mark.el [new file with mode: 0644]
mu4e/mu4e-message.el [new file with mode: 0644]
mu4e/mu4e-org.el [new file with mode: 0644]
mu4e/mu4e-search.el [new file with mode: 0644]
mu4e/mu4e-server.el [new file with mode: 0644]
mu4e/mu4e-speedbar.el [new file with mode: 0644]
mu4e/mu4e-update.el [new file with mode: 0644]
mu4e/mu4e-vars.el [new file with mode: 0644]
mu4e/mu4e-view.el [new file with mode: 0644]
mu4e/mu4e.el [new file with mode: 0644]
mu4e/mu4e.texi [new file with mode: 0644]
mu4e/obsolete/org-mu4e.el [new file with mode: 0644]
mu4e/texinfo-klare.css [new file with mode: 0644]
mu4e/version.texi.in [new file with mode: 0644]
version.texi.in [new file with mode: 0644]

diff --git a/.editorconfig b/.editorconfig
new file mode 100644 (file)
index 0000000..824f406
--- /dev/null
@@ -0,0 +1,34 @@
+#-*-mode:conf-*-
+# editorconfig file (see EditorConfig.org), with some
+# lowest-denominator settings that should work for many editors.
+
+
+root                        = true # this is the top-level
+
+[*]
+end_of_line                 = lf
+insert_final_newline        = true
+charset                     = utf-8
+trim_trailing_whitespace    = true
+
+# The "best" answer is "tabs-for-indentation; spaces for alignment".
+
+[*.{cc,cpp,hh,hpp}]
+indent_style                = tab
+indent_size                 = 8
+max_line_length             = 90
+
+[*.{c,h}]
+indent_style                = tab
+indent_size                 = 8
+max_line_length             = 80
+
+[configure.ac]
+indent_style                = tab
+indent_size                 = 4
+max_line_length             = 100
+
+[Makefile.am]
+indent_style                = tab
+indent_size                 = 8
+max_line_length             = 100
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
new file mode 100644 (file)
index 0000000..194f656
--- /dev/null
@@ -0,0 +1,20 @@
+---
+name: Mu4e Feature request
+about: Suggest an idea for this project
+title: "[mu4e rfe]"
+labels: rfe, mu4e, new
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/guile.md b/.github/ISSUE_TEMPLATE/guile.md
new file mode 100644 (file)
index 0000000..020b849
--- /dev/null
@@ -0,0 +1,20 @@
+---
+name: Guile
+about: mu-guile related item
+title: "[guile]"
+labels: new, guile
+assignees: ''
+
+---
+
+**Describe the item**
+A clear and concise description of what you expected or wished to happen and what actually happened while using mu-guile.
+
+**To Reproduce**
+Steps to reproduce the behavior.
+
+**Environment**
+Please describe the versions of OS, Emacs, mu/mu4e etc. you are using.
+
+**Checklist**
+- [ ] you are running either the latest 1.4.x release, or a 1.5.11+ development release (otherwise, please upgrade).
diff --git a/.github/ISSUE_TEMPLATE/misc.md b/.github/ISSUE_TEMPLATE/misc.md
new file mode 100644 (file)
index 0000000..7f942cd
--- /dev/null
@@ -0,0 +1,16 @@
+---
+name: Misc
+about: Miscellaneous items you want to share
+title: "[misc]"
+labels: new
+assignees: ''
+
+---
+
+**Note**: for questions, please use the mailing-list: https://groups.google.com/g/mu-discuss
+
+**Describe the issue**
+A clear and concise description, i.e. what you expected/desired to happen and what actually happened.
+
+**Environment**
+If applicable, please describe the versions of OS, Emacs, mu etc. you are using.
diff --git a/.github/ISSUE_TEMPLATE/mu-bug-report.md b/.github/ISSUE_TEMPLATE/mu-bug-report.md
new file mode 100644 (file)
index 0000000..44604f4
--- /dev/null
@@ -0,0 +1,20 @@
+---
+name: Mu Bug Report
+about: Create a report to help us improve
+title: "[mu bug]"
+labels: bug, mu, new
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is, what you expected to happen and what actually happened.
+
+**To Reproduce**
+Detailed steps to reproduce the behavior. If this is about a specific (kind of) message, **always** attach an (anonymized as need) example message.
+
+**Environment**
+Please describe the versions of OS, Emacs, mu etc. you are using.
+
+**Checklist**
+- [ ] you are running either the latest 1.6.x release, or a 1.7.x development release (otherwise, please upgrade).
diff --git a/.github/ISSUE_TEMPLATE/mu4e-bug-report.md b/.github/ISSUE_TEMPLATE/mu4e-bug-report.md
new file mode 100644 (file)
index 0000000..738fb8e
--- /dev/null
@@ -0,0 +1,29 @@
+---
+name: Mu4e Bug Report
+about: Create a report to help us improve
+title: "[mu4e bug]"
+labels: bug, mu4e, new
+assignees: ''
+---
+
+**Describe the bug**
+
+Please provide a clear and concise description of what you expected to happen
+and what actually happened.
+
+**How to Reproduce**
+
+Include the exact steps of what you were doing (commands executed etc.). Include
+any relevant logs and outputs.
+
+If this is about a specific (kind of) message, attach an example message. (Open
+the message, press `.` (`mu4e-view-raw-message`), then `C-x C-w` and attach.
+Anonymize as needed, all that matters is that the issue still reproduces.
+
+**Environment**
+Please describe the versions of OS, Emacs, mu/mu4e etc. you are using.
+
+**Checklist**
+- [ ] you are running either the latest 1.6.x release, or a 1.8.x release (otherwise, please upgrade)
+- [ ] you are running mu4e without any third-party extensions (otherwise, make sure you can reproduce without those)
+- [ ] you have read all of the above
diff --git a/.github/issue_template.md b/.github/issue_template.md
new file mode 100644 (file)
index 0000000..a684c39
--- /dev/null
@@ -0,0 +1,39 @@
+# Important! Before filing an issue, please consider the following:
+
+    * Ensure your mu/mu4e setup is no older than the latest stable release (1.6.x).
+
+    * Disable any third-party mu4e extensions; this includes customizations like the ones in "Doom" /
+      "Evil" etc.
+
+    * If a problem occurs with a certain (type of) message, attach an (anonymized) example of
+      such a message
+
+    * Please provide some minimal steps to reproduce
+
+    * Please follow the below template
+
+    Thanks!
+
+## Expected or desired behavior
+
+Please describe the behavior you expect or want
+
+## Actual behavior
+
+Please describe the behavior you are actually seeing.
+
+For bug-reports, if applicable, include error messages, emacs stack traces, example messages
+etc. Try to be as specific as possible - when do you see this happening? Does it happen always?
+Sometimes? How often?
+
+## Steps to reproduce
+
+For bug-reports, please describe in as much detail as possible how one can reproduce the problem.
+
+If there's a problem with a specific (type of) message, please attach such a message to the report.
+
+## Versions of mu, mu4e/emacs, operating system etc.
+
+## Any other detail
+
+E.g. are you using the gnus-based message view?
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
new file mode 100644 (file)
index 0000000..60e5271
--- /dev/null
@@ -0,0 +1,39 @@
+name: Build & run tests
+
+on:
+  - push
+  - pull_request
+
+jobs:
+  build:
+    runs-on: ${{ matrix.os }}
+    timeout-minutes: 30
+    strategy:
+      fail-fast: false
+      matrix:
+        os:
+          - ubuntu-latest
+          - macos-latest
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - if: contains(matrix.os, 'ubuntu')
+        name: ubuntu-deps
+        run: |
+          sudo apt update
+          sudo apt-get install meson ninja-build libglib2.0-dev libxapian-dev libgmime-3.0-dev pkg-config
+
+      - if: contains(matrix.os, 'macos')
+        name: macos-deps
+        run: |
+          brew install meson ninja libgpg-error libtool pkg-config glib gmime xapian
+
+      - name: configure
+        run: ./autogen.sh -Dguile=disabled -Db_sanitize=address
+
+      - name: build
+        run: make
+
+      - name: test
+        run: make test-verbose-if-fail
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..88ee280
--- /dev/null
@@ -0,0 +1,142 @@
+www/mu
+mug
+/mu/mu
+/mu/mu-help-strings.h
+mug2
+.desktop
+*html
+.deps
+.libs
+autom4te*
+Makefile
+Makefile.in
+INSTALL
+aclocal.m4
+config.*
+configure
+install-sh
+depcomp
+libtool
+ltmain.sh
+
+# Added automatically by `autoreconf`
+m4/libtool.m4
+m4/ltoptions.m4
+m4/ltsugar.m4
+m4/ltversion.m4
+m4/lt~obsolete.m4
+
+missing
+nohup.out
+vgdump
+stamp-h1
+GPATH
+GRTAGS
+GSYMS
+GTAGS
+*.lo
+*.o
+*.la
+*.x
+*.go
+*.gz
+*.bz2
+\#*
+*.aux
+*.cp
+*.fn
+*.info
+*.ky
+*.log
+*.pg
+*.toc
+*.tp
+*.vr
+*.elc
+*.gcda
+*.gcno
+*.trs
+*.exe
+*.lib
+aminclude_static.am
+elisp-comp
+elc-stamp
+dummy.cc
+msg2pdf
+gmime-test
+test-mu-cmd
+test-mu-cmd-cfind
+test-mu-contacts
+test-mu-container
+test-mu-date
+test-mu-flags
+test-mu-maildir
+test-mu-msg
+test-mu-msg-fields
+test-mu-query
+test-mu-runtime
+test-mu-store
+test-mu-str
+test-mu-threads
+test-mu-util
+test-parser
+test-tokenizer
+test-utils
+tokenize
+test-command-parser
+test-mu-utils
+test-sexp-parser
+test-scanner
+/guile/tests/test-mu-guile
+
+mu4e-config.el
+mu4e.pdf
+texinfo.tex
+texi.texi
+*.tex
+*.pdf
+/www/auto/*
+configure.lineno
+/test.xml
+/mu4e/mdate-sh
+/mu4e/mu4e-about.el
+/mu4e/stamp-vti
+/mu4e/version.texi
+/lib/doxyfile
+/version.texi
+/compile
+build-aux/
+/TAGS
+parse
+
+*_flymake.*
+*_flymake_*
+/perf.data
+perf.data
+perf.data.old
+*vgdump
+/lib/asan.log*
+/man/mu-mfind.1
+/mu/mu-memcheck
+mu-*-coverage
+mu*tar.xz
+compile_commands.json
+/lib/utils/test-sexp
+/lib/utils/test-option
+/lib/test-mu-threader
+/lib/test-mu-tokenizer
+/lib/test-mu-parser
+/lib/test-mu-query-threader
+/lib/test-contacts
+/lib/test-flags
+/lib/test-maildir
+/lib/test-msg
+/lib/test-msg-fields
+/lib/test-query
+/lib/test-store
+/lib/test-threader
+/mu/test-cmd
+/mu/test-cmd-cfind
+/mu/test-query
+/mu/test-threads
+/lib/test-threads
diff --git a/.mailmap b/.mailmap
new file mode 100644 (file)
index 0000000..3a54641
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1 @@
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..3a54641
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 of the License, 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, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..eee38d2
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1 @@
+See NEWS.org
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..124fb47
--- /dev/null
@@ -0,0 +1,60 @@
+## Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+if BUILD_GUILE
+guile=guile
+else
+guile=
+endif
+
+if BUILD_MU4E
+mu4e=mu4e
+else
+mu4e=
+endif
+
+SUBDIRS=m4 man lib $(guile) mu $(mu4e) contrib
+
+ACLOCAL_AMFLAGS=-I m4
+
+# so we can say 'make test'
+check: test cleanupnote
+
+cleanupnote:
+       @echo -e  "\nNote: you can remove the mu-test-<uid> dir in your tempdir"
+       @echo "after 'make check' has finished."
+
+tags:
+       gtags
+
+EXTRA_DIST=                                                    \
+       TODO                                                    \
+       README.org                                              \
+       gtest.mk                                                \
+       NEWS                                                    \
+       NEWS.org                                                \
+       autogen.sh
+
+doc_DATA =                                                     \
+       NEWS.org
+
+include $(top_srcdir)/aminclude_static.am
+
+CODE_COVERAGE_IGNORE_PATTERN=                                  \
+       '/usr/*'                                                \
+       '*test-*'
diff --git a/Makefile.meson b/Makefile.meson
new file mode 100644 (file)
index 0000000..411233d
--- /dev/null
@@ -0,0 +1,105 @@
+## Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+# Makefile with some useful targets for meson/ninja
+
+NINJA             ?= ninja
+BUILDDIR          ?= $(CURDIR)/build
+COVERAGE_BUILDDIR ?= $(CURDIR)/build-coverage
+MESON             ?= meson
+V                 ?= 0
+
+ifneq ($(V),0)
+  VERBOSE=--verbose
+endif
+
+
+.PHONY: all
+.PHONY: check test test-verbose-if-fail test-valgrind test-helgrind
+.PHONY: benchmark coverage
+.PHONY: dist install clean distclean
+.PHONY: mu4e-doc-html
+
+# MESON_FLAGS, e.g. "-Dreadline=enabled"
+
+# examples:
+# 1. build with clang, and the thread-sanitizer
+#   make clean all MESON_FLAGS="-Db_sanitize=thread" CXX=clang++ CC=clang
+all: $(BUILDDIR)
+       $(NINJA) -C $(BUILDDIR) $(VERBOSE)
+
+$(BUILDDIR):
+       $(MESON) $(MESON_FLAGS) $(BUILDDIR)
+
+check: test
+
+test: all
+       $(MESON) test $(VERBOSE) -C $(BUILDDIR)
+
+
+install: $(BUILDDIR)
+       @cd $(BUILDDIR); $(MESON) install
+
+clean:
+       @rm -rf $(BUILDDIR) $(COVERAGE_BUILDDIR)
+
+
+#
+# below targets are just for development/testing/debugging. They may or
+# may not work on your system.
+#
+
+test-verbose-if-fail: all
+       @cd $(BUILDDIR); $(MESON) test || $(MESON) test --verbose
+
+test-valgrind: $(BUILDDIR)
+       @cd $(BUILDDIR); $(MESON) test                                  \
+               --wrap='valgrind --leak-check=full --error-exitcode=1'  \
+               --timeout-multiplier 100
+
+# we do _not_ pass helgrind; but this seems to be a false-alarm
+#    https://gitlab.gnome.org/GNOME/glib/-/issues/2662
+# test-helgrind: $(BUILDDIR)
+#      @cd $(BUILDDIR); TEST=HELGRIND $(MESON) test                    \
+#      --wrap='valgrind --tool=helgrind --error-exitcode=1'            \
+#      --timeout-multiplier 100
+
+benchmark: $(BUILDDIR)
+       $(NINJA) -C $(BUILDDIR) benchmark
+
+$(COVERAGE_BUILDDIR):
+       $(MESON) -Db_coverage=true --buildtype=debug $(COVERAGE_BUILDDIR)
+
+covfile:=$(COVERAGE_BUILDDIR)/meson-logs/coverage.info
+
+# generate by hand, meson's built-ins are unflexible
+coverage: $(COVERAGE_BUILDDIR)
+       $(NINJA) -C $(COVERAGE_BUILDDIR) test
+       lcov --capture --directory . --output-file $(covfile)
+       @lcov --remove $(covfile) '/usr/*' '*guile*' '*thirdparty*' '*/tests/*' '*mime-object*' --output $(covfile)
+       @lcov --remove $(covfile) '*mu/mu/*' --output $(covfile)
+       @mkdir -p $(COVERAGE_BUILDDIR)/meson-logs/coverage
+       @genhtml $(covfile) --output-directory $(COVERAGE_BUILDDIR)/meson-logs/coverage/
+       @echo "coverage report at: file://$(COVERAGE_BUILDDIR)/meson-logs/coverage/index.html"
+dist: $(BUILDDIR)
+       @cd $(BUILDDIR); $(MESON) dist
+
+distclean: clean
+
+HTMLPATH=${BUILDDIR}/mu4e/mu4e
+mu4e-doc-html:
+       @mkdir -p ${HTMLPATH} && cp mu4e/texinfo-klare.css ${HTMLPATH}
+       @makeinfo --html --css-ref=texinfo-klare.css -o ${HTMLPATH} mu4e/mu4e.texi
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..23e9add
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,2 @@
+See NEWS.org
+
diff --git a/NEWS.org b/NEWS.org
new file mode 100644 (file)
index 0000000..4ee4d3c
--- /dev/null
+++ b/NEWS.org
@@ -0,0 +1,1190 @@
+#+STARTUP:showall
+* NEWS (user visible changes & bigger non-visible ones)
+
+* 1.8 (released on June 25, 2022)
+
+  (there are some changes in the installation procedure compared to 1.6.x; see
+  Installation below)
+
+*** mu
+
+   - The server protocol (as used my mu4e) has seen a number of updates, to
+     allow for faster rendering. As before, there's no compatibility between
+     minor release numbers (1.4 vs 1.6 vs 1.8) nor within development series
+     (such as 1.7). However, within a stable release (such as all 1.6.x) the
+     protocol won't change (except if required to fix some severe bug; this
+     never happened in practice)
+
+   - The ~processed~ number in the indexing statistics has been renamed into
+     ~checked~ and describes the number of message files considered for updating,
+     which is a bit more useful that the old value, which was more-or-less
+     synonymous with the ~updated~ number (which are the messages that got
+     (re)parsed / (re)added to the database.
+
+     Basically, it counts all the messages for which we checked their timestamp.
+
+   - The internals of the message handling in ~mu~ have been heavily reworked;
+     much of this is not immediately visible but is an enabler for some new
+     features.
+
+   - instead of passing ~--muhome~, you can now also set an environment variable
+     ~MUHOME~.
+
+   - the ~info~ command now includes information about the last indexing
+     operation and the last database change that took place; note that the
+     information may be slightly delayed due to database caching.
+
+   - the ~verify~ command for checking signatures has been updated, and is more
+     informative
+
+   - a new command ~fields~ provides information about the message fields and
+     flags for use in queries. The information is the same information that ~mu~
+     uses and so stays up to date.
+
+   - a new message field ~changed~, which refers to the time/date of the last
+     time a message was changed (the file ~ctime~)
+
+   - new message flags ~personal~ to search for "personal" messages, which are
+     defined as a message with at least one personal contact, and ~calendar~ for
+     messages with calendar-invitations.
+
+   - message sexps are now cached in the store, which makes delivering
+     sexp-based search results (as used by ~mu4e~) much faster.
+
+   - Windows/MSYS support is deprecated; it doesn't work well (if at all) and
+     there's currently not sufficient developer interest/expertise to change
+     this.
+
+*** mu4e
+
+   - the old mu4e-view is *gone*; only the gnus-based one remains. This allowed
+     for removing quite a bit of old code.
+
+   - the mu4e headers rendering is much faster (a factor of 3+), which makes
+     displaying big results snappier. This required some updates in the headers
+     handling and in the server protocol. Separate from that, the cached
+     message sexps (see the ~mu~ section) make getting the results much faster.
+     This becomes esp. clear when there are a lot of query results.
+
+   - "related" messages are now recognizable as such in the headers-view, with
+     their own face, ~mu4e-related-face~; by default with an italic slant.
+
+   - For performance testing, you can set the variable
+     ~mu4e-headers-report-render-time~ to ~t~ and ~mu4e~ will report the
+     search/rendering speed of each query operation.
+
+   - Removed header-fields ~:attachments~, ~:signature~, ~:encryption~ and
+     ~:user-agent~. They're obsolete with the Gnus-based message viewer.
+
+   - The various "toggles" for the headers-view (full-search, include-related,
+     skip-duplicates, threading) were a bit hard to find and with non-obvious
+     key-bindings. For that, there is now ~mu4e-headers-toggle-setting~ (bound
+     to ~M~) to handle all of that. The toggles are also reflected in the
+     mode-line; so e.g. 'RTU' means we're including [R]elated messages, and show
+     [T]hreads, skip duplicates ([U]nique).
+
+   - A new ~defcustom~, ~mu4e-view-open-program~ for starting the appropriate
+     program for a give file (e.g., ~xdg-open~). There are some reasonable
+     defaults for various systems. This can also be set to a function.
+
+   - indexing happens in the background now and mu4e can interact with the
+     server while it is ongoing; this allows for using mu4e during lengthy
+     indexing operations.
+
+   - ~mu4e-index-updated-hook~ now fires after indexing completed, regardless of
+     whether anything changed (before, it fired only if something changed). In
+     your hook-functions (or elsewhere) you can check if anything changed using
+     the new variable ~mu4e-index-update-status~. And note that ~processed~ has
+     been renamed into ~checked~, with a slightly different meaning, see the mu
+     section.
+
+   - ~message-user-organization~ can now be used to set the ~Organization:~
+     header. See its docstring for details.
+
+   - ~mu4e-compose-context-switch~ no longer attempts to update the draft folder
+     (which turned out to be a little fragile). However, it has been updated to
+     automatically change the ~Organization:~ header, and attempts to update the
+     message signature. Also, there's a key-binding now: ~C-c ;~
+
+   - Changed the default for ~mu4e-compose-complete-only-after~ to 2018-01-01,
+     to filter out contacts not seen after that date.
+
+   - As an additional measure to limit the number of contacts that mu4e loads
+     for auto-completions, there's ~mu4e-compose-complete-max~, to set a precise
+     numerical match (*before* any possible filtering). Set to ~nil~ (no maximum
+     by default).
+
+   - Updated the "fancy" characters for some header fields. Added new ones for
+     personal and list messages.
+
+   - Removed ~make-mu4e-bookmark~ which was obsoleted in version 1.3.9.
+
+   - Add command ~mu4e-sexp-at-point~ for showing/hiding the s-expression for
+     the message-at-point. Useful for development / debugging. Bound to ~,~ in
+     headers and view mode.
+
+   - undo is now supported across message-saves
+
+   - a lot of the internals have been changed:
+
+     - =mu4e= is slowly moving from using the '=~'= to the more common '=--'=
+       separator for private functions; i.e., =mu4e-foo= becomes =mu4e--foo=.
+
+     - =mu4e-utils.el= had become a bit of a dumping ground for bits of code;
+       it's gone now, with the functionality move to topic-specific files --
+       =mu4e-folders.el=, =mu4e-bookmarks.el=, =mu4e-update.el=, and included in
+       existing files.
+
+     - the remaining common functionality has ended up in =mu4e-helpers.el=
+
+     - =mu4e-search.el= takes the search-specific code from =mu4e-headers.el=,
+       and adds a minor-mode for the keybindings.
+
+     - =mu4e-context.el= and =mu4e-update.el= also define minor modes with
+       keybindings, which saves a lot of code in the various views, since they
+       don't need explicitly bind all those function.
+
+     - also =mu4e-vars.el= had become very big, we're refactoring the =defvar= /
+       =defcustom= declarations to the topic-specific files.
+
+     - =mu4e-proc.el= has been renamed =mu4e-server.el=.
+
+     - Between =mu= and =mu4e=, contact cells are now represented as a plist ~(:name
+       "Foo Bar" :email "foobar@example.com")~ rather than a cons-cell ~("Foo
+       Bar" . "foobar@example.com").~
+
+       If you have scripts depending on the old format, there's the
+       ~mu4e-contact-cons~ function which takes a news-style contact and yields
+       the old form.
+
+    - Because of all these changes, it is recommended you remove older version
+      of ~mu4e~ before reinstalling.
+
+*** guile
+
+    - the current guile support has been deprecated. It may be revamped at some
+      point, but will be different from the current one, which is to be removed
+      after 1.8
+
+*** toys
+
+    - the ~toys~ (~mug~) has been removed, as they no longer worked with the rest of
+      the code.
+
+** Installation
+
+    - =mu= switched to the [[https://mesonbuild.com][meson]] build system by default. The existing =autotools=
+      is still available, but is to be removed after the 1.8 release.
+
+      Using =meson= (which you may need to install), you can use something like
+      the following in the mu top source directory:
+
+#+BEGIN_SRC sh
+       $ meson build && ninja -C build
+#+END_SRC
+
+    - However, note that =autogen.sh= has been updated, and there's a
+      convenience =Makefile= with some useful targets, so you can also do:
+#+BEGIN_SRC sh
+       $ ./autogen.sh && make # and optionally, 'sudo make install'
+#+END_SRC
+
+   - After that, either =ninja -C build= or =make= should be enough to rebuild
+
+   - NOTE: development versions 1.7.18 - 17.7.25 had a bug where the mail file
+     names sometimes got misnamed (with some extra ':2,'). This can be restored
+     with something like:
+#+begin_example
+    $ find ~/Maildir -name '*:2,*:*' | \
+       sed "s/\(\([^:]*\)\(:2,\)\{1,\}\(:2,.*$\)\)/mv '\0' '\2\4'/" > rename.sh
+#+end_example
+     (replace 'Maildir' with the path to your maildir)
+
+     once this is done, do check the generated 'rename.sh' and after convincing
+     yourself it does the right thing, do
+#+begin_example
+     $ sh rename.sh
+#+end_example
+     after that, re-index.
+
+   - Before installing, it is recommended that you *remove* any older versions
+     of ~mu~ and especially ~mu4e~, since they may conflict with the newer ones.
+
+   - =mu= now requires C++17 support for building
+
+
+** Contributor for this release
+
+  - As per ~git~: c0dev0id, Christophe Troestler, Daniel Fleischer, Daniel Nagy,
+    Dirk-Jan C. Binnema, Dr. Rich Cordero, Kai von Fintel, Marcelo Henrique
+    Cerri, Nicholas Vollmer, PRESFIL, Tassilo Horn, Thierry Volpiatto, Yaman
+    Qalieh, Yuri D'Elia, Zero King
+  - And of course all the people filing issues, suggesting features and helping
+    out on the maling list.
+
+
+
+* 1.6 (released, as of July 27 2021)
+
+  NOTE: After upgrading, you need to call ~mu init~, with your prefered parameters
+  before you can use ~mu~ / ~mu4e~. This is because the underlying database-schema
+  has changed.
+
+*** mu
+
+    - Where available (and with suitably equiped ~libglib~), log to the ~systemd~
+      journal instead of =~/.cache/mu.log=. Passing the ~--debug~ option to ~mu~
+      increases the amount that is logged.
+
+    - Follow symlinks in maildirs, and support moving messsages across
+      filesystems. Obviously, that is typically quite a bit slower than the
+      single-filesystem case, but can be still be useful.
+
+    - Optionally provide readline support for the ~mu~ server (when in tty-mode)
+
+    - Reworked the way mu generates s-expressions for mu4e; they are created
+      programmatically now instead of through string building.
+
+    - The indexer (the part of mu that scans maildirs and updates the message
+      store) has been rewritten so it can work asynchronously and take advantage
+      of multiple cores. Note that for now, indexing in ~mu4e~ is still a blocking
+      operation.
+
+    - Portability updates for dealing with non-POSIX systems, and in particular
+      VFAT filesystem, and building using Clang/libc++.
+
+    - The personal addresses (as per ~--my-address=~ for ~mu init~) can now also
+      include regular expressions (basic POSIX); wrap the expression in ~/~, e.g.,
+      ~--my-address='/.*@example.*/~'.
+
+    - Modernized the querying/threading machinery; this makes some old code a
+      lot easier to understand and maintain, and even while not an explicit
+      goal, is also faster.
+
+    - Experimental support for the Meson build system.
+
+*** mu4e
+
+    - Use the gnus-based message viewer as the default; the new viewer has quite
+      a few extra features compared to the old, mu4e-specific one, such as
+      faster crypto, support for S/MIME, syntax-highlighting, calendar
+      invitations and more.
+
+      The new view is superior in most ways, but if you still depend on
+      something from the old one, you can use:
+      #+begin_example
+      ;; set *before* loading mu4e; and restart emacs if you want to change it
+      ;; users of use-packag~ should can use the :init section for this.
+      (setq mu4e-view-use-old t)
+      #+end_example
+
+      (The older variable ~mu4e-view-use-gnus~ with the opposite meaning is
+      obsolete now, and no longer in use).
+
+    - Include maildir-shortcuts in the main-view with overall/unread counts,
+      similar to bookmarks, and with the same ~:hide~ and ~:hide-unread~ properties.
+      Note that for the latter, you need to update your maildir-shortcuts to the
+      new format, as explained in the ~mu4e-maildir-shortcuts~ docstring.
+
+      You can set ~mu4e-main-hide-fully-read~ to hide any bookmarks/maildirs that
+      have no unread messages.
+
+    - Add some more properties for use in capturing org-mode links to messages /
+      queries. See [[info:mu4e#Org-mode links][the mu4e manual]] for details.
+
+    - Honor ~truncate-string-ellipsis~ so you can now use 'fancy' ellipses for
+      truncated strings with ~(setq truncate-string-ellipsis "…")~
+
+    - Add a variable ~mu4e-mu-debug~ which, when set to non-~nil,~ makes the ~mu~
+      server log more verbosely (to ~mu.log~ or the journal)
+
+    - Better alignment in headers-buffers; this looks nicer, but is also a bit
+      slower, hence you need to enable ~mu4e-headers-precise-alignment~ for this.
+
+    - Support ~mu~'s new regexp-based personal addresses, and add
+      ~mu4e-personal-address-p~ to check whether a given string matches a personal
+      address.
+
+    - TAB-Completion for writing ~mu~ queries
+
+    - Switch the context for existing draft messages using
+      ~mu4e-compose-context-switch~ or ~C-c C-;~ in ~mu4e-compose-mode~.
+
+* 1.4 (released, as of April 18 2020)
+
+*** mu
+
+    - mu now defaults to the [[https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html][XDG Base Directory Specification]] for the default
+      locations for various files. E.g. on Unix the mu database now lives under
+      ~~/.cache/mu/~ rather than ~~/.mu~. You can still use the old location by
+      passing ~--muhome=~/.mu~ to various ~mu~ commands, or setting ~(setq
+      mu4e-mu-home "~/.mu")~ for ~mu4e~.
+
+      If your ~~/.cache~ is volatile (e.g., is cleared on reboot), you may want
+      use ~--muhome~. Some mailing-list dicussion suggest that's fairly rare
+      though.
+
+      After upgrading, you may wish to delete the files in the old location to
+      recover some diskspace.
+
+    - There's a new subcommand ~mu init~ to initialize the mu database, which
+      takes the ~--maildir~ and ~--my-address~ parameters that ~index~ used to take.
+      These parameters are persistent so ~index~ does not need (or accept) them
+      anymore. ~mu4e~ now depends on those parameters.
+
+      ~init~ only needs to be run once or when changing these parameters. That
+      implies that you need to re-index after changing these parameters. The
+      ~.noupdate~ files are ignored when indexing the first time after ~mu init~ (or
+      in general, when the database is empty).
+
+    - There is another new subcommand ~mu info~ to get information about the mu
+      database, the personal addresses etc.
+
+    - The contacts cache (which is used by ~mu cfind~ and ~mu4e~'s
+      contact-completion) is now stored as part of the Xapian database rather
+      than as a separate file.
+
+    - The ~--xbatchsize~ and ~--autoupgrade~ options for indexing are gone; both are
+      determined implicitly now.
+
+*** mu4e
+
+    - ~mu4e~ no longer uses the ~mu4e-maildir~ and ~mu4e-user-mail-address-list~
+      variables; instead it uses the information it gets from ~mu~ (see the ~mu~
+      section above). If you have a non-default ~mu4e-mu-home~, make sure to set
+      it before ~mu4e~ starts.
+
+      It is strongly recommended that you run ~mu init~ with the appropriate
+      parameters to (re)initialize the Xapian database, as mentioned in the
+      mu-section above.
+
+      The main screen shows your address(es), and issues a warning if
+      ~user-email-address~ is not part of that (and refer you to ~mu init~). You can
+      avoid the addresses in the main screen and the warning by setting
+      ~mu4e-main-view-hide-addresses~ to non-nil.
+
+    - In many cases, ~mu4e~ used to receive /all/ contacts after each indexing
+      operation; this was slow for some users, so we have updated this to /only/
+      get the contacts that have changed since the last round.
+
+      We also moved sorting the contacts to the mu-side, which speeds things up
+      further. However, as a side-effect of this, ~mu4e-contact-rewrite-function~
+      and ~mu4e-compose-complete-ignore-address-regexp~ have been obsoleted; users
+      of those should migrate to ~mu4e-contact-process-function~; see its
+      docstring for details.
+
+    - Christophe Troestler contributed support for Gnus' calender-invitation
+      handling in mu4e (i.e., you should be able to accept/reject invitations
+      etc.). It's very fresh code, and likely it'll be tweaked in the future.
+      But it's available now for testing. Note that this requires the gnus-based
+      viewer, as per ~(setq mu4e-view-use-gnus t)~
+
+    - In addition, he added support for custom headers, so the ones for for the
+      non-gnus-view should work just as well.
+
+    - ~org-mode~ support is enabled by default now. ~speedbar~ support is disabled
+      by default. The support org functionality has been moved to ~mu4e-org.el~,
+      with ~org-mu4e.el~ remaining for older things.
+
+    - ~mu4e~ now adds message-ids to messages when saving drafts, so we can find
+      them even with ~mu4e-headers-skip-duplicates~.
+
+    - Bookmarks (as in ~mu4e-bookmarks~) are now simple plists (instead of cl
+      structs). ~make-mu4e-bookmark~ has been updated to produce such plists (for
+      backward compatibility). A bookmark now looks like a list of e.g. ~(:name
+      "My bookmark" :query "banana OR pear" :key ?f)~ this format is a bit easier
+      extensible.
+
+    - ~mu4e~ recognizes an attribute ~:hide t~, which will hide the bookmark item
+      from the main-screen (and speedbar), but keep it available through the
+      completion UI.
+
+    - ~mu4e-maildir-shortcuts~ have also become plists. The older format is still
+      recognized for backward compatibility, but you are encouraged to upgrade.
+
+    - Replying to mailing-lists has been improved, allowing for choosing for
+      replying to all, sender, list-only.
+
+    - A very visible change, ~mu4e~ now shows unread/all counts for bookmarks in
+      the main screen that are strings. This is on by default, but can be
+      disabled by setting ~:hide-unread~ in the bookmark ~plist~ to ~t~. For
+      speed-reasons, these counts do _not_ filter out duplicates nor messages that
+      have been removed from the filesystem.
+
+    - ~mu4e-attachment-dir~ now also applies to composing messages; it determines
+      the default directory for inclusion.
+
+    - The mu4e <-> mu interaction has been rewritten to communicate using
+      s-expressions, with a repl for testing.
+
+*** guile
+
+    - guile 3.0 is now supported; guile 2.2 still works.
+
+*** toys
+
+    - Updated the ~mug~ toy UI to use Webkit2/GTK+. Note that this is just a toy
+      which is not meant for distribution. ~msg2pdf~ is disabled for now.
+
+
+*** How to upgrade mu4e
+
+    - upgrade ~mu~ to the latest stable version (1.4.x)
+
+    - shut down emacs
+
+    - Run ~mu init~ in a terminal
+
+    - Make sure ~mu init~ points to the right Maildir folder and add your email
+      address(es) the following way:
+
+      ~mu init --maildir=~/Maildir --my-address=jim@example.com --my-address=bob@example.com~
+
+    - once this is done, run ~mu index~
+
+    - Don't forget to delete your old mail cache location if necessary (see
+      release notes for more detail).
+
+* Old news
+  :PROPERTIES:
+  :VISIBILITY: folded
+  :END:
+
+** 1.2
+
+   After a bit over a year since version 1.0, here is version 1.2. This is
+   mostly a bugfix release, but there are also a number of new features.
+
+*** mu
+
+    - Substantial (algorithmic) speed-up of message-threading; this also (or
+      especially) affects mu4e, since threading is the default. See commit
+      eb9bfbb1ca3c for all the details, and thanks to Nicolas Avrutin.
+
+    - The query-parser now generates better queries for wildcard searches, by
+      using the Xapian machinery for that (when available) rather than
+      transforming into regexp queries.
+
+    - The perl backend is hardly used and will be removed; for now we just
+      disable it in the build.
+
+    - Allow outputting messages in json format, closely following the sexp
+      output. This adds an (optional) dependency on the Json-Glib library.
+
+*** mu4e
+
+    - Bump the minimal required emacs version to 24.4. This was already de-facto
+      true, now it is enforced.
+
+    - In mu4e-bookmarks, allow the `:query` element to take a function (or
+      lambda) to dynamically generate the query string.
+
+    - There is a new message-view for mu4e, based on the Gnus' article-view.
+      This bring a lot of (but not all) of the very rich Gnus article-mode
+      feature-set to mu4e, such as S/MIME-support, syntax-highlighting,
+
+      For now this is experimental ("tech preview"), but might replace the
+      current message-view in a future release. Enable it with:
+              (setq mu4e-view-use-gnus t)
+
+      Thanks to Christophe Troestler for his work on fixing various encoding
+      issues.
+
+    - Many bug fixes
+
+*** guile
+
+    - Now requires guile 2.2.
+
+*** Contributors for this release:
+
+    Ævar Arnfjörð Bjarmason, Albert Krewinkel, Alberto Luaces, Alex Bennée, Alex
+    Branham, Alex Murray, Cheong Yiu Fung, Chris Nixon, Christian Egli,
+    Christophe Troestler, Dirk-Jan C. Binnema, Eric Danan, Evan Klitzke, Ian
+    Kelling, ibizaman, James P. Ascher, John Whitbeck, Junyeong Jeong, Kevin
+    Foley, Marcelo Henrique Cerri, Nicolas Avrutin, Oleh Krehel, Peter W. V.
+    Tran-Jørgensen, Piotr Oleskiewicz, Sebastian Miele, Ulrich Ölmann,
+
+** 1.0
+
+   After a decade of development, *mu 1.0*!
+
+   Note: the new release requires a C++14 capable compiler.
+
+*** mu
+
+    - New, custom query parser which replaces Xapian's 'QueryParser'
+      both in mu and mu4e. Existing queries should still work, but the new
+      engine handles non-alphanumeric queries much better.
+    - Support regular expressions in queries (with the new query engine),
+      e.g. "subject:/foo.*bar/". See the new `mu-query` and updated `mu-easy`
+      manpages for examples.
+    - cfind: ensure nicks are unique
+    - auxiliary programs invoked from mu/mu4e survive terminating the
+      shell / emacs
+
+*** mu4e
+
+    - Allow for rewriting message bodies
+    - Toggle-menus for header settings
+    - electric-quote-(local-)mode work when composing emails
+    - Respect format=flowed and delsp=yes for viewing plain-text
+      messages
+    - Added new mu4e-split-view mode: single-window
+    - Add menu item for `untrash'.
+    - Unbreak abbrevs in mu4e-compose-mode
+    - Allow forwarding messages as attachments
+      (`mu4e-compose-forward-as-attachment')
+    - New defaults: default to 'skip duplicates' and 'include related'
+      in headers-view, which should be good defaults for most users. Can be
+      customized using `mu4e-headers-skip-duplicates' and
+      `mu4e-headers-include-related', respectively.
+    - Many bug fixed (see github for all the details).
+    - Updated documentation
+
+*** Contributors for this release:
+
+    Ævar Arnfjörð Bjarmason, Alex Bennée, Arne Köhn, Christophe Troestler,
+    Damien Garaud, Dirk-Jan C. Binnema, galaunay, Hong Xu, Ian Kelling, John
+    Whitbeck, Josiah Schwab, Jun Hao, Krzysztof Jurewicz, maxime, Mekeor Melire,
+    Nathaniel Nicandro, Ronald Evers, Sean 'Shaleh' Perry, Sébastien Le
+    Callonnec, Stig Brautaset, Thierry Volpiatto, Titus von der Malsburg,
+    Vladimir Sedach, Wataru Ashihara, Yuri D'Elia.
+
+    And all the people on the mailing-list and in github, with bug reports,
+    questions and suggestions.
+
+
+** 0.9.18
+
+   New development series which will lead to 0.9.18.
+
+*** mu
+
+    - Increase the default maximum size for messages to index to 500
+      Mb; you can customize this using the --max-msg-size parameter to mu index.
+    - implement "lazy-checking", which makes mu not descend into
+      subdirectories when the directory-timestamp is up to date; greatly speeds
+      up indexing (see --lazy-check)
+    - prefer gpg2 for crypto
+    - fix a crash when running on OpenBSD
+    - fix --clear-links (broken filenames)
+    - You can now set the MU_HOME environment variable as an
+      alternative way of setting the mu homedir via the --muhome command-line
+      parameter.
+
+*** mu4e
+
+**** reading messages
+
+     - Add `mu4e-action-view-with-xwidget`, and action for viewing
+       e-mails inside a Webkit-widget inside emacs (requires emacs 25.x with
+       xwidget/webkit/gtk3 support)
+     - Explicitly specify utf8 for external html viewing, so browsers
+       can handle it correctly.
+     - Make `shr' the default renderer for rich-text emails (when
+       available)
+     - Add a :user-agent field to the message-sexp (in mu4e-view), which
+       is either the User-Agent or X-Mailer field, when present.
+
+**** composing messages
+
+     - Cleanly handle early exits from message composition as well as while
+       composing.
+     - Allow for resending existing messages, possibly editing them. M-x
+       mu4e-compose-resend, or use the menu; no shortcut.
+     - Better handle the closing of separate compose frames
+     - Improved font-locking for the compose buffers, and more extensive
+       checks for cited parts.
+     - automatically sign/encrypt replies to signed/encrypted messages
+       (subject to `mu4e-compose-crypto-reply-policy')
+
+**** searching & marking
+
+     - Add a hook `mu4e-mark-execute-pre-hook`, which is run just before
+       executing marks.
+     - Just before executing any search, a hook-function
+       `mu4e-headers-search-hook` is invoked, which receives the search
+       expression as its parameter.
+     - In addition, there's a `mu4e-headers-search-bookmark-hook` which
+       gets called when searches get invoked as a bookmark (note that
+       `mu4e-headers-search-hook` will also be called just afterwards). This
+       hook also receives the search expression as its parameter.
+     - Remove the 'z' keybinding for leaving the headers
+       view. Keybindings are precious!
+     - Fix parentheses/precedence in narrowing search terms
+
+**** indexing
+
+     - Allow for indexing in the background; see
+       `mu4e-index-update-in-background`.
+     - Better handle mbsync output in the update buffer
+     - Add variables mu4e-index-cleanup and mu4e-index-lazy to enable
+       lazy checking from mu4e; you can sit from mu4e using something like:
+#+begin_src elisp
+(setq mu4e-index-cleanup nil ;; don't do a full cleanup check
+  mu4e-index-lazy-check t) ;; don't consider up-to-date dirs #+END_SRC
+#+end_src
+**** misc
+
+     - don't overwrite global-mode-string, append to it.
+     - Make org-links (and more general, all users of
+       mu4e-view-message-with-message-id) use a headers buffer, then view the
+       message. This way, those linked message are just like any other, and can
+       be deleted, moved etc.
+     - Support org-mode 9.x
+     - Improve file-name escaping, and make it support non-ascii filenames
+     - Attempt to jump to the same messages after a re-search update operation
+     - Add action for spam-filter options
+     - Let `mu4e~read-char-choice' become case-insensitive if there is
+       no exact match; small convenience that affects most the single-char
+       option-reading in mu4e.
+
+*** Perl
+
+    - an experimental Perl binding ("mup") is available now. See
+      perl/README.md for details.
+
+*** Contributors:
+
+   Aaron LI, Abdo Roig-Maranges, Ævar Arnfjörð Bjarmason, Alex Bennée, Allen,
+   Anders Johansson, Antoine Levitt, Arthur Lee, attila, Charles-H. Schulz,
+   Christophe Troestler, Chunyang Xu, Dirk-Jan C. Binnema, Jakub Sitnicki,
+   Josiah Schwab, jsrjenkins, Jun Hao, Klaus Holst, Lukas Fürmetz, Magnus
+   Therning, Maximilian Matthe, Nicolas Richard, Piotr Trojanek, Prashant
+   Sachdeva, Remco van 't Veer, Stephen Eglen, Stig Brautaset, Thierry
+   Volpiatto, Thomas Moulia, Titus von der Malsburg, Yuri D'Elia, Vladimir
+   Sedach
+
+** 0.9.16
+
+*** Release
+
+    2016-01-20: Release from the 0.9.15 series
+
+*** Contributors:
+
+    Adam Sampson, Ævar Arnfjörð Bjarmason, Bar Shirtcliff, Charles-H. Schulz,
+    Clément Pit--Claudel, Damien Cassou, Declan Qian, Dima Kogan, Dirk-Jan C.
+    Binnema, Foivos S. Zakkak, Hinrik Örn Sigurðsson, Jeroen Tiebout, JJ Asghar,
+    Jonas Bernoulli, Jun Hao, Martin Yrjölä, Maximilian Matthé, Piotr Trojanek,
+    prsarv, Thierry Volpiatto, Titus von der Malsburg
+
+    (and of course all people who reported issues, provided suggestions etc.)
+
+** 0.9.15
+
+   - bump version to 0.9.15. From now on, odd minor version numbers
+     are for development versions; thus, 0.9.16 is to be the next stable
+     release.
+   - special case text/calendar attachments to get .vcs
+     extensions. This makes it easier to process those with external tools.
+   - change the message file names to better conform to the maildir
+     spec; this was confusing some tools.
+   - fix navigation when not running in split-view mode
+   - add `mu4e-view-body-face', so the body-face for message in the
+     view can be customized; e.g. (set-face-attribute 'mu4e-view-body-face nil
+     :font "Liberation Serif-10")
+   - add `mu4e-action-show-thread`, an action for the headers and view
+     buffers to search for messages in the same thread as the current one.
+   - allow for transforming mailing-list names for display, using
+     `mu4e-mailing-list-patterns'.
+   - some optimizations in indexing (~30% faster in some cases)
+   - new variable mu4e-user-agent-string, to customize the User-Agent:
+     header.
+   - when removing the "In-reply-to" header from replies, mu4e will
+     also remove the (hidden) References header, effectively creating a new
+     message-thread.
+   - implement 'mu4e-context', for defining and switching between
+     various contexts, which are groups of settings. This can be used for
+     instance for switch between e-mail accounts. See the section in the manual
+     for details.
+   - correctly decode mailing-list headers
+   - allow for "fancy" mark-characters; and improve the default set
+   - by default, the maildirs are no longer cached; please see the
+     variable ~mu4e-cache-maildir-list~ if you have a lot of maildirs and it
+     gets slow.
+   - change the default value for
+     ~org-mu4e-link-query-in-headers-mode~ to ~nil~, ie. by default link to the
+     message, not the query, as this is usually more useful behavior.
+   - overwrite target message files that already exist, rather than
+     erroring out.
+   - set mu4e-view-html-plaintext-ratio-heuristic to 5, as 10 was too
+     high to detect some effectively html-only messages
+   - add mu4e-view-toggle-html (keybinding: 'h') to toggle between
+     text and html display. The existing 'mu4e-view-toggle-hide-cited' gets the
+     new binding '#'.
+   - add a customization variable `mu4e-view-auto-mark-as-read'
+     (defaults to t); if set to nil, mu4e won't mark messages as read when you
+     open them. This can be useful on read-only file-systems, since
+     marking-as-read implies a file-move operation.
+   - use smaller chunks for mu server on Cygwin, allowing for better
+     mu4e support there.
+
+** 0.9.13
+
+*** contributors
+
+    Attila, Daniele Pizzolli, Charles-H.Schulz, David C Sterrat, Dirk-Jan C.
+    Binnema, Eike Kettner, Florian Lindner, Foivos S. Zakkak, Gour, KOMURA
+    Takaaki, Pan Jie, Phil Hagelberg, thdox, Tiago Saboga, Titus von der
+    Malsburg
+
+    (and of course all people who reported issues, provided suggestions etc.)
+
+*** mu/mu4e/guile
+
+    - NEWS (this file) is now visible from within mu4e – "N" in the main-menu.
+
+    - make `mu4e-headers-sort-field', `mu4e-headers-sort-direction'
+      public (that, is change the prefix from mu4e~ to mu4e-), so users can
+      manipulate them
+
+    - make it possible the 'fancy' (unicode) characters separately for
+      headers and marks (see the variable `mu4e-use-fancy-chars'.)
+
+    - allow for composing in a separate frame (see
+      `mu4e-compose-in-new-frame')
+
+    - add the `:thread-subject' header field, for showing the subject
+      for a thread only once. So, instead of (from the manual):
+
+#+begin_example
+06:32      Nu To Edmund Dantès   GstDev   + Re: Gstreamer-V4L...
+15:08      Nu Abbé Busoni        GstDev   + Re: Gstreamer-V...
+18:20      Nu Pierre Morrel      GstDev   \ Re: Gstreamer...
+2013-03-18 S  Jacopo             EmacsUsr + emacs server on win...
+2013-03-18 S  Mercédès           EmacsUsr  \ RE: emacs server ...
+2013-03-18 S  Beachamp           EmacsUsr  + Re: Copying a whole...
+22:07      Nu Albert de Moncerf  EmacsUsr   \ Re: Copying a who...
+2013-03-18 S  Gaspard Caderousse GstDev   | Issue with GESSimpl...
+2013-03-18 Ss Baron Danglars     GuileUsr | Guile-SDL 0.4.2 ava...
+End of search results
+#+end_example
+
+the headers list would now look something like:
+#+begin_example
+06:32      Nu To Edmund Dantès   GstDev   + Re: Gstreamer-V4L...
+15:08      Nu Abbé Busoni        GstDev   +
+18:20      Nu Pierre Morrel      GstDev   \ Re: Gstreamer...
+2013-03-18 S  Jacopo             EmacsUsr + emacs server on win...
+2013-03-18 S  Mercédès           EmacsUsr  \
+2013-03-18 S  Beachamp           EmacsUsr + Re: Copying a whole...
+22:07      Nu Albert de Moncerf  EmacsUsr   \
+2013-03-18 S  Gaspard Caderousse GstDev   | Issue with GESSimpl...
+2013-03-18 Ss Baron Danglars     GuileUsr | Guile-SDL 0.4.2 ava...
+End of search results
+#+end_example
+
+      This is a feature known from e.g. `mutt' and `gnus` and many other
+      clients, and can be enabled by customizing `mu4e-headers-fields'
+      (replacing `:subject' with `:thread-subject')
+
+      It's not the default yet, but may become so in the future.
+
+    - add some spam-handling actions to mu4e-contrib.el
+
+    - mu4e now targets org 8.x, which support for previous versions
+      relegated to `org-old-mu4e.el`. Some of the new org-features are improved
+      capture templates.
+
+    - updates to the documentation, in particular about using BBDB.
+
+    - improved URL-handling (use emacs built-in functionality)
+
+    - many bug fixes, including some crash fixes on BSD
+
+*** guile
+
+    – add --delete option to the find-dups scripts, to automatically delete
+    them. Use with care!
+
+** Release 0.9.12
+
+*** mu
+
+    - truncate /all/ terms the go beyond xapian's max term length
+    - lowercase the domain-part of email addresses in mu cfind (and mu4e), if
+      the domain is in ascii
+    - give messages without msgids fake-message-ids; this fixes the problem
+      where such messages were not found in --include-related queries
+    - cleanup of the query parser
+    - provide fake message-ids for messages without it; fixes #183
+    - allow showing tags in 'mu find' output
+    - fix CSV quoting
+
+*** mu4e
+
+    - update the emacs <-> backend protocol; documented in the mu-server man page
+    - show 'None' as date for messages without it (Headers View)
+    - add `mu4e-headers-found-hook', `mu4e-update-pre-hook'.
+    - split org support in org-old-mu4e.el (org <= 7.x) and org-mu4e.el
+    - org: improve template keywords
+    - rework URL handling
+
+** Release 0.9.5
+
+*** mu
+
+    - allow 'contact:' as a shortcut in queries for 'from:foo OR to:foo OR
+      cc:foo OR bcc:foo', and 'recip:' as a shortcut for 'to:foo OR cc:foo OR
+      bcc:foo'
+    - support getting related messages (--include-related), which includes
+      messages that may not match the query, but that are in the same threads as
+      messages that were
+    - support "list:"/"v:" for matching mailing list names, and the "v"
+      format-field to show them. E.g 'mu find list:emacs-orgmode.gnu.org'
+
+*** mu4e
+
+    - scroll down in message view takes you to next message (but see
+      `mu4e-view-scroll-to-next')
+    - support 'human dates', that is, show the time for today's messages, and
+      the date for older messages in the headers view
+    - replace `mu4e-user-mail-address-regexp' and `mu4e-my-mail-addresses' with
+      `mu4e-user-mail-address-list'
+    - support tags (i.e.., X-Keywords and friends) in the headers-view, and the
+      message view. Thanks to Abdó Roig-Maranges. New field ":tags".
+    - automatically update the headers buffer when new messages are found during
+      indexing; set `mu4e-headers-auto-update' to nil to disable this.
+    - update mail/index with M-x mu4e-update-mail-and-index; which everywhere in
+      mu4e is available with key C-S-u. Use prefix argument to run in
+      background.
+    - add function `mu4e-update-index' to only update the index
+    - add 'friendly-names' for mailing lists, so they should up nicely in the
+      headers view
+
+*** guile
+
+    - add 'mu script' command to run mu script, for example to do statistics on
+      your message corpus. See the mu-script man-page.
+
+*** mug
+
+    - ported to gtk+ 3; remove gtk+ 2.x code
+
+
+
+** Release 0.9.9 <2012-10-14>
+
+*** mu4e
+    - view: address can be toggled long/short, compose message
+    - sanitize opening urls (mouse-1, and not too eager)
+    - tooltips for header labels, flags
+    - add sort buttons to header-labels
+    - support signing / decryption of messages
+    - improve address-autocompletion (e.g., ensure it's case-insensitive)
+    - much faster when there are many maildirs
+    - improved line wrapping
+    - better handle attached messages
+    - improved URL-matching
+    - improved messages to user (mu4e-(warn|error|message))
+    - add refiling functionality
+    - support fancy non-ascii in the UI
+    - dynamic folders (i.e.., allow mu4e-(sent|draft|trash|refile)-folder) to
+      be a function
+    - dynamic attachment download folder (can be a function now)
+    - much improved manual
+
+*** mu
+    - remove --summary (use --summary-len instead)
+    - add --after for mu find, to limit to messages after T
+    - add new command `mu verify', to verify signatures
+    - fix iso-2022-jp decoding (and other 7-bit clean non-ascii)
+    - add support for X-keywords
+    - performance improvements for threaded display (~ 25% for 23K msgs)
+    - mu improved user-help (and the 'mu help' command)
+    - toys/mug2 replaces toys/mug
+
+*** mu-guile
+    - automated tests
+    - add mu:timestamp, mu:count
+    - handle db reopenings in the background
+
+
+** Release 0.9.8.5 <2012-07-01>
+
+*** mu4e
+
+    - auto-completion of e-mail addresses
+    - inline display of images (see `mu4e-view-show-images'), uses imagemagick
+      if available
+    - interactively change number of headers / columns for showing headers with
+      C-+ and C-- in headers, view mode
+    - support flagging message
+    - navigate to previous/next queries like a web browser (with <M-left>,
+      <M-right>)
+    - narrow search results with '/'
+    - next/previous take a prefix arg now, to move to the nth previous/next message
+    - allow for writing rich-text messages with org-mode
+    - enable marking messages as Flagged
+    - custom marker functions (see manual)
+    - better "dwim" handling of buffer switching / killing
+    - deferred marking of message (i.e.., mark now, decide what to mark for
+      later)
+    - enable changing of sort order, display of threads
+    - clearer marks for marked messages
+    - fix sorting by subject (disregarding Re:, Fwd: etc.)
+    - much faster handling when there are many maildirs (speedbar)
+    - handle mailto: links
+    - improved, extended documentation
+
+*** mu
+
+    - support .noupdate files (parallel to .noindex, dir is ignored unless we're
+      doing a --rebuild).
+    - append all inline text parts, when getting the text body
+    - respect custom maildir flags
+    - correctly handle the case where g_utf8_strdown (str) > len (str)
+    - make gtk, guile, webkit dependency optional, even if they are installed
+
+
+** Release 0.9.8.4 <2012-05-08>
+
+*** mu4e
+
+    - much faster header buffers
+    - split view mode (headers, view); see `mu4e-split-view'.
+    - add search history for queries
+    - ability to open attachments with arbitrary programs, pipe through shell
+      commands or open in the current emacs
+    - quote names in recipient addresses
+    - mu4e-get-maildirs works now for recursive maildirs as well
+    - define arbitrary operations for headers/messages/attachments using the
+      actions system -- see the chapter 'Actions' in the manual
+    - allow mu4e to be uses as the default emacs mailer (`mu4e-user-agent')
+    - mark headers based on a regexp, `mu4e-mark-matches', or '%'
+    - mark threads, sub-threads (mu4e-hdrs-mark-thread,
+      mu4e-hdrs-mark-subthread, or 'T', 't')
+    - add msg2pdf toy
+    - easy logging (using `mu4e-toggle-logging')
+    - improve mu4e-speedbar for use in headers/view
+    - use the message-mode FCC system for saving messages to the sent-messages
+      folder
+    - fix: off-by-one in number of matches shown
+
+*** general
+
+    - fix for opening files with non-ascii names
+    - much improved support for searching non-Latin (Cyrillic etc.) languages
+      we can now match 'Тесла' or 'Аркона' without problems
+    - smarter escaping (fixes issues with finding message ids)
+    - fixes for queries with brackets
+    - allow --summary-len for the length of message summaries
+    - numerous other small fixes
+
+
+** Release 0.9.8.3 <2012-04-06>
+
+   *NOTE*: existing mu/mu4e are recommended to run `mu index --rebuild' after
+   installation.
+
+*** mu4e
+
+    - allow for searching by editing bookmarks
+      (`mu4e-search-bookmark-edit-first') (keybinding 'B')
+    - make it configurable what to do with sent messages (see
+      `mu4e-sent-messages-behavior')
+    - speedbar support (initial patch by Antono V)
+    - better handling of drafts:
+      - don't save too early
+      - more descriptive buffer names (based on Subject, if any)
+      - don't put "--text-follows-this-line--" markers in files
+    - automatically include signatures, if set
+    - add user-settable variables mu4e-view-wrap-lines and mu4e-view-hide-cited,
+      which determine the initial way a message is displayed
+    - improved documentation
+
+*** general
+
+    - much improved searching for GMail folders (i.e. maildir:/ matching);
+      this requires a 'mu index --rebuild'
+    - correctly handle utf-8 messages, even if they don't specify this explicitly
+    - fix compiler warnings for newer/older gcc and clang/clang++
+    - fix unit tests (and some code) for Ubuntu 10.04 and FreeBSD9
+    - fix warnings for compilation with GTK+ 3.2 and recent glib (g_set_error)
+    - fix mu_msg_move_to_maildir for top-level messages
+    - fix in maildir scanning
+    - plug some memleaks
+
+** Release 0.9.8.2 <2012-03-11>
+
+*** mu4e:
+
+    - make mail updating non-blocking
+    - allow for automatic periodic update ('mu4e-update-interval')
+    - allow for external triggering of update
+    - make behavior when leaving the headers buffer customizable, ie.
+      ask/apply/ignore ('mu4e-headers-leave-behaviour')
+
+*** general
+
+    - fix output for some non-UTF8 locales
+    - open ('play') file names with spaces
+    - don't show unnecessary errors for --format=links
+    - make build warning-free for clang/clang++
+    - allow for slightly older autotools
+    - fix unit tests for some hidden assumptions (locale, dir structure etc.)
+    - some documentation updates / clarifications
+
+** Release 0.9.8.1 <2012-02-18 Sat>
+
+*** mu
+    - show only leaf/rfc822 MIME-parts
+
+*** mu4e
+
+    - 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').
+    - don't repeat 'Re:' / 'Fwd:'
+    - colorize cited message parts
+    - better handling of text-based, embedded message attachments
+    - for text-bodies, concatenate all text/plain parts
+    - make filladapt dep optional
+    - documentation improvements
+
+** Release 0.9.8 <2012-01-31>
+
+   - '--descending' has  been renamed into '--reverse'
+   - search for attachment MIME-type using 'mime:' or 'y:'
+   - search for text in text-attachments using 'embed:' or 'e:'
+   - searching for attachment file names now uses 'file:' (was: 'attach:')
+   - experimental emacs-based mail client -- "mu4e"
+   - added more unit tests
+   - improved guile binding - no special binary is needed anymore, it's
+     installable are works with the normal guile system; code has been
+     substantially improved. still 'experimental'
+
+** Release 0.9.7 <2011-09-03 Sat>
+
+   - don't enforce UTF-8 output, use locale (fixes issue #11)
+   - add mail threading to mu-find (using -t/--threads) (sorta fixes issue #13)
+   - add header line to --format=mutt-ab (mu cfind), (fixes issue #42)
+   - terminate mu view results with a form-feed marker (use --terminate) (fixes
+     issue #41)
+   - search X-Label: tags (fixes issue #40)
+   - added toys/muile, the mu guile shells, which allows for message stats etc.
+   - fix date handling (timezones)
+
+** Release 0.9.6 <2011-05-28 Sat>
+
+   - FreeBSD build fix
+   - fix matching for mu cfind to be as expected
+   - fix mu-contacts for broken names/emails
+   - clear the contacts-cache too when doing a --rebuild
+   - wildcard searches ('*') for fields (except for path/maildir)
+   - search for attachment file names (with 'a:'/'attach:') -- also works with
+     wildcards
+   - remove --xquery completely; use --output=xquery instead
+   - fix progress info in 'mu index'
+   - display the references for a message using the 'r' character (xmu find)
+   - remove --summary-len/-k, instead use --summary for mu view and mu find, and
+   - support colorized output for some sub-commands (view, cfind and
+     extract). Disabled by default, use --color to enable, or set env MU_COLORS
+     to non-empty
+   - update documentation, added more examples
+
+** Release 0.9.5 <2011-04-25 Mon>
+
+   - bug fix for infinite loop in Maildir detection
+   - minor fixes in tests, small optimizations
+
+** Release 0.9.4 <2011-04-12 Tue>
+
+   - add the 'cfind' command, to search/export contact information
+   - add 'flag:unread' as a synonym for 'flag:new OR NOT flag:unseen'
+   - updated documentation
+
+** Release 0.9.3 <2011-02-13 Sun>
+
+   - don't warn about missing files with --quiet
+
+** Release 0.9.2 <2011-02-02 Wed>
+
+   - stricter checking of options; and options must now *follow* the sub-command
+     (if any); so, something like: 'mu index --maildir=/foo/bar'
+   - output searches as plain text (default), XML, JSON or s-expressions using
+     --format=plain|xml|json|sexp. For example: 'mu find foobar --output=json'.
+     These format options are experimental (except for 'plain')
+   - the --xquery option should now be used as --format=xquery, for output
+     symlinks, use --format=links. This is a change in the options.
+   - search output can include the message size using the 'z' shortcut
+   - match message size ranges (i.e.. size:500k..2M)
+   - fix: honor the --overwrite (or lack thereof) parameter
+   - support folder names with special characters (@, ' ', '.' and so on)
+   - better check for already-running mu index
+   - when --maildir= is not provided for mu index, default to the last one
+   - add --max-msg-size, to specify a new maximum message size
+   - move the 'mug' UI to toys/mug; no longer installable
+   - better support for Solaris builds, Gentoo.
+
+** Release 0.9.1 <2010-12-05 Sun>
+
+   - Add missing icon for mug
+   - Fix unit tests (Issue #30)
+   - Fix Fedora 14 build (broken GTK+ 3) (Issue #31)
+
+** Release 0.9 <2010-12-04 Sat>
+
+   - you can now search for the message priority ('prio:high', 'prio:low',
+     'prio:normal')
+   - you can now search for message flags, e.g. 'flag:attach' for messages with
+     attachment, or 'flag:encrypted' for encrypted messages
+   - you can search for time-intervals, e.g. 'date:2010-11-26..2010-11-29' for
+     messages in that range. See the mu-find(1) and mu-easy(1) man-pages for
+     details and examples.
+   - you can store bookmarked queries in ~/.mu/bookmarks
+   - the 'flags' parameter has been renamed in 'flag'
+   - add a simple graphical UI for searching, called 'mug'
+   - fix --clearlinks for file systems without entry->d_type (fixes issue #28)
+   - make matching case-insensitive and accent-insensitive (accent-insensitive
+     for characters in Unicode Blocks 'Latin-1 Supplement' and 'Latin
+     Extended-A')
+   - more extensive pre-processing is done to make searching for email-addresses
+     and message-ids less likely to not work (issue #21)
+   - updated the man-pages
+   - experimental support for Fedora 14, which uses GMime 2.5.x (fixes issue #29)
+
+** Release 0.8 <2010-10-30 Sat>
+
+   - There's now 'mu extract' for getting information about MIME-parts
+     (attachments) and extracting them
+   - Queries are now internally converted to lowercase; this solves some of the
+     false-negative issues
+   - All mu sub-commands now have their own man-page
+   - 'mu find' now takes a --summary-len=<n> argument to print a summary of
+     up-to-n lines of the message
+   - Same for 'mu view'; the summary replaces the full body
+   - Setting the mu home dir now goes with -m, --muhome
+   - --log-stderr, --reindex, --rebuild, --autoupgrade, --nocleanup, --mode,
+     --linksdir, --clearlinks lost their single char version
+
+** Release 0.7 <2010-02-27 Sat>
+
+   - Database format changed
+   - Automatic database scheme version check, notifies users when an upgrade
+     is needed
+   - 'mu view', to view mail message files
+   - Support for >10K matches
+   - Support for unattended upgrades - that is, the database can automatically
+     by upgraded (--autoupgrade). Also, the log file is automatically cleaned
+     when it gets too big (unless you use --nocleanup)
+   - Search for a certain Maildir using the maildir:,m: search prefixes. For
+     example, you can find all messages located in ~/Maildir/foo/bar/cur/msg
+     ~/Maildir/foo/bar/new/msg and with m:/foo/bar this replace the search for
+     path/p in 0.6
+   - Fixes for reported issues ()
+   - A test suite with a growing number of unit tests
+
+
+** Release 0.6 <2010-01-23 Sat>
+
+   - First new release of mu since 2008
+   - No longer depends on sqlite
+
+
+# Local Variables:
+# mode: org; org-startup-folded: nil
+# fill-column:80
+# End:
diff --git a/README.org b/README.org
new file mode 100644 (file)
index 0000000..73426be
--- /dev/null
@@ -0,0 +1,96 @@
+#+TITLE:mu
+[[https://github.com/djcb/mu/blob/master/COPYING][https://img.shields.io/github/license/djcb/mu?logo=gnu&.svg]]
+[[https://en.cppreference.com][https://img.shields.io/badge/Made%20with-C/CPP-1f425f?logo=c&.svg]]
+[[https://img.shields.io/github/v/release/djcb/mu][https://img.shields.io/github/v/release/djcb/mu.svg]]
+[[https://github.com/djcb/mu/graphs/contributors][https://img.shields.io/github/contributors/djcb/mu.svg]]
+[[https://github.com/djcb/mu/issues][https://img.shields.io/github/issues/djcb/mu.svg]]
+[[https://github.com/djcb/mu/issues?q=is%3Aissue+is%3Aopen+label%3Arfe][https://img.shields.io/github/issues/djcb/mu/rfe?color=008b8b.svg]]
+[[https://github.com/djcb/mu/pull/new][https://img.shields.io/badge/PRs-welcome-brightgreen.svg]]\\
+[[https://melpa.org/#/?q=mu4e&sort=version&asc=false][https://img.shields.io/badge/Emacs-25.3-922793?logo=gnu-emacs&logoColor=b39ddb&.svg]]
+[[https://www.djcbsoftware.nl/code/mu/mu4e/Installation.html#Dependencies-for-Debian_002fUbuntu][https://img.shields.io/badge/Platform-Linux-2e8b57?logo=linux&.svg]]
+[[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]]
+
+Welcome to ~mu~!
+
+*Note*: you are looking at the *development* branch, which is where new code is
+being developed and tested, and which may occasionally break.
+
+Distribution and non-adventurous users are instead recommended to use the [[https://github.com/djcb/mu/tree/release/1.8][1.8
+Release Branch]] or to pick up one of the [[https://github.com/djcb/mu/releases][1.8 Releases]].
+
+Given the enormous amounts of e-mail many people gather and the importance of
+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 fully documented.
+
+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.
+
+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
+  later)
+
+~mu~ is written in C and C++; ~mu4e~ is written in ~elisp~ and ~mu-guile~ in a mix of C++ and
+Scheme.
+
+Note, ~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).
+
+* Installation
+
+Note: building from source is an /advanced/ subject; esp. if something goes
+wrong. The below simple examples are a start, but all tools involved have many
+options; there are differences between systems, versions etc. So if this is all
+a bit daunting we recommend to wait for someone else to build it for you, such
+as a Linux distribution. Many have packages available.
+
+** Requirements
+
+To be able to build ~mu~, ensure you have:
+
+- a C++17 compiler (~gcc~ or ~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~
+- ~meson~
+
+For ~mu4e~, you also need ~emacs~.
+
+** Building
+
+#+begin_example
+$ git clone git://github.com/djcb/mu.git
+$ cd mu
+#+end_example
+
+Now, you have a choice. ~mu~ uses ~meson~ for building, but includes a good-old
+~Makefile~ with some useful targets, which should work for typical cases.
+
+#+begin_example
+$ ./autogen.sh && make
+$ sudo make install
+#+end_example
+
+Alternatively, for more control, you can run ~meson~ directly:
+#+begin_example
+$ meson build && ninja -C build
+$ ninja -C build install
+#+end_example
+
+This allows for passing various ~meson~ options, such as ~--prefix~. Consult the
+~meson~ documentation for details.
+
+
+
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..f152855
--- /dev/null
+++ b/TODO
@@ -0,0 +1,159 @@
+#+STARTUP: showall
+
+* TODO (fixes, ideas, etc.)
+
+** Future stuff
+
+*** mu
+
+  - put threading information in the database, and enable getting the complete
+     threads when searching
+  - refactor fill_database function in test cases
+  - don't show duplicate e-mails (i.e.. for Gmail); check the message-id
+
+*** mu-guile
+
+  - move contact export to separate scm
+  - fix logging
+
+*** mu4e
+    
+  - special-case replying to messages sent by self
+  - identities (see Jacek's 'mu4e: From field in replies' mail)
+    ==> [ workaround available, using mu4e-pre-compose-hook, dynamic folders ]
+  - new-mail warning
+    ==> [ workaround available, using mu4e-index-updated-hook ]
+  - custom header fields in headers-view, message-view
+  - show maildirs as a tree, not a list in speed bar
+  - review emacs menus
+  - re-factor / separate window/buffer management
+    - enable keeping message view buffers around
+    - better naming for draft/view buffers
+  - header updating interferes with marks (when updating for 'mark as read',
+    when reading a marked message)
+  - set/unset flag editing command
+  - handling of database upgrades
+  - restore point after rerunning a search
+  - make the mu4e-bookmarks format similar to the other ones
+  - refresh current query after update?
+  - fix mu4e-mark-set to work from the view buffer as well
+    - open links to mails through headers-mode somehow (i.e..,
+     mu4e-view-message-with-msgid)
+  - improve mouse interaction (i.e., cursor vs point)
+  - show counts of messages in searches (in main view)
+  - show flush only if there's something to flush (and # of flushables)
+  - fix unsafe temp-file handling
+  - make copy paste name/address in mu4e-view possible
+
+
+* Done (0.9.9.x)
+
+  - mu4e: scroll down –> go to next message
+  - mu: add contact: as a shortcut for matching from/to/cc/bcc:
+  - guile integration
+    - statistics
+  - 'human' dates in the headers view
+  - :tags in headers, message view
+
+* Done
+  :PROPERTIES:
+  :VISIBILITY: folded
+  :END:
+
+
+** Done (0.9.9)
+
+   - make contacts in the view clickable (toggle long/short display, compose message)
+   - opening urls is too eager (now use M-RET for opening url at point, not just
+     RET, which conflicted with using RET for scrolling)
+   - document quoting of queries
+   - use mu-error
+   - tooltips in header labels
+   - tooltip for flags field
+   - remove --summary option (for mu find, mu view); use --summary-len instead
+   - add sort buttons to header labels (and do the sorting)
+   - cleanup mu-cmd-find
+   - implement --after for mu find, to only show message files changed after a
+     certain time (mtime)
+   - add mu:timestamp for guile (referring to the message file's mtime)
+   - guile automated tests
+   - add 'mu verify'
+     - automated tests
+     - handle verbose/quiet/normal output 'mu verify'
+     - check gmime 2.4 does not break
+   - hook up mu4e with 'mu verify'
+   - add 'help' command
+   - refactor mu-msg-part
+   - move widgets/ into toys/mug2, remove toys/mug/, rename toys/mug2 -> toys/mug
+   - add guile mu:count
+   - don't show GPG/PKCS7 sigs as attachments
+   - fix address completion (quote names)
+   - add support for X-Keywords (in addition to X-Label)
+   - guile: add stats test cases
+   - fixed iso-2022-jp (japanese) decoding
+   - make address completion case-insensitive
+   - recognize '*' in urls
+   - handle exception 'The revision being read has been discarded - you should
+     call Xapian::Database::reopen() and retry the operation'
+   - handle passwords from get-mail shell command
+   - support fancy (non-ascii) chars for header flags, thread prefix strings
+   - improve performance of getting the list of maildirs
+   - fix setting wrapped/hide state in viewer
+   - fix ' realpath() failed for...' stuff
+   - allow for fancy chars (> ascii), make it configurable (mu4e-use-fancy-chars)
+   - don't user `error' for user-errors
+   - better echo-area reporting
+   - improve help feedback for user (command line)
+   - handling of encrypted messages
+   - improved checked for gmime-2.6 crypto funcs
+   - handling of command line options / help
+   - fix / add support for :size
+   - mu4e~view-wrap-lines (use visual-line-mode? see Jacek's mu4e~view-wrap-lines
+     mail)
+   - better help
+   - threading optimizations
+   - actions for /all/ headers, actions for /all/ attachment
+   - handle attached messages with attachments
+
+** Done (0.8.9.5)
+
+  - make next/prev header respect prefix argument (Jacek's patch)
+  - make search results a stack (well, multiple stacks)
+  - optionally keep cc with user's email
+  - enable setting/unsetting 'Flagged' on messages
+  - allow narrowing of search results
+  - interactive split-view control (Jacek)
+  - view images inline
+  - *FIX* slow maildirs when there are many
+  - *FIX* ignore unrecognized maildir flag letters
+  - *FIX*: reply-to does not make it to the frontend
+  - *FIX* wrong buffer deleted after sending (see '(non mu) buffer is killed')
+  - rich text composing (with org-mode)
+  - let message-mode deal with burying/killing compose buffers
+  - *FIX* add runtime check for imagemagick
+  - *FIX* no error note if target message already exists (when moving)
+  - sorting + show / hide threads
+  - *FIX* having multiple header views visible
+  - *FIX* fix for strings where len (g_utf8_strdown (str)) > len (str)
+  - make sure marks correspond to the *current* message in message view (see
+    https://github.com/djcb/mu/issues/26)
+  - *FIX* don't remove unknown message flags when moving
+  - make guile/gtk/webkit dependency optional
+  - improve fringe marks (see https://github.com/djcb/mu/issues/21)
+  - mark message, decide what to do with them later (i.e.. 'deferred marking')
+  - custom predicate functions for marking
+  - make mu4e buffer killing less aggressive (i.e.., DWIM)
+  - about mu4e
+  - hide some headers when composing
+  - fix sorting subjects with ':' (but not 'Re:' or 'Fwd:')
+  - strip signature from original when replying
+  - make refresh after changing sort, threads the default
+  - contact completion (see Jacek's 'mu4e: using' mail)
+  - *FIX* emacs23 mailto: handling
+  - *FIX* message interference
+  - *FIX* emacs23.2+ auto-completion
+
+
+# Local Variables:
+# mode: org
+# End:
diff --git a/autogen.sh b/autogen.sh
new file mode 100755 (executable)
index 0000000..eac4d6b
--- /dev/null
@@ -0,0 +1,30 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+echo "*** meson build setup"
+
+test -f mu/mu.cc || {
+    echo "*** Run this script from the top-level mu source directory"
+    exit 1
+}
+
+BUILDDIR=build
+
+command -v meson 2> /dev/null
+if [ $? != 0 ]; then
+    echo "*** No meson found, please install it ***"
+    exit 1
+fi
+
+# we could remove build/ but let's avoid rm -rf risks...
+if test -d ${BUILDDIR}; then
+    meson --reconfigure ${BUILDDIR} $@
+else
+    meson ${BUILDDIR} $@
+fi
+
+# Add a Makefile with some useful target
+cp Makefile.meson Makefile
+
+echo "*** Now run 'ninja -C ${BUILDDIR}' to build mu"
+echo "*** Or check the Makefile for some useful targets"
diff --git a/build-aux/config.rpath b/build-aux/config.rpath
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/build-aux/meson-install-info.sh b/build-aux/meson-install-info.sh
new file mode 100644 (file)
index 0000000..9efe63d
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+infodir=$1
+infofile=$2
+
+# Meson post-install script to update info metadata
+# If DESTDIR is set, do _not_ install-info, since it's only a temporary
+# install
+if test -z "${DESTDIR}"; then
+    install-info --info-dir ${MESON_INSTALL_DESTDIR_PREFIX}/${infodir} \
+                ${MESON_INSTALL_DESTDIR_PREFIX}/${infodir}/${infofile}
+fi
+
+gzip --force ${MESON_INSTALL_DESTDIR_PREFIX}/${infodir}/${infofile}
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..5e44d5d
--- /dev/null
@@ -0,0 +1,320 @@
+## Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+AC_PREREQ([2.68])
+AC_INIT([mu],[1.8.13],[https://github.com/djcb/mu/issues],[mu])
+AC_COPYRIGHT([Copyright (C) 2008-2022 Dirk-Jan C. Binnema])
+AC_CONFIG_HEADERS([config.h])
+AC_CONFIG_SRCDIR([mu/mu.cc])
+# libtoolize wants to put some stuff in here; if you have an old
+# autotools/libtool setup. you can try to comment this out
+AC_CONFIG_MACRO_DIR([m4])
+AC_CONFIG_AUX_DIR([build-aux])
+
+AM_INIT_AUTOMAKE([1.14 foreign no-dist-gzip tar-ustar dist-xz])
+
+# silent build if we have a new enough automake
+m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
+
+AS_IF([test x$prefix = xNONE],[prefix=/usr/local])
+AC_SUBST(prefix)
+
+# AC_PROG_CXX *before* AC_PROG_CC, otherwise configure won't error out
+# when a c++ compiler is not found. Weird, huh?
+AC_PROG_CXX
+AC_PROG_CC
+AC_PROG_CPP
+AC_PROG_INSTALL
+AC_CHECK_INCLUDES_DEFAULT
+AC_PROG_EGREP
+
+
+extra_flags="-Wformat-security         \
+        -Wstack-protector              \
+        -Wstack-protector-all          \
+        -Wno-cast-function-type        \
+        -Wno-bad-function-cast         \
+        -Wno-switch-enum"
+
+AX_CXX_COMPILE_STDCXX_17
+AX_COMPILER_FLAGS_CXXFLAGS([],[],[${extra_cflags}])
+AX_APPEND_COMPILE_FLAGS([-Wno-inline],[CXXFLAGS])
+AX_VALGRIND_CHECK
+
+LT_INIT
+
+AX_CODE_COVERAGE
+
+AC_PROG_AWK
+AC_CHECK_PROG(SORT,sort,sort)
+
+AC_CHECK_HEADERS([wordexp.h])
+
+# use the 64-bit versions
+AC_SYS_LARGEFILE
+
+# asan is somewhat similar to valgrind, but has low enough overhead so it
+# can be used during normal operation.
+AC_ARG_ENABLE([asan],[AS_HELP_STRING([--enable-asan],
+         [Enable Address Sanitizer])], [use_asan=$enableval], [use_asan=no])
+AS_IF([test "x$use_asan" = "xyes"],[
+  AC_SUBST(ASAN_CFLAGS,  "-fsanitize=address -static-libasan -fno-omit-frame-pointer")
+  AC_SUBST(ASAN_CXXFLAGS,"-fsanitize=address -static-libasan -fno-omit-frame-pointer")
+  AC_SUBST(ASAN_LDFLAGS, "-fsanitize=address -static-libasan -fno-omit-frame-pointer")
+])
+
+
+# check for makeinfo
+AC_CHECK_PROG(have_makeinfo,makeinfo,yes,no)
+AM_CONDITIONAL(HAVE_MAKEINFO,test "x$have_makeinfo" = "xyes")
+AM_COND_IF(HAVE_MAKEINFO,[],[
+  # seems build *insists* on trying to makeinfo, erroring out
+  # if it does not exist. Let's work around that.
+  AC_SUBST(MAKEINFO,[true])
+])
+
+# we need emacs for byte-compiling mu4e
+build_mu4e=no
+AC_ARG_ENABLE([mu4e],
+   AS_HELP_STRING([--disable-mu4e],[Disable building mu4e]))
+AS_IF([test "x$enable_mu4e" != "xno"], [
+  AM_PATH_LISPDIR
+  AS_IF([test "x$lispdir" != "xno"], [
+    emacs_version="$($EMACS --version | head -1)"
+    lispdir="${lispdir}/mu4e/"
+  ])
+  AS_CASE([$emacs_version],
+    [*25.3*],[build_mu4e=yes],
+    [*26*|*27*|*28*|*29*],[build_mu4e=yes],
+    [AC_MSG_WARN(emacs is too old to build mu4e (need emacs >= 25.3))])
+])
+AM_CONDITIONAL(BUILD_MU4E, test "x$build_mu4e" = "xyes")
+
+# we need some special tricks for filesystems that don't have d_type;
+# e.g. Solaris. See mu-maildir.c. Explicitly disabling it is for
+# testing purposes only
+AC_ARG_ENABLE([dirent-d-type],
+    AS_HELP_STRING([--disable-dirent-d-type],[Don't use dirent->d_type, even if you have it]),
+    [], [AC_STRUCT_DIRENT_D_TYPE]
+)
+AS_IF([test "x$ac_cv_member_struct_dirent_d_type" != "xyes"],
+    [use_dirent_d_type="no"], [use_dirent_d_type="yes"])
+
+# support for d_ino (inode) in struct dirent is optional; if it's
+# available we can sort direntries by inode and access them in that
+# order; this is much faster on some file systems (such as extfs3).
+# Explicitly disabling it is for testing purposes only.
+AC_ARG_ENABLE([dirent-d-ino],
+    AS_HELP_STRING([--disable-dirent-d-ino],[Don't use dirent->d_ino, even if you have it]),
+    [], [AC_STRUCT_DIRENT_D_INO]
+)
+AS_IF([test "x$ac_cv_member_struct_dirent_d_ino" != "xyes"],
+    [use_dirent_d_ino="no"], [use_dirent_d_ino="yes"])
+
+AC_CHECK_FUNCS([memset memcpy realpath setlocale strerror getpass setsid])
+AC_CHECK_FUNCS([vasprintf strptime])
+# timegm is no longer used in the source
+# AC_CHECK_FUNC(timegm,[],AC_MSG_ERROR([missing required function timegm]))
+
+# require pkg-config >= 0.28 (release in 2013; should be old enough...)
+# with that version, we don't need the AC_SUBST stuff after PKG_CHECK.
+m4_ifndef([PKG_PROG_PKG_CONFIG],
+   [m4_fatal([please install pkg-config >= 0.28 before running autoconf/autogen])])
+PKG_PROG_PKG_CONFIG(0.28) # latest version in buildroot
+AS_IF([test -z "$PKG_CONFIG"],
+   AC_MSG_ERROR([
+   *** pkg-config with version >= 0.28 could not be found.
+   ***
+   *** Make sure it is in your path, or set the PKG_CONFIG environment variable
+   *** to the full path to pkg-config.])
+)
+
+# glib2?
+PKG_CHECK_MODULES(GLIB,glib-2.0 >= 2.58 gobject-2.0 gio-2.0)
+glib_version="$($PKG_CONFIG --modversion glib-2.0)"
+
+# gmime, version 3.0 or higher
+PKG_CHECK_MODULES(GMIME,gmime-3.0)
+gmime_version="$($PKG_CONFIG --modversion gmime-3.0)"
+
+# xapian checking - we need 1.4 at least
+PKG_CHECK_MODULES(XAPIAN,xapian-core >= 1.4,[
+  have_xapian=yes
+  xapian_version=$($PKG_CONFIG xapian-core --modversion)
+  AC_SUBST(XAPIAN_CXXFLAGS,${XAPIAN_CFLAGS})
+],[
+  # fall back to the xapian-config script. Not sure if there are cases where the
+  # pkgconfig does not work, but xapian-config does, so keep this for now.
+  AC_MSG_NOTICE([falling back to xapian-config])
+  AC_CHECK_PROG(XAPIAN_CONFIG,xapian-config,xapian-config,no)
+  AS_IF([test "x$XAPIAN_CONFIG" = "xno"],[
+     AC_MSG_ERROR([
+     *** xapian could not be found; please install it
+     *** e.g., in debian/ubuntu the package would be 'libxapian-dev'
+     *** If you compiled it yourself, you should ensure that xapian-config
+     *** is in your PATH.])],
+     [xapian_version=$($XAPIAN_CONFIG --version | sed -e 's/.* //')])
+
+  AS_CASE([$xapian_version],
+      [1.[[4-9]].[[0-9]]*],
+    [AC_MSG_NOTICE([xapian $xapian_version found.])],
+    [AC_MSG_ERROR([*** xapian version >= 1.4 needed, but version $xapian_version found.])])
+
+  XAPIAN_CXXFLAGS="$($XAPIAN_CONFIG --cxxflags)"
+  XAPIAN_LIBS="$($XAPIAN_CONFIG --libs)"
+  have_xapian="yes"
+
+  AC_SUBST(XAPIAN_CXXFLAGS)
+  AC_SUBST(XAPIAN_LIBS)
+])
+###############################################################################
+# we set the set the version of the MuStore (Xapian database) layout
+# here; it will become part of the db name, so we can automatically
+# recreate the database when we have incompatible changes.
+#
+# note that MU_STORE_SCHEMA_VERSION does not follow mu versioning, as we
+# hopefully don't have updates for each version; also, this has nothing to do
+# with Xapian's software versionx
+AC_DEFINE(MU_STORE_SCHEMA_VERSION,["465"],['Schema' version of the database])
+###############################################################################
+
+################################################################################
+# should we try to build an emacs dynamic module?
+#AC_CHECK_HEADER([emacs-module.h],[
+#  AC_DEFINE([HAVE_EMACS_MODULE_H],[1], [Whether we have the emacs-module header])],
+#  AC_MSG_NOTICE([emacs-module.h not found; not building module])
+#)
+#AM_CONDITIONAL([BUILD_EMACS_MODULE],[test "x$ac_cv_header_emacs_module_h" != "x"])
+################################################################################
+
+###############################################################################
+# build with guile 3.0/2.2 when available and not disabled.
+AC_ARG_ENABLE([guile], AS_HELP_STRING([--disable-guile],[Disable guile]))
+AS_IF([test "x$enable_guile" != "xno"],[
+
+  PKG_CHECK_MODULES(GUILE, [guile-3.0], [have_guile=yes],[
+    PKG_CHECK_MODULES(GUILE, [guile-2.2], [have_guile=yes], [have_guile=no])])
+    AS_IF([test "x$have_guile" = "xyes"],[
+      GUILE_PKG([3.0 2.2])
+      GUILE_PROGS
+      GUILE_FLAGS
+      AC_DEFINE_UNQUOTED([GUILE_BINARY],"$GUILE",[guile binary])
+      vsnarf=guile-snarf${GUILE_EFFECTIVE_VERSION}
+      AC_CHECK_PROGS(GUILE_SNARF,[${vsnarf} guile-snarf], [no])
+      guile_version=$($PKG_CONFIG guile-$GUILE_EFFECTIVE_VERSION --modversion)
+   ])
+])
+
+AM_CONDITIONAL(BUILD_GUILE,[test "x$have_guile" = "xyes" -a \
+                "x$ac_cv_prog_GUILE_SNARF" != "xno"])
+AM_COND_IF([BUILD_GUILE],[AC_DEFINE(BUILD_GUILE,[1], [Do we support Guile?])])
+###############################################################################
+
+###############################################################################
+# optional readline
+AC_ARG_ENABLE([readline], AS_HELP_STRING([--disable-readline],[Disable readline]))
+AS_IF([test "x$enable_readline" != "xno"], [
+  saved_libs=$LIBS
+  AX_LIB_READLINE
+  AC_SUBST(READLINE_LIBS,${LIBS})
+  LIBS=$saved_libs
+])
+###############################################################################
+
+###############################################################################
+# check for makeinfo
+AC_CHECK_PROG(have_makeinfo,makeinfo,yes,no)
+AM_CONDITIONAL(HAVE_MAKEINFO, [test "x$have_makeinfo" = "xyes"])
+###############################################################################
+
+###############################################################################
+# docdir, so we can use it in mu4e-meta.el.in
+AC_SUBST(MU_DOC_DIR, "${prefix}/share/doc/mu")
+###############################################################################
+
+AC_CONFIG_FILES([
+Makefile
+mu/Makefile
+lib/Makefile
+lib/doxyfile
+lib/thirdparty/Makefile
+lib/utils/Makefile
+lib/message/Makefile
+lib/index/Makefile
+mu4e/Makefile
+mu4e/mu4e-config.el
+guile/Makefile
+guile/mu/Makefile
+guile/examples/Makefile
+guile/scripts/Makefile
+man/Makefile
+m4/Makefile
+contrib/Makefile
+])
+AC_CONFIG_FILES([mu/mu-memcheck], [chmod +x mu/mu-memcheck])
+
+AC_OUTPUT
+
+dnl toys/msg2pdf/Makefile
+
+echo
+echo "mu configuration is complete."
+echo "------------------------------------------------"
+
+echo "mu version                           : $VERSION"
+echo
+echo "Xapian version                       : $xapian_version"
+echo "GLib version                         : $glib_version"
+echo "GMime version                        : $gmime_version"
+
+AM_COND_IF([BUILD_GUILE],[
+echo "Guile version                        : $guile_version"
+])
+echo
+echo "Have direntry->d_ino                 : $use_dirent_d_ino"
+echo "Have direntry->d_type                : $use_dirent_d_type"
+echo "------------------------------------------------"
+echo
+
+#
+# Warnings / notes
+#
+
+# makeinfo
+if test "x$have_makeinfo" != "xyes"; then
+    echo "* You do not seem to have the makeinfo program; if you are building from git"
+    echo "  you need that to create documentation for guile and emacs. It is in the"
+    echo "  texinfo package in debian/ubuntu/fedora/... "
+    echo
+fi
+
+# wordexp
+AS_IF([test "x$ac_cv_header_wordexp_h" != "xyes"],[
+   echo "* Your system does not seem to have the 'wordexp' function."
+   echo "  This means that you cannot use shell-like expansion in options and "
+   echo "  some other places. So, for example, instead of"
+   echo "    --maildir=~/Maildir"
+   echo "  you should use the complete path, something like:"
+   echo "    --maildir=/home/user/Maildir"
+])
+
+
+echo "NOTE: autotools support has been deprecated and will be removed"
+echo "      after the next stable release. use the meson build instead"
+
+echo
+echo "Now, type 'make' (or 'gmake') to build mu"
+echo
diff --git a/contrib/Makefile.am b/contrib/Makefile.am
new file mode 100644 (file)
index 0000000..00687a6
--- /dev/null
@@ -0,0 +1,26 @@
+## Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+AM_CPPFLAGS=$(GMIME_CFLAGS) $(GLIB_CFLAGS) -I${prefix}/include
+AM_CFLAGS=-Wall -Wextra -Wno-unused-parameter -Wdeclaration-after-statement -pedantic
+
+
+EXTRA_DIST=                    \
+       mu-completion.zsh       \
+       mu-sexp-convert         \
+       mu.spec
diff --git a/contrib/mu-completion.zsh b/contrib/mu-completion.zsh
new file mode 100644 (file)
index 0000000..ea2bdbd
--- /dev/null
@@ -0,0 +1,124 @@
+#compdef mu
+
+## Copyright (C) 2011-2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+# zsh completion for mu. Install this by copying/linking to this file somewhere in
+# your $fpath; the link/copy must have a name starting with an underscore "_"
+
+# main dispatcher function
+_mu() {
+  if (( CURRENT > 2 )) ; then
+      local cmd=${words[2]}
+      curcontext="${curcontext%:*:*}:mu-$cmd"
+      (( CURRENT-- ))
+      shift words
+      _call_function ret _mu_$cmd
+      return ret
+  else
+      _mu_commands
+  fi
+}
+
+
+
+_mu_commands() {
+  local -a mu_commands
+  mu_commands=(
+    'index:scan your maildirs and import their metadata in the database'
+    'find:search for messages in the database'
+    'view:display specific messages'
+    'cfind:search for contacts (name + email) in the database'
+    'extract:extract message-parts (attachments) and save or open them'
+    'mkdir:create maildirs'
+# below are not generally very useful, so let's not auto-complete them
+#    'add: add a message to the database.'
+#    'remove:remove a message from the database.'
+#    'server:sart the mu server'
+)
+
+  _describe -t command 'command' mu_commands
+}
+
+_mu_common_options=(
+    '--debug[output information useful for debugging mu]'
+    '--quiet[do not give any non-critical information]'
+    '--nocolor[do not use colors in some of the output]'
+    '--version[display mu version and copyright information]'
+    '--log-stderr[log to standard error]'
+)
+
+_mu_db_options=(
+    '--muhome[use some non-default location for the mu database]:directory:_files'
+)
+
+_mu_find_options=(
+    '--fields[fields to display in the output]'
+    '--sortfield[field to sort the output by]'
+    '--descending[sort in descending order]'
+    '--summary[include a summary of the message]'
+    '--summary-len[number of lines to use for the summary]'
+    '--bookmark[use a named bookmark]'
+    '--output[set the kind of output for the query]'
+)
+
+_mu_view_options=(
+    '--summary[only show a summary of the message]'
+    '--summary-len[number of lines to use for the summary]'
+)
+
+
+_mu_view() {
+    _arguments -s : \
+        $_mu_common_options \
+       $_mu_view_options
+}
+
+_mu_extract() {
+ _files
+}
+
+_mu_find() {
+    _arguments -s : \
+        $_mu_common_options \
+       $_mu_db_options \
+       $_mu_find_options
+}
+
+_mu_index() {
+    _arguments -s : \
+       $_mu_db_options \
+        $_mu_common_options
+}mu
+
+_mu_cleanup() {
+    _arguments -s : \
+       $_mu_db_options \
+       $_mu_common_options
+}
+
+
+_mu_mkdir() {
+   _arguments -s : \
+    '--mode=[file mode for the new Maildir]:file mode: ' \
+    $_mu_common_options
+}
+
+_mu "$@"
+
+# Local variables:
+# mode: sh
+# End:
diff --git a/contrib/mu-sexp-convert b/contrib/mu-sexp-convert
new file mode 100755 (executable)
index 0000000..b2835ac
--- /dev/null
@@ -0,0 +1,204 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;; Copyright (C) 2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;;
+;; a little hack to convert the output of
+;;    mu find <expr> --format=sexp
+;; and
+;;    mu view <expr>  --format=sexp
+;; into XML or JSON
+
+(use-modules (ice-9 getopt-long) (ice-9 format) (ice-9 regex))
+(use-modules (sxml simple))
+
+(define (mapconcat func lst sepa)
+  "Apply FUNC to elements of LST, concat the result as strings
+separated by SEPA."
+  (if (null? lst)
+    ""
+    (string-append
+      (func (car lst))
+      (if (null? (cdr lst))
+       ""
+       (string-append sepa (mapconcat func (cdr lst) sepa))))))
+
+(define (property-list? obj)
+  "Is OBJ a elisp-style property list (ie. a list of the
+form (:symbol1 something :symbol2 somethingelse), as in an elisp
+proplilst."
+  (and (list? obj)
+    (not (null? obj))
+    (symbol? (car obj))
+    (string= ":" (substring (symbol->string (car obj)) 0 1))))
+
+(define (plist->pairs plist)
+  "Convert an elisp-style property list; e.g:
+   (:prop1 foo :prop2: bar ...)
+into a list of pairs
+   ((prop1 . foo) (prop2 . bar) ...)."
+  (if (null? plist)
+    '()
+    (cons
+      (cons
+       (substring (symbol->string (car plist)) 1)
+       (cadr plist))
+      (plist->pairs (cddr plist)))))
+
+(define (string->xml str)
+  "XML-encode STR."
+  ;; sneakily re-using sxml->xml
+  (call-with-output-string (lambda (port) (sxml->xml str port))))
+
+(define (string->json str)
+  "Convert string into a JSON-encoded string."
+  (letrec ((convert
+            (lambda (lst)
+              (if (null? lst)
+                ""
+                (string-append
+                  (cond
+                    ((equal? (car lst) #\")  "\\\"")
+                    ((equal? (car lst) #\\)  "\\\\")
+                    ((equal? (car lst) #\/)  "\\/")
+                    ((equal? (car lst) #\bs) "\\b")
+                    ((equal? (car lst) #\ff) "\\f")
+                    ((equal? (car lst) #\lf) "\\n")
+                    ((equal? (car lst) #\cr) "\\r")
+                    ((equal? (car lst) #\ht) "\\t")
+                    (#t (string (car lst))))
+                  (convert (cdr lst)))))))
+    (convert (string->list str))))
+
+(define (etime->time_t t)
+  "Convert elisp time object T into a time_t value."
+  (logior (ash (car t) 16) (car (cdr t))))
+
+(define (sexp->xml)
+  "Convert string INPUT to XML, return the XML (string)."
+  (letrec ((convert-xml
+            (lambda* (expr #:optional parent)
+           (cond
+             ((property-list? expr)
+               (mapconcat
+                 (lambda (pair)
+                   (format #f "\t<~a>~a</~a>\n"
+                     (car pair) (convert-xml (cdr pair) (car pair)) (car pair)))
+                 (plist->pairs expr) " "))
+             ((list? expr)
+               (cond
+                 ((member parent '("from" "to" "cc" "bcc"))
+                   (mapconcat (lambda (addr)
+                                (format #f "<address>~a~a</address>"
+                                  (if (string? (car addr))
+                                    (format #f "<name>~a</name>"
+                                      (string->xml (car addr))) "")
+                                  (if (string? (cdr addr))
+                                    (format #f "<email>~a</email>"
+                                      (string->xml (cdr addr))) "")))
+                     expr " "))
+                 ((string= parent "parts") "<!-- message parts -->") ;; for now, ignore
+                 ;; convert the crazy emacs time thingy to time_t...
+                 ((string= parent "date") (format #f "~a" (etime->time_t expr)))
+                 (#t
+                   (mapconcat
+                     (lambda (elm) (format #f "<item>~a</item>" (convert-xml elm))) expr ""))))
+             ((string? expr) (string->xml expr))
+             ((symbol? expr) (format #f "~a" expr))
+             ((number? expr) (number->string expr))
+             (#t "."))))
+           (msg->xml
+             (lambda ()
+               (let ((expr (read)))
+                 (if (not (eof-object? expr))
+                   (string-append (format #f "<message>\n~a</message>\n" (convert-xml expr)) (msg->xml))
+                   "")))))
+    (format #f "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<messages>\n~a</messages>" (msg->xml))))
+
+
+(define (sexp->json)
+  "Convert string INPUT to JSON, return the JSON (string)."
+  (letrec ((convert-json
+            (lambda* (expr #:optional parent)
+           (cond
+             ((property-list? expr)
+               (mapconcat
+                 (lambda (pair)
+                   (format #f "\n\t\"~a\": ~a"
+                     (car pair) (convert-json (cdr pair) (car pair)))) 
+                 (plist->pairs expr) ", "))
+             ((list? expr)
+               (cond
+                 ((member parent '("from" "to" "cc" "bcc"))
+                   (string-append "["
+                     (mapconcat (lambda (addr)
+                                  (format #f "{~a~a}"
+                                    (if (string? (car addr))
+                                      (format #f "\"name\": \"~a\","
+                                        (string->json (car addr))) "")
+                                    (if (string? (cdr addr))
+                                      (format #f "\"email\": \"~a\""
+                                        (string->json (cdr addr))) "")))
+                       expr ", ")
+                     "]"))
+                 ((string= parent "parts") "[]") ;; todo 
+                 ;; convert the crazy emacs time thingy to time_t...
+                 ((string= parent "date")
+                   (format #f "~a" (format #f "~a" (etime->time_t expr))))
+                 (#t
+                   (string-append "["
+                     (mapconcat (lambda (elm) (format #f "~a" (convert-json elm))) expr ",") "]")))) 
+             ((string? expr)
+               (format #f "\"~a\"" (string->json expr)))
+             ((symbol? expr)
+               (format #f "\"~a\"" expr))             
+             ((number? expr) (number->string expr))
+             (#t "."))))
+           (msg->json
+             (lambda (first)
+               (let ((expr (read)))
+                 (if (not (eof-object? expr))
+                   (string-append (format #f "~a{~a\n}"
+                                    (if first "" ",\n")
+                                    (convert-json expr)) (msg->json #f))
+                   "")))))
+    (format #f "[\n~a\n]" (msg->json #t))))
+(define (main args)
+  (let* ((optionspec '((format  (value #t))))
+         (options (getopt-long args optionspec))
+         (msg (string-append
+                "usage: mu-sexp-convert "
+                "--format=<xml|json>\n"
+                "reads from standard-input and prints to standard output\n"))
+         (outformat (or (option-ref options 'format #f)
+                      (begin (display msg) (exit 1)))))
+    (cond
+      ((string= outformat "xml")
+       (format #t "~a\n" (sexp->xml)))
+      ((string= outformat "json")
+       (format #t "~a\n" (sexp->json)))
+      (#t (begin
+           (display msg)
+           (exit 1))))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/contrib/mu.spec b/contrib/mu.spec
new file mode 100644 (file)
index 0000000..1ed66d4
--- /dev/null
@@ -0,0 +1,129 @@
+
+# These refer to the release version
+# When 0.9.9.6 gets out, remove the global pre line
+%global pre    pre2
+%global rel    1
+
+Summary:       A lightweight email search engine for Maildirs
+Name:          mu
+Version:       0.9.9.6
+URL:           https://github.com/djcb/mu
+# From Packaging:NamingGuidelines for pre-relase versions:
+# Release: 0.%{X}.%{alphatag} where %{X} is the release number
+%if %{pre}
+Release:       0.%{rel}.%{prerelease}%{?dist}
+%else
+Release:       %{rel}%{?dist}
+%endif
+
+License:       GPLv3
+Group:         Applications/Internet
+BuildRoot:     %{_tmppath}/%{name}-%{version}-build
+
+# Source is at ssaavedra repo because djcb has not yet this version tag created
+Source0:       http://github.com/ssaavedra/%{name}/archive/v%{version}%{?pre}.tar.gz
+BuildRequires: emacs-el
+BuildRequires: emacs
+BuildRequires: gmime-devel
+BuildRequires: guile-devel
+BuildRequires: xapian-core-devel
+BuildRequires: libuuid-devel
+BuildRequires: texinfo
+Requires:      gmime
+Requires:      guile
+Requires:      xapian-core-libs
+Requires:      emacs-filesystem >= %{_emacs_version}
+
+
+%description
+E-mail is the 'flow' in the work flow of many people. Consequently, one spends a lot of time searching for old e-mails, to dig up some important piece of information. With people having tens of thousands of e-mails (or more), this is becoming harder and harder. How to find that one e-mail in an ever-growing haystack?
+Enter mu.
+'mu' is a set of command-line tools for Linux/Unix that enable you to quickly find the e-mails you are looking for, assuming that you store your e-mails in Maildirs (if you don't know what 'Maildirs' are, you are probably not using them). 
+
+%package gtk
+Group:         Applications/Internet
+Summary:       GUI for using mu (called mug)
+BuildRequires: gtk3-devel
+BuildRequires: webkitgtk3-devel
+Requires:      gtk3
+Requires:      gmime
+Requires:      webkitgtk3
+Requires:      mu = %{version}-%{release}
+
+%description gtk
+Mug is a simple GUI for mu from version 0.9.
+
+%package guile
+Group:         Applications/Internet
+Summary:       Guile scripting capabilities for mu
+Requires:      guile
+Requires:      mu = %{version}-%{release}
+Requires(post):        info
+Requires(preun):       info
+
+%description guile
+Bindings for Guile to interact with mu.
+
+
+%prep
+%setup -n %{name}-%{version}%{?pre} -q
+
+%build
+autoreconf -i
+%configure
+make %{?_smp_mflags}
+
+%install
+rm -rf %{buildroot}
+make install DESTDIR=%{buildroot}
+install -p -c -m 755 %{_builddir}/%{buildsubdir}/toys/mug/mug %{buildroot}%{_bindir}/mug
+cp -p %{_builddir}/%{buildsubdir}/mu4e/*.el %{buildroot}%{_emacs_sitelispdir}/mu4e/
+rm -f %{buildroot}%{_infodir}/dir
+
+%clean
+rm -rf %{buildroot}
+
+%post
+/sbin/install-info \
+       --info-dir=%{_infodir} %{_infodir}/mu4e.info.gz || :
+%preun
+if [ $1 = 0 -a -f %{_infodir}/mu4e.info.gz ]; then
+       /sbin/install-info --delete \
+       --info-dir=%{_infodir} %{_infodir}/mu4e.info.gz || :
+fi
+
+%post guile
+/sbin/install-info \
+       --info-dir=%{_infodir} %{_infodir}/mu-guile.info.gz || :
+
+%preun guile
+if [ $1 = 0 -a -f %{_infodir}/mu-guile.info.gz ]; then
+       /sbin/install-info --delete \
+       --info-dir=%{_infodir} %{_infodir}/mu-guile.info.gz || :
+fi
+
+
+%files
+%defattr(-,root,root)
+%{_bindir}/mu
+%{_mandir}/man1/*
+%{_mandir}/man5/*
+%{_datadir}/mu/*
+
+%{_emacs_sitelispdir}/mu4e
+%{_emacs_sitelispdir}/mu4e/*.elc
+%{_emacs_sitelispdir}/mu4e/*.el
+%{_infodir}/mu4e.info.gz
+
+%files gtk
+%{_bindir}/mug
+
+%files guile
+%{_libdir}/libguile-mu.*
+%{_datadir}/guile/site/2.0/mu/*
+%{_datadir}/guile/site/2.0/mu.scm
+%{_infodir}/mu-guile.info.gz
+
+%changelog
+* Wed Feb 12 2014 Santiago Saavedra <ssaavedra@gpul.org> - 0.9.9.5-1
+- Create first SPEC.
diff --git a/gtest.mk b/gtest.mk
new file mode 100644 (file)
index 0000000..159576e
--- /dev/null
+++ b/gtest.mk
@@ -0,0 +1,34 @@
+## Copyright (C) 2011 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify it
+## under the terms of the GNU General Public License as published by the
+## Free Software Foundation; either version 3, or (at your option) any
+## later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+TEST_PROGS=
+
+
+# NOTE: we set the locale/tz to some well-know values, so the tests
+# (at least when running under 'make check') run in a predictable
+# environment. There are specific tests different timezone, though.
+#
+test: all $(TEST_PROGS)
+        @export LC_ALL="en_US.utf8"
+        @export TZ="Europe/Helsinki"
+        @test -z "$(TEST_PROGS)" || gtester --verbose $(TEST_PROGS) || exit $$?;       \
+        test -z "$(SUBDIRS)" ||                                                        \
+               for subdir in $(SUBDIRS); do                                            \
+                       test "$$subdir" = "." ||                                        \
+               (cd ./$$subdir && $(MAKE) $(AM_MAKEFLAGS) $@ ) || exit $$? ;            \
+               done
+
+.PHONY: test gprof
diff --git a/guile/Makefile.am b/guile/Makefile.am
new file mode 100644 (file)
index 0000000..dd91108
--- /dev/null
@@ -0,0 +1,97 @@
+## Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+# note, we need top_builddir for snarfing with 'make distcheck' (ie.,
+# with separate builddir)
+SUBDIRS= . mu scripts examples
+
+AM_CPPFLAGS=                                           \
+       -I. -I${top_builddir} -I${top_srcdir}/lib       \
+       ${GUILE_CFLAGS}                                 \
+       ${GLIB_CFLAGS}
+
+# don't use -Werror, as it might break on other compilers
+# use -Wno-unused-parameters, because some callbacks may not
+# really need all the params they get
+AM_CFLAGS=                                             \
+       $(ASAN_CFLAGS)                                  \
+       ${WARN_CFLAGS}                                  \
+       -Wno-suggest-attribute=noreturn                 \
+       -Wno-missing-prototypes                         \
+       -Wno-missing-declarations
+
+AM_CXXFLAGS=                                           \
+       $(ASAN_CXXFLAGS)                                \
+       ${WARN_CXXFLAGS}                                \
+       -Wno-redundant-decls                            \
+       -Wno-missing-declarations                       \
+       -Wno-suggest-attribute=noreturn
+
+lib_LTLIBRARIES=                                       \
+       libguile-mu.la
+
+libguile_mu_la_SOURCES=                                        \
+       mu-guile.cc                                     \
+       mu-guile.hh                                     \
+       mu-guile-message.cc                             \
+       mu-guile-message.hh
+
+libguile_mu_la_CFLAGS=$(AM_CFLAGS)
+libguile_mu_la_CXXFLAGS=$(AM_CXXFLAGS)
+
+libguile_mu_la_LIBADD=                                 \
+       ${top_builddir}/lib/libmu.la                    \
+       ${top_builddir}/lib/utils/libmu-utils.la        \
+       $(READLINE_LIBS)                                \
+       ${GUILE_LIBS}
+
+libguile_mu_la_LDFLAGS=                                        \
+       $(ASAN_LDFLAGS)                                 \
+       -shared                                         \
+       -export-dynamic
+
+XFILES=                                                        \
+       mu-guile.x                                      \
+       mu-guile-message.x
+
+info_TEXINFOS=                                         \
+       mu-guile.texi
+mu_guile_TEXINFOS=                                     \
+       fdl.texi
+
+# we include pre-snarfed files now; see meson.build for explanation
+#
+# BUILT_SOURCES=$(XFILES)
+# export CPP
+# snarfcxxopts= $(DEFS) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS)
+# SUFFIXES = .x .doc
+# .cc.x:
+#      $(AM_V_GEN) $(GUILE_SNARF) -o $@ $< $(snarfcxxopts)
+#SNARF_DATA=$(XFILES)
+
+# FIXME: GUILE_SITEDIR would be better, but that
+# breaks 'make distcheck'
+scmdir=${prefix}/share/guile/site/${GUILE_EFFECTIVE_VERSION}
+scm_DATA=mu.scm
+
+EXTRA_DIST=$(scm_DATA) $(XFILES)
+
+## Add -MG to make the .x magic work with auto-dep code.
+MKDEP = $(CC) -M -MG $(snarfcppopts)
+
+#CLEANFILES=$(XFILES)
diff --git a/guile/compile-scm.in b/guile/compile-scm.in
new file mode 100644 (file)
index 0000000..04cc0f9
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/sh
+## Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+@abs_builddir@/build-env @guild@ compile "$@"
+
+# Local-Variables:
+# mode: sh
+# End:
diff --git a/guile/examples/Makefile.am b/guile/examples/Makefile.am
new file mode 100644 (file)
index 0000000..7e126c5
--- /dev/null
@@ -0,0 +1,23 @@
+## Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+EXTRA_DIST=                    \
+       msg-graphs              \
+       contacts-export         \
+       org2mu4e                \
+       mu-biff
diff --git a/guile/examples/contacts-export b/guile/examples/contacts-export
new file mode 100755 (executable)
index 0000000..7e33c54
--- /dev/null
@@ -0,0 +1,85 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;;
+;; Copyright (C) 2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+(use-modules (ice-9 getopt-long) (ice-9 format))
+(use-modules (srfi srfi-1))
+(use-modules (mu))
+
+(define (sort-by-freq c1 c2)
+  (< (mu:frequency c1) (mu:frequency c2)))
+
+(define (sort-by-newness c1 c2)
+  (< (mu:last-seen c1) (mu:last-seen c2)))
+
+(define (main args)
+  (let* ((optionspec   '( (muhome   (value #t))
+                         (sort-by  (value #t))
+                         (revert   (value #f))
+                         (format   (value #t))
+                         (limit    (value #t))
+                         (help    (single-char #\h) (value #f))))
+         (options (getopt-long args optionspec))
+         (msg (string-append
+                "usage: contacts-export [--help] [--muhome=<muhome>] "
+                "--format=<org-contact|mutt-alias|mutt-ab|wanderlust|quoted|plain(*)> "
+                "--sort-by=<frequency(*)|newness> [--revert] [--limit=<n>]\n"))
+         (help (option-ref options 'help #f))
+         (muhome (option-ref options 'muhome #f))
+         (sort-by (or (option-ref options 'sort-by #f) "frequency"))
+         (revert (option-ref options 'revert #f))
+         (form  (or (option-ref options 'format #f) "plain"))
+         (limit (string->number (option-ref options 'limit "1000000"))))
+    (if help
+      (begin
+       (display msg)
+       (exit 0))
+     (begin
+       (setlocale LC_ALL "")
+       (mu:initialize muhome)
+       (let* ((sort-func
+               (cond
+                 ((string= sort-by "frequency") sort-by-freq)
+                 ((string= sort-by "newness")  sort-by-newness)
+                 (else (begin (display msg) (exit 1)))))
+              (contacts '()))
+        ;; make a list of all contacts
+        (mu:for-each-contact
+          (lambda (c) (set! contacts (cons c contacts))))
+
+        ;; should we sort it?
+        (if sort-by
+          (set! contacts (sort! contacts
+                           (if revert (negate sort-func) sort-func))))
+
+        ;; should we limit the number?
+        (if (and limit (< limit (length contacts)))
+          (set! contacts (take! contacts limit)))
+        ;; export!
+        (for-each
+          (lambda (c)
+            (format #t "~a\n" (mu:contact->string c form)))
+          contacts))))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/examples/msg-graphs b/guile/examples/msg-graphs
new file mode 100755 (executable)
index 0000000..654dd28
--- /dev/null
@@ -0,0 +1,133 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2011-2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+(setlocale LC_ALL "")
+
+(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format))
+(use-modules (mu) (mu stats) (mu plot))
+;;(use-modules (mu) (mu message) (mu stats) (mu plot))
+
+(define (per-hour expr output)
+  "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+  (mu:plot
+    (sort
+      (mu:tabulate
+       (lambda (msg)
+         (tm:hour (localtime (mu:date msg)))) expr)
+      (lambda (x y) (< (car x) (car y))))
+    (format #f "Messages per hour matching ~a" expr) "Hour" "Messages" output))
+
+(define (per-day expr output)
+  "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+  (mu:plot
+    (mu:weekday-numbers->names
+      (sort (mu:tabulate
+             (lambda (msg)
+               (tm:wday (localtime (mu:date msg)))) expr)
+       (lambda (x y) (< (car x) (car y)))))
+    (format #f "Messages per weekday matching ~a" expr) "Day" "Messages" output))
+
+(define (per-month expr output)
+  "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+  (mu:plot
+    (mu:month-numbers->names
+      (sort
+       (mu:tabulate
+         (lambda (msg)
+           (tm:mon (localtime (mu:date msg)))) expr)
+       (lambda (x y) (< (car x) (car y)))))
+    (format #f "Messages per month matching ~a" expr) "Month" "Messages" output))
+
+
+(define (per-year-month expr output)
+  "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+  (mu:plot
+    (sort (mu:tabulate
+           (lambda (msg)
+             (string->number
+               (format #f "~d~2'0d"
+                 (+ 1900 (tm:year (localtime (mu:date msg))))
+                 (tm:mon (localtime (mu:date msg))))))
+           expr)
+      (lambda (x y) (< (car x) (car y))))
+    (format #f "Messages per year/month matching ~a" expr)
+    "Year/Month" "Messages" output))
+
+
+
+(define (per-year expr output)
+  "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+  (mu:plot
+    (sort (mu:tabulate
+           (lambda (msg)
+             (+ 1900 (tm:year (localtime (mu:date msg))))) expr)
+      (lambda (x y) (< (car x) (car y))))
+    (format #f "Messages per year matching ~a" expr) "Year" "Messages" output))
+
+
+
+(define (main args)
+  (let* ((optionspec   '( (muhome     (value #t))
+                         (what       (value #t))
+                         (text       (value #f))
+                         (help       (single-char #\h) (value #f))))
+         (options (getopt-long args optionspec))
+         (msg (string-append
+                "usage: mu-msg-stats [--help] [--text] "
+                "[--muhome=<muhome>] "
+                "--what=<per-hour|per-day|per-month|per-year-month|"
+                "per-year> [searchexpr]\n"))
+         (help (option-ref options 'help #f))
+         (what (option-ref options 'what #f))
+         (text (option-ref options 'text #f))
+         ;; if `text' is `#f', use a graphical window by setting output to "wxt",
+         ;; else use text-mode plotting ("dumb")
+         (output (if text "dumb" "wxt"))
+         (muhome (option-ref options 'muhome #f))
+         (restargs (option-ref options '() #f))
+         (expr (if restargs (string-join restargs) "")))
+    (if (or help (not what))
+      (begin
+       (display msg)
+       (exit (if help 0 1))))
+    (mu:initialize muhome)
+    (cond
+      ((string= what "per-hour")  (per-hour expr output))
+      ((string= what "per-day")   (per-day expr output))
+      ((string= what "per-month") (per-month expr output))
+      ((string= what "per-year-month") (per-year-month expr output))
+      ((string= what "per-year")  (per-year expr output))
+      (else (begin
+             (display msg)
+             (exit 1))))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/examples/mu-biff b/guile/examples/mu-biff
new file mode 100755 (executable)
index 0000000..bc6d507
--- /dev/null
@@ -0,0 +1,59 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;;
+;; Copyright (C) 2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; script to list the message matching <query> which are newer than
+;; <n> minutes
+
+;; use it, eg. like:
+;;   $ mu-biff --newer-than=`date +%s --date='5 minutes ago'` "maildir:/inbox"
+
+
+(use-modules (ice-9 getopt-long) (ice-9 format))
+(use-modules (mu))
+
+(define (main args)
+  (let* ((optionspec   '((muhome      (value #t))
+                         (newer-than (value #t))
+                         (help       (single-char #\h) (value #f))))
+         (options (getopt-long args optionspec))
+         (msg (string-append
+                "usage: mu-biff [--help] [--muhome=<muhome>]"
+                " [--newer-than=<timestamp>] <query>"))
+         (help (option-ref options 'help #f))
+         (newer-than (string->number (option-ref options 'newer-than "0")))
+         (muhome (option-ref options 'muhome #f))
+         (query (string-concatenate (option-ref options '() '()))))
+    (if help
+      (begin (display msg) (newline) (exit 0))
+      (begin
+       (mu:initialize muhome)
+       (mu:for-each-message
+         (lambda (msg)
+           (if (> (mu:timestamp msg) newer-than)
+             (format #t "~a ~a\n"
+               (mu:from msg)
+               (mu:subject msg))))
+         query)))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/examples/org2mu4e b/guile/examples/org2mu4e
new file mode 100755 (executable)
index 0000000..3556b9a
--- /dev/null
@@ -0,0 +1,78 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;;
+;; Copyright (C) 2011-2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(use-modules (ice-9 getopt-long) (ice-9 format))
+(use-modules (mu))
+
+(define (display-org-header query)
+  "Print the header for the org-file for QUERY."
+  (format #t "* Messages matching '~a'\n\n" query))
+
+(define (org-mu4e-link msg)
+  "Create a link for this message understandable by org-mu4e."
+  (let* ((subject ;; cleanup subject
+          (string-map
+            (lambda (kar)
+              (if (member kar '(#\] #\[)) #\space kar))
+            (or (mu:subject msg) "No subject"))))
+    (format #f "[[mu4e:msgid:~a][~s]]"
+      (mu:message-id msg) subject)))
+
+(define (display-org-entry msg tag)
+  "Write an org entry for MSG."
+  (format #t "** ~a ~a\n\t~s\n\t~s\n"
+    (org-mu4e-link msg)
+    (if tag (string-concatenate `(":" ,tag "::")) "")
+    (or (mu:from msg) "?")
+    (let ((body (mu:body-txt msg)))
+      (if (not body) ;; get a 'summary' of the body text
+       "<no plain-text body>"
+       (string-map
+         (lambda (c)
+           (if (or (char=? c #\newline) (char=? c #\return))
+                 #\space
+                 c))
+         (substring body 0 (min (string-length body) 100)))))))
+
+(define (main args)
+  (let* ((optionspec   '( (muhome   (value #t))
+                         (tag      (value #t))
+                         (help    (single-char #\h) (value #f))))
+         (options (getopt-long args optionspec))
+         (msg (string-append
+                "usage: mu4e-org [--help] [--muhome=<muhome>] [--tag=<tag>] <query>"))
+         (help (option-ref options 'help #f))
+         (tag  (option-ref options 'tag #f))
+         (muhome (option-ref options 'muhome #f))
+         (query (string-concatenate (option-ref options '() '()))))
+    (if help
+      (begin (display msg) (exit 0))
+      (begin
+       (mu:initialize muhome)
+       (display-org-header query)
+       (mu:for-each-message
+           (lambda (msg) (display-org-entry msg tag))
+         query)))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/fdl.texi b/guile/fdl.texi
new file mode 100644 (file)
index 0000000..96ce74e
--- /dev/null
@@ -0,0 +1,451 @@
+@c The GNU Free Documentation License.
+@center Version 1.2, November 2002
+
+@c This file is intended to be included within another document,
+@c hence no sectioning command or @node.
+
+@display
+Copyright @copyright{} 2000,2001,2002 Free Software Foundation, Inc.
+51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+@end display
+
+@enumerate 0
+@item
+PREAMBLE
+
+The purpose of this License is to make a manual, textbook, or other
+functional and useful document @dfn{free} in the sense of freedom: to
+assure everyone the effective freedom to copy and redistribute it,
+with or without modifying it, either commercially or noncommercially.
+Secondarily, this License preserves for the author and publisher a way
+to get credit for their work, while not being considered responsible
+for modifications made by others.
+
+This License is a kind of ``copyleft'', which means that derivative
+works of the document must themselves be free in the same sense.  It
+complements the GNU General Public License, which is a copyleft
+license designed for free software.
+
+We have designed this License in order to use it for manuals for free
+software, because free software needs free documentation: a free
+program should come with manuals providing the same freedoms that the
+software does.  But this License is not limited to software manuals;
+it can be used for any textual work, regardless of subject matter or
+whether it is published as a printed book.  We recommend this License
+principally for works whose purpose is instruction or reference.
+
+@item
+APPLICABILITY AND DEFINITIONS
+
+This License applies to any manual or other work, in any medium, that
+contains a notice placed by the copyright holder saying it can be
+distributed under the terms of this License.  Such a notice grants a
+world-wide, royalty-free license, unlimited in duration, to use that
+work under the conditions stated herein.  The ``Document'', below,
+refers to any such manual or work.  Any member of the public is a
+licensee, and is addressed as ``you''.  You accept the license if you
+copy, modify or distribute the work in a way requiring permission
+under copyright law.
+
+A ``Modified Version'' of the Document means any work containing the
+Document or a portion of it, either copied verbatim, or with
+modifications and/or translated into another language.
+
+A ``Secondary Section'' is a named appendix or a front-matter section
+of the Document that deals exclusively with the relationship of the
+publishers or authors of the Document to the Document's overall
+subject (or to related matters) and contains nothing that could fall
+directly within that overall subject.  (Thus, if the Document is in
+part a textbook of mathematics, a Secondary Section may not explain
+any mathematics.)  The relationship could be a matter of historical
+connection with the subject or with related matters, or of legal,
+commercial, philosophical, ethical or political position regarding
+them.
+
+The ``Invariant Sections'' are certain Secondary Sections whose titles
+are designated, as being those of Invariant Sections, in the notice
+that says that the Document is released under this License.  If a
+section does not fit the above definition of Secondary then it is not
+allowed to be designated as Invariant.  The Document may contain zero
+Invariant Sections.  If the Document does not identify any Invariant
+Sections then there are none.
+
+The ``Cover Texts'' are certain short passages of text that are listed,
+as Front-Cover Texts or Back-Cover Texts, in the notice that says that
+the Document is released under this License.  A Front-Cover Text may
+be at most 5 words, and a Back-Cover Text may be at most 25 words.
+
+A ``Transparent'' copy of the Document means a machine-readable copy,
+represented in a format whose specification is available to the
+general public, that is suitable for revising the document
+straightforwardly with generic text editors or (for images composed of
+pixels) generic paint programs or (for drawings) some widely available
+drawing editor, and that is suitable for input to text formatters or
+for automatic translation to a variety of formats suitable for input
+to text formatters.  A copy made in an otherwise Transparent file
+format whose markup, or absence of markup, has been arranged to thwart
+or discourage subsequent modification by readers is not Transparent.
+An image format is not Transparent if used for any substantial amount
+of text.  A copy that is not ``Transparent'' is called ``Opaque''.
+
+Examples of suitable formats for Transparent copies include plain
+@sc{ascii} without markup, Texinfo input format, La@TeX{} input
+format, @acronym{SGML} or @acronym{XML} using a publicly available
+@acronym{DTD}, and standard-conforming simple @acronym{HTML},
+PostScript or @acronym{PDF} designed for human modification.  Examples
+of transparent image formats include @acronym{PNG}, @acronym{XCF} and
+@acronym{JPG}.  Opaque formats include proprietary formats that can be
+read and edited only by proprietary word processors, @acronym{SGML} or
+@acronym{XML} for which the @acronym{DTD} and/or processing tools are
+not generally available, and the machine-generated @acronym{HTML},
+PostScript or @acronym{PDF} produced by some word processors for
+output purposes only.
+
+The ``Title Page'' means, for a printed book, the title page itself,
+plus such following pages as are needed to hold, legibly, the material
+this License requires to appear in the title page.  For works in
+formats which do not have any title page as such, ``Title Page'' means
+the text near the most prominent appearance of the work's title,
+preceding the beginning of the body of the text.
+
+A section ``Entitled XYZ'' means a named subunit of the Document whose
+title either is precisely XYZ or contains XYZ in parentheses following
+text that translates XYZ in another language.  (Here XYZ stands for a
+specific section name mentioned below, such as ``Acknowledgements'',
+``Dedications'', ``Endorsements'', or ``History''.)  To ``Preserve the Title''
+of such a section when you modify the Document means that it remains a
+section ``Entitled XYZ'' according to this definition.
+
+The Document may include Warranty Disclaimers next to the notice which
+states that this License applies to the Document.  These Warranty
+Disclaimers are considered to be included by reference in this
+License, but only as regards disclaiming warranties: any other
+implication that these Warranty Disclaimers may have is void and has
+no effect on the meaning of this License.
+
+@item
+VERBATIM COPYING
+
+You may copy and distribute the Document in any medium, either
+commercially or noncommercially, provided that this License, the
+copyright notices, and the license notice saying this License applies
+to the Document are reproduced in all copies, and that you add no other
+conditions whatsoever to those of this License.  You may not use
+technical measures to obstruct or control the reading or further
+copying of the copies you make or distribute.  However, you may accept
+compensation in exchange for copies.  If you distribute a large enough
+number of copies you must also follow the conditions in section 3.
+
+You may also lend copies, under the same conditions stated above, and
+you may publicly display copies.
+
+@item
+COPYING IN QUANTITY
+
+If you publish printed copies (or copies in media that commonly have
+printed covers) of the Document, numbering more than 100, and the
+Document's license notice requires Cover Texts, you must enclose the
+copies in covers that carry, clearly and legibly, all these Cover
+Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
+the back cover.  Both covers must also clearly and legibly identify
+you as the publisher of these copies.  The front cover must present
+the full title with all words of the title equally prominent and
+visible.  You may add other material on the covers in addition.
+Copying with changes limited to the covers, as long as they preserve
+the title of the Document and satisfy these conditions, can be treated
+as verbatim copying in other respects.
+
+If the required texts for either cover are too voluminous to fit
+legibly, you should put the first ones listed (as many as fit
+reasonably) on the actual cover, and continue the rest onto adjacent
+pages.
+
+If you publish or distribute Opaque copies of the Document numbering
+more than 100, you must either include a machine-readable Transparent
+copy along with each Opaque copy, or state in or with each Opaque copy
+a computer-network location from which the general network-using
+public has access to download using public-standard network protocols
+a complete Transparent copy of the Document, free of added material.
+If you use the latter option, you must take reasonably prudent steps,
+when you begin distribution of Opaque copies in quantity, to ensure
+that this Transparent copy will remain thus accessible at the stated
+location until at least one year after the last time you distribute an
+Opaque copy (directly or through your agents or retailers) of that
+edition to the public.
+
+It is requested, but not required, that you contact the authors of the
+Document well before redistributing any large number of copies, to give
+them a chance to provide you with an updated version of the Document.
+
+@item
+MODIFICATIONS
+
+You may copy and distribute a Modified Version of the Document under
+the conditions of sections 2 and 3 above, provided that you release
+the Modified Version under precisely this License, with the Modified
+Version filling the role of the Document, thus licensing distribution
+and modification of the Modified Version to whoever possesses a copy
+of it.  In addition, you must do these things in the Modified Version:
+
+@enumerate A
+@item
+Use in the Title Page (and on the covers, if any) a title distinct
+from that of the Document, and from those of previous versions
+(which should, if there were any, be listed in the History section
+of the Document).  You may use the same title as a previous version
+if the original publisher of that version gives permission.
+
+@item
+List on the Title Page, as authors, one or more persons or entities
+responsible for authorship of the modifications in the Modified
+Version, together with at least five of the principal authors of the
+Document (all of its principal authors, if it has fewer than five),
+unless they release you from this requirement.
+
+@item
+State on the Title page the name of the publisher of the
+Modified Version, as the publisher.
+
+@item
+Preserve all the copyright notices of the Document.
+
+@item
+Add an appropriate copyright notice for your modifications
+adjacent to the other copyright notices.
+
+@item
+Include, immediately after the copyright notices, a license notice
+giving the public permission to use the Modified Version under the
+terms of this License, in the form shown in the Addendum below.
+
+@item
+Preserve in that license notice the full lists of Invariant Sections
+and required Cover Texts given in the Document's license notice.
+
+@item
+Include an unaltered copy of this License.
+
+@item
+Preserve the section Entitled ``History'', Preserve its Title, and add
+to it an item stating at least the title, year, new authors, and
+publisher of the Modified Version as given on the Title Page.  If
+there is no section Entitled ``History'' in the Document, create one
+stating the title, year, authors, and publisher of the Document as
+given on its Title Page, then add an item describing the Modified
+Version as stated in the previous sentence.
+
+@item
+Preserve the network location, if any, given in the Document for
+public access to a Transparent copy of the Document, and likewise
+the network locations given in the Document for previous versions
+it was based on.  These may be placed in the ``History'' section.
+You may omit a network location for a work that was published at
+least four years before the Document itself, or if the original
+publisher of the version it refers to gives permission.
+
+@item
+For any section Entitled ``Acknowledgements'' or ``Dedications'', Preserve
+the Title of the section, and preserve in the section all the
+substance and tone of each of the contributor acknowledgements and/or
+dedications given therein.
+
+@item
+Preserve all the Invariant Sections of the Document,
+unaltered in their text and in their titles.  Section numbers
+or the equivalent are not considered part of the section titles.
+
+@item
+Delete any section Entitled ``Endorsements''.  Such a section
+may not be included in the Modified Version.
+
+@item
+Do not retitle any existing section to be Entitled ``Endorsements'' or
+to conflict in title with any Invariant Section.
+
+@item
+Preserve any Warranty Disclaimers.
+@end enumerate
+
+If the Modified Version includes new front-matter sections or
+appendices that qualify as Secondary Sections and contain no material
+copied from the Document, you may at your option designate some or all
+of these sections as invariant.  To do this, add their titles to the
+list of Invariant Sections in the Modified Version's license notice.
+These titles must be distinct from any other section titles.
+
+You may add a section Entitled ``Endorsements'', provided it contains
+nothing but endorsements of your Modified Version by various
+parties---for example, statements of peer review or that the text has
+been approved by an organization as the authoritative definition of a
+standard.
+
+You may add a passage of up to five words as a Front-Cover Text, and a
+passage of up to 25 words as a Back-Cover Text, to the end of the list
+of Cover Texts in the Modified Version.  Only one passage of
+Front-Cover Text and one of Back-Cover Text may be added by (or
+through arrangements made by) any one entity.  If the Document already
+includes a cover text for the same cover, previously added by you or
+by arrangement made by the same entity you are acting on behalf of,
+you may not add another; but you may replace the old one, on explicit
+permission from the previous publisher that added the old one.
+
+The author(s) and publisher(s) of the Document do not by this License
+give permission to use their names for publicity for or to assert or
+imply endorsement of any Modified Version.
+
+@item
+COMBINING DOCUMENTS
+
+You may combine the Document with other documents released under this
+License, under the terms defined in section 4 above for modified
+versions, provided that you include in the combination all of the
+Invariant Sections of all of the original documents, unmodified, and
+list them all as Invariant Sections of your combined work in its
+license notice, and that you preserve all their Warranty Disclaimers.
+
+The combined work need only contain one copy of this License, and
+multiple identical Invariant Sections may be replaced with a single
+copy.  If there are multiple Invariant Sections with the same name but
+different contents, make the title of each such section unique by
+adding at the end of it, in parentheses, the name of the original
+author or publisher of that section if known, or else a unique number.
+Make the same adjustment to the section titles in the list of
+Invariant Sections in the license notice of the combined work.
+
+In the combination, you must combine any sections Entitled ``History''
+in the various original documents, forming one section Entitled
+``History''; likewise combine any sections Entitled ``Acknowledgements'',
+and any sections Entitled ``Dedications''.  You must delete all
+sections Entitled ``Endorsements.''
+
+@item
+COLLECTIONS OF DOCUMENTS
+
+You may make a collection consisting of the Document and other documents
+released under this License, and replace the individual copies of this
+License in the various documents with a single copy that is included in
+the collection, provided that you follow the rules of this License for
+verbatim copying of each of the documents in all other respects.
+
+You may extract a single document from such a collection, and distribute
+it individually under this License, provided you insert a copy of this
+License into the extracted document, and follow this License in all
+other respects regarding verbatim copying of that document.
+
+@item
+AGGREGATION WITH INDEPENDENT WORKS
+
+A compilation of the Document or its derivatives with other separate
+and independent documents or works, in or on a volume of a storage or
+distribution medium, is called an ``aggregate'' if the copyright
+resulting from the compilation is not used to limit the legal rights
+of the compilation's users beyond what the individual works permit.
+When the Document is included in an aggregate, this License does not
+apply to the other works in the aggregate which are not themselves
+derivative works of the Document.
+
+If the Cover Text requirement of section 3 is applicable to these
+copies of the Document, then if the Document is less than one half of
+the entire aggregate, the Document's Cover Texts may be placed on
+covers that bracket the Document within the aggregate, or the
+electronic equivalent of covers if the Document is in electronic form.
+Otherwise they must appear on printed covers that bracket the whole
+aggregate.
+
+@item
+TRANSLATION
+
+Translation is considered a kind of modification, so you may
+distribute translations of the Document under the terms of section 4.
+Replacing Invariant Sections with translations requires special
+permission from their copyright holders, but you may include
+translations of some or all Invariant Sections in addition to the
+original versions of these Invariant Sections.  You may include a
+translation of this License, and all the license notices in the
+Document, and any Warranty Disclaimers, provided that you also include
+the original English version of this License and the original versions
+of those notices and disclaimers.  In case of a disagreement between
+the translation and the original version of this License or a notice
+or disclaimer, the original version will prevail.
+
+If a section in the Document is Entitled ``Acknowledgements'',
+``Dedications'', or ``History'', the requirement (section 4) to Preserve
+its Title (section 1) will typically require changing the actual
+title.
+
+@item
+TERMINATION
+
+You may not copy, modify, sublicense, or distribute the Document except
+as expressly provided for under this License.  Any other attempt to
+copy, modify, sublicense or distribute the Document is void, and will
+automatically terminate your rights under this License.  However,
+parties who have received copies, or rights, from you under this
+License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+@item
+FUTURE REVISIONS OF THIS LICENSE
+
+The Free Software Foundation may publish new, revised versions
+of the GNU Free Documentation License from time to time.  Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.  See
+@uref{http://www.gnu.org/copyleft/}.
+
+Each version of the License is given a distinguishing version number.
+If the Document specifies that a particular numbered version of this
+License ``or any later version'' applies to it, you have the option of
+following the terms and conditions either of that specified version or
+of any later version that has been published (not as a draft) by the
+Free Software Foundation.  If the Document does not specify a version
+number of this License, you may choose any version ever published (not
+as a draft) by the Free Software Foundation.
+@end enumerate
+
+@page
+@heading ADDENDUM: How to use this License for your documents
+
+To use this License in a document you have written, include a copy of
+the License in the document and put the following copyright and
+license notices just after the title page:
+
+@smallexample
+@group
+  Copyright (C)  @var{year}  @var{your name}.
+  Permission is granted to copy, distribute and/or modify this document
+  under the terms of the GNU Free Documentation License, Version 1.2
+  or any later version published by the Free Software Foundation;
+  with no Invariant Sections, no Front-Cover Texts, and no Back-Cover
+  Texts.  A copy of the license is included in the section entitled ``GNU
+  Free Documentation License''.
+@end group
+@end smallexample
+
+If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
+replace the ``with@dots{}Texts.'' line with this:
+
+@smallexample
+@group
+    with the Invariant Sections being @var{list their titles}, with
+    the Front-Cover Texts being @var{list}, and with the Back-Cover Texts
+    being @var{list}.
+@end group
+@end smallexample
+
+If you have Invariant Sections without Cover Texts, or some other
+combination of the three, merge those two alternatives to suit the
+situation.
+
+If your document contains nontrivial examples of program code, we
+recommend releasing these examples in parallel under your choice of
+free software license, such as the GNU General Public License,
+to permit their use in free software.
+
+@c Local Variables:
+@c ispell-local-pdict: "ispell-dict"
+@c End:
+
diff --git a/guile/meson.build b/guile/meson.build
new file mode 100644 (file)
index 0000000..a427644
--- /dev/null
@@ -0,0 +1,113 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+#
+# create a shell script for compiling from the source dirs
+compile_scm_conf = configuration_data()
+compile_scm_conf.set('abs_builddir', meson.current_build_dir())
+compile_scm_conf.set('guild', 'guild')
+compile_scm=configure_file(
+  input: 'compile-scm.in',
+  output: 'compile-scm',
+  configuration: compile_scm_conf,
+  install: false
+)
+run_command('chmod', '+x', compile_scm, check: true)
+scm_compiler=join_paths(meson.current_build_dir(), 'compile-scm')
+
+#
+# NOTE: snarfing works but you get:
+# ,----
+# | cc1plus: warning: command-line option ‘-std=gnu11’ is valid for C/ObjC
+# | but not for C++
+# `----
+# this is because the snarf-script hardcodes the '-std=gnu11' but we're
+# building for c++; even worse, e.g. on some MacOS, the warning is a
+# hard error.
+#
+# We can override flag through a env variable CPP; but then we _also_ need to
+# override the compiler, so e.g. CPP="g++ -std=c++17'; but it's a bit
+# hairy/ugly/fragile to derive the raw compiler name in meson; also the
+# generator expression doesn't take an 'env:' parameter, so we'd need
+# to rewrite using custom_target...
+#
+# for now, we avoid all that by simply including the generated files.
+do_snarf=false
+
+if do_snarf
+  snarf = find_program('guile-snarf3.0','guile-snarf')
+  # there must be a better way of feeding the include paths to snarf...
+  snarf_args=['-o', '@OUTPUT@', '@INPUT@', '-I' + meson.current_source_dir() + '/..',
+              '-I' + meson.current_source_dir() + '/../lib',
+              '-I' + meson.current_build_dir() + '/..']
+  snarf_args += '-I' + join_paths(glib_dep.get_pkgconfig_variable('includedir'),
+                                  'glib-2.0')
+  snarf_args += '-I' + join_paths(glib_dep.get_pkgconfig_variable('libdir'),
+                                  'glib-2.0', 'include')
+  snarf_args += '-I' + join_paths(guile_dep.get_pkgconfig_variable('includedir'),
+                                  'guile', '3.0')
+  snarf_gen=generator(snarf,
+                      output: '@BASENAME@.x',
+                      arguments: snarf_args)
+  snarf_srcs=['mu-guile.cc', 'mu-guile-message.cc']
+  snarf_x=snarf_gen.process(snarf_srcs)
+else
+  snarf_x = [ 'mu-guile-message.x', 'mu-guile.x' ]
+endif
+
+lib_guile_mu = shared_module(
+  'guile-mu',
+  [ 'mu-guile.cc',
+    'mu-guile-message.cc' ],
+  dependencies: [guile_dep, glib_dep, lib_mu_dep, config_h_dep, thread_dep ],
+  install: true)
+
+if makeinfo.found()
+  custom_target('mu_guile_info',
+                input: 'mu-guile.texi',
+                output: 'mu-guile.info',
+                install: true,
+                install_dir: infodir,
+                command: [makeinfo,
+                          '-o', join_paths(meson.current_build_dir(), 'mu-guile.info'),
+                          join_paths(meson.current_source_dir(), 'mu-guile.texi'),
+                          '-I', join_paths(meson.current_build_dir(), '..')])
+
+  if install_info.found()
+    meson.add_install_script(install_info_script, 'share/info', 'mu-guile.info')
+  endif
+endif
+
+guile_scm_dir=join_paths(datadir, 'guile', 'site', '3.0', 'mu')
+install_data(['mu.scm','mu/script.scm', 'mu/message.scm', 'mu/stats.scm', 'mu/plot.scm'],
+             install_dir: guile_scm_dir)
+
+
+mu_guile_scripts=[
+  join_paths('scripts', 'find-dups.scm'),
+  join_paths('scripts', 'msgs-count.scm'),
+  join_paths('scripts', 'msgs-per-day.scm'),
+  join_paths('scripts', 'msgs-per-hour.scm'),
+  join_paths('scripts', 'msgs-per-month.scm'),
+  join_paths('scripts', 'msgs-per-year-month.scm'),
+  join_paths('scripts', 'msgs-per-year.scm')
+]
+mu_guile_script_dir=join_paths(datadir, 'mu', 'scripts')
+install_data(mu_guile_scripts, install_dir: mu_guile_script_dir)
+
+guile_builddir=meson.current_build_dir()
+
+subdir('tests')
diff --git a/guile/mu-guile-message.cc b/guile/mu-guile-message.cc
new file mode 100644 (file)
index 0000000..184bd91
--- /dev/null
@@ -0,0 +1,484 @@
+/*
+** Copyright (C) 2011-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "mu-guile-message.hh"
+
+#include <libguile.h>
+#include "message/mu-message.hh"
+#include "utils/mu-utils.hh"
+#include <config.h>
+
+#include <glib-object.h>
+#include <memory>
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wredundant-decls"
+#include <libguile.h>
+#pragma GCC diagnostic pop
+
+#include "mu-guile.hh"
+
+#include <mu-runtime.hh>
+#include <mu-store.hh>
+#include <mu-query.hh>
+
+using namespace Mu;
+
+/* pseudo field, not in Xapian */
+constexpr auto MU_GUILE_MSG_FIELD_ID_TIMESTAMP = Field::id_size() + 1;
+
+/* some symbols */
+static SCM     SYMB_PRIO_LOW, SYMB_PRIO_NORMAL, SYMB_PRIO_HIGH;
+static std::array<SCM, AllMessageFlagInfos.size()> SYMB_FLAGS;
+static SCM     SYMB_CONTACT_TO, SYMB_CONTACT_CC, SYMB_CONTACT_BCC, SYMB_CONTACT_FROM;
+static long    MSG_TAG;
+
+
+using MessageSPtr = std::unique_ptr<Message>;
+
+static gboolean
+mu_guile_scm_is_msg(SCM scm)
+{
+       return SCM_NIMP(scm) && (long)SCM_CAR(scm) == MSG_TAG;
+}
+
+static SCM
+message_scm_create(Xapian::Document&& doc)
+{
+       /* placement-new */
+
+       void *scm_mem{scm_gc_malloc(sizeof(Message), "msg")};
+       Message* msgp = new(scm_mem)Message(std::move(doc));
+
+       SCM_RETURN_NEWSMOB(MSG_TAG, msgp);
+}
+
+static const Message*
+message_from_scm(SCM msg_smob)
+{
+       return reinterpret_cast<Message*>(SCM_CDR(msg_smob));
+}
+
+static size_t
+message_scm_free(SCM msg_smob)
+{
+       if (auto msg = message_from_scm(msg_smob); msg)
+               msg->~Message();
+
+       return sizeof(Message);
+}
+
+static int
+message_scm_print(SCM msg_smob, SCM port, scm_print_state* pstate)
+{
+       scm_puts("#<msg ", port);
+
+       if (auto msg = message_from_scm(msg_smob); msg)
+               scm_puts(msg->path().c_str(), port);
+
+       scm_puts(">", port);
+       return 1;
+}
+
+struct FlagData {
+       Flags   flags;
+       SCM     lst;
+};
+
+#define MU_GUILE_INITIALIZED_OR_ERROR                                            \
+       do {                                                                     \
+               if (!(mu_guile_initialized())) {                                 \
+                       mu_guile_error(FUNC_NAME,                                \
+                                      0,                                        \
+                                      "mu not initialized; call mu:initialize", \
+                                      SCM_UNDEFINED);                           \
+                       return SCM_UNSPECIFIED;                                  \
+               }                                                                \
+       } while (0)
+
+
+static SCM
+get_flags_scm(const Message& msg)
+{
+       SCM lst{SCM_EOL};
+       const auto flags{msg.flags()};
+
+       for (auto i = 0; i != AllMessageFlagInfos.size(); ++i) {
+               const auto& info{AllMessageFlagInfos.at(i)};
+               if (any_of(info.flag & flags))
+                       scm_append_x(scm_list_2(lst, scm_list_1(SYMB_FLAGS.at(i))));
+       }
+
+       return lst;
+}
+
+static SCM
+get_prio_scm(const Message& msg)
+{
+       switch (msg.priority()) {
+       case Priority::Low: return SYMB_PRIO_LOW;
+       case Priority::Normal: return SYMB_PRIO_NORMAL;
+       case Priority::High: return SYMB_PRIO_HIGH;
+
+       default: g_return_val_if_reached(SCM_UNDEFINED);
+       }
+}
+
+static SCM
+msg_string_list_field(const Message& msg, Field::Id field_id)
+{
+       SCM           scmlst{SCM_EOL};
+       for (auto&& val: msg.document().string_vec_value(field_id)) {
+               SCM item;
+               item   = scm_list_1(mu_guile_scm_from_string(val));
+               scmlst = scm_append_x(scm_list_2(scmlst, item));
+       }
+
+       return scmlst;
+}
+
+static SCM
+msg_contact_list_field(const Message& msg, Field::Id field_id)
+{
+       return scm_from_utf8_string(
+               to_string(msg.document().contacts_value(field_id)).c_str());
+}
+
+static SCM
+get_body(const Message& msg, bool html)
+{
+       if (const auto body = html ? msg.body_html() : msg.body_text(); body)
+               return mu_guile_scm_from_string(*body);
+       else
+               return SCM_BOOL_F;
+}
+
+SCM_DEFINE(get_field,
+          "mu:c:get-field",
+          2,
+          0,
+          0,
+          (SCM MSG, SCM FIELD),
+          "Get the field FIELD from message MSG.\n")
+#define FUNC_NAME s_get_field
+{
+       SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+       auto msg{message_from_scm(MSG)};
+       SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME);
+
+       SCM_ASSERT(scm_integer_p(FIELD), FIELD, SCM_ARG2, FUNC_NAME);
+       const auto field_opt{field_from_number(static_cast<size_t>(scm_to_int(FIELD)))};
+       SCM_ASSERT(!!field_opt, FIELD, SCM_ARG2, FUNC_NAME);
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+       switch (field_opt->id) {
+       case Field::Id::Priority:
+               return get_prio_scm(*msg);
+       case Field::Id::Flags:
+               return get_flags_scm(*msg);
+       case Field::Id::BodyText:
+               return get_body(*msg, false);
+       default: break;
+       }
+#pragma GCC diagnostic pop
+
+       switch (field_opt->type) {
+       case Field::Type::String:
+               return mu_guile_scm_from_string(msg->document().string_value(field_opt->id));
+       case Field::Type::ByteSize:
+       case Field::Type::TimeT:
+       case Field::Type::Integer:
+               return scm_from_uint(msg->document().integer_value(field_opt->id));
+       case Field::Type::StringList:
+               return msg_string_list_field(*msg, field_opt->id);
+       case Field::Type::ContactList:
+               return msg_contact_list_field(*msg, field_opt->id);
+       default:
+               SCM_ASSERT(0, FIELD, SCM_ARG2, FUNC_NAME);
+       }
+}
+#undef FUNC_NAME
+
+static SCM
+contacts_to_list(const Message& msg, Option<Field::Id> field_id)
+{
+       SCM list{SCM_EOL};
+
+       const auto contacts{field_id ?
+               msg.document().contacts_value(*field_id) :
+               msg.all_contacts()};
+
+       for (auto&& contact: contacts) {
+               SCM item{scm_list_1(
+                               scm_cons(mu_guile_scm_from_string(contact.name),
+                                        mu_guile_scm_from_string(contact.email)))};
+               list = scm_append_x(scm_list_2(list, item));
+       }
+
+       return list;
+}
+
+SCM_DEFINE(get_contacts,
+          "mu:c:get-contacts",
+          2,
+          0,
+          0,
+          (SCM MSG, SCM CONTACT_TYPE),
+          "Get a list of contact information pairs.\n")
+#define FUNC_NAME s_get_contacts
+{
+       SCM     list;
+
+       MU_GUILE_INITIALIZED_OR_ERROR;
+
+       SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+       auto msg{message_from_scm(MSG)};
+       SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME);
+
+       SCM_ASSERT(scm_symbol_p(CONTACT_TYPE) || scm_is_bool(CONTACT_TYPE),
+                  CONTACT_TYPE,
+                  SCM_ARG2,
+                  FUNC_NAME);
+
+       if (CONTACT_TYPE == SCM_BOOL_F)
+               return SCM_UNSPECIFIED; /* nothing to do */
+
+       Option<Field::Id> field_id;
+       if (CONTACT_TYPE == SCM_BOOL_T)
+               field_id = {}; /* get all */
+       else {
+               if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_TO))
+                       field_id = Field::Id::To;
+               else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_CC))
+                       field_id = Field::Id::Cc;
+               else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_BCC))
+                       field_id = Field::Id::Bcc;
+               else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_FROM))
+                       field_id = Field::Id::From;
+               else {
+                       mu_guile_error(FUNC_NAME, 0, "invalid contact type", SCM_UNDEFINED);
+                       return SCM_UNSPECIFIED;
+               }
+       }
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wcast-function-type"
+       list = contacts_to_list(*msg, field_id);
+#pragma GCC diagnostic pop
+
+       /* explicitly close the file backend, so we won't run out of fds */
+
+
+       return list;
+}
+#undef FUNC_NAME
+
+SCM_DEFINE(get_parts,
+          "mu:c:get-parts",
+          1,
+          1,
+          0,
+          (SCM MSG, SCM ATTS_ONLY),
+          "Get the list of mime-parts for MSG. If ATTS_ONLY is #t, only"
+          "get parts that are (look like) attachments. The resulting list has "
+          "elements which are list of the form (index name mime-type size).\n")
+#define FUNC_NAME s_get_parts
+{
+       MU_GUILE_INITIALIZED_OR_ERROR;
+
+       SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+       auto msg{message_from_scm(MSG)};
+       SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME);
+       SCM_ASSERT(scm_is_bool(ATTS_ONLY), ATTS_ONLY, SCM_ARG2, FUNC_NAME);
+
+       SCM     attlist          = SCM_EOL;     /* empty list */
+       bool    attachments_only = ATTS_ONLY == SCM_BOOL_T ? TRUE : FALSE;
+
+       size_t n{};
+       for (auto&& part: msg->parts()) {
+
+               if (attachments_only && !part.is_attachment())
+                       continue;
+
+               const auto mime_type{part.mime_type()};
+               const auto filename{part.cooked_filename()};
+
+               SCM elm = scm_list_5(
+                       /* msg */
+                       mu_guile_scm_from_string(msg->path().c_str()),
+                       /* index */
+                       scm_from_uint(n++),
+                       /* filename or #f */
+                       filename ? mu_guile_scm_from_string(*filename) : SCM_BOOL_F,
+                       /* mime-type */
+                       mime_type ? mu_guile_scm_from_string(*mime_type) : SCM_BOOL_F,
+                       /* size */
+                       part.size() > 0 ? scm_from_uint(part.size()) : SCM_BOOL_F);
+
+               attlist = scm_cons(elm, attlist);
+       }
+
+       /* explicitly close the file backend, so we won't run of fds */
+       msg->unload_mime_message();
+
+       return attlist;
+}
+#undef FUNC_NAME
+
+SCM_DEFINE(get_header,
+          "mu:c:get-header",
+          2,
+          0,
+          0,
+          (SCM MSG, SCM HEADER),
+          "Get an arbitrary HEADER from MSG.\n")
+#define FUNC_NAME s_get_header
+{
+       MU_GUILE_INITIALIZED_OR_ERROR;
+
+       SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+       auto msg{message_from_scm(MSG)};
+       SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME);
+
+       SCM_ASSERT(scm_is_string(HEADER) || HEADER == SCM_UNDEFINED, HEADER, SCM_ARG2, FUNC_NAME);
+
+       char *header  = scm_to_utf8_string(HEADER);
+       SCM val     = mu_guile_scm_from_string(msg->header(header).value_or(""));
+       free(header);
+
+       /* explicitly close the file backend, so we won't run of fds */
+       msg->unload_mime_message();
+
+       return val;
+}
+#undef FUNC_NAME
+SCM_DEFINE(for_each_message,
+          "mu:c:for-each-message",
+          3,
+          0,
+          0,
+          (SCM FUNC, SCM EXPR, SCM MAXNUM),
+          "Call FUNC for each msg in the message store matching EXPR. EXPR is"
+          "either a string containing a mu search expression or a boolean; in the former "
+          "case, limit the messages to only those matching the expression, in the "
+          "latter case, match /all/ messages if the EXPR equals #t, and match "
+          "none if EXPR equals #f.")
+#define FUNC_NAME s_for_each_message
+{
+       char* expr{};
+
+       MU_GUILE_INITIALIZED_OR_ERROR;
+
+       SCM_ASSERT(scm_procedure_p(FUNC), FUNC, SCM_ARG1, FUNC_NAME);
+       SCM_ASSERT(scm_is_bool(EXPR) || scm_is_string(EXPR), EXPR, SCM_ARG2, FUNC_NAME);
+       SCM_ASSERT(scm_is_integer(MAXNUM), MAXNUM, SCM_ARG3, FUNC_NAME);
+
+       if (EXPR == SCM_BOOL_F)
+               return SCM_UNSPECIFIED; /* nothing to do */
+
+       if (EXPR == SCM_BOOL_T)
+               expr = strdup("\"\""); /* note, "" matches *all* messages */
+       else
+               expr = scm_to_utf8_string(EXPR);
+
+       const auto res = mu_guile_store().run_query(expr,{}, {}, scm_to_int(MAXNUM));
+       free(expr);
+       if (!res)
+               return SCM_UNSPECIFIED;
+
+       for (auto&& mi : *res) {
+               if (auto xdoc{mi.document()}; xdoc) {
+                       scm_call_1(FUNC, message_scm_create(std::move(xdoc.value())));
+               }
+       }
+
+       return SCM_UNSPECIFIED;
+}
+#undef FUNC_NAME
+
+static SCM
+register_symbol(const char* name)
+{
+       SCM scm;
+
+       scm = scm_from_utf8_symbol(name);
+       scm_c_define(name, scm);
+       scm_c_export(name, NULL);
+
+       return scm;
+}
+
+static void
+define_symbols(void)
+{
+       SYMB_CONTACT_TO   = register_symbol("mu:contact:to");
+       SYMB_CONTACT_CC   = register_symbol("mu:contact:cc");
+       SYMB_CONTACT_FROM = register_symbol("mu:contact:from");
+       SYMB_CONTACT_BCC  = register_symbol("mu:contact:bcc");
+
+       SYMB_PRIO_LOW    = register_symbol("mu:prio:low");
+       SYMB_PRIO_NORMAL = register_symbol("mu:prio:normal");
+       SYMB_PRIO_HIGH   = register_symbol("mu:prio:high");
+
+       for (auto i = 0U; i != AllMessageFlagInfos.size(); ++i) {
+               const auto& info{AllMessageFlagInfos.at(i)};
+               const auto name = "mu:flag:" + std::string{info.name};
+               SYMB_FLAGS[i] = register_symbol(name.c_str());
+       }
+}
+static void
+define_vars(void)
+{
+       field_for_each([](auto&& field){
+
+               auto defvar = [&](auto&& fname, auto&& ffield) {
+                       const auto name{"mu:field:" + std::string{fname}};
+                       scm_c_define(name.c_str(), scm_from_uint(field.value_no()));
+                       scm_c_export(name.c_str(), NULL);
+               };
+
+               // define for both name and (if exists) alias.
+               if (!field.name.empty())
+                       defvar(field.name, field);
+               if (!field.alias.empty())
+                       defvar(field.alias, field);
+       });
+
+       /* non-Xapian field: timestamp */
+       scm_c_define("mu:field:timestamp",
+                    scm_from_uint(MU_GUILE_MSG_FIELD_ID_TIMESTAMP));
+       scm_c_export("mu:field:timestamp", NULL);
+
+}
+
+void*
+mu_guile_message_init(void* data)
+{
+       MSG_TAG = scm_make_smob_type("message", sizeof(Message));
+
+       scm_set_smob_free(MSG_TAG, message_scm_free);
+       scm_set_smob_print(MSG_TAG, message_scm_print);
+
+       define_vars();
+       define_symbols();
+
+#ifndef SCM_MAGIC_SNARFER
+#include "mu-guile-message.x"
+#endif /*SCM_MAGIC_SNARFER*/
+
+       return NULL;
+}
diff --git a/guile/mu-guile-message.hh b/guile/mu-guile-message.hh
new file mode 100644 (file)
index 0000000..0e7201d
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+** Copyright (C) 2011-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_GUILE_MESSAGE_H__
+#define MU_GUILE_MESSAGE_H__
+
+/**
+ * Initialize this mu guile module.
+ *
+ * @param data
+q *
+ * @return
+ */
+extern "C" {
+void* mu_guile_message_init(void* data);
+}
+
+#endif /*MU_GUILE_MESSAGE_HH__*/
diff --git a/guile/mu-guile-message.x b/guile/mu-guile-message.x
new file mode 100644 (file)
index 0000000..6127b39
--- /dev/null
@@ -0,0 +1,6 @@
+/* cpp arguments: mu-guile-message.cc -DHAVE_CONFIG_H -I. -I.. -I../lib -I/usr/local/include/guile/3.0 -pthread -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -I/usr/include/libmount -I/usr/include/blkid -pthread -fno-strict-aliasing -Wall -Wextra -Wundef -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wformat=2 -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wreturn-type -Wno-overloaded-virtual -Wswitch-enum -Wswitch-default -Wno-error=unused-parameter -Wno-error=missing-field-initializers -Wno-error=overloaded-virtual -Wno-redundant-decls -Wno-missing-declarations -Wno-suggest-attribute=noreturn -O2 -Wno-inline */
+scm_c_define_gsubr (s_get_field, 2, 0, 0, (scm_t_subr) get_field);;
+scm_c_define_gsubr (s_get_contacts, 2, 0, 0, (scm_t_subr) get_contacts);;
+scm_c_define_gsubr (s_get_parts, 1, 1, 0, (scm_t_subr) get_parts);;
+scm_c_define_gsubr (s_get_header, 2, 0, 0, (scm_t_subr) get_header);;
+scm_c_define_gsubr (s_for_each_message, 3, 0, 0, (scm_t_subr) for_each_message);;
diff --git a/guile/mu-guile.cc b/guile/mu-guile.cc
new file mode 100644 (file)
index 0000000..ff62273
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+** Copyright (C) 2011-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-guile.hh"
+
+#include <config.h>
+#include <locale.h>
+#include <glib-object.h>
+
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wredundant-decls"
+#include <libguile.h>
+#pragma GCC diagnostic pop
+
+#include <mu-runtime.hh>
+#include <mu-store.hh>
+#include <mu-query.hh>
+
+using namespace Mu;
+
+SCM
+mu_guile_scm_from_string(const std::string& str)
+{
+       if (str.empty())
+               return SCM_BOOL_F;
+       else
+               return scm_from_stringn(str.c_str(), str.size(),
+                                       "UTF-8",
+                                       SCM_FAILED_CONVERSION_QUESTION_MARK);
+}
+
+SCM
+mu_guile_error(const char* func_name, int status, const char* fmt, SCM args)
+{
+       scm_error_scm(scm_from_locale_symbol("MuError"),
+                     scm_from_utf8_string(func_name ? func_name : "<nameless>"),
+                     scm_from_utf8_string(fmt),
+                     args,
+                     scm_list_1(scm_from_int(status)));
+
+       return SCM_UNSPECIFIED;
+}
+
+SCM
+mu_guile_g_error(const char* func_name, GError* err)
+{
+       scm_error_scm(scm_from_locale_symbol("MuError"),
+                     scm_from_utf8_string(func_name),
+                     scm_from_utf8_string(err ? err->message : "error"),
+                     SCM_UNDEFINED,
+                     SCM_UNDEFINED);
+
+       return SCM_UNSPECIFIED;
+}
+
+/* there can be only one */
+
+static Option<Mu::Store> StoreSingleton = Nothing;
+
+static bool
+mu_guile_init_instance(const char* muhome)
+try {
+       setlocale(LC_ALL, "");
+       if (!mu_runtime_init(muhome, "guile", true) || StoreSingleton)
+               return FALSE;
+
+       const auto path{mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB)};
+       auto store = Store::make(path);
+       if (!store) {
+               g_critical("error creating store @ %s: %s", path, store.error().what());
+               throw store.error();
+       } else
+               StoreSingleton.emplace(std::move(store.value()));
+
+       g_debug("mu-guile: opened store @ %s (n=%zu); maildir: %s",
+               StoreSingleton->properties().database_path.c_str(),
+               StoreSingleton->size(),
+               StoreSingleton->properties().root_maildir.c_str());
+
+       return true;
+
+} catch (const Xapian::Error& xerr) {
+       g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str());
+       return false;
+} catch (const std::runtime_error& re) {
+       g_critical("%s: error: %s", __func__, re.what());
+       return false;
+} catch (const std::exception& e) {
+       g_critical("%s: caught exception: %s", __func__, e.what());
+       return false;
+} catch (...) {
+       g_critical("%s: caught exception", __func__);
+       return false;
+}
+
+static void
+mu_guile_uninit_instance()
+{
+       StoreSingleton.reset();
+
+       mu_runtime_uninit();
+}
+
+Mu::Store&
+mu_guile_store()
+{
+       if (!StoreSingleton)
+               g_error("mu guile not initialized");
+
+       return StoreSingleton.value();
+}
+
+gboolean
+mu_guile_initialized()
+{
+       g_debug("initialized ? %u", !!StoreSingleton);
+
+       return !!StoreSingleton;
+}
+
+SCM_DEFINE_PUBLIC(mu_initialize,
+                 "mu:initialize",
+                 0,
+                 1,
+                 0,
+                 (SCM MUHOME),
+                 "Initialize mu - needed before you call any of the other "
+                 "functions. Optionally, you can provide MUHOME which should be an "
+                 "absolute path to your mu home directory "
+                 "-- typically, the default, ~/.cache/mu, should be just fine.")
+#define FUNC_NAME s_mu_initialize
+{
+       char*    muhome;
+
+       SCM_ASSERT(scm_is_string(MUHOME) || MUHOME == SCM_BOOL_F || SCM_UNBNDP(MUHOME),
+                  MUHOME,
+                  SCM_ARG1,
+                  FUNC_NAME);
+
+       if (mu_guile_initialized())
+               return mu_guile_error(FUNC_NAME, 0, "Already initialized", SCM_UNSPECIFIED);
+
+       if (SCM_UNBNDP(MUHOME) || MUHOME == SCM_BOOL_F)
+               muhome = NULL;
+       else
+               muhome = scm_to_utf8_string(MUHOME);
+
+       if (!mu_guile_init_instance(muhome)) {
+               free(muhome);
+               mu_guile_error(FUNC_NAME, 0, "Failed to initialize mu", SCM_UNSPECIFIED);
+       }
+
+       g_debug("mu-guile: initialized @ %s", muhome ? muhome : "<default>");
+       free(muhome);
+
+       /* cleanup when we're exiting */
+       atexit(mu_guile_uninit_instance);
+
+       return SCM_UNSPECIFIED;
+}
+#undef FUNC_NAME
+
+SCM_DEFINE_PUBLIC(mu_initialized_p,
+                 "mu:initialized?",
+                 0,
+                 0,
+                 0,
+                 (void),
+                 "Whether mu is initialized or not.\n")
+#define FUNC_NAME s_mu_initialized_p
+{
+       return mu_guile_initialized() ? SCM_BOOL_T : SCM_BOOL_F;
+}
+#undef FUNC_NAME
+
+SCM_DEFINE(log_func,
+          "mu:c:log",
+          1,
+          0,
+          1,
+          (SCM LEVEL, SCM FRM, SCM ARGS),
+          "log some message at LEVEL using a list of ARGS applied to FRM"
+          "(in 'simple-format' notation).\n")
+#define FUNC_NAME s_log_func
+{
+       gchar* output;
+       SCM    str;
+       int    level;
+
+       SCM_ASSERT(scm_integer_p(LEVEL), LEVEL, SCM_ARG1, FUNC_NAME);
+       SCM_ASSERT(scm_is_string(FRM), FRM, SCM_ARG2, "<write_log>");
+       SCM_VALIDATE_REST_ARGUMENT(ARGS);
+
+       level = scm_to_int(LEVEL);
+       if (level != G_LOG_LEVEL_MESSAGE && level != G_LOG_LEVEL_WARNING &&
+           level != G_LOG_LEVEL_CRITICAL)
+               return mu_guile_error(FUNC_NAME, 0, "invalid log level", SCM_UNSPECIFIED);
+
+       str = scm_simple_format(SCM_BOOL_F, FRM, ARGS);
+
+       if (!scm_is_string(str))
+               return SCM_UNSPECIFIED;
+
+       output = scm_to_utf8_string(str);
+       g_log(G_LOG_DOMAIN, (GLogLevelFlags)level, "%s", output);
+       free(output);
+
+       return SCM_UNSPECIFIED;
+}
+#undef FUNC_NAME
+
+static struct {
+       const char* name;
+       unsigned    val;
+} VAR_PAIRS[] = {
+
+    {"mu:message", G_LOG_LEVEL_MESSAGE},
+    {"mu:warning", G_LOG_LEVEL_WARNING},
+    {"mu:critical", G_LOG_LEVEL_CRITICAL}};
+
+static void
+define_vars(void)
+{
+       unsigned u;
+       for (u = 0; u != G_N_ELEMENTS(VAR_PAIRS); ++u) {
+               scm_c_define(VAR_PAIRS[u].name, scm_from_uint(VAR_PAIRS[u].val));
+               scm_c_export(VAR_PAIRS[u].name, NULL);
+       }
+}
+
+void*
+mu_guile_init(void* data)
+{
+       define_vars();
+
+#ifndef SCM_MAGIC_SNARFER
+#include "mu-guile.x"
+#endif /*SCM_MAGIC_SNARFER*/
+
+       return NULL;
+}
diff --git a/guile/mu-guile.hh b/guile/mu-guile.hh
new file mode 100644 (file)
index 0000000..6265995
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+** Copyright (C) 2011-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef __MU_GUILE_H__
+#define __MU_GUILE_H__
+
+#include <glib.h>
+#include <libguile.h>
+#include <mu-query.hh>
+
+/**
+ * get the singleton Store instance
+ */
+Mu::Store& mu_guile_store();
+
+/**
+ * whether mu-guile is initialized
+ *
+ * @return TRUE if MuGuile is Initialized, FALSE otherwise
+ */
+gboolean mu_guile_initialized();
+
+/**
+ * raise a guile error (based on a GError)
+ *
+ * @param func_name function name
+ * @param err the error
+ *
+ * @return SCM_UNSPECIFIED
+ */
+SCM mu_guile_g_error(const char* func_name, GError* err);
+
+/**
+ * raise a guile error
+ *
+ * @param func_name function
+ * @param status err code
+ * @param fmt format string for error msg
+ * @param args params for format string
+ *
+ * @return SCM_UNSPECIFIED
+ */
+SCM mu_guile_error(const char* func_name, int status, const char* fmt, SCM args);
+
+/**
+ * convert a string into an SCM -- . It assumes str is in UTF8 encoding, and
+ * replace characters with '?' if needed.
+ *
+ * @param str a string
+ *
+ * @return a guile string or #f for empty
+ */
+SCM mu_guile_scm_from_string(const std::string& str);
+
+/**
+ * Initialize this mu guile module.
+ *
+ * @param data
+ *
+ * @return
+ */
+extern "C" {
+void* mu_guile_init(void* data);
+}
+#endif /*__MU_GUILE_H__*/
diff --git a/guile/mu-guile.texi b/guile/mu-guile.texi
new file mode 100644 (file)
index 0000000..ae238b2
--- /dev/null
@@ -0,0 +1,995 @@
+\input texinfo.tex    @c -*-texinfo-*-
+@c %**start of header
+@setfilename mu-guile.info
+@settitle mu-guile user manual
+
+@c Use proper quote and backtick for code sections in PDF output
+@c Cf. Texinfo manual 14.2
+@set txicodequoteundirected
+@set txicodequotebacktick
+
+@documentencoding UTF-8
+@c %**end of header
+
+@include version.texi
+
+@copying
+Copyright @copyright{} 2012 Dirk-Jan C. Binnema
+
+@quotation
+Permission is granted to copy, distribute and/or modify this document
+under the terms of the GNU Free Documentation License, Version 1.3 or
+any later version published by the Free Software Foundation; with no
+Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.  A
+copy of the license is included in the section entitled ``GNU Free
+Documentation License.''
+@end quotation
+@end copying
+
+@titlepage
+@title @t{mu-guile} - extending @t{mu} with Guile Scheme
+@subtitle version  @value{VERSION}
+@author Dirk-Jan C. Binnema
+
+@c The following two commands start the copyright page.
+@page
+@vskip 0pt plus 1filll
+@insertcopying
+@end titlepage
+
+@dircategory The Algorithmic Language Scheme
+@direntry
+* Mu-guile: (mu-guile).        Guile-bindings for the mu e-mail indexer/searcher
+@end direntry
+
+@contents
+
+@ifnottex
+@node Top
+@top mu-guile manual
+@end ifnottex
+
+@iftex
+@node Welcome to mu-guile
+@unnumbered Welcome to mu-guile
+@end iftex
+
+Welcome to @t{mu-guile}!
+
+@t{mu} is a program for indexing and searching your e-mails. It can search
+your messages in many different ways, but sometimes that may not be
+enough. If you have very specific queries, or want do generate some
+statistics, you need some more power.
+
+@t{mu-guile} is made for those cases. @t{mu-guile} exposes the internals of
+@t{mu} and its database to the @t{guile} programming language. Guile is the
+@emph{GNU Ubiquitous Intelligent Language for Extensions} - a version of the
+@emph{Scheme} programming language and the official GNU extension language.
+
+Guile/Scheme is a member of the @emph{Lisp} family of programming languages --
+like emacs-lisp, @emph{Racket}, Common Lisp. If you're not familiar with
+Scheme, @t{mu-guile} is an excellent opportunity to learn a bit about!
+
+Trust me, it's not very hard -- and it's @emph{fun}!
+
+@menu
+* Getting started::
+* Initializing mu-guile::
+* Messages::
+* Contacts::
+* Attachments and other parts::
+* Statistics::
+* Plotting data::
+* Writing scripts::
+
+Appendices
+
+* Recipes:: Snippets do specific things
+* GNU Free Documentation License::  The license of this manual.
+@end menu
+
+@node Getting started
+@chapter Getting started
+
+@menu
+* Installation::
+* Making sure it works::
+@end menu
+
+This chapter walks you through the installation and the basic setup.
+
+@node Installation
+@section Installation
+
+@t{mu-guile} is part of @t{mu} - by installing the latter, the former is
+necessarily installed as well. At the time of writing, there are no
+distribution-provided packaged versions of @t{mu-guile}; so for now, you need
+to follow the steps below.
+
+@subsection Guile 2.x
+
+@t{mu-guile} is built automatically when @t{mu} is built, if you have
+@t{guile} version 2 or higher. (@t{mu} checks for this during
+@t{configure}). Thus, the first step is to ensure you have @t{guile}
+installed.
+
+On Debian/Ubuntu you can install @t{guile} 2.x using the @t{guile-2.0-dev}
+package (and its dependencies):
+@example
+$ sudo apt-get install guile-2.0-dev
+@end example
+
+At the time of writing, there are no official packages for
+Fedora@footnote{@url{https://bugzilla.redhat.com/show_bug.cgi?id=678238}}.  If
+you are using Fedora or any other system that does not have packages, you need
+to compile @t{guile} from
+source@footnote{@url{http://www.gnu.org/software/guile/manual/html_node/Obtaining-and-Installing-Guile.html#Obtaining-and-Installing-Guile}}.
+
+@subsection gnuplot
+
+For creating graphs with @t{mu-guile}, you need the @t{gnuplot} program --
+most likely, there is a package available for your system; for example:
+
+@example
+$ sudo apt-get install gnuplot
+@end example
+
+and in Fedora:
+
+@example
+$ sudo yum install gnuplot
+@end example
+
+@subsection mu
+
+Assuming @t{guile} 2.x is installed correctly, @t{mu} finds it during its
+@t{configure}-stage, and creates @t{mu-guile}. Building @t{mu} follows the
+normal steps -- please see the @t{mu} documentation for the details.
+
+The output of @t{./configure} should end with a little text describing the
+detected versions of various libraries @t{mu} depends on. In particular, it
+should mention the @t{guile} version, e.g.
+
+@example
+Guile version                        : 2.0.3.82-a2c66
+@end example
+
+If you don't see any line referring to @t{guile}, please install it, and run
+@t{configure} again. After a successful @t{./configure}, we can make and
+install the package:
+
+@example
+$ make && sudo make install
+@end example
+
+@subsection mu-guile
+
+After this, @t{mu} and @t{mu-guile} are installed -- usually somewhere under
+@t{/usr/local}.You may need to update @t{guile}'s @code{%load-path} to find it
+there. You can check the current @code{%load-path} with the following:
+
+@example
+guile -c '(display %load-path)(newline)'
+@end example
+
+If necessary, you can add the @t{%load-path} by adding to your
+@file{~/.guile}:
+
+@lisp
+(set! %load-path (cons "/usr/local/share/guile/site/2.0" %load-path))
+@end lisp
+
+Or, alternatively, you can set @t{GUILE_LOAD_PATH}:
+@example
+export GUILE_LOAD_PATH=/usr/local/share/guile/site/2.0
+@end example
+
+In both cases the directory should be the directory that contains the
+installed @t{mu.scm}; if you installed @t{mu} under a different prefix, you
+must change the @code{%load-path} accordingly. After this, you should be ready
+to go!
+
+Furthermore, you need to ensure that @t{guile} can find the mu-guile
+library; for this we can use @code{LTDL_LIBRARY_PATH}, e.g.
+@example
+export LTDL_LIBRARY_PATH=/usr/local/lib
+@end example
+
+@node Making sure it works
+@section Making sure it works
+
+Assuming @t{mu-guile} has been installed correctly (@ref{Installation}), and
+also assuming that you have already indexed your e-mail messages (if
+necessary, see the @t{mu-index} man-page), we are ready to start @t{mu-guile};
+a session may look something like this:
+
+@cartouche
+@verbatim
+GNU Guile 2.0.5.123-4bd53
+Copyright (C) 1995-2012 Free Software Foundation, Inc.
+
+Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
+This program is free software, and you are welcome to redistribute it
+under certain conditions; type `,show c' for details.
+
+Enter `,help' for help.
+scheme@(guile-user)>
+@end verbatim
+@end cartouche
+
+@noindent
+Now, copy-paste the following after the prompt:
+
+@cartouche
+@lisp
+(use-modules (mu))
+(mu:initialize)
+(for-each
+    (lambda(msg)
+       (format #t "Subject: ~a\n" (mu:subject msg)))
+        (mu:message-list "hello"))
+@end lisp
+@end cartouche
+
+@noindent
+After pressing @key{Enter}, you should get a list of all subjects of messages
+that match @t{hello}:
+
+@verbatim
+...
+Subject: RE: The Bird Serpent War Cataclysm
+Subject: Hello!
+Subject: Re: post-run tomorrow
+Subject: When all is lost
+...
+@end verbatim
+
+@noindent
+If all this works, congratulations! @t{mu-guile} is installed now, ready to
+serve your every searching need!
+
+@node Initializing mu-guile
+@chapter Initializing mu-guile
+
+We now have installed @t{mu-guile}, and in @ref{Making sure it works}
+confirmed that things work by trying some simple script. In this and the
+following chapters, we take a closer look at programming with @t{mu-guile}.
+
+It is possible to write separate programs with @t{mu-guile}, but for now we'll
+do things @emph{interactively}, that is, from the Guile-prompt
+(``@abbr{REPL}'').
+
+As we have seen, we start our @t{mu-guile} session by starting @t{guile}:
+
+@verbatim
+$ guile
+@end verbatim
+
+@cartouche
+@verbatim
+GNU Guile 2.0.5.123-4bd53
+Copyright (C) 1995-2012 Free Software Foundation, Inc.
+
+Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
+This program is free software, and you are welcome to redistribute it
+under certain conditions; type `,show c' for details.
+
+Enter `,help' for help.
+scheme@(guile-user)>
+@end verbatim
+@end cartouche
+
+The first thing we need to do is loading the modules. All the basics are in
+the @t{(mu)} module, with some statistical extras in @t{(mu stats)}, and some
+graph plotting functionality in @t{(mu plot)}@footnote{@code{(mu plot)}
+requires the @t{gnuplot} program}. Let's load all of them:
+@verbatim
+scheme@(guile-user)> (use-modules (mu) (mu stats) (mu plot))
+@end verbatim
+
+The first time you do this, @t{guile} will probably respond by showing some
+messages about compiling the modules, and then return to you with another
+prompt. Before we can do anything with @t{mu guile}, we need to initialize the
+system:
+
+@verbatim
+scheme@(guile-user)> (mu:initialize)
+@end verbatim
+
+This opens the database for reading, using the default location of
+@file{~/.cache/mu}@footnote{If you keep your @t{mu} database in a non-standard
+place, use @code{(mu:initialize "/path/to/my/mu/")}}
+
+Now, @t{mu-guile} is ready to go. In the next chapter, we go through the
+modules and show what you can do with them.
+
+@node Messages
+@chapter Messages
+
+In this chapter, we discuss searching messages and doing things with them.
+
+@menu
+* Finding messages:: query for messages in the database
+* Message methods::  what methods are available for messages?
+* Example - the longest subject:: find the messages with the longest subject
+@end menu
+
+@node Finding messages
+@section Finding messages
+Now we are ready to retrieve some messages from the system. There are two main
+procedures to do this:
+
+@itemize
+@item @code{(mu:message-list [<search-expression>])}
+@item @code{(mu:for-each-message <procedure> [<search-expression>])}
+@end itemize
+
+@noindent
+The first procedure, @code{mu:message-list} returns a list of all messages
+matching @t{<search-expression>}; if you leave @t{<search-expression>} out, it
+returns @emph{all} messages. For example, to get all messages with @t{coffee}
+in the subject line:
+
+@verbatim
+scheme@(guile-user)> (mu:message-list "subject:coffee")
+$1 = (#<<mu:message> 9040640> #<<mu:message> 9040630>
+      #<<mu:message> 9040570>)
+@end verbatim
+
+@noindent
+Apparently, we have three messages matching @t{subject:coffee}, so we get a
+list of three @code{<mu:message>} objects. Let's just use the
+@code{mu:subject} procedure ('method') provided by @code{<mu:message>} objects
+to retrieve the subject-field (more about methods in the next section).
+
+For your convenience, @t{guile} has saved the result of our last query in a
+variable called @t{$1}, so to get the subject of the first message in the
+list, we can do:
+
+@verbatim
+scheme@(guile-user)> (mu:subject (car $1))
+$2 = "Re: best coffee ever!"
+@end verbatim
+
+@noindent
+The second procedure we mentioned, @code{mu:for-each-message}, executes some
+procedure for each message matched by the search expression (or @emph{all}
+messages if the search expression is omitted):
+
+@verbatim
+scheme@(guile-user)> (mu:for-each-message
+                       (lambda(msg)
+                         (display (mu:subject msg))
+                         (newline))
+                      "subject:coffee")
+Re: best coffee ever!
+best coffee ever!
+Coffee beans
+scheme@(guile-user)>
+@end verbatim
+
+@noindent
+Using @code{mu:message-list} and/or
+@code{mu:for-each-message}@footnote{Implementation node:
+@code{mu:message-list} is implemented in terms of @code{mu:for-each-message},
+not the other way around. Due to the way @t{mu} works,
+@code{mu:for-each-message} is rather more efficient than a combination of
+@code{for-each} and @code{mu:message-list}} and a couple of @t{<mu:message>}
+methods, together with what Guile/Scheme provides, should allow for many
+interesting programs.
+
+@node Message methods
+@section Message methods
+
+Now that we've seen how to retrieve lists of message objects
+(@code{<mu:message>}), let's see what we can do with such an object.
+
+@code{<mu:message>} defines the following methods that all take a single
+@code{<mu:message>} object as a parameter. We won't go into the exact meanings
+for all of these procedures here - for the details about various flags /
+properties, please refer to the @t{mu-find} man-page.
+
+@itemize
+@item @code{(mu:bcc msg)}: the @t{Bcc} field of the message, or @t{#f} if there is none
+@item @code{(mu:body-html msg)}: : the html body of the message, or @t{#f} if there is none
+@item @code{(mu:body-txt msg)}: the plain-text body of the message, or @t{#f} if there is none
+@item @code{(mu:cc msg)}: the @t{Bcc} field of the message, or @t{#f} if there is none
+@item @code{(mu:date msg)}: the @t{Date} field of the message, or 0 if there is none
+@item @code{(mu:flags msg)}: list of message-flags for this message
+@item @code{(mu:from msg)}: the @t{From} field of the message, or @t{#f} if there is none
+@item @code{(mu:maildir msg)}: the maildir this message lives in, or @t{#f} if there is none
+@item @code{(mu:message-id msg)}: the @t{Message-Id} field of the message, or @t{#f} if there is none
+@item @code{(mu:path msg)}: the file system path for this message
+@item @code{(mu:priority msg)}: the priority of this message (either @t{mu:prio:low}, @t{mu:prio:normal} or @t{mu:prio:high}
+@item @code{(mu:references msg)}: the list of messages (message-ids) this message
+refers to in(mu: the @t{References:} header
+@item @code{(mu:size msg)}: size of the message in bytes
+@item @code{(mu:subject msg)}: the @t{Subject} field of the message, or @t{#f} if there is none.
+@item @code{(mu:tags msg)}: list of tags for this message
+@item @code{(mu:timestamp msg)}: the timestamp (mtime) of the message file, or
+#f if there is none.
+message file
+@item @code{(mu:to msg)}: the sender of the message, or @t{#f} if there is none
+@end itemize
+
+With these methods, we can query messages for their properties; for example:
+
+@verbatim
+scheme@(guile-user)> (define msg (car (mu:message-list "snow")))
+scheme@(guile-user)> (mu:subject msg)
+$1 = "Re: Running in the snow is beautiful"
+scheme@(guile-user)> (mu:flags msg)
+$2 = (mu:flag:replied mu:flag:seen)
+scheme@(guile-user)> (strftime "%F" (localtime (mu:date msg)))
+$3 = "2011-01-15"
+@end verbatim
+
+There are a couple more methods:
+@itemize
+@item @code{(mu:header msg "<header-name>")} returns an arbitrary message
+header (or @t{#f} if not found) -- e.g. @code{(header msg "User-Agent")}
+@item If you include the @t{mu contact} module, the @code{(mu:contacts
+msg [contact-type])} method (to get a list of contacts) is
+added. @xref{Contacts}.
+@item If you include the @t{mu part} module, the @code{((mu:parts msg)} and
+@code{(mu:attachments msg)} methods are added. @xref{Attachments and other parts}.
+@end itemize
+
+@node Example - the longest subject
+@section Example - the longest subject
+
+Now, let's write a little example -- let's find out what is the @emph{longest
+subject} of any e-mail messages we received in the year 2011.  You can try
+this if you put the following in a separate file, make it executable, and run
+it like any program.
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu))
+(use-modules (srfi srfi-1))
+
+(mu:initialize)
+
+;; note: (subject msg) => #f if there is no subject
+(define list-of-subjects
+  (map (lambda (msg)
+      (or (mu:subject msg) "")) (mu:message-list "date:2011..2011")))
+;; see the mu-find manpage for the date syntax
+
+(define longest-subject
+  (fold (lambda (subj1 subj2)
+      (if (> (string-length subj1) (string-length subj2))
+       subj1 subj2))
+    "" list-of-subjects))
+
+(format #t "Longest subject: ~s\n" longest-subject)
+@end lisp
+
+There are many other ways to solve the same problem, for example by using an
+iterative approach with @code{mu:for-each-message}, but it should show how one
+can easily write little programs to answer specific questions about your
+e-mail corpus.
+
+@node Contacts
+@chapter Contacts
+
+We can retrieve the sender and recipients of an e-mail message using methods
+like @code{mu:from}, @code{mu:to} etc.; @xref{Message methods}. These
+procedures return the list of recipients as a single string; however, often it
+is more useful to deal with recipients as separate objects.
+
+@menu
+* Contact procedures and objects::
+* All contacts::
+* Utility procedures::
+* Example - mutt export::
+@end menu
+
+
+@node Contact procedures and objects
+@section Contact procedures and objects
+
+Message objects (@pxref{Messages}) have a method @t{mu:contacts}:
+
+    @code{(mu:contacts <message-object> [<contact-type>])}
+
+The @t{<contact-type>} is a symbol, one of @code{mu:to}, @code{mu:from},
+@code{mu:cc} or @code{mu:bcc}. This will then get the contact objects for the
+contacts of the corresponding type. If you leave out the contact-type (or
+specify @t{#t} for it, you will get a list of @emph{all} contact objects for
+the message.
+
+A contact object (@code{<mu:contact>}) has two methods:
+@itemize
+@item @code{mu:name} returns the name of the contact, or #f if there is none
+@item @code{mu:email} returns the e-mail address of the contact, or #f if there is none
+@end itemize
+
+Let's get a list of all names and e-mail addresses in the 'To:' field, of
+messages matching 'book':
+
+@lisp
+(use-modules (mu))
+(mu:initialize)
+(mu:for-each-message
+   (lambda (msg)
+      (for-each
+        (lambda (contact)
+           (format #t "~a => ~a\n"
+             (or (mu:email contact) "") (or (mu:name contact) "no-name")))
+        (mu:contacts msg mu:contact:to)))
+    "book")
+@end lisp
+
+This shows what the methods do, but for many uses, it would be more useful to
+have each of the contacts only show up @emph{once} - for that, please refer to
+@xref{All contacts}.
+
+@node All contacts
+@section All contacts
+
+Sometimes you may want to inspect @emph{all} the different contacts in the
+@t{mu} database. This is useful, for instance, when exporting contacts to some
+external format that can then be important in an e-mail program.
+
+To enable this, there is the procedure @code{mu:for-each-contact}, defined as
+
+   @code{(mu:for-each-contact procedure [search-expression])}.
+
+This will aggregate the unique contacts from @emph{all} messages matching
+@t{<search-expression>} (when it is left empty, it will match all messages in
+the database), and execute @t{procedure} for each of them.
+
+The @t{procedure} receives an object of the type @t{<mu:contact-with-stats>},
+which is a @emph{subclass} of the @t{<mu:contact>} class discussed in
+@xref{Contact procedures and objects}. @t{<mu:contact-with-stats>} objects
+expose the following additional methods:
+
+@itemize
+@item @code{(mu:frequency <contact>)}: returns the @emph{number of times} this contact occurred in
+one of the address fields
+@item @code{(mu:last-seen <contact>)}: returns the @emph{most recent time} the contact was
+seen in one of the address fields, as a @t{time_t} value
+@end itemize
+
+The method assumes an e-mail address is unique for a certain contact; if a
+certain e-mail address occurs with different names, it uses the most recent
+non-empty name.
+
+@node Utility procedures
+@section Utility procedures
+
+To make dealing with contacts even easier, there are a number of utility
+procedures that can save you a bit of typing.
+
+For converting contacts to some textual form, there is @code{(mu:contact->string
+<mu:contact> format)}, which takes a contact and returns a text string with
+the given format. Currently supported formats are @t{"org-contact}, @t{"mutt-alias"},
+@t{"mutt-ab"}, @t{"wanderlust"} and @t{"plain"}.
+
+
+@node Example - mutt export
+@section Example - mutt export
+
+Let's see how we could export the addresses in the @t{mu} database to the
+addressbook format of the venerable
+@t{mutt}@footnote{@url{http://www.mutt.org/}} e-mail client.
+
+The addressbook format that @t{mutt} uses is a sequence of lines that look
+something like:
+@verbatim
+alias <nick> [<name>] "<" <email> ">"
+@end verbatim
+
+@t{mu guile} provides the procedure @code{(mu:contact->string <mu:contact>
+format)} that we can use to do the conversion.
+
+We may want to focus on people with whom we have frequent correspondence; so
+we may want to limit ourselves to people we have seen at least 10 times in the
+last year.
+
+It is a bit hard to @emph{guess} the nick name for e-mail contacts, but
+@code{mu:contact->string} tries something based on the name. You can always
+adjust them later by hand, obviously.
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu))
+(mu:initialize)
+
+;; Get a list of contacts that were seen at least 20 times since 2010
+(define (selected-contacts)
+  (let  ((addrs '())
+         (start (car (mktime (car (strptime "%F" "2010-01-01")))))
+         (minfreq 20))
+    (mu:for-each-contact
+      (lambda (contact)
+       (if (and (mu:email contact)
+             (>= (mu:frequency contact) minfreq)
+             (>= (mu:last-seen contact) start))
+         (set! addrs (cons contact addrs)))))
+      addrs))
+
+(for-each
+  (lambda (contact)
+    (format #t "~a\n" (mu:contact->string contact "mutt-alias")))
+  (selected-contacts))
+@end lisp
+
+This simple program could be improved in many ways; this is left as an
+exercise to the reader.
+
+@node Attachments and other parts
+@chapter Attachments and other parts
+
+To deal with @emph{attachments}, or, more in general @emph{MIME-parts}, there
+is the @t{mu part} module.
+
+@menu
+* Parts and their methods::
+* Attachment example::
+@end menu
+
+@node Parts and their methods
+@section Parts and their methods
+The module defines the @code{<mu-part>} class, and adds two methods to
+@code{<mu:message>} objects:
+@itemize
+@item @code{(mu:parts msg)} - returns a list @code{<mu-part>} objects, one for
+each MIME-parts in the message.
+@item @code{(mu:attachments msg)} - like @code{parts}, but only list those MIME-parts
+that look like proper attachments.
+@end itemize
+
+A @code{<mu:part>} object exposes a few methods to get information about the
+part:
+@itemize
+@item @code{(mu:name <part>)} - returns the file name of the mime-part, or @code{#f} if
+there is none.
+@item @code{(mu:mime-type <part>)} - returns the mime-type of the mime-part, or @code{#f}
+if there is none.
+@item @code{(mu:size <part>)} - returns the size in bytes of the mime-part
+@end itemize
+
+@c Then, we may want to save the part to a file; this can be done using either:
+@c @itemize
+@c @item @code{(mu:save part <part>)} - save a part to a temporary file, return the file
+@c name@footnote{the temporary filename is a predictable procedure of (user-id,
+@c msg-path, part-index)}
+@c @item @code{(mu:save-as <part> <path>)} - save part to file at path
+@c @end itemize
+
+@node Attachment example
+@section Attachment example
+
+Let's look at some small example. Let's get a list of the biggest attachments
+in messages about Luxemburg:
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu))
+(mu:initialize)
+
+(define (all-attachments expr)
+  "Return a list of (name . size) for all attachments in messages
+matching EXPR."
+  (let ((pairs '()))
+    (mu:for-each-message
+      (lambda (msg)
+       (for-each
+         (lambda (att) ;; add (filename . size) to the list
+           (set! pairs (cons (cons (mu:name att) (or (mu:size att) 0)) pairs)))
+         (mu:attachments msg)))
+      expr)
+    pairs))
+
+(for-each
+  (lambda (att)
+    (format #t "~a: ~,1fKb\n"
+      (car att) (exact->inexact (/ (cdr att) 1024))))
+  (sort (all-attachments "Luxemburg")
+    (lambda (att1 att2)
+      (< (cdr att1) (cdr att2)))))
+@end lisp
+
+As an exercise for the reader, you might want to re-rewrite the
+@code{all-attachments} in terms of @code{mu:message-list}, which would
+probably be a bit more elegant.
+
+
+@node Statistics
+@chapter Statistics
+
+@t{mu-guile} offers some convenience procedures to determine various statistics
+about the messages in the database.
+
+@menu
+* Basics:: @code{mu:count}, @code{mu:average}, ...
+* Tabulating values:: @code{mu:tabulate}
+* Most frequent values:: @code{mu:top-n-most-frequent}
+@end menu
+
+@node Basics
+@section Basics
+
+Let's look at some of the basic statistical operations available, in an
+interactive session:
+@example
+GNU Guile 2.0.5.123-4bd53
+Copyright (C) 1995-2012 Free Software Foundation, Inc.
+
+Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
+This program is free software, and you are welcome to redistribute it
+under certain conditions; type `,show c' for details.
+
+Enter `,help' for help.
+scheme@@(guile-user)> ;; load modules, initialize mu
+scheme@@(guile-user)> (use-modules (mu) (mu stats))
+scheme@@(guile-user)> (mu:initialize)
+scheme@@(guile-user)>
+scheme@@(guile-user)> ;; count the number of messages with 'hello' in their subject
+scheme@@(guile-user)> (mu:count "subject:hello")
+$1 = 162
+scheme@@(guile-user)> ;; average the size of messages with hello in their subject
+scheme@@(guile-user)> (mu:average mu:size "subject:hello")
+$2 = 34597733/81
+scheme@@(guile-user)> (exact->inexact $2)
+$3 = 427132.506172839
+scheme@@(guile-user)> ;; calculate the correlation between message size and
+scheme@@(guile-user)> ;; subject length
+scheme@@(guile-user)> (mu:correl mu:size (lambda (msg)
+                        (string-length (mu:subject msg))) "subject:hello")
+$5 = -0.10804368622292
+scheme@@(guile-user)>
+@end example
+
+@node Tabulating values
+@section Tabulating values
+
+@code{(mu:tabulate <procedure> [<search-expr>])} applies @t{<procedure>} to each
+message matching @t{<search-expr>} (leave empty to match @emph{all} messages),
+and returns a associative list (a list of pairs) with each of the different
+results of @t{<procedure>} and their frequencies. For fields that contain lists
+of values (such as address-fields), each of the values in the list is added
+separately.
+
+@subsection Example: messages per weekday
+
+We demonstrate @code{mu:tabulate} with an example. Suppose we want to know how
+many messages we receive per weekday:
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu) (mu stats) (mu plot))
+(mu:initialize)
+
+;; create a list like (("Sun" . 13) ("Mon" . 23) ...)
+(define weekday-table
+  (mu:weekday-numbers->names
+    (sort
+      (mu:tabulate
+       (lambda (msg)
+         (tm:wday (localtime (mu:date msg)))))
+      (lambda (a b) (< (car a) (car b))))))
+
+(for-each
+  (lambda (elm)
+    (format #t "~a: ~a\n" (car elm) (cdr elm)))
+  weekday-table)
+@end lisp
+
+
+The procedure @code{weekday-table} uses @code{mu:tabulate-message} to get the
+frequencies per hour -- this returns a list of pairs:
+@verbatim
+((5 . 2339) (0 . 2278) (4 . 2800) (2 . 3184) (6 . 1856) (3 . 2833) (1 . 2993))
+@end verbatim
+
+We sort these pairs by the day number, and then apply
+@code{mu:weekday-numbers->names}, which takes the list, and returns a list
+where the day numbers are replace by there abbreviated name (in the current
+locale). Note, there is also @code{mu:month-numbers->names}.
+
+The script then outputs these numbers in the following form:
+
+@verbatim
+Sun: 2278
+Mon: 2993
+Tue: 3184
+Wed: 2833
+Thu: 2800
+Fri: 2339
+Sat: 1856
+@end verbatim
+
+Clearly, Saturday is a slow day for e-mail...
+
+@node Most frequent values
+@section Most frequent values
+
+In the above example, the number of values is small (the seven weekdays);
+however, in many cases there can be many different values (for example, all
+different message subjects), many of which may not be very interesting -- all
+we need to know is the top-10 of most frequently seen values.
+
+This is fairly easy to achieve using @code{mu:tabulate} -- to get the top-10
+subjects@footnote{this requires the @code{(srfi srfi-1)}-module}, we can use
+something like this:
+@lisp
+(take
+  (sort
+    (mu:tabulate mu:subject)
+    (lambda (a b) (> (cdr a) (cdr b))))
+  10)
+@end lisp
+
+If this is not short enough, @t{mu-guile} offers a convenience procedure to do
+this: @code{mu:top-n-most-frequent}. For example, to get the top-10 people we
+sent mail to most often:
+
+@lisp
+(mu:top-n-most-frequent mu:to 10 "maildir:/sent")
+@end lisp
+
+Can't make it much easier than that!
+
+
+@node Plotting data
+@chapter Plotting data
+
+You can plot the results in the format produced by @code{mu:tabulate} with the
+@t{(mu plot)} module, an experimental module that requires the
+@t{gnuplot}@footnote{@url{http://www.gnuplot.info/}} program to be installed
+on your system.
+
+The @code{mu:plot-histogram} procedure takes the following arguments:
+
+@code{(mu:plot-histogram <data> <title> <x-label> <y-label> [<want-ascii>])}
+
+Here, @code{<data>} is a table of data in the format that @code{mu:tabulate}
+produces. @code{<title>}, @code{<x-label>} and @code{<y-lablel>} are,
+respectively, the title of the graph, and the labels for X- and
+Y-axis. Finally, if you pass @t{#t} for the final @code{<want-ascii>}
+parameter, a plain-text rendering of the graph will be produced; otherwise, a
+graphical window will be shown.
+
+An example should clarify how this works in practice; let's plot the number of
+message per hour:
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu) (mu stats) (mu plot))
+(mu:initialize)
+
+(define (mail-per-hour-table)
+  (sort
+    (mu:tabulate
+      (lambda (msg)
+       (tm:hour (localtime (mu:date msg)))))
+    (lambda (x y) (< (car x) (car y)))))
+
+(mu:plot-histogram (mail-per-hour-table) "Mail per hour" "Hour" "Frequency")
+@end lisp
+
+@cartouche
+@verbatim
+                                Mail per hour
+Frequency
+  1200 ++--+--+--+--+-+--+--+--+--+-+--+--+--+-+--+--+--+--+-+--+--+--+--++
+       |+  +  +         +  + +  + "/tmp/fileHz7D2u" using 2:xticlabels(1) ********
+  1100 ++                                                           *** +*
+       ****                                                         * *  *
+  1000 *+ *                                                      **** * +*
+       *  *                          ******  ****                * ** *  *
+   900 *+ *                          * ** ****  *             **** ** * +*
+       *  *                          * ** *  *  *********     * ** ** *  *
+   800 *+ *                       **** ** *  *  *  * ** *     * ** ** * +*
+   700 *+ ***                  ****  * ** *  *  *  * ** ****  * ** ** * +*
+       *  * *               ****  *  * ** *  *  *  * ** *  **** ** ** *  *
+   600 *+ * ****            *  *  *  * ** *  *  *  * ** *  *  * ** ** * +*
+       *  * ** *            *  *  *  * ** *  *  *  * ** *  *  * ** ** *  *
+   500 *+ * ** *            *  *  *  * ** *  *  *  * ** *  *  * ** ** * +*
+       *  * ** ****       ***  *  *  * ** *  *  *  * ** *  *  * ** ** *  *
+   400 *+ * ** ** ****    * *  *  *  * ** *  *  *  * ** *  *  * ** ** * +*
+       *+ *+**+**+* +*******+* +* +*+ *+**+* +*+ *+ *+**+* +*+ *+**+**+* +*
+   300 ********************************************************************
+       0  1  2  3  4 5  6  7  8  910 11 12 1314 15 16 17 1819 20 21 22 23
+                                       Hour
+@end verbatim
+@end cartouche
+
+@node Writing scripts
+@chapter Writing scripts
+
+The @t{mu} program has built-in support for running guile-scripts, and comes
+with a number of examples.
+
+You can get a list of all scripts with the @t{mu script} command:
+@verbatim
+$ mu script
+Available scripts (use --verbose for details):
+  * find-dups: find duplicate messages
+  * msgs-count: count the number of messages matching some query
+  * msgs-per-day: graph the number of messages per day
+  * msgs-per-hour: graph the number of messages per hour
+  * msgs-per-month: graph the number of messages per month
+  * msgs-per-year: graph the number of messages per year
+  * msgs-per-year-month: graph the number of messages per year-month
+@end verbatim
+
+You can then execute such a script by its name:
+@verbatim
+$ mu msgs-per-month --textonly --query=hello
+
+
+                        Messages per month matching hello
+
+  240 ++-+-----+----+-----+-----+-----+----+-----+-----+-----+----+-----+-++
+      |  +     +    +     + "/tmp/filewi9H0N" using 2:xticlabels(1) ****** |
+  220 ++                                       *    *                 ******
+      |                                        *    *                 *    *
+  200 ++                                       *    *                 *   +*
+      |                                        *    *                 *    *
+  180 ++                     ******            *    *                 *   +*
+      |                      *    *            *    *                 *    *
+  160 ******                 *    *            *    *                 *   +*
+      *    *                 *    *            *    *                 *    *
+      *    *******           *    *            *    *     ******      *    *
+  140 *+   **    *           *    *            *    *     *    ********   +*
+      *    **    *******     *    *            *    *     *    **    **    *
+  120 *+   **    **    *******    *            *    *     *    **    **   +*
+      *    **    **    **    *    *            *    *******    **    **    *
+  100 *+   **    **    **    *    *            *    *    **    **    **   +*
+      *  + **  + ** +  ** +  *  + *   +    +   * +  *  + **  + ** +  ** +  *
+   80 **********************************************************************
+        Jan   Feb  Mar   Apr   May   Jun  Jul   Aug   Sep   Oct  Nov   Dec
+                                      Month
+@end verbatim
+
+Please refer to the @t{mu-script} man-page for some details on writing your
+own scripts.
+
+
+@node Recipes
+@appendix Recipes
+
+@itemize
+@item Calculating the average length of subject-lines
+@lisp
+;; the average length of all our
+(let ((len 0) (n 0))
+  (mu:for-each-message
+    (lambda (msg)
+      (set! len (+ len (string-length (or (mu:subject msg) ""))))
+      (set! n (+ n 1))))
+  (if (= n 0) 0 (/ len n)))
+  ;; this gives a rational, exact result;
+  ;; use exact->inexact to get decimals
+
+;; we we can make this short with the mu:average (with (mu stats))
+(mu:average (lambda (msg) (string-length (or (mu:subject msg) ""))))
+
+
+@end lisp
+@end itemize
+
+@node GNU Free Documentation License
+@appendix GNU Free Documentation License
+
+@include fdl.texi
+@bye
diff --git a/guile/mu-guile.x b/guile/mu-guile.x
new file mode 100644 (file)
index 0000000..8aa8020
--- /dev/null
@@ -0,0 +1,4 @@
+/* cpp arguments: mu-guile.cc -DHAVE_CONFIG_H -I. -I.. -I../lib -I/usr/local/include/guile/3.0 -pthread -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -I/usr/include/libmount -I/usr/include/blkid -pthread -fno-strict-aliasing -Wall -Wextra -Wundef -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wformat=2 -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wreturn-type -Wno-overloaded-virtual -Wswitch-enum -Wswitch-default -Wno-error=unused-parameter -Wno-error=missing-field-initializers -Wno-error=overloaded-virtual -Wno-redundant-decls -Wno-missing-declarations -Wno-suggest-attribute=noreturn -O2 -Wno-inline */
+scm_c_define_gsubr (s_mu_initialize, 0, 1, 0, (scm_t_subr) mu_initialize); scm_c_export (s_mu_initialize,   __null  );;
+scm_c_define_gsubr (s_mu_initialized_p, 0, 0, 0, (scm_t_subr) mu_initialized_p); scm_c_export (s_mu_initialized_p,   __null  );;
+scm_c_define_gsubr (s_log_func, 1, 0, 1, (scm_t_subr) log_func);;
diff --git a/guile/mu.scm b/guile/mu.scm
new file mode 100644 (file)
index 0000000..08eae1f
--- /dev/null
@@ -0,0 +1,318 @@
+;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(define-module (mu)
+  :use-module (oop goops)
+  :use-module (ice-9 optargs)
+  :use-module (texinfo string-utils)
+  :export
+  ( ;; classes
+    <mu:message>
+    <mu:contact>
+    <mu:part>
+    ;; general
+;;    mu:initialize
+ ;;   mu:initialized?
+    mu:log-warning
+    mu:log-message
+    mu:log-critical
+    ;; search funcs
+    mu:for-each-message
+    mu:for-each-msg
+    mu:message-list
+    ;; message funcs
+    mu:header
+    ;; message accessors
+    mu:field:bcc
+    mu:field:body-html
+    mu:field:body-txt
+    mu:field:cc
+    mu:field:date
+    mu:field:flags
+    mu:field:from
+    mu:field:maildir
+    mu:field:message-id
+    mu:field:path
+    mu:field:prio
+    mu:field:refs
+    mu:field:size
+    mu:field:subject
+    mu:field:tags
+    mu:field:timestamp
+    mu:field:to
+    ;; contact funcs
+    mu:name
+    mu:email
+    mu:contact->string
+    ;;
+    mu:for-each-contact
+    ;;
+    mu:contacts
+    ;;
+    ;; <mu:contact-with-stats>
+    mu:frequency
+    mu:last-seen
+    ;; parts
+
+    <mu:part>
+    ;; message function
+    mu:attachments
+    mu:parts
+    ;; <mu:part> methods
+    mu:name
+    mu:mime-type
+    ;;     size
+    ;; mu:save
+    ;; mu:save-as
+    ))
+
+;; this is needed for guile < 2.0.4
+(setlocale LC_ALL "")
+
+;; load the binary
+(load-extension "libguile-mu" "mu_guile_init")
+(load-extension "libguile-mu" "mu_guile_message_init")
+
+;; define some dummies so we don't get errors during byte compilation
+(eval-when (compile)
+  (define mu:c:get-field)
+  (define mu:c:get-contacts)
+  (define mu:c:for-each-message)
+  (define mu:c:get-header)
+  (define mu:critical)
+  (define mu:c:log)
+  (define mu:message)
+  (define mu:c:log)
+  (define mu:warning)
+  (define mu:c:log)
+  (define mu:c:get-parts))
+
+(define (mu:log-warning frm . args)
+  "Log FRM with ARGS at warning."
+  (mu:c:log mu:warning frm args))
+
+(define (mu:log-message frm . args)
+  "Log FRM with ARGS at warning."
+  (mu:c:log mu:message frm args))
+
+(define (mu:log-critical frm . args)
+  "Log FRM with ARGS at warning."
+  (mu:c:log mu:critical frm args))
+
+(define-class <mu:message> ()
+  (msg  #:init-keyword #:msg)) ;; the MuMsg-smob we're wrapping
+
+(define-syntax define-getter
+  (syntax-rules ()
+    ((define-getter method-name field)
+      (begin
+       (define-method (method-name (msg <mu:message>))
+         (mu:c:get-field (slot-ref msg 'msg) field))
+       (export method-name)))))
+
+(define-getter mu:bcc       mu:field:bcc)
+(define-getter mu:body-html  mu:field:body-html)
+(define-getter mu:body-txt   mu:field:body-txt)
+(define-getter mu:cc        mu:field:cc)
+(define-getter mu:date      mu:field:date)
+(define-getter mu:flags             mu:field:flags)
+(define-getter mu:from      mu:field:from)
+(define-getter mu:maildir    mu:field:maildir)
+(define-getter mu:message-id mu:field:message-id)
+(define-getter mu:path      mu:field:path)
+(define-getter mu:priority   mu:field:prio)
+(define-getter mu:references mu:field:refs)
+(define-getter mu:size      mu:field:size)
+(define-getter mu:subject    mu:field:subject)
+(define-getter mu:tags      mu:field:tags)
+(define-getter mu:timestamp  mu:field:timestamp)
+(define-getter mu:to        mu:field:to)
+
+(define-method (mu:header (msg <mu:message>) (hdr <string>))
+  "Get an arbitrary header HDR from message MSG; return #f if it does
+not exist."
+  (mu:c:get-header (slot-ref msg 'msg) hdr))
+
+(define* (mu:for-each-message func #:optional (expr #t) (maxresults -1))
+  "Execute function FUNC for each message that matches mu search expression EXPR.
+If EXPR is not provided, match /all/ messages in the store. MAXRESULTS
+specifies the maximum of messages to return, or -1 (the default) for
+no limit."
+  (mu:c:for-each-message
+    (lambda (msg)
+      (func (make <mu:message> #:msg msg)))
+    expr
+    maxresults))
+
+;; backward-compatibility alias
+(define mu:for-each-msg mu:for-each-message)
+
+(define* (mu:message-list #:optional (expr #t) (maxresults -1))
+  "Return a list of all messages matching mu search expression
+EXPR. If EXPR is not provided, return a list of /all/ messages in the
+store. MAXRESULTS specifies the maximum of messages to return, or
+-1 (the default) for no limit."
+  (let ((lst '()))
+    (mu:for-each-message
+      (lambda (m)
+       (set! lst (append! lst (list m)))) expr maxresults)
+    lst))
+
+;; contacts
+(define-class <mu:contact> ()
+  (name #:init-value #f  #:accessor mu:name  #:init-keyword #:name)
+  (email #:init-value #f #:accessor mu:email #:init-keyword #:email))
+
+(define-method (mu:contacts (msg <mu:message>) contact-type)
+  "Get all contacts for MSG of the given CONTACT-TYPE. MSG is of type <mu-message>,
+while contact type is either `mu:contact:to', `mu:contact:cc',
+`mu:contact:from' or `mu:contact:bcc' to get the corresponding type of
+contacts, or #t to get all.
+
+Returns a list of <mu-contact> objects."
+  (map (lambda (pair) ;; a pair (na . addr)
+        (make <mu:contact>  #:name (car pair) #:email (cdr pair)))
+    (mu:c:get-contacts (slot-ref msg 'msg) contact-type)))
+
+(define-method (mu:contacts (msg <mu:message>))
+  "Get contacts of all types for message MSG as a list of <mu-contact>
+objects."
+  (mu:contacts msg #t))
+
+(define-class <mu:contact-with-stats> (<mu:contact>)
+  (tstamp #:init-value 0 #:accessor mu:timestamp #:init-keyword #:timestamp)
+  (last-seen #:init-value 0 #:accessor mu:last-seen)
+  (freq #:init-value 1 #:accessor mu:frequency))
+
+(define* (mu:for-each-contact proc #:optional (expr #t))
+  "Execute PROC for each contact. PROC receives a <mu-contact> instance
+as parameter. If EXPR is specified, only consider contacts in messages
+matching EXPR."
+  (let ((c-hash (make-hash-table 4096)))
+    (mu:for-each-message
+      (lambda (msg)
+       (for-each
+         (lambda (ct)
+           (let ((ct-ws (make <mu:contact-with-stats>
+                          #:name      (mu:name  ct)
+                          #:email     (mu:email ct)
+                          #:timestamp (mu:date msg))))
+             (update-contacts-hash c-hash ct-ws)))
+         (mu:contacts msg #t)))
+      expr)
+    (hash-for-each ;; c-hash now contains a map of email->contact
+      (lambda (email ct-ws) (proc ct-ws)) c-hash)))
+
+(define-method (update-contacts-hash c-hash (nc <mu:contact-with-stats>))
+  "Update the contacts hash with a new and/or existing contact."
+  ;; xc: existing-contact, nc: new contact
+  (let ((xc (hash-ref c-hash (mu:email nc))))
+    (if (not xc) ;; no existing contact with this email address?
+      (hash-set! c-hash (mu:email nc) nc) ;; store the new contact.
+      ;; otherwise:
+      (begin
+       ;; 1) update the frequency for the existing contact
+       (set! (mu:frequency xc) (1+ (mu:frequency xc)))
+       ;; 2) update the name if the new one is not empty and its timestamp is newer
+       ;;    in that case, also update the timestamp
+       (if (and (mu:name nc) (> (string-length (mu:name nc)))
+             (> (mu:timestamp nc) (mu:timestamp xc)))
+         (set! (mu:name xc) (mu:name nc))
+         (set! (mu:timestamp xc) (mu:timestamp nc)))
+       ;; 3) update last-seen with timestamp, if x's timestamp is newer
+       (if (> (mu:timestamp nc) (mu:last-seen xc))
+         (set! (mu:last-seen xc) (mu:timestamp nc)))
+       ;; okay --> now xc has been updated; but it back in the hash
+       (hash-set! c-hash (mu:email xc) xc)))))
+
+(define-method (mu:contact->string (contact <mu:contact>) (form <string>))
+  "Convert a contact to a string in format FORM, which is a string,
+either \"org-contact\", \"mutt-alias\", \"mutt-ab\",
+\"wanderlust\", \"quoted\" \"plain\"."
+  (let* ((name (mu:name contact)) (email (mu:email contact))
+         (nick ;; simplistic nick guessing...
+           (string-map
+             (lambda(kar)
+               (if (char-alphabetic? kar) kar #\_))
+             (string-downcase (or name email)))))
+    (cond
+      ((string= form "plain")
+       (format #f "~a~a~a" (or name "") (if name " " "") email))
+      ((string= form "org-contact")
+       (format #f "* ~s\n:PROPERTIES:\n:EMAIL:~a\n:NICK:~a\n:END:"
+         (or name email) email nick))
+      ((string= form "wanderlust")
+       (format #f "~a ~s ~s"
+         nick (or name email) email))
+      ((string= form "mutt-alias")
+       (format #f "alias ~a ~a <~a>"
+         nick (or name email) email))
+      ((string= form "mutt-ab")
+       (format #f "~a\t~a\t"
+        email (or name "")))
+      ((string= form  "quoted")
+         (string-append
+           "\""
+           (escape-special-chars
+             (string-append
+               (if name
+                 (format #f "\"~a\" " name)
+                 "")
+               (format #f "<~a>" email))
+             "\"" #\\)
+             "\""))
+      (else (error "Unsupported format")))))
+
+;; message parts
+
+
+(define-class <mu:part> ()
+  (msgpath   #:init-value #f #:init-keyword #:msgpath)
+  (index     #:init-value #f #:init-keyword #:index)
+  (name      #:init-value #f #:getter mu:name #:init-keyword #:name)
+  (mime-type #:init-value #f #:getter mu:mime-type #:init-keyword #:mime-type)
+  (size      #:init-value 0  #:getter mu:size #:init-keyword #:size))
+
+(define-method (get-parts (msg <mu:message>) (files-only <boolean>))
+    "Get the part for MSG as a list of <mu:part> objects; if FILES-ONLY is #t,
+only get the part with file names."
+  (map (lambda (part)
+        (make <mu:part>
+          #:msgpath    (list-ref part 0)
+          #:index      (list-ref part 1)
+          #:name       (list-ref part 2)
+          #:mime-type  (list-ref part 3)
+          #:size       (list-ref part 4)))
+    (mu:c:get-parts (slot-ref msg 'msg) files-only)))
+
+(define-method (mu:attachments (msg <mu:message>))
+  "Get the attachments for MSG as a list of <mu:part> objects."
+  (get-parts msg #t))
+
+(define-method (mu:parts (msg <mu:message>))
+  "Get the MIME-parts for MSG as a list of <mu-part> objects."
+  (get-parts msg #f))
+
+;; (define-method (mu:save (part <mu:part>))
+;;   "Save PART to a temporary file, and return the file name. If the
+;; part had a filename, the temporary file's file name will be just that;
+;; otherwise a name is made up."
+;;   (mu:save-part (slot-ref part 'msgpath) (slot-ref part 'index)))
+
+;; (define-method (mu:save-as (part <mu:part>) (filepath <string>))
+;;   "Save message-part PART to file system path PATH."
+;;     (copy-file (save part) filepath))
diff --git a/guile/mu/Makefile.am b/guile/mu/Makefile.am
new file mode 100644 (file)
index 0000000..9339ad9
--- /dev/null
@@ -0,0 +1,26 @@
+## Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+scmdir=${prefix}/share/guile/site/${GUILE_EFFECTIVE_VERSION}/mu/
+
+scm_DATA=              \
+       stats.scm       \
+       plot.scm        \
+       script.scm
+
+EXTRA_DIST=$(scm_DATA)
diff --git a/guile/mu/README b/guile/mu/README
new file mode 100644 (file)
index 0000000..634ad8b
--- /dev/null
@@ -0,0 +1,207 @@
+* OUTDATED *
+
+* README
+
+** What is muile?
+
+  `muile' is a little experiment/toy using the equally experimental mu guile
+  bindings, to be found in libmuguile/ in the top-level source directory.
+
+  `guile'[1] is an interpreter/library for the Scheme programming language[2],
+  specifically meant for extending other programs. It is, in fact, the
+  official GNU language for doing so. 'muile' requires guile 2.x to get the full
+  support.
+
+  Older versions may not support e.g. the 'mu-stats.scm' things discussed below.
+
+  The combination of mu + guile is called `muile', and allows you to write
+  little Scheme-programs to query the mu-database, or inspect individual
+  messages. It is still in an experimental stage, but useful already.
+
+** How do I get it?
+
+   The git-version and the future 0.9.7 version of mu will automatically build
+   muile if you have guile. I've been using guile 2.x from git, but installing
+   the 'guile-1.8-dev' package (Ubuntu/Debian) should do the trick. (I only did
+   very minimal testing with guile 1.8 though).
+
+   Then, configure mu. The configure output should tell you about whether guile
+   was found (and where). If it's found, build mu, and toys/muile should be
+   created, as well.
+
+** What can I do with it?
+
+   Go to toys/muile and start muile. You'll end up with a guile-shell where you
+   can type scheme [1], it looks something like this (for guile 2.x):
+
+   ,----
+   | scheme@(guile-user)>
+   `----
+
+  Now, let's load a message (of course, replace with a message on your system):
+
+   ,----
+   | scheme@(guile-user)> (define msg (mu:msg:make-from-file "/home/djcb/Maildir/cur/12131e7b20a2:2,S"))
+   `----
+
+  This defines a variable 'msg', which holds some message on your file
+  system. It's now easy to inspect this message:
+
+   ,----
+   | scheme@(guile-user)> (define msg (mu:msg:make-from-file "/home/djcb/Maildir/cur/12131e7b20a2:2,S"))
+   `----
+
+   Now, we can inspect this message a bit:
+   ,----
+   | scheme@(guile-user)> (mu:msg:subject msg)
+   | $1 = "See me in bikini :-)"
+   | scheme@(guile-user)> (mu:msg:flags msg)
+   | $2 = (mu:attach mu:unread)
+   `----
+
+   and so on. Note, it's probably easiest to explore the various mu: methods
+   using autocompletion; to enable that make sure you have
+
+
+   ,----
+   | (use-modules (ice-9 readline))
+   | (activate-readline)
+   `----
+
+   in your ~/.guile configuration.
+
+** does this tool have some parameters?
+
+   Yes, there is --muhome to set a non-default place for the message database
+   (see the documentation on --muhome in the mu-find manpage).
+
+   And there is --msg=<path> where you specify some particular message file;
+   it will be available as 'mu:current-msg' in the guile (muile) environment. For
+   example:
+
+   ,----
+   | ./muile --msg=~/Maildir/inbox/cur/1311310172_1234:2,S
+   |  [...]
+   | scheme@(guile-user)> mu:current-msg
+   | $1 = #<msg /home/djcb/Maildir/inbox/cur/1311310172_1234:2,S>
+   | scheme@(guile-user)> (mu:msg:size mu:current-msg)
+   | $2 = 7206
+   `----
+
+** What about searching messages in the database?
+
+   That's easy, too - it does require a little more scheme knowledge. For
+   searching messages there is the mu:store:for-each function, which takes two
+   arguments; the first argument is a function that will be called for each
+   message found. The optional second argument is the search expression (following
+   'mu find' syntax); if don't provide the argument, all messages match.
+
+   So how does this work in practice? Let's see I want to see the subject and
+   sender for messages about milk:
+
+   ,----
+   | (mu:store:for-each (lambda(msg) (format #t "~s ~s\n" (mu:msg:from msg) (mu:msg:subject msg))) "milk")
+   `----
+
+   or slightly more readable:
+
+   ,----
+   | (mu:store:for-each
+   |   (lambda(msg)
+   |     (format #t "~s ~s\n" (mu:msg:from msg) (mu:msg:subject msg)))
+   |   "milk")
+   `----
+
+   As you can see, I provide an anonymous ('lambda') function which will be
+   called for each message matching 'milk'. Admittedly, this requires a bit of
+   Scheme-knowledge... but this time is good as any to learn this nice
+   language.
+
+
+** Can I do some statistics on my messages?
+
+   Yes you can. In fact, it's pretty easy. If you load (in the muile/ directory)
+   the file 'mu-stats.scm':
+
+   ,----
+   | (load "mu-stats.scm")
+   `----
+
+   you'll get a bunch of functions (with names starting with 'mu:stats') to make
+   this very easy. Let's see, suppose I want to see how many messages I get per
+   weekday:
+
+   ,----
+   | scheme@(guile-user)> (mu:stats:per-weekday)
+   | $1 = ((0 . 2255) (1 . 2788) (2 . 2868) (3 . 2599) (4 . 2629) (5 . 2287) (6 . 1851))
+   `----
+
+   Note, Sunday=0, Monday=1 and so on. Apparently, I get/send most of e-mail on
+   Tuesdays, and least on Saturday.
+
+   And note that mu:stats:per-weekdays takes an optional search expression
+   argument, to limit the results to messages matching that, e.g., to only
+   consider messages related to emacs during this year:
+
+   ,----
+   | scheme@(guile-user)> (mu:stats:per-weekday "emacs date:2011..now")
+   | $8 = ((0 . 54) (1 . 22) (2 . 46) (3 . 47) (4 . 39) (5 . 54) (6 . 50))
+   `----
+
+   There's also 'mu:stats:per-month', 'mu:stats:per-year', 'mu:stats:per-hour'.
+   I learnt that during 3-4am I sent/receive only about a third of what I sent
+   during 11-12pm.
+
+** What about getting the top-10 people in the To:-field?
+
+   Easy.
+
+   ,----
+   |  scheme@(guile-user)> (mu:stats:top-n-to)
+   |  $1 = ((("Abc" "myself@example.com") . 4465) (("Def" "somebodyelse@example.com") . 2114)
+   |     (and so on)
+   `----
+
+   I've changed the names a bit to protect the innocent, but what the function
+   does is return a list of pairs of
+
+       (<name> <email>) . <frequency>
+
+   descending in order of frequency. Note, 'mu:stats:top-n-to' takes two
+   optional arguments - first the 'n' in top-n (default is 10), and seconds as
+   search expression to limit the messages considered.
+
+   There are also the functions 'mu:stats:top-n-subject' and
+   'mu:stats:top-n-from' which do the same, mutatis mutandis, and it's quite
+   easy to add your own (see the mu-stats.scm for examples)
+
+** What about showing the results in a table?
+
+   Even easier. Try:
+
+   ,----
+   | (mu:stats:table (mu:stats:top-n-to))
+   `----
+
+   or
+
+   ,----
+   | (mu:stats:table (mu:stats:per-weekday))
+   `----
+
+   You can also export the table:
+
+   ,----
+   | (mu:stats:export (mu:stats:per-weekday))
+   `----
+
+   which will create a temporary file with the results, for further processing
+   in e.g. 'R' or 'gnuplot'.
+
+
+[1] http://www.gnu.org/s/guile/
+[2] http://en.wikipedia.org/wiki/Scheme_(programming_language)
+
+# Local Variables:
+# mode: org; org-startup-folded: nil
+# End:
diff --git a/guile/mu/contact.scm b/guile/mu/contact.scm
new file mode 100644 (file)
index 0000000..843d9c4
--- /dev/null
@@ -0,0 +1,4 @@
+(define-module (mu contact) :use-module(mu))
+(display "(mu contact) is deprecated, please remove from (use-modules ...)")
+(newline)
+
diff --git a/guile/mu/message.scm b/guile/mu/message.scm
new file mode 100644 (file)
index 0000000..bc9b27a
--- /dev/null
@@ -0,0 +1,4 @@
+(define-module (mu message) :use-module (mu))
+(display "(mu message) is deprecated, please remove from (use-modules ...)")
+(newline)
+
diff --git a/guile/mu/part.scm b/guile/mu/part.scm
new file mode 100644 (file)
index 0000000..f9b9cd3
--- /dev/null
@@ -0,0 +1,4 @@
+(define-module (mu part) :use-module (mu))
+(display "(mu part) is deprecated, please remove from (use-modules ...)")
+(newline)
+
diff --git a/guile/mu/plot.scm b/guile/mu/plot.scm
new file mode 100644 (file)
index 0000000..adeb80f
--- /dev/null
@@ -0,0 +1,80 @@
+;;
+;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(define-module (mu plot)
+  :use-module (mu)
+  :use-module (ice-9 popen)
+  :export ( mu:plot ;; alias for mu:plot-histogram
+           mu:plot-histogram
+           ))
+
+(define (export-pairs pairs)
+  "Write a temporary file with the list of PAIRS in table format, and
+return the file name."
+  (let* ((datafile (tmpnam))
+         (output (open datafile (logior O_CREAT O_WRONLY) #O0600)))
+    (for-each
+      (lambda(pair)
+       (display (format #f "~a ~a\n" (car pair) (cdr pair)) output))
+      pairs)
+    (close output)
+    datafile))
+
+(define (find-program-in-path prog)
+  "Find exutable program PROG in PATH; return the full path, or #f if
+not found."
+  (let* ((path (parse-path (getenv "PATH")))
+         (progpath (search-path path prog)))
+    (if (not progpath)
+      #f
+      (if (access? progpath X_OK) ;; is
+       progpath
+       #f))))
+
+(define* (mu:plot-histogram data title x-label y-label
+          #:optional (output "dumb") (extra-gnuplot-opts '()))
+  "Plot DATA with TITLE, X-LABEL and X-LABEL using the gnuplot
+program. DATA is a list of cons-pairs (X . Y).
+
+ OUTPUT is a string
+that determines the type of output that gnuplot produces, depending on
+the system. Which options are available depends on the particulars for
+the gnuplot installation, but typical examples would be \"dumb\" for
+text-only display, \"wxterm\" to write to a graphical window, or
+\"png\" to write a PNG-image to stdout.
+
+EXTRA-GNUPLOT-OPTS is a list
+of any additional options for gnuplot."
+  (if (not (find-program-in-path "gnuplot"))
+    (error "cannot find 'gnuplot' in path"))
+  (let ((datafile (export-pairs data))
+        (gnuplot (open-pipe "gnuplot -p" OPEN_WRITE)))
+    (display (string-append
+              "reset\n"
+              "set term " (or output "dumb") "\n"
+              "set title \"" title "\"\n"
+              "set xlabel \"" x-label "\"\n"
+              "set ylabel \"" y-label "\"\n"
+              "set boxwidth 0.9\n"
+              (string-join extra-gnuplot-opts "\n")
+              "plot \"" datafile "\" using 2:xticlabels(1) with boxes fs solid\n")
+      gnuplot)
+    (close-pipe gnuplot)))
+
+;; backward compatibility
+(define mu:plot mu:plot-histogram)
diff --git a/guile/mu/script.scm b/guile/mu/script.scm
new file mode 100644 (file)
index 0000000..3a62948
--- /dev/null
@@ -0,0 +1,57 @@
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+(define-module (mu script)
+  :export (mu:run-stats))
+
+(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format))
+(use-modules (mu) (mu stats) (mu plot))
+
+(define (help-and-exit)
+  "Show some help."
+  (display
+    (string-append "usage: script [--help] [--textonly] "
+      "[--muhome=<muhome>] [--query=<query>")
+    (newline))
+  (exit 0))
+
+(define (mu:run-stats args func)
+  "Run some statistics function.
+Interpret argument-list ARGS (like command-line
+arguments). Possible arguments are:
+  --help (show some help and exit)
+  --muhome (path to alternative mu home directory)
+  --output (a string describing the output, e.g. \"dumb\", \"png\" \"wxt\")
+  searchexpr (a search query)
+then call FUNC with args SEARCHEXPR and OUTPUT."
+  (setlocale LC_ALL  "")
+  (let* ((optionspec   '((muhome  (value #t))
+                         (query   (value #t))
+                         (output  (value #f))
+                         (help    (single-char #\h) (value #f))))
+         (options (getopt-long args optionspec))
+         (query (option-ref options 'query #f))
+         (help (option-ref options 'help #f))
+         (output (option-ref options 'output #f))
+         (muhome (option-ref options 'muhome #f))
+         (restargs (option-ref options '() #f)))
+    (if help (help-and-exit))
+    (mu:initialize muhome)
+    (func (or query "") output)))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/mu/stats.scm b/guile/mu/stats.scm
new file mode 100644 (file)
index 0000000..90ab836
--- /dev/null
@@ -0,0 +1,165 @@
+;;
+;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(define-module (mu stats)
+  :use-module (oop goops)
+  :use-module (mu)
+  :use-module (srfi srfi-1)
+  :use-module (ice-9 i18n)
+  :use-module (ice-9 r5rs)
+  :export ( mu:tabulate
+           mu:top-n-most-frequent
+           mu:count
+           mu:average
+           mu:stddev
+           mu:correl
+           mu:max
+           mu:min
+           mu:weekday-numbers->names
+           mu:month-numbers->names))
+
+
+(define* (mu:tabulate func #:optional (expr #t))
+  "Execute FUNC for each message matching EXPR, and return an alist
+with maps each result of FUNC to its frequency. If the result of FUNC
+is a list, add each of its values separately.
+ FUNC is a function takes a <mu-message> instance as its argument. For
+example, to tabulate messages by weekday, one could use:
+   (mu:tabulate (lambda(msg) (tm:wday (localtime (date msg))))), and
+get back a list like
+   ((1 . 2) (2 . 5)(3 . 4)(4 . 4)(5 . 12)(6 . 7)(7. 2))."
+  (let* ((table '())
+        ;; func to add a value to our table
+        (update-table
+          (lambda (val)
+            (let ((old-freq (or (assoc-ref table val) 0))) 
+              (set! table (assoc-set! table val (1+ old-freq)))))))
+    (mu:for-each-message
+      (lambda(msg)
+       (let ((val (func msg)))
+         (if (list? val)
+           (for-each update-table val)
+           (update-table val))))
+      expr)
+    table))
+
+(define* (top-n func less n #:optional (expr #t))
+  "Take the results of (mu:tabulate FUNC EXPR), sort using LESS (a
+function taking two arguments A and B (cons cells, (VAL . FREQ)), and
+returns #t if A < B, #f otherwise), and then take the first N."
+  (take (sort (mu:tabulate func expr) less) n))
+
+(define* (mu:top-n-most-frequent func n #:optional (expr #t))
+  "Take the results of (mu:tabulate FUNC EXPR), and return the N items with the highest frequency."
+  (top-n func (lambda (a b) (> (cdr a) (cdr b))) n expr))
+(define* (mu:count #:optional (expr #t))
+  "Count the number of messages matching EXPR. If EXPR is not
+provided, match /all/ messages."
+  (let ((num 0))
+    (mu:for-each-message
+      (lambda (msg) (set! num (1+ num)))
+      expr)
+    num))
+
+(define (average lst)
+  "Calculate the average of a list LST of numbers, or #f if undefined."
+  (if (null? lst)
+    #f
+    (/ (apply + lst) (length lst))))
+
+(define (stddev lst)
+  "Calculate the standard deviation of a list LST of numbers or #f if
+undefined."
+  (let* ((avg (average lst))
+         (sosq (if avg
+                 (apply + (map (lambda (x)(* (- x avg) (- x avg))) lst)))))
+    (if sosq
+      (sqrt (/ sosq (length lst))))))
+
+
+(define* (mu:average func #:optional (expr #t))
+  "Get the average value of FUNC applied to all messages matching
+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
+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))))
+
+(define* (mu:max func #:optional (expr #t))
+  "Get the maximum value of FUNC applied to all messages matching
+EXPR (or #t for all). Returns #f if undefined."
+  (apply max (map func (mu:message-list expr))))
+
+(define* (mu:min func #:optional (expr #t))
+  "Get the minimum value of FUNC applied to all messages matching
+EXPR (or #t for all). Returns #f if undefined."
+  (apply min (map func (mu:message-list expr))))
+
+
+(define (correl lst)
+  "Calculate Pearson's correlation coefficient for a list LST of cons
+pair, where the car and cdr of the pairs are values from data set 1
+and 2, respectively."
+  (let ((n (length lst))
+        (sx (apply + (map car lst)))
+        (sy (apply + (map cdr lst)))
+        (sxy (apply + (map (lambda (cell) (* (car cell) (cdr cell))) lst)))
+        (sxx (apply + (map (lambda (cell) (* (car cell) (car cell))) lst)))
+        (syy (apply + (map (lambda (cell) (* (cdr cell) (cdr cell))) lst))))
+    (/ (- (* n sxy) (* sx sy))
+      (sqrt (* (-  (* n sxx) (* sx sx)) (- (* n syy) (* sy sy)))))))
+
+(define* (mu:correl func1 func2 #:optional (expr #t))
+  "Determine Pearson's correlation coefficient between the value for
+functions FUNC1 and FUNC2 to all messages matching EXPR (or #t for
+all). Returns #f if undefined."
+  (let ((data
+         (map (lambda (msg)
+                (cons (func1 msg) (func2 msg)))
+           (mu:message-list expr))))
+    (if data (correl data) #f)))
+
+
+;; a list of abbreviated, localized day names
+(define day-names
+  (map locale-day-short (iota 7 1)))
+
+(define (mu:weekday-numbers->names table)
+  "Convert a list of pairs with the car denoting a day number (0-6)
+into a list of pairs with the car replaced by the corresponding day
+name (abbreviated) for the current locale."
+    (map
+      (lambda (pair)
+       (cons (list-ref day-names (car pair)) (cdr pair)))
+      table))
+
+;; a list of abbreviated, localized month names
+(define month-names
+  (map locale-month-short (iota 12 1)))
+
+(define (mu:month-numbers->names table)
+    "Convert a list of pairs with the car denoting a month number (0-11)
+into a list of pairs with the car replaced by the corresponding day
+name (abbreviated)."
+    (map
+      (lambda (pair)
+       (cons (list-ref month-names (car pair)) (cdr pair)))
+      table))
diff --git a/guile/scripts/Makefile.am b/guile/scripts/Makefile.am
new file mode 100644 (file)
index 0000000..c846596
--- /dev/null
@@ -0,0 +1,29 @@
+## Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+EXTRA_DIST=                    \
+       msgs-count.scm          \
+       msgs-per-year.scm       \
+       msgs-per-hour.scm       \
+       msgs-per-month.scm      \
+       msgs-per-day.scm        \
+       msgs-per-year-month.scm \
+       find-dups.scm
+
+muguiledistscriptdir      = $(pkgdatadir)/scripts/
+muguiledistscript_SCRIPTS = $(EXTRA_DIST)
diff --git a/guile/scripts/find-dups.scm b/guile/scripts/find-dups.scm
new file mode 100755 (executable)
index 0000000..778acfe
--- /dev/null
@@ -0,0 +1,119 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2013-2015 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: find duplicate messages
+;; INFO: options:
+;; INFO:   --muhome=<muhome>: path to mu home dir
+;; INFO:   --delete: delete all but the first one (experimental, be careful!)
+
+(use-modules (mu) (mu script) (mu stats))
+(use-modules (ice-9 getopt-long) (ice-9 optargs)
+  (ice-9 popen) (ice-9 format) (ice-9 rdelim))
+
+(define (md5sum path)
+  (let* ((port (open-pipe* OPEN_READ "md5sum" path))
+         (md5 (read-delimited " " port)))
+    (close-pipe port)
+    md5))
+(define (find-dups delete expr)
+  (let ((id-table (make-hash-table 20000)))
+    ;; fill the hash with <msgid-size> => <list of paths>
+    (mu:for-each-message
+      (lambda (msg)
+       (let* ((id (format #f "~a-~d" (mu:message-id msg)
+                   (mu:size msg)))
+              (lst (hash-ref id-table id)))
+         (if lst
+           (set! lst (cons (mu:path msg) lst))
+           (set! lst (list (mu:path msg))))
+         (hash-set! id-table id lst)))
+      expr)
+    ;; list all the paths with multiple elements; check the md5sum to
+    ;; make 100%-minus-ε sure they are really the same file.
+    (hash-for-each
+      (lambda (id paths)
+       (if (> (length paths) 1)
+         (let ((hash (make-hash-table 10)))
+           (for-each
+             (lambda (path)
+               (when (file-exists? path)
+                     (let* ((md5 (md5sum path)) (lst (hash-ref hash md5)))
+                       (if lst
+                           (set! lst (cons path lst))
+                           (set! lst (list path)))
+                       (hash-set! hash md5 lst))))
+             paths)
+           ;; hash now maps the md5sum to the messages...
+           (hash-for-each
+             (lambda (md5 mpaths)
+               (if (> (length mpaths) 1)
+                 (begin
+                   ;;(format #t "md5sum: ~a:\n" md5)
+                   (let ((num 1))
+                     (for-each
+                       (lambda (path)
+                         (if (equal? num 1)
+                          (format #t "~a\n" path)
+                          (begin
+                            (format #t "~a: ~a\n" (if delete "deleting" "dup") path)
+                            (if delete (delete-file path))))
+                         (set! num (+ 1 num)))
+                       mpaths)))))
+             hash))))
+      id-table)))
+
+
+
+(define (main args)
+  "Find duplicate messages and, potentially, delete the dups.
+   Be careful with that!
+Interpret argument-list ARGS (like command-line
+arguments). Possible arguments are:
+  --muhome (path to alternative mu home directory).
+  --delete (delete all but the first one). Run mu index afterwards.
+  --expr   (expression to constrain search)."
+  (setlocale LC_ALL "")
+  (let* ((optionspec   '( (muhome     (value #t))
+                          (delete     (value #f))
+                         (expr       (value #t))
+                         (help       (single-char #\h) (value #f))))
+         (options (getopt-long args optionspec))
+         (help (option-ref options 'help #f))
+         (delete (option-ref options 'delete #f))
+         (expr (option-ref options 'expr #t))
+         (muhome (option-ref options 'muhome #f)))
+    (mu:initialize muhome)
+    (find-dups delete expr)))
+
+
+;; Local Variables:
+;; mode: scheme
+;; End:
+
+
+
+
+
+
+
+
+
diff --git a/guile/scripts/msgs-count.scm b/guile/scripts/msgs-count.scm
new file mode 100755 (executable)
index 0000000..923e3a5
--- /dev/null
@@ -0,0 +1,40 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO:   --query=<query>:   limit to messages matching query
+;; INFO:   --muhome=<muhome>: path to mu home dir
+
+(use-modules (mu) (mu script) (mu stats))
+
+(define (count expr output)
+  "Print the total number of messages matching the query EXPR.
+OUTPUT is ignored."
+  (display (mu:count expr))
+  (newline))
+
+(define (main args)
+  (mu:run-stats args count))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/scripts/msgs-per-day.scm b/guile/scripts/msgs-per-day.scm
new file mode 100755 (executable)
index 0000000..824f556
--- /dev/null
@@ -0,0 +1,49 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO:   --query=<query>:   limit to messages matching query
+;; INFO:   --muhome=<muhome>: path to mu home dir
+;; INFO:   --output:          the output format, such as "png", "wxt"
+;; INFO:                      (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-day expr output)
+  "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+  (mu:plot-histogram
+    (mu:weekday-numbers->names
+      (sort (mu:tabulate
+             (lambda (msg)
+               (tm:wday (localtime (mu:date msg)))) expr)
+       (lambda (x y) (< (car x) (car y)))))
+    (format #f "Messages per weekday matching ~a" expr)
+    "Day" "Messages" output))
+
+(define (main args)
+  (mu:run-stats args per-day))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/scripts/msgs-per-hour.scm b/guile/scripts/msgs-per-hour.scm
new file mode 100755 (executable)
index 0000000..d96f9b6
--- /dev/null
@@ -0,0 +1,49 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO:   --query=<query>:   limit to messages matching query
+;; INFO:   --muhome=<muhome>: path to mu home dir
+;; INFO:   --output:          the output format, such as "png", "wxt"
+;; INFO:                      (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-hour expr output)
+  "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+  (mu:plot-histogram
+    (sort
+      (mu:tabulate
+       (lambda (msg)
+         (tm:hour (localtime (mu:date msg)))) expr)
+      (lambda (x y) (< (car x) (car y))))
+    (format #f "Messages per hour matching ~a" expr)
+    "Hour" "Messages" output))
+
+(define (main args)
+  (mu:run-stats args per-hour))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/scripts/msgs-per-month.scm b/guile/scripts/msgs-per-month.scm
new file mode 100755 (executable)
index 0000000..50dbbed
--- /dev/null
@@ -0,0 +1,50 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO:   --query=<query>:   limit to messages matching query
+;; INFO:   --muhome=<muhome>: path to mu home dir
+;; INFO:   --output:          the output format, such as "png", "wxt"
+;; INFO:                      (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-month expr output)
+  "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+  (mu:plot-histogram
+    (mu:month-numbers->names
+      (sort
+       (mu:tabulate
+         (lambda (msg)
+           (tm:mon (localtime (mu:date msg)))) expr)
+       (lambda (x y) (< (car x) (car y)))))
+    (format #f "Messages per month matching ~a" expr)
+    "Month" "Messages" output))
+
+(define (main args)
+  (mu:run-stats args per-month))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/scripts/msgs-per-year-month.scm b/guile/scripts/msgs-per-year-month.scm
new file mode 100755 (executable)
index 0000000..33b1447
--- /dev/null
@@ -0,0 +1,52 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO:   --query=<query>:   limit to messages matching query
+;; INFO:   --muhome=<muhome>: path to mu home dir
+;; INFO:   --output:          the output format, such as "png", "wxt"
+;; INFO:                      (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-year-month expr output)
+  "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+  (mu:plot-histogram
+    (sort (mu:tabulate
+           (lambda (msg)
+             (string->number
+               (format #f "~d~2'0d"
+                 (+ 1900 (tm:year (localtime (mu:date msg))))
+                 (tm:mon (localtime (mu:date msg))))))
+           expr)
+      (lambda (x y) (< (car x) (car y))))
+    (format #f "Messages per year/month matching ~a" expr)
+    "Year/Month" "Messages" output))
+
+(define (main args)
+  (mu:run-stats args per-year-month))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/scripts/msgs-per-year.scm b/guile/scripts/msgs-per-year.scm
new file mode 100755 (executable)
index 0000000..242e299
--- /dev/null
@@ -0,0 +1,48 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: graph the number of messages per day (using gnuplot)
+;; INFO: options:
+;; INFO:   --query=<query>:   limit to messages matching query
+;; INFO:   --muhome=<muhome>: path to mu home dir
+;; INFO:   --output:          the output format, such as "png", "wxt"
+;; INFO:                      (depending on the environment)
+
+(use-modules (mu) (mu script) (mu stats) (mu plot))
+
+(define (per-year expr output)
+  "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+  (mu:plot-histogram
+    (sort (mu:tabulate
+           (lambda (msg)
+             (+ 1900 (tm:year (localtime (mu:date msg))))) expr)
+      (lambda (x y) (< (car x) (car y))))
+    (format #f "Messages per year matching ~a" expr)
+    "Year" "Messages"  output))
+
+(define (main args)
+  (mu:run-stats args per-year))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/guile/tests/meson.build b/guile/tests/meson.build
new file mode 100644 (file)
index 0000000..dc89051
--- /dev/null
@@ -0,0 +1,31 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+
+guile_load_path=':'.join([ # meson 0.56 has project_source_root
+    join_paths(meson.source_root(), 'guile'),
+    join_paths(meson.current_build_dir(), '..')])
+
+test('test-mu-guile',
+     executable('test-mu-guile',
+               'test-mu-guile.cc',
+               install: false,
+                cpp_args: [
+                  '-DABS_SRCDIR="' + meson.current_source_dir() + '"',
+                  '-DGUILE_LOAD_PATH="' + guile_load_path + '"',
+                  '-DGUILE_EXTENSIONS_PATH="' + guile_load_path + '"'
+                ],
+               dependencies: [glib_dep, lib_mu_dep]))
diff --git a/guile/tests/test-mu-guile.cc b/guile/tests/test-mu-guile.cc
new file mode 100644 (file)
index 0000000..a630d16
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+** Copyright (C) 2012-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <lib/mu-query.hh>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "utils/mu-test-utils.hh"
+#include <lib/mu-store.hh>
+#include <utils/mu-utils.hh>
+
+using namespace Mu;
+
+static std::string test_dir;
+
+static std::string
+fill_database(void)
+{
+       const auto cmdline = format(
+               "/bin/sh -c '"
+               "%s init  --muhome=%s --maildir=%s --quiet; "
+               "%s index --muhome=%s  --quiet'",
+               MU_PROGRAM,
+               test_dir.c_str(),
+               MU_TESTMAILDIR2,
+               MU_PROGRAM,
+               test_dir.c_str());
+
+       if (g_test_verbose())
+               g_print("%s\n", cmdline.c_str());
+
+       GError *err{};
+       if (!g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, NULL, &err)) {
+               g_printerr("Error: %s\n", err ? err->message : "?");
+               g_clear_error(&err);
+               g_assert(0);
+       }
+
+       return test_dir;
+}
+
+static void
+test_something(const char* what)
+{
+       g_setenv("GUILE_AUTO_COMPILE", "0", TRUE);
+       g_setenv("GUILE_LOAD_PATH", GUILE_LOAD_PATH, TRUE);
+       g_setenv("GUILE_EXTENSIONS_PATH",GUILE_EXTENSIONS_PATH, TRUE);
+
+       if (g_test_verbose())
+               g_print("GUILE_LOAD_PATH: %s\n", GUILE_LOAD_PATH);
+
+       const auto dir = fill_database();
+       const auto cmdline  = format("%s -q -e main %s/test-mu-guile.scm "
+                                    "--muhome=%s --test=%s",
+                                    GUILE_BINARY,
+                                    ABS_SRCDIR,
+                                    dir.c_str(), what);
+
+       if (g_test_verbose())
+               g_print("cmdline: %s\n", cmdline.c_str());
+
+       GError *err{};
+       int status{};
+       if (!g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, &status, &err) ||
+           status != 0) {
+               g_printerr("Error: %s\n", err ? err->message : "something went wrong");
+               g_clear_error(&err);
+               g_assert(0);
+       }
+}
+
+static void
+test_mu_guile_queries(void)
+{
+       test_something("queries");
+}
+
+static void
+test_mu_guile_messages(void)
+{
+       test_something("message");
+}
+
+static void
+test_mu_guile_stats(void)
+{
+       test_something("stats");
+}
+
+int
+main(int argc, char* argv[])
+{
+       int rv;
+       TempDir tempdir;
+       test_dir = tempdir.path();
+
+       mu_test_init(&argc, &argv);
+
+       if (!set_en_us_utf8_locale())
+               return 0; /* don't error out... */
+
+       g_test_add_func("/guile/queries", test_mu_guile_queries);
+       g_test_add_func("/guile/message", test_mu_guile_messages);
+       g_test_add_func("/guile/stats", test_mu_guile_stats);
+
+       rv = g_test_run();
+
+       return rv;
+}
diff --git a/guile/tests/test-mu-guile.scm b/guile/tests/test-mu-guile.scm
new file mode 100755 (executable)
index 0000000..d4d3740
--- /dev/null
@@ -0,0 +1,124 @@
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; This program is free software; you can redistribute it and/or modify it
+;; under the terms of the GNU General Public License as published by the
+;; Free Software Foundation; either version 3, or (at your option) any
+;; later version.
+;;
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+(setlocale LC_ALL "")
+
+(use-modules (srfi srfi-1))
+(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format))
+(use-modules (mu) (mu stats))
+
+(define (n-results-or-exit query n)
+  "Run QUERY, and exit 1 if the number of results != N."
+  (let ((lst (mu:message-list query)))
+    (if (not (= (length lst) n))
+      (begin
+       (simple-format (current-error-port) "Query: \"~A\"; expected ~A, got ~A\n"
+         query n (length lst))
+       (exit 1)))))
+
+(define (test-queries)
+  "Test a bunch of queries (or die trying)."
+  (n-results-or-exit "hello" 1)
+  (n-results-or-exit "f:john fruit" 1)
+  (n-results-or-exit "f:soc@example.com" 1)
+  (n-results-or-exit "t:alki@example.com" 1)
+  (n-results-or-exit "t:alcibiades" 1)
+  (n-results-or-exit "f:soc@example.com OR f:john" 2)
+  (n-results-or-exit "f:soc@example.com OR f:john OR t:edmond" 3)
+  (n-results-or-exit "t:julius" 1)
+  (n-results-or-exit "s:dude" 1)
+  (n-results-or-exit "t:dantès" 1)
+  (n-results-or-exit "file:sittingbull.jpg" 1)
+  (n-results-or-exit "file:custer.jpg" 1)
+  (n-results-or-exit "file:custer.*" 1)
+  (n-results-or-exit "j:sit*" 1)
+  (n-results-or-exit "mime:image/jpeg" 1)
+  (n-results-or-exit "mime:text/plain" 13)
+  (n-results-or-exit "y:text*" 13)
+  (n-results-or-exit "y:image*" 1)
+  (n-results-or-exit "mime:message/rfc822" 2))
+
+(define (error-exit msg . args)
+  "Print error and exit."
+  (let ((msg (apply format #f msg args)))
+    (simple-format (current-error-port) "*ERROR*: ~A\n" msg)
+    (exit 1)))
+
+(define (str-equal-or-exit got exp)
+  "S1 == S2 or exit 1."
+  ;; (format #t "'~A' <=> '~A'\n" s1 s2)
+  (if (not (string= exp got))
+    (error-exit "Expected \"~A\", got \"~A\"\n" exp got)))
+
+(define (test-message)
+  "Test functions for a particular message."
+
+  (let ((msg (car (mu:message-list "hello"))))
+    (str-equal-or-exit (mu:subject msg) "Fwd: rfc822")
+    (str-equal-or-exit (mu:to msg) "martin")
+    (str-equal-or-exit (mu:from msg) "foobar <foo@example.com>")
+    (str-equal-or-exit (mu:header msg "X-Mailer") "Ximian Evolution 1.4.5")
+
+    (if (not (equal? (mu:priority msg) mu:prio:normal))
+      (error-exit "Expected ~A, got ~A"  (mu:priority msg) mu:prio:normal)))
+
+  (let ((msg (car (mu:message-list "atoms"))))
+    (str-equal-or-exit (mu:subject msg) "atoms")
+    (str-equal-or-exit (mu:to      msg) "Democritus <demo@example.com>")
+    (str-equal-or-exit (mu:from    msg) "Richard P. Feynman <rpf@example.com>")
+    ;;(str-equal-or-exit (mu:header msg "Content-transfer-encoding") "7BIT")
+
+    (if (not (equal? (mu:priority msg) mu:prio:high))
+      (error-exit "Expected ~a, got ~a"  (mu:priority msg) mu:prio:high))))
+
+(define (num-equal-or-exit got exp)
+  "S1 == S2 or exit 1."
+  ;; (format #t "'~A' <=> '~A'\n" s1 s2)
+  (if (not (= exp got))
+    (error-exit "Expected \"~S\", got \"~S\"\n" exp got)))
+
+(define (test-stats)
+  "Test statistical functions."
+  ;; average
+  (num-equal-or-exit (mu:average mu:size) 82152/13)
+  (num-equal-or-exit (floor (mu:stddev mu:size)) 13020.0)
+  (num-equal-or-exit (mu:max mu:size) 46308)
+  (num-equal-or-exit (mu:min mu:size) 111))
+
+(define (main args)
+  (let* ((optionspec  '((muhome  (value #t))
+                        (test (value #t))))
+         (options (getopt-long args optionspec))
+         (muhome (option-ref options 'muhome #f))
+         (test   (option-ref options 'test #f)))
+
+    (mu:initialize muhome)
+
+    (if test
+      (cond
+       ((string= test "queries") (test-queries))
+       ((string= test "message") (test-message))
+       ((string= test "stats")   (test-stats))
+       (#t (exit 1))))))
+
+
+;; Local Variables:
+;; mode: scheme
+;; End:
diff --git a/lib/Makefile.am b/lib/Makefile.am
new file mode 100644 (file)
index 0000000..f0fef68
--- /dev/null
@@ -0,0 +1,119 @@
+## Copyright (C) 2010-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+# enforce compiling guile (optionally) first,then this dir first
+# before descending into tests/
+include $(top_srcdir)/gtest.mk
+
+SUBDIRS= thirdparty utils message index
+
+TESTDEFS=                                               \
+        -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\"      \
+        -DMU_TESTMAILDIR2=\"${abs_srcdir}/testdir2\"    \
+        -DMU_TESTMAILDIR4=\"${abs_srcdir}/testdir4\"    \
+        -DABS_CURDIR=\"${abs_builddir}\"                \
+        -DABS_SRCDIR=\"${abs_srcdir}\"
+
+
+AM_CFLAGS=                                              \
+        $(WARN_CFLAGS)                                  \
+        $(GMIME_CFLAGS)                                 \
+        $(XAPIAN_CFLAGS)                                \
+        $(GLIB_CFLAGS)                                  \
+        $(GUILE_CFLAGS)                                 \
+        $(ASAN_CFLAGS)                                  \
+        $(CODE_COVERAGE_CFLAGS)                         \
+        $(TESTDEFS)                                     \
+        -Wno-format-nonliteral                          \
+        -Wno-switch-enum                                \
+        -Wno-deprecated-declarations                    \
+        -Wno-inline
+
+AM_CXXFLAGS=                                            \
+        $(GMIME_CFLAGS)                                 \
+        $(GLIB_CFLAGS)                                  \
+        $(GUILE_CFLAGS)                                 \
+        $(WARN_CXXFLAGS)                                \
+        $(XAPIAN_CXXFLAGS)                              \
+        $(ASAN_CXXFLAGS)                                \
+        $(CODE_COVERAGE_CFLAGS)                         \
+        $(TESTDEFS)
+
+AM_CPPFLAGS=                                            \
+        $(CODE_COVERAGE_CPPFLAGS)
+
+noinst_LTLIBRARIES=                                     \
+        libmu.la
+
+libmu_la_SOURCES=                                       \
+        mu-bookmarks.cc                                 \
+        mu-bookmarks.hh                                 \
+        mu-contacts-cache.cc                            \
+        mu-contacts-cache.hh                            \
+        mu-parser.cc                                    \
+        mu-parser.hh                                    \
+        mu-query.cc                                     \
+        mu-query.hh                                     \
+        mu-query-results.hh                             \
+        mu-query-match-deciders.cc                      \
+        mu-query-match-deciders.hh                      \
+        mu-query-threads.cc                             \
+        mu-query-threads.hh                             \
+        mu-runtime.cc                                   \
+        mu-runtime.hh                                   \
+        mu-script.cc                                    \
+        mu-script.hh                                    \
+        mu-server.cc                                    \
+        mu-server.hh                                    \
+        mu-store.cc                                     \
+        mu-store.hh                                     \
+        mu-tokenizer.cc                                 \
+        mu-tokenizer.hh                                 \
+        mu-tree.hh                                      \
+        mu-xapian.cc                                    \
+        mu-xapian.hh                                    \
+        mu-maildir.cc                                   \
+        mu-maildir.hh
+
+libmu_la_LIBADD=                                        \
+        $(XAPIAN_LIBS)                                  \
+        $(GMIME_LIBS)                                   \
+        $(GLIB_LIBS)                                    \
+        $(GUILE_LIBS)                                   \
+        ${builddir}/message/libmu-message.la            \
+        ${builddir}/index/libmu-index.la                \
+        $(CODE_COVERAGE_LIBS)
+
+libmu_la_LDFLAGS=                                       \
+        $(ASAN_LDFLAGS)
+
+noinst_PROGRAMS=                                        \
+        tokenize
+
+tokenize_SOURCES=                                       \
+        tokenize.cc
+
+tokenize_LDADD=                                         \
+        $(WARN_LDFLAGS)                                 \
+        libmu.la                                        \
+        utils/libmu-utils.la
+
+EXTRA_DIST=                                             \
+        doxyfile.in
+
+CLEANFILES=*.log *.trs *core* *vgdump* *.gcda *.gcno
+
+include $(top_srcdir)/aminclude_static.am
diff --git a/lib/doxyfile.in b/lib/doxyfile.in
new file mode 100644 (file)
index 0000000..6ad94ac
--- /dev/null
@@ -0,0 +1,181 @@
+# Doxyfile 0.1
+
+#---------------------------------------------------------------------------
+# General configuration options
+#---------------------------------------------------------------------------
+PROJECT_NAME           = mu
+PROJECT_NUMBER         = @VERSION@
+OUTPUT_DIRECTORY       = apidocs
+OUTPUT_LANGUAGE        = English
+EXTRACT_ALL            = NO
+EXTRACT_PRIVATE        = NO
+EXTRACT_STATIC         = NO
+HIDE_UNDOC_MEMBERS     = NO
+HIDE_UNDOC_CLASSES     = NO
+BRIEF_MEMBER_DESC      = YES
+REPEAT_BRIEF           = YES
+ALWAYS_DETAILED_SEC    = NO
+FULL_PATH_NAMES        = NO
+STRIP_FROM_PATH        =
+INTERNAL_DOCS          = NO
+STRIP_CODE_COMMENTS    = YES
+CASE_SENSE_NAMES       = YES
+SHORT_NAMES            = NO
+HIDE_SCOPE_NAMES       = NO
+VERBATIM_HEADERS       = YES
+SHOW_INCLUDE_FILES     = YES
+JAVADOC_AUTOBRIEF      = YES
+INHERIT_DOCS           = YES
+INLINE_INFO            = YES
+SORT_MEMBER_DOCS       = YES
+DISTRIBUTE_GROUP_DOC   = NO
+TAB_SIZE               = 8
+GENERATE_TODOLIST      = YES
+GENERATE_TESTLIST      = YES
+GENERATE_BUGLIST       = YES
+ALIASES                =
+ENABLED_SECTIONS       =
+MAX_INITIALIZER_LINES  = 30
+OPTIMIZE_OUTPUT_FOR_C  = YES
+SHOW_USED_FILES        = YES
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+QUIET                  = YES
+WARNINGS               = YES
+WARN_IF_UNDOCUMENTED   = YES
+WARN_FORMAT            =
+WARN_LOGFILE           =
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+INPUT                  = @top_srcdir@/lib/
+FILE_PATTERNS          = *.c *.h
+RECURSIVE              = YES
+EXCLUDE                = tests
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+
+EXCLUDE_PATTERNS       = Makefile.* ChangeLog CHANGES CHANGES.* README \
+                         README.* *.png AUTHORS DESIGN DESIGN.* *.desktop \
+                         DESKTOP* COMMENTS HOWTO magic NOTES TODO THANKS
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or
+# directories that contain example code fragments that are included (see
+# the \include command).
+
+EXAMPLE_PATH           =
+EXAMPLE_PATTERNS       =
+EXAMPLE_RECURSIVE      = NO
+IMAGE_PATH             =
+INPUT_FILTER           =
+FILTER_SOURCE_FILES    = NO
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+SOURCE_BROWSER         = YES
+INLINE_SOURCES         = NO
+REFERENCED_BY_RELATION = YES
+REFERENCES_RELATION    = YES
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+ALPHABETICAL_INDEX     = NO
+COLS_IN_ALPHA_INDEX    = 5
+IGNORE_PREFIX          =
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+GENERATE_HTML          = YES
+HTML_OUTPUT            =
+HTML_HEADER            =
+HTML_FOOTER            =
+HTML_STYLESHEET        =
+HTML_ALIGN_MEMBERS     = YES
+GENERATE_HTMLHELP      = NO
+GENERATE_CHI           = NO
+BINARY_TOC             = NO
+TOC_EXPAND             = NO
+DISABLE_INDEX          = NO
+ENUM_VALUES_PER_LINE   = 4
+GENERATE_TREEVIEW      = NO
+TREEVIEW_WIDTH         = 250
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+GENERATE_LATEX         = NO
+LATEX_OUTPUT           =
+COMPACT_LATEX          = NO
+PAPER_TYPE             = a4wide
+EXTRA_PACKAGES         =
+LATEX_HEADER           =
+PDF_HYPERLINKS         = NO
+USE_PDFLATEX           = NO
+LATEX_BATCHMODE        = NO
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+GENERATE_RTF           = NO
+RTF_OUTPUT             =
+COMPACT_RTF            = NO
+RTF_HYPERLINKS         = NO
+RTF_STYLESHEET_FILE    =
+RTF_EXTENSIONS_FILE    =
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+GENERATE_MAN           = YES
+MAN_OUTPUT             = man
+MAN_EXTENSION          = .3mu
+MAN_LINKS              = YES
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+GENERATE_XML           = YES
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+ENABLE_PREPROCESSING   = YES
+MACRO_EXPANSION        = YES
+EXPAND_ONLY_PREDEF     = YES
+SEARCH_INCLUDES        = YES
+INCLUDE_PATH           =
+INCLUDE_FILE_PATTERNS  =
+PREDEFINED             = "G_BEGIN_DECLS="                      \
+                        "G_END_DECLS="
+#                       "DOXYGEN_SHOULD_SKIP_THIS"             \
+#                         "DBUS_GNUC_DEPRECATED="               \
+#                       "_DBUS_DEFINE_GLOBAL_LOCK(name)="      \
+#                       "_DBUS_GNUC_PRINTF(from,to)="
+SKIP_FUNCTION_MACROS   = YES
+#---------------------------------------------------------------------------
+# Configuration::addtions related to external references
+#---------------------------------------------------------------------------
+TAGFILES               =
+GENERATE_TAGFILE       =
+ALLEXTERNALS           = NO
+PERL_PATH              =
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+CLASS_DIAGRAMS         = YES
+HAVE_DOT               = NO
+CLASS_GRAPH            = YES
+COLLABORATION_GRAPH    = YES
+TEMPLATE_RELATIONS     = YES
+HIDE_UNDOC_RELATIONS   = YES
+INCLUDE_GRAPH          = YES
+INCLUDED_BY_GRAPH      = YES
+GRAPHICAL_HIERARCHY    = YES
+DOT_PATH               =
+DOTFILE_DIRS           =
+MAX_DOT_GRAPH_WIDTH    = 640
+MAX_DOT_GRAPH_HEIGHT   = 1024
+GENERATE_LEGEND        = YES
+DOT_CLEANUP            = YES
+#---------------------------------------------------------------------------
+# Configuration::addtions related to the search engine
+#---------------------------------------------------------------------------
+SEARCHENGINE           = NO
diff --git a/lib/index/Makefile.am b/lib/index/Makefile.am
new file mode 100644 (file)
index 0000000..25f62b2
--- /dev/null
@@ -0,0 +1,46 @@
+## Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+AM_CPPFLAGS=                                                   \
+       $(CODE_COVERAGE_CPPFLAGS)
+
+AM_CXXFLAGS=                                                   \
+       $(WARN_CXXFLAGS)                                        \
+       $(GLIB_CFLAGS)                                          \
+       $(XAPIAN_CFLAGS)                                        \
+       $(ASAN_CXXFLAGS)                                        \
+       $(CODE_COVERAGE_CFLAGS)                                 \
+       -I${top_srcdir}/lib
+
+AM_LDFLAGS=                                                    \
+       $(ASAN_LDFLAGS)
+
+noinst_LTLIBRARIES=                                            \
+       libmu-index.la
+
+libmu_index_la_SOURCES=                                                \
+       mu-indexer.cc                                           \
+       mu-indexer.hh                                           \
+       mu-scanner.cc                                           \
+       mu-scanner.hh
+
+libmu_index_la_LIBADD=                                         \
+       $(GLIB_LIBS)                                            \
+       $(CODE_COVERAGE_LIBS)
+
+include $(top_srcdir)/aminclude_static.am
diff --git a/lib/index/meson.build b/lib/index/meson.build
new file mode 100644 (file)
index 0000000..34161ab
--- /dev/null
@@ -0,0 +1,37 @@
+## Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+index_srcs=[
+       'mu-indexer.hh',
+       'mu-indexer.cc',
+       'mu-scanner.hh',
+       'mu-scanner.cc'
+]
+
+xapian_incs = xapian_dep.get_pkgconfig_variable('includedir')
+lib_mu_index_inc_dep = declare_dependency(
+  include_directories: include_directories(['.', '..', xapian_incs]))
+lib_mu_index=static_library('mu-index', [index_srcs],
+                           dependencies: [
+                             config_h_dep,
+                             glib_dep,
+                             lib_mu_index_inc_dep
+                           ],
+                           install: false)
+
+lib_mu_index_dep = declare_dependency(
+  link_with: lib_mu_index
+)
diff --git a/lib/index/mu-indexer.cc b/lib/index/mu-indexer.cc
new file mode 100644 (file)
index 0000000..8d96154
--- /dev/null
@@ -0,0 +1,456 @@
+/*
+** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-indexer.hh"
+
+#include <config.h>
+
+#include <atomic>
+#include <algorithm>
+#include <mutex>
+#include <vector>
+#include <thread>
+#include <condition_variable>
+#include <iostream>
+#include <atomic>
+#include <chrono>
+using namespace std::chrono_literals;
+
+#include "mu-scanner.hh"
+#include "utils/mu-async-queue.hh"
+#include "utils/mu-error.hh"
+#include "../mu-store.hh"
+
+using namespace Mu;
+
+struct IndexState {
+       enum State { Idle,
+                    Scanning,
+                    Finishing,
+                    Cleaning
+       };
+       static const char* name(State s) {
+               switch (s) {
+               case Idle:
+                       return "idle";
+               case Scanning:
+                       return "scanning";
+               case Finishing:
+                       return "finishing";
+               case Cleaning:
+                       return "cleaning";
+               default:
+                       return "<error>";
+               }
+       }
+
+       bool operator==(State rhs) const {
+               return state_.load() == rhs;
+       }
+       bool operator!=(State rhs) const {
+               return state_.load() != rhs;
+       }
+       void change_to(State new_state) {
+               g_debug("changing indexer state %s->%s", name((State)state_),
+                       name((State)new_state));
+               state_.store(new_state);
+       }
+
+private:
+       std::atomic<State> state_{Idle};
+};
+
+struct Indexer::Private {
+       Private(Mu::Store& store)
+           : store_{store}, scanner_{store_.properties().root_maildir,
+                                     [this](auto&& path, auto&& statbuf, auto&& info) {
+                                             return handler(path, statbuf, info);
+                                     }},
+             max_message_size_{store_.properties().max_message_size} {
+               g_message("created indexer for %s -> %s (batch-size: %zu)",
+                         store.properties().root_maildir.c_str(),
+                         store.properties().database_path.c_str(), store.properties().batch_size);
+       }
+
+       ~Private() {
+               stop();
+       }
+
+       bool dir_predicate(const std::string& path, const struct dirent* dirent) const;
+       bool handler(const std::string& fullpath, struct stat* statbuf, Scanner::HandleType htype);
+
+       void maybe_start_worker();
+       void item_worker();
+       void scan_worker();
+
+       bool add_message(const std::string& path);
+
+       bool cleanup();
+       bool start(const Indexer::Config& conf);
+       bool stop();
+
+       Indexer::Config conf_;
+       Store&          store_;
+       Scanner         scanner_;
+       const size_t    max_message_size_;
+
+       time_t                   dirstamp_{};
+       std::size_t              max_workers_;
+       std::vector<std::thread> workers_;
+       std::thread              scanner_worker_;
+
+       struct WorkItem {
+               std::string full_path;
+               enum Type {
+                       Dir,
+                       File
+               };
+               Type type;
+       };
+
+       AsyncQueue<WorkItem> todos_;
+
+       Progress   progress_;
+       IndexState state_;
+       std::mutex lock_, w_lock_;
+
+       std::atomic<time_t> completed_;
+};
+
+bool
+Indexer::Private::handler(const std::string& fullpath, struct stat* statbuf,
+                         Scanner::HandleType htype)
+{
+       switch (htype) {
+       case Scanner::HandleType::EnterDir:
+       case Scanner::HandleType::EnterNewCur: {
+               // in lazy-mode, we ignore this dir if its dirstamp suggest it
+               // is up-to-date (this is _not_ always true; hence we call it
+               // lazy-mode); only for actual message dirs, since the dir
+               // tstamps may not bubble up.
+               dirstamp_ = store_.dirstamp(fullpath);
+               if (conf_.lazy_check && dirstamp_ >= statbuf->st_ctime &&
+                   htype == Scanner::HandleType::EnterNewCur) {
+                       g_debug("skip %s (seems up-to-date: %s >= %s)", fullpath.c_str(),
+                               time_to_string("%FT%T", dirstamp_).c_str(),
+                               time_to_string("%FT%T", statbuf->st_ctime).c_str());
+                       return false;
+               }
+
+               // don't index dirs with '.noindex'
+               auto noindex = ::access((fullpath + "/.noindex").c_str(), F_OK) == 0;
+               if (noindex) {
+                       g_debug("skip %s (has .noindex)", fullpath.c_str());
+                       return false; // don't descend into this dir.
+               }
+
+               // don't index dirs with '.noupdate', unless we do a full
+               // (re)index.
+               if (!conf_.ignore_noupdate) {
+                       auto noupdate = ::access((fullpath + "/.noupdate").c_str(), F_OK) == 0;
+                       if (noupdate) {
+                               g_debug("skip %s (has .noupdate)", fullpath.c_str());
+                               return false;
+                       }
+               }
+
+               g_debug("checked %s", fullpath.c_str());
+               return true;
+       }
+       case Scanner::HandleType::LeaveDir: {
+               todos_.push({fullpath, WorkItem::Type::Dir});
+               return true;
+       }
+
+       case Scanner::HandleType::File: {
+               ++progress_.checked;
+
+               if ((size_t)statbuf->st_size > max_message_size_) {
+                       g_debug("skip %s (too big: %" G_GINT64_FORMAT " bytes)", fullpath.c_str(),
+                               (gint64)statbuf->st_size);
+                       return false;
+               }
+
+               // if the message is not in the db yet, or not up-to-date, queue
+               // it for updating/inserting.
+               if (statbuf->st_ctime <= dirstamp_ && store_.contains_message(fullpath)) {
+                       // g_debug ("skip %s: already up-to-date");
+                       return false;
+               }
+
+               // push the remaining messages to our "todo" queue for
+               // (re)parsing and adding/updating to the database.
+               todos_.push({fullpath, WorkItem::Type::File});
+               return true;
+       }
+       default:
+               g_return_val_if_reached(false);
+               return false;
+       }
+}
+
+void
+Indexer::Private::maybe_start_worker()
+{
+       std::lock_guard lock{w_lock_};
+
+       if (todos_.size() > workers_.size() && workers_.size() < max_workers_) {
+               workers_.emplace_back(std::thread([this] { item_worker(); }));
+               g_debug("added worker %zu", workers_.size());
+       }
+}
+
+bool
+Indexer::Private::add_message(const std::string& path)
+{
+       /*
+        * Having the lock here makes things a _lot_ slower.
+        *
+        * The reason for having the lock is some helgrind warnings;
+        * but it believed those are _false alarms_
+        * https://gitlab.gnome.org/GNOME/glib/-/issues/2662
+        *
+        *      std::unique_lock lock{w_lock_};
+        */
+       auto msg{Message::make_from_path(path)};
+       if (!msg) {
+               g_warning("failed to create message from %s: %s",
+                         path.c_str(), msg.error().what());
+               return false;
+       }
+       auto res = store_.add_message(msg.value(), true /*use-transaction*/);
+       if (!res) {
+               g_warning("failed to add message @ %s: %s",
+                         path.c_str(), res.error().what());
+               return false;
+       }
+
+       return true;
+}
+
+void
+Indexer::Private::item_worker()
+{
+       WorkItem item;
+
+       g_debug("started worker");
+
+       while (state_ == IndexState::Scanning) {
+               if (!todos_.pop(item, 250ms))
+                       continue;
+               try {
+                       switch (item.type) {
+                       case WorkItem::Type::File: {
+                               if (G_LIKELY(add_message(item.full_path)))
+                                       ++progress_.updated;
+                       } break;
+                       case WorkItem::Type::Dir:
+                               store_.set_dirstamp(item.full_path, ::time(NULL));
+                               break;
+                       default:
+                               g_warn_if_reached();
+                               break;
+                       }
+               } catch (const Mu::Error& er) {
+                       g_warning("error adding message @ %s: %s",
+                                 item.full_path.c_str(), er.what());
+               }
+
+               maybe_start_worker();
+               std::this_thread::yield();
+       }
+}
+
+bool
+Indexer::Private::cleanup()
+{
+       g_debug("starting cleanup");
+
+       size_t                 n{};
+       std::vector<Store::Id> orphans; // store messages without files.
+       store_.for_each_message_path([&](Store::Id id, const std::string& path) {
+               ++n;
+               if (::access(path.c_str(), R_OK) != 0) {
+                       g_debug("cannot read %s (id=%u); queueing for removal from store",
+                               path.c_str(), id);
+                       orphans.emplace_back(id);
+               }
+
+               return state_ == IndexState::Cleaning;
+       });
+
+       if (orphans.empty())
+               g_debug("nothing to clean up");
+       else {
+               g_debug("removing up %zu stale message(s) from store", orphans.size());
+               store_.remove_messages(orphans);
+               progress_.removed += orphans.size();
+       }
+
+       return true;
+}
+
+void
+Indexer::Private::scan_worker()
+{
+       progress_.reset();
+
+       if (conf_.scan) {
+               g_debug("starting scanner");
+               if (!scanner_.start()) { // blocks.
+                       g_warning("failed to start scanner");
+                       state_.change_to(IndexState::Idle);
+                       return;
+               }
+               g_debug("scanner finished with %zu file(s) in queue", todos_.size());
+       }
+
+       // now there may still be messages in the work queue...
+       // finish those; this is a bit ugly; perhaps we should
+       // handle SIGTERM etc.
+
+       if (!todos_.empty()) {
+               const auto workers_size = std::invoke([this] {
+                       std::lock_guard lock{w_lock_};
+                       return workers_.size();
+               });
+               g_debug("process %zu remaining message(s) with %zu worker(s)",
+                       todos_.size(), workers_size);
+               while (!todos_.empty())
+                       std::this_thread::sleep_for(100ms);
+       }
+       // and let the worker finish their work.
+       state_.change_to(IndexState::Finishing);
+       for (auto&& w : workers_)
+               if (w.joinable())
+                       w.join();
+
+       if (conf_.cleanup) {
+               g_debug("starting cleanup");
+
+               state_.change_to(IndexState::Cleaning);
+               cleanup();
+               g_debug("cleanup finished");
+       }
+
+       completed_ = ::time({});
+       state_.change_to(IndexState::Idle);
+}
+
+bool
+Indexer::Private::start(const Indexer::Config& conf)
+{
+       stop();
+
+       conf_ = conf;
+       if (conf_.max_threads == 0) {
+               /* benchmarking suggests that ~4 threads is the fastest (the
+                * real bottleneck is the database, so adding more threads just
+                * slows things down)
+                */
+               max_workers_ = std::min(4U, std::thread::hardware_concurrency());
+       } else
+               max_workers_ = conf.max_threads;
+
+       g_debug("starting indexer with <= %zu worker thread(s)", max_workers_);
+       g_debug("indexing: %s; clean-up: %s", conf_.scan ? "yes" : "no",
+               conf_.cleanup ? "yes" : "no");
+
+       state_.change_to(IndexState::Scanning);
+       /* kick off the first worker, which will spawn more if needed. */
+       workers_.emplace_back(std::thread([this] { item_worker(); }));
+       /* kick the disk-scanner thread */
+       scanner_worker_ = std::thread([this] { scan_worker(); });
+
+       g_debug("started indexer");
+
+       return true;
+}
+
+bool
+Indexer::Private::stop()
+{
+       scanner_.stop();
+
+       todos_.clear();
+       if (scanner_worker_.joinable())
+               scanner_worker_.join();
+
+       state_.change_to(IndexState::Idle);
+       for (auto&& w : workers_)
+               if (w.joinable())
+                       w.join();
+       workers_.clear();
+
+       return true;
+}
+
+Indexer::Indexer(Store& store)
+    : priv_{std::make_unique<Private>(store)}
+{}
+
+Indexer::~Indexer() = default;
+
+bool
+Indexer::start(const Indexer::Config& conf)
+{
+       const auto mdir{priv_->store_.properties().root_maildir};
+       if (G_UNLIKELY(access(mdir.c_str(), R_OK) != 0)) {
+               g_critical("'%s' is not readable: %s", mdir.c_str(), g_strerror(errno));
+               return false;
+       }
+
+       std::lock_guard lock(priv_->lock_);
+       if (is_running())
+               return true;
+
+       return priv_->start(conf);
+}
+
+bool
+Indexer::stop()
+{
+       std::lock_guard lock{priv_->lock_};
+
+       if (!is_running())
+               return true;
+
+       g_debug("stopping indexer");
+       return priv_->stop();
+}
+
+bool
+Indexer::is_running() const
+{
+       return priv_->state_ != IndexState::Idle;
+}
+
+const Indexer::Progress&
+Indexer::progress() const
+{
+       priv_->progress_.running = priv_->state_ == IndexState::Idle ? false : true;
+
+       return priv_->progress_;
+}
+
+time_t
+Indexer::completed() const
+{
+       return priv_->completed_;
+}
diff --git a/lib/index/mu-indexer.hh b/lib/index/mu-indexer.hh
new file mode 100644 (file)
index 0000000..0c678a6
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_INDEXER_HH__
+#define MU_INDEXER_HH__
+
+#include <atomic>
+#include <memory>
+#include <chrono>
+
+namespace Mu {
+
+class Store;
+
+/// An object abstracting the index process.
+class Indexer {
+public:
+       /**
+        * Construct an indexer object
+        *
+        * @param store the message store to use
+        */
+       Indexer(Store& store);
+
+       /**
+        * DTOR
+        */
+       ~Indexer();
+
+       /// A configuration object for the indexer
+       struct Config {
+               bool scan{true};
+               /**< scan for new messages */
+               bool cleanup{true};
+               /**< clean messages no longer in the file system */
+               size_t max_threads{};
+               /**< maximum # of threads to use */
+               bool ignore_noupdate{};
+               /**< ignore .noupdate files */
+               bool lazy_check{};
+               /**< whether to skip directories that don't have a changed
+                * mtime */
+       };
+
+       /**
+        * Start indexing. If already underway, do nothing. This returns
+        * immediately after starting, with the work being done in the
+        * background.
+        *
+        * @param conf a configuration object
+        *
+        * @return true if starting worked or an indexing process was already
+        * underway; false otherwise.
+        *
+        */
+       bool start(const Config& conf);
+
+       /**
+        * Stop indexing. If not indexing, do nothing.
+        *
+        *
+        * @return true if we stopped indexing, or indexing was not underway.
+        * False otherwise.
+        */
+       bool stop();
+
+       /**
+        * Is an indexing process running?
+        *
+        * @return true or false.
+        */
+       bool is_running() const;
+
+       // Object describing current progress
+       struct Progress {
+               void reset()
+               {
+                       running = false;
+                       checked = updated = removed = 0;
+               }
+
+               std::atomic<bool>   running{}; /**< Is an index operation in progress? */
+               std::atomic<size_t> checked{}; /**< Number of messages checked for changes */
+               std::atomic<size_t> updated{}; /**< Number of messages (re)parsed/added/updated */
+               std::atomic<size_t> removed{}; /**< Number of message removed from store */
+       };
+
+       /**
+        * Get an object describing the current progress. The progress object
+        * describes the most recent indexing job, and is reset upon a fresh
+        * start().
+        *
+        * @return a progress object.
+        */
+       const Progress& progress() const;
+
+       /**
+        * Last time indexing was completed.
+        *
+        * @return the time or 0
+        */
+       time_t completed() const;
+
+private:
+       struct Private;
+       std::unique_ptr<Private> priv_;
+};
+
+} // namespace Mu
+#endif /* MU_INDEXER_HH__ */
diff --git a/lib/index/mu-scanner.cc b/lib/index/mu-scanner.cc
new file mode 100644 (file)
index 0000000..dfa6984
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+** Copyright (C) 2020-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "mu-scanner.hh"
+
+#include "config.h"
+
+#include <chrono>
+#include <mutex>
+#include <atomic>
+#include <thread>
+#include <cstring>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <glib.h>
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-error.hh"
+
+using namespace Mu;
+
+struct Scanner::Private {
+       Private(const std::string& root_dir, Scanner::Handler handler)
+           : root_dir_{root_dir}, handler_{handler}
+       {
+               if (!handler_)
+                       throw Mu::Error{Error::Code::Internal, "missing handler"};
+       }
+       ~Private()
+       {
+               stop();
+       }
+
+       bool start();
+       bool stop();
+       bool process_dentry(const std::string& path, struct dirent* dentry, bool is_maildir);
+       bool process_dir(const std::string& path, bool is_maildir);
+
+       const std::string      root_dir_;
+       const Scanner::Handler handler_;
+       std::atomic<bool>      running_{};
+       std::mutex             lock_;
+};
+
+static bool
+is_special_dir(const char* d_name)
+{
+       return d_name[0] == '\0' || (d_name[1] == '\0' && d_name[0] == '.') ||
+              (d_name[2] == '\0' && d_name[0] == '.' && d_name[1] == '.');
+}
+
+bool
+Scanner::Private::process_dentry(const std::string& path, struct dirent* dentry,
+                                 bool is_maildir)
+{
+       const auto d_name{dentry->d_name};
+
+       if (is_special_dir(d_name) || std::strcmp(d_name, "tmp") == 0)
+               return true; // ignore.
+
+       const auto  fullpath{path + "/" + d_name};
+       struct stat statbuf {
+       };
+       if (::stat(fullpath.c_str(), &statbuf) != 0) {
+               g_warning("failed to stat %s: %s", fullpath.c_str(), g_strerror(errno));
+               return false;
+       }
+
+       if (S_ISDIR(statbuf.st_mode)) {
+               const auto new_cur =
+                   std::strcmp(d_name, "cur") == 0 || std::strcmp(d_name, "new") == 0;
+               const auto htype =
+                   new_cur ? Scanner::HandleType::EnterNewCur : Scanner::HandleType::EnterDir;
+               const auto res = handler_(fullpath, &statbuf, htype);
+               if (!res)
+                       return true; // skip
+
+               process_dir(fullpath, new_cur);
+
+               return handler_(fullpath, &statbuf, Scanner::HandleType::LeaveDir);
+
+       } else if (S_ISREG(statbuf.st_mode) && is_maildir)
+               return handler_(fullpath, &statbuf, Scanner::HandleType::File);
+
+       g_debug("skip %s (neither maildir-file nor directory)", fullpath.c_str());
+
+       return true;
+}
+
+bool
+Scanner::Private::process_dir(const std::string& path, bool is_maildir)
+{
+       if (!running_)
+               return true; /* we're done */
+
+       const auto dir = opendir(path.c_str());
+       if (G_UNLIKELY(!dir)) {
+               g_warning("failed to scan dir %s: %s", path.c_str(), g_strerror(errno));
+               return false;
+       }
+
+       // TODO: sort dentries by inode order, which makes things faster for extfs.
+       // see mu-maildir.c
+
+       while (running_) {
+               errno = 0;
+               const auto dentry{readdir(dir)};
+
+               if (G_LIKELY(dentry)) {
+                       process_dentry(path, dentry, is_maildir);
+                       continue;
+               }
+
+               if (errno != 0) {
+                       g_warning("failed to read %s: %s", path.c_str(), g_strerror(errno));
+                       continue;
+               }
+
+               break;
+       }
+       closedir(dir);
+
+       return true;
+}
+
+bool
+Scanner::Private::start()
+{
+       const auto& path{root_dir_};
+       if (G_UNLIKELY(path.length() > PATH_MAX)) {
+               g_warning("path too long");
+               return false;
+       }
+
+       const auto mode{F_OK | R_OK};
+       if (G_UNLIKELY(access(path.c_str(), mode) != 0)) {
+               g_warning("'%s' is not readable: %s", path.c_str(), g_strerror(errno));
+               return false;
+       }
+
+       struct stat statbuf {
+       };
+       if (G_UNLIKELY(stat(path.c_str(), &statbuf) != 0)) {
+               g_warning("'%s' is not stat'able: %s", path.c_str(), g_strerror(errno));
+               return false;
+       }
+
+       if (G_UNLIKELY(!S_ISDIR(statbuf.st_mode))) {
+               g_warning("'%s' is not a directory", path.c_str());
+               return false;
+       }
+
+       running_ = true;
+       g_debug("starting scan @ %s", root_dir_.c_str());
+
+       auto       basename{g_path_get_basename(root_dir_.c_str())};
+       const auto is_maildir =
+           (g_strcmp0(basename, "cur") == 0 || g_strcmp0(basename, "new") == 0);
+       g_free(basename);
+
+       const auto start{std::chrono::steady_clock::now()};
+       process_dir(root_dir_, is_maildir);
+       const auto elapsed = std::chrono::steady_clock::now() - start;
+       g_debug("finished scan of %s in %" G_GINT64_FORMAT " ms", root_dir_.c_str(),
+               to_ms(elapsed));
+       running_ = false;
+
+       return true;
+}
+
+bool
+Scanner::Private::stop()
+{
+       if (!running_)
+               return true; // nothing to do
+
+       g_debug("stopping scan");
+       running_ = false;
+
+       return true;
+}
+
+Scanner::Scanner(const std::string& root_dir, Scanner::Handler handler)
+    : priv_{std::make_unique<Private>(root_dir, handler)}
+{
+}
+
+Scanner::~Scanner() = default;
+
+bool
+Scanner::start()
+{
+       if (priv_->running_)
+               return true; // nothing to do
+
+       const auto res  = priv_->start(); /* blocks */
+       priv_->running_ = false;
+
+       return res;
+}
+
+bool
+Scanner::stop()
+{
+       std::lock_guard l(priv_->lock_);
+
+       return priv_->stop();
+}
+
+bool
+Scanner::is_running() const
+{
+       return priv_->running_;
+}
diff --git a/lib/index/mu-scanner.hh b/lib/index/mu-scanner.hh
new file mode 100644 (file)
index 0000000..0d2f1c1
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_SCANNER_HH__
+#define MU_SCANNER_HH__
+
+#include <functional>
+#include <memory>
+
+#include <dirent.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+namespace Mu {
+
+/// @brief Maildir scanner
+///
+/// Scans maildir (trees) recursively, and calls the Handler callback for
+/// directories & files.
+///
+/// It filters out (i.e., does *not* call the handler for):
+///  - files starting with '.'
+///  - files that do not live in a cur / new leaf maildir
+///  - directories '.' and '..' and 'tmp'
+///
+class Scanner {
+       public:
+       enum struct HandleType {
+               File,
+               EnterNewCur, /* cur/ or new/ */
+               EnterDir,    /* some other directory */
+               LeaveDir
+       };
+
+       /// Prototype for a handler function
+       using Handler = std::function<
+           bool(const std::string& fullpath, struct stat* statbuf, HandleType htype)>;
+       /**
+        * Construct a scanner object for scanning a directory, recursively.
+        *
+        * If handler is a directory
+        *
+        *
+        * @param root_dir root dir to start scanning
+        * @param handler handler function for some direntry
+        */
+       Scanner(const std::string& root_dir, Handler handler);
+
+       /**
+        * DTOR
+        */
+       ~Scanner();
+
+       /**
+        * Start the scan; this is a blocking call than runs until
+        * finished or (from another thread) stop() is called.
+        *
+        * @return true if starting worked; false otherwise
+        */
+       bool start();
+
+       /**
+        * Stop the scan
+        *
+        * @return true if stopping worked; false otherwi%sse
+        */
+       bool stop();
+
+       /**
+        * Is a scan currently running?
+        *
+        * @return true or false
+        */
+       bool is_running() const;
+
+       private:
+       struct Private;
+       std::unique_ptr<Private> priv_;
+};
+
+} // namespace Mu
+
+#endif /* MU_SCANNER_HH__ */
diff --git a/lib/index/test-scanner.cc b/lib/index/test-scanner.cc
new file mode 100644 (file)
index 0000000..4835aa1
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+
+#include "mu-scanner.hh"
+#include "mu-utils.hh"
+
+using namespace Mu;
+
+static void
+test_scan_maildir()
+{
+       allow_warnings();
+
+       Scanner scanner{
+           "/home/djcb/Maildir",
+           [](const dirent* dentry) -> bool {
+                   g_print("%02x %s\n", dentry->d_type, dentry->d_name);
+                   return true;
+           },
+           [](const std::string& fullpath, const struct stat* statbuf, auto&& info) -> bool {
+                   g_print("%s %zu\n", fullpath.c_str(), statbuf->st_size);
+                   return true;
+           }};
+       g_assert_true(scanner.start());
+
+       while (scanner.is_running()) {
+               sleep(1);
+       }
+}
+
+int
+main(int argc, char* argv[])
+try {
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/utils/scanner/scan-maildir", test_scan_maildir);
+
+       return g_test_run();
+
+} catch (const std::runtime_error& re) {
+       std::cerr << re.what() << "\n";
+       return 1;
+}
diff --git a/lib/meson.build b/lib/meson.build
new file mode 100644 (file)
index 0000000..eb04859
--- /dev/null
@@ -0,0 +1,81 @@
+## Copyright (C) 2021-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+
+subdir('utils')
+subdir('message')
+subdir('index')
+
+lib_mu=static_library(
+  'mu',
+  [
+    'mu-bookmarks.cc',
+    'mu-contacts-cache.cc',
+    'mu-maildir.cc',
+    'mu-parser.cc',
+    'mu-query-match-deciders.cc',
+    'mu-query-threads.cc',
+    'mu-query.cc',
+    'mu-runtime.cc',
+    'mu-script.cc',
+    'mu-server.cc',
+    'mu-store.cc',
+    'mu-tokenizer.cc',
+    'mu-xapian.cc'
+  ],
+  dependencies: [
+    glib_dep,
+    gio_dep,
+    gmime_dep,
+    xapian_dep,
+    guile_dep,
+    config_h_dep,
+    lib_mu_utils_dep,
+    lib_mu_message_dep,
+    lib_mu_index_dep
+  ],
+  install: false)
+
+
+lib_mu_dep = declare_dependency(
+  link_with: lib_mu,
+  dependencies: [ lib_mu_message_dep, thread_dep ],
+  include_directories:
+    include_directories(['.', '..']))
+
+# dev helpers
+tokenize = executable(
+  'tokenize',
+  [ 'mu-tokenizer.cc', 'tokenize.cc' ],
+  dependencies: [ lib_mu_utils_dep, glib_dep ],
+  install: false)
+
+# actual tests
+
+test('test-threads',
+     executable('test-threads',
+               'mu-query-threads.cc',
+               install: false,
+               cpp_args: ['-DBUILD_TESTS'],
+               dependencies: [glib_dep, lib_mu_dep]))
+test('test-contacts-cache',
+     executable('test-contacts-cache',
+               'mu-contacts-cache.cc',
+               install: false,
+               cpp_args: ['-DBUILD_TESTS'],
+               dependencies: [glib_dep, lib_mu_dep]))
+
+subdir('tests')
diff --git a/lib/message/Makefile.am b/lib/message/Makefile.am
new file mode 100644 (file)
index 0000000..d10815f
--- /dev/null
@@ -0,0 +1,51 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+AM_CXXFLAGS=                   \
+       $(WARN_CXXFLAGS)        \
+       $(GLIB_CFLAGS)          \
+       $(GMIME_CFLAGS)         \
+       $(XAPIAN_CFLAGS)        \
+       -I${top_srcdir}/lib
+
+noinst_LTLIBRARIES=            \
+       libmu-message.la
+
+libmu_message_la_SOURCES=      \
+        mu-message.cc          \
+        mu-message.hh          \
+       mu-message-file.cc      \
+       mu-message-file.hh      \
+        mu-message-part.cc     \
+        mu-message-part.hh     \
+        mu-contact.hh          \
+        mu-contact.cc          \
+        mu-document.cc         \
+        mu-document.hh         \
+        mu-fields.hh           \
+        mu-fields.cc           \
+        mu-flags.hh            \
+        mu-flags.cc            \
+        mu-priority.hh         \
+        mu-priority.cc         \
+        mu-mime-object.cc      \
+        mu-mime-object.hh
+
+libmu_message_la_LIBADD=       \
+       $(GLIB_LIBS)            \
+       $(GMIME_LIBS)           \
+       $(XAPIAN_LIBS)
diff --git a/lib/message/meson.build b/lib/message/meson.build
new file mode 100644 (file)
index 0000000..3997fe2
--- /dev/null
@@ -0,0 +1,95 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+
+lib_mu_message=static_library(
+  'mu-message',
+  [
+    'mu-message.cc',
+    'mu-message-file.cc',
+    'mu-message-part.cc',
+    'mu-contact.cc',
+    'mu-document.cc',
+    'mu-fields.cc',
+    'mu-flags.cc',
+    'mu-priority.cc',
+    'mu-mime-object.cc',
+  ],
+  dependencies: [
+    glib_dep,
+    gmime_dep,
+    xapian_dep,
+    config_h_dep,
+    lib_mu_utils_dep],
+  install: false)
+
+lib_mu_message_dep = declare_dependency(
+  link_with: lib_mu_message,
+  dependencies: [ xapian_dep, gmime_dep ],
+  include_directories:
+    include_directories(['.', '..']))
+
+#
+# tests
+#
+
+test('test-contact',
+     executable('test-contact',
+               'mu-contact.cc',
+               install: false,
+               cpp_args: ['-DBUILD_TESTS'],
+               dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-document',
+     executable('test-document',
+               'mu-document.cc',
+               install: false,
+               cpp_args: ['-DBUILD_TESTS'],
+               dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-fields',
+     executable('test-fields',
+               'mu-fields.cc',
+               install: false,
+               cpp_args: ['-DBUILD_TESTS'],
+               dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-flags',
+     executable('test-flags',
+               'mu-flags.cc',
+               install: false,
+               cpp_args: ['-DBUILD_TESTS'],
+               dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-message',
+     executable('test-message',
+               'test-mu-message.cc',
+               install: false,
+               dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-priority',
+     executable('test-priority',
+               'mu-priority.cc',
+               install: false,
+               cpp_args: ['-DBUILD_TESTS'],
+               dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-message-file',
+     executable('test-message-file',
+               'mu-message-file.cc',
+               install: false,
+                cpp_args: ['-DBUILD_TESTS'],
+               dependencies: [glib_dep, lib_mu_message_dep]))
diff --git a/lib/message/mu-contact.cc b/lib/message/mu-contact.cc
new file mode 100644 (file)
index 0000000..711f4d2
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-contact.hh"
+#include "mu-message.hh"
+#include "utils/mu-utils.hh"
+#include "mu-mime-object.hh"
+
+#include <gmime/gmime.h>
+#include <glib.h>
+#include <string>
+
+using namespace Mu;
+
+static bool
+needs_quoting(const std::string& name)
+{
+       for (auto& c: name)
+               if (c == ',' || c == '"')
+                       return true;
+       return false;
+}
+
+std::string
+Contact::display_name(bool quote) const
+{
+
+       if (name.empty())
+               return email;
+       else if (!quote || !needs_quoting(name))
+               return name + " <" + email + '>';
+       else
+               return address_rfc2047(*this);
+}
+
+std::string
+Mu::to_string(const Mu::Contacts& contacts)
+{
+       std::string res;
+
+       seq_for_each(contacts, [&](auto&& contact) {
+               if (res.empty())
+                       res = contact.display_name();
+               else
+                       res += ", " + contact.display_name();
+       });
+
+       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<size_t>(g_ascii_tolower(c));
+       return djb;
+}
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_ctor_foo()
+{
+       Contact c{
+               "foo@example.com",
+               "Foo Bar",
+               Contact::Type::Bcc,
+               1645214647
+       };
+
+       assert_equal(c.email, "foo@example.com");
+       assert_equal(c.name, "Foo Bar");
+       g_assert_true(*c.field_id() == Field::Id::Bcc);
+       g_assert_cmpuint(c.message_date,==,1645214647);
+
+       assert_equal(c.display_name(), "Foo Bar <foo@example.com>");
+}
+
+
+static void
+test_ctor_blinky()
+{
+       Contact c{
+               "bar@example.com",
+               "Blinky",
+               1645215014,
+               true, /* personal */
+               13, /*freq*/
+               12345 /* tstamp */
+       };
+
+       assert_equal(c.email, "bar@example.com");
+       assert_equal(c.name, "Blinky");
+       g_assert_true(c.personal);
+       g_assert_cmpuint(c.frequency,==,13);
+       g_assert_cmpuint(c.tstamp,==,12345);
+       g_assert_cmpuint(c.message_date,==,1645215014);
+
+       assert_equal(c.display_name(), "Blinky <bar@example.com>");
+}
+
+static void
+test_ctor_cleanup()
+{
+       Contact c{
+               "bar@example.com",
+               "Bli\nky",
+               1645215014,
+               true, /* personal */
+               13, /*freq*/
+               12345 /* tstamp */
+       };
+
+       assert_equal(c.email, "bar@example.com");
+       assert_equal(c.name, "Bli ky");
+       g_assert_true(c.personal);
+       g_assert_cmpuint(c.frequency,==,13);
+       g_assert_cmpuint(c.tstamp,==,12345);
+       g_assert_cmpuint(c.message_date,==,1645215014);
+
+       assert_equal(c.display_name(), "Bli ky <bar@example.com>");
+}
+
+static void
+test_encode()
+{
+       Contact c{
+               "cassius@example.com",
+               "Ali, Muhammad \"The Greatest\"",
+               345,
+               false, /* personal */
+               333, /*freq*/
+               768 /* tstamp */
+       };
+
+       assert_equal(c.email, "cassius@example.com");
+       assert_equal(c.name, "Ali, Muhammad \"The Greatest\"");
+       g_assert_false(c.personal);
+       g_assert_cmpuint(c.frequency,==,333);
+       g_assert_cmpuint(c.tstamp,==,768);
+       g_assert_cmpuint(c.message_date,==,345);
+
+       assert_equal(c.display_name(true),
+                    "\"Ali, Muhammad \\\"The Greatest\\\"\" <cassius@example.com>");
+}
+
+
+static void
+test_sender()
+{
+       Contact c{"aa@example.com", "Anders Ångström",
+               Contact::Type::Sender, 54321};
+
+       assert_equal(c.email, "aa@example.com");
+       assert_equal(c.name, "Anders Ångström");
+       g_assert_false(c.personal);
+       g_assert_cmpuint(c.frequency,==,1);
+       g_assert_cmpuint(c.message_date,==,54321);
+
+       g_assert_false(!!c.field_id());
+}
+
+
+static void
+test_misc()
+{
+       g_assert_false(!!contact_type_from_field_id(Field::Id::Subject));
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       mu_test_init(&argc, &argv);
+       g_mime_init();
+
+       g_test_add_func("/message/contact/ctor-foo", test_ctor_foo);
+       g_test_add_func("/message/contact/ctor-blinky", test_ctor_blinky);
+       g_test_add_func("/message/contact/ctor-cleanup", test_ctor_cleanup);
+       g_test_add_func("/message/contact/encode", test_encode);
+
+       g_test_add_func("/message/contact/sender", test_sender);
+       g_test_add_func("/message/contact/misc", test_misc);
+
+       return g_test_run();
+}
+#endif /*BUILD_TESTS*/
diff --git a/lib/message/mu-contact.hh b/lib/message/mu-contact.hh
new file mode 100644 (file)
index 0000000..06d9d16
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_MESSAGE_CONTACT_HH__
+#define MU_MESSAGE_CONTACT_HH__
+
+#include <functional>
+#include <string>
+#include <vector>
+#include <functional>
+#include <cctype>
+#include <cstring>
+#include <cstdlib>
+#include <ctime>
+
+#include <utils/mu-option.hh>
+#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
+       };
+
+       /**
+        * 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
+        */
+       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{}
+               { 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
+        */
+       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();}
+
+       /**
+        * Get the "display name" for this contact; basically, if there's a
+        * non-empty name, it's
+        *     Jane Doe <email@example.com>
+        * otherwise it's just the e-mail address.
+        *
+        * @param quote_if_needed if true, handle quoting of the name-part as well. This
+        * is useful when the address is to be used directly in emails.
+        *
+        * @return the display name
+        */
+       std::string display_name(bool quote_if_needed=false) const;
+
+
+       /**
+        * Operator==; based on the hash values (ie. lowercase e-mail address)
+        *
+        * @param rhs some other Contact
+        *
+        * @return true orf false.
+        */
+       bool operator== (const Contact& rhs) const noexcept {
+               return hash() == rhs.hash();
+       }
+
+       /**
+        * Get a hash-value for this contact, which gets lazily calculated. This
+        * * is for use with container classes. This uses the _lowercase_ email
+        * address.
+        *
+        * @return the hash
+        */
+       size_t hash() const {
+               static size_t cached_hash;
+               if (cached_hash == 0) {
+                       cached_hash = lowercase_hash(email);
+               }
+               return  cached_hash;
+       }
+
+       /**
+        * Get the corresponding Field::Id (if any)
+        * for this contact.
+        *
+        * @return the field-id or Nothing.
+        */
+       constexpr Option<Field::Id> field_id() const noexcept {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+               switch(type) {
+               case Type::Bcc:
+                       return Field::Id::Bcc;
+               case Type::Cc:
+                       return Field::Id::Cc;
+               case Type::From:
+                       return Field::Id::From;
+               case Type::To:
+                       return Field::Id::To;
+               default:
+                       return Nothing;
+               }
+#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) */
+
+private:
+       void cleanup_name() { // replace control characters by spaces.
+               for (auto& c: name)
+                       if (iscntrl(c))
+                               c = ' ';
+       }
+};
+
+constexpr Option<Contact::Type>
+contact_type_from_field_id(Field::Id id) noexcept {
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+       switch(id) {
+       case Field::Id::Bcc:
+               return Contact::Type::Bcc;
+       case Field::Id::Cc:
+               return Contact::Type::Cc;
+       case Field::Id::From:
+               return Contact::Type::From;
+       case Field::Id::To:
+               return Contact::Type::To;
+       default:
+               return Nothing;
+       }
+#pragma GCC diagnostic pop
+}
+
+using Contacts = std::vector<Contact>;
+
+/**
+ * Get contacts as a comma-separated list.
+ *
+ * @param contacts contacs
+ *
+ * @return string with contacts.
+ */
+std::string to_string(const Contacts& contacts);
+
+} // namespace Mu
+
+/**
+ * Implement our hash int std::
+ */
+template<> struct std::hash<Mu::Contact> {
+       std::size_t operator()(const Mu::Contact& c) const noexcept {
+               return c.hash();
+       }
+};
+
+#endif /* MU_CONTACT_HH__ */
diff --git a/lib/message/mu-document.cc b/lib/message/mu-document.cc
new file mode 100644 (file)
index 0000000..f0c230c
--- /dev/null
@@ -0,0 +1,513 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-document.hh"
+#include "mu-message.hh"
+
+#include <cstdint>
+#include <glib.h>
+#include <numeric>
+#include <algorithm>
+#include <charconv>
+#include <cinttypes>
+
+#include <string>
+#include <utils/mu-utils.hh>
+
+using namespace Mu;
+
+constexpr uint8_t SepaChar1 = 0xfe;
+constexpr uint8_t SepaChar2 = 0xff;
+
+static void
+add_search_term(Xapian::Document& doc, const Field& field, const std::string& val)
+{
+       if (field.is_normal_term()) {
+               doc.add_term(field.xapian_term(val));
+       } else if (field.is_boolean_term()) {
+               doc.add_boolean_term(field.xapian_term(val));
+       } else if (field.is_indexable_term()) {
+               Xapian::TermGenerator termgen;
+               termgen.set_document(doc);
+               termgen.index_text(utf8_flatten(val), 1, field.xapian_term());
+                /* also add as 'normal' term, so some queries where the indexer
+                 * eats special chars also match */
+               if (field.id != Field::Id::BodyText &&
+                   field.id != Field::Id::EmbeddedText) {
+                       doc.add_term(field.xapian_term(val));
+               }
+       } else
+               throw std::logic_error("not a search term");
+}
+
+
+static std::string
+make_prop_name(const Field& field)
+{
+       return ":" + std::string(field.name);
+}
+
+void
+Document::add(Field::Id id, const std::string& val)
+{
+       const auto field{field_from_id(id)};
+
+       if (field.is_value())
+               xdoc_.add_value(field.value_no(), val);
+
+       if (field.is_searchable())
+               add_search_term(xdoc_, field, val);
+
+       if (field.include_in_sexp())
+               sexp_list().add_prop(make_prop_name(field),
+                                    Sexp::make_string(std::move(val)));
+}
+
+void
+Document::add(Field::Id id, const std::vector<std::string>& 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));
+
+       if (field.is_searchable())
+               std::for_each(vals.begin(), vals.end(),
+                             [&](const auto& val) {
+                                     add_search_term(xdoc_, field, val); });
+
+       if (field.include_in_sexp()) {
+               Sexp::List elms;
+               for(auto&& val: vals)
+                       elms.add(Sexp::make_string(val));
+               sexp_list().add_prop(make_prop_name(field),
+                                    Sexp::make_list(std::move(elms)));
+       }
+}
+
+
+std::vector<std::string>
+Document::string_vec_value(Field::Id field_id) const noexcept
+{
+       return Mu::split(string_value(field_id), SepaChar1);
+}
+
+static Sexp
+make_contacts_sexp(const Contacts& contacts)
+{
+       Sexp::List clist;
+
+       seq_for_each(contacts, [&](auto&& c) {
+               if (!c.name.empty())
+                       clist.add(Sexp::make_prop_list(
+                                         ":name",  Sexp::make_string(c.name),
+                                         ":email", Sexp::make_string(c.email)));
+               else
+                       clist.add(Sexp::make_prop_list(
+                                         ":email", Sexp::make_string(c.email)));
+       });
+
+       return Sexp::make_list(std::move(clist));
+}
+
+void
+Document::add(Field::Id id, const Contacts& contacts)
+{
+       if (contacts.empty())
+               return;
+
+       const auto field{field_from_id(id)};
+       std::vector<std::string> cvec;
+
+       const std::string sepa2(1, SepaChar2);
+
+       Xapian::TermGenerator termgen;
+       termgen.set_document(xdoc_);
+
+       for (auto&& contact: contacts) {
+
+               const auto cfield_id{contact.field_id()};
+               if (!cfield_id || *cfield_id != id)
+                       continue;
+
+               const auto e{contact.email};
+               xdoc_.add_term(field.xapian_term(e));
+
+               /* allow searching for address components, too */
+               const auto atpos = e.find('@');
+               if (atpos != std::string::npos && atpos < e.size() - 1) {
+                       xdoc_.add_term(field.xapian_term(e.substr(0, atpos)));
+                       xdoc_.add_term(field.xapian_term(e.substr(atpos + 1)));
+               }
+
+               if (!contact.name.empty())
+                       termgen.index_text(utf8_flatten(contact.name), 1,
+                                          field.xapian_term());
+               cvec.emplace_back(contact.email + sepa2 + contact.name);
+       }
+
+       if (!cvec.empty())
+               xdoc_.add_value(field.value_no(), join(cvec, SepaChar1));
+
+       if (field.include_in_sexp())
+               sexp_list().add_prop(make_prop_name(field),
+                                    make_contacts_sexp(contacts));
+
+}
+
+Contacts
+Document::contacts_value(Field::Id id) const noexcept
+{
+       const auto vals{string_vec_value(id)};
+       Contacts contacts;
+       contacts.reserve(vals.size());
+
+       const auto ctype{contact_type_from_field_id(id)};
+       if (G_UNLIKELY(!ctype)) {
+               g_critical("invalid field-id for contact-type: <%zu>",
+                          static_cast<size_t>(id));
+               return {};
+       }
+
+       for (auto&& s: vals) {
+
+               const auto pos = s.find(SepaChar2);
+               if (G_UNLIKELY(pos == std::string::npos)) {
+                       g_critical("invalid contact data '%s'", s.c_str());
+                       break;
+               }
+
+               contacts.emplace_back(s.substr(0, pos), s.substr(pos + 1), *ctype);
+       }
+
+       return contacts;
+}
+
+void
+Document::add_extra_contacts(const std::string& propname, const Contacts& contacts)
+{
+       if (!contacts.empty())
+               sexp_list().add_prop(std::string{propname},
+                                    make_contacts_sexp(contacts));
+}
+
+
+static Sexp
+make_emacs_time_sexp(::time_t t)
+{
+       Sexp::List dlist;
+
+       dlist.add(Sexp::make_number(static_cast<unsigned>(t >> 16)));
+       dlist.add(Sexp::make_number(static_cast<unsigned>(t & 0xffff)));
+       dlist.add(Sexp::make_number(0));
+
+       return Sexp::make_list(std::move(dlist));
+}
+
+void
+Document::add(Field::Id id, int64_t val)
+{
+       /*
+        * Xapian stores everything (incl. numbers) as strings.
+        *
+        * we comply, by storing a number a base-16 and prefixing with 'f' +
+        * length; such that the strings are sorted in the numerical order.
+        */
+
+       const auto field{field_from_id(id)};
+
+       if (field.is_value())
+               xdoc_.add_value(field.value_no(), to_lexnum(val));
+
+       if (field.include_in_sexp()) {
+               if (field.is_time_t())
+                       sexp_list().add_prop(make_prop_name(field),
+                                            make_emacs_time_sexp(val));
+               else
+                       sexp_list().add_prop(make_prop_name(field),
+                                            Sexp::make_number(val));
+       }
+}
+
+int64_t
+Document::integer_value(Field::Id field_id) const noexcept
+{
+       if (auto&& v{string_value(field_id)}; v.empty())
+               return 0;
+       else
+               return from_lexnum(v);
+}
+
+void
+Document::add(Priority prio)
+{
+       constexpr auto field{field_from_id(Field::Id::Priority)};
+
+       xdoc_.add_value(field.value_no(), std::string(1, to_char(prio)));
+       xdoc_.add_boolean_term(field.xapian_term(to_char(prio)));
+
+       if (field.include_in_sexp())
+               sexp_list().add_prop(make_prop_name(field),
+                                    Sexp::make_symbol_sv(priority_name(prio)));
+}
+
+Priority
+Document::priority_value() const noexcept
+{
+       const auto val{string_value(Field::Id::Priority)};
+       return priority_from_char(val.empty() ? 'n' : val[0]);
+}
+
+void
+Document::add(Flags flags)
+{
+       constexpr auto field{field_from_id(Field::Id::Flags)};
+
+       Sexp::List flaglist;
+       xdoc_.add_value(field.value_no(), to_lexnum(static_cast<int64_t>(flags)));
+       flag_infos_for_each([&](auto&& flag_info) {
+               auto term=[&](){return field.xapian_term(flag_info.shortcut_lower());};
+               if (any_of(flag_info.flag & flags)) {
+                       xdoc_.add_boolean_term(term());
+                       flaglist.add(Sexp::make_symbol_sv(flag_info.name));
+               }
+       });
+
+       if (field.include_in_sexp())
+               sexp_list().add_prop(make_prop_name(field),
+                                    Sexp::make_list(std::move(flaglist)));
+}
+
+
+Sexp::List&
+Document::sexp_list()
+{
+       /* perhaps we need get the sexp_ from the document first? */
+       if (sexp_list_.empty()) {
+               const auto str{xdoc_.get_data()};
+               if (!str.empty()) {
+                       Sexp sexp{Sexp::make_parse(str)};
+                       sexp_list_ = sexp.list();
+               }
+       }
+
+       return sexp_list_;
+}
+
+std::string
+Document::cached_sexp() const
+{
+       return xdoc_.get_data();
+}
+
+void
+Document::update_cached_sexp(void)
+{
+       if (sexp_list_.empty())
+               return; /* nothing to do; i.e. the exisiting sexp is still up to
+                        * date */
+       xdoc_.set_data(Sexp::make_list(Sexp::List{sexp_list()}).to_sexp_string());
+}
+
+Flags
+Document::flags_value() const noexcept
+{
+       return static_cast<Flags>(integer_value(Field::Id::Flags));
+}
+
+void
+Document::remove(Field::Id field_id)
+{
+       const auto field{field_from_id(field_id)};
+       const auto pfx{field.xapian_prefix()};
+
+       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());
+               }
+
+               std::vector<std::string> kill_list;
+               for (auto&& it = xdoc_.termlist_begin();
+                    it != xdoc_.termlist_end(); ++it)  {
+                       const auto term{*it};
+                       if (!term.empty() && term.at(0) == pfx)
+                               kill_list.emplace_back(term);
+               }
+
+               for (auto&& term: kill_list) {
+                       // g_debug("removing term '%s'", term.c_str());
+                       try {
+                               xdoc_.remove_term(term);
+                       } catch(const Xapian::InvalidArgumentError& xe) {
+                               g_critical("failed to remove '%s'", term.c_str());
+                       }
+               }
+       });
+
+}
+
+
+#ifdef BUILD_TESTS
+
+#include "utils/mu-test-utils.hh"
+
+#define assert_same_contact(C1,C2) do {                                \
+       g_assert_cmpstr(C1.email.c_str(),==,C2.email.c_str());  \
+       g_assert_cmpstr(C2.name.c_str(),==,C2.name.c_str());    \
+       } while (0)
+
+#define assert_same_contacts(CV1,CV2) do {                     \
+       g_assert_cmpuint(CV1.size(),==,CV2.size());             \
+       for (auto i = 0U; i != CV1.size(); ++i)                 \
+               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},
+               Contact{"paul@example.com", "Paul", Contact::Type::Cc},
+               Contact{"george@example.com", "George",  Contact::Type::Cc},
+               Contact{"james@example.com", "James", Contact::Type::From},
+               Contact{"lars@example.com", "Lars",  Contact::Type::To},
+               Contact{"kirk@example.com", "Kirk", Contact::Type::To},
+               Contact{"jason@example.com", "Jason",  Contact::Type::To}
+       }};
+
+static void
+test_bcc()
+{
+       {
+               Document doc;
+               doc.add(Field::Id::Bcc, test_contacts);
+
+               Contacts expected_contacts = {{
+                               Contact{"john@example.com", "John",
+                                       Contact::Type::Bcc},
+                               Contact{"ringo@example.com", "Ringo",
+                                       Contact::Type::Bcc},
+                       }};
+               const auto actual_contacts = doc.contacts_value(Field::Id::Bcc);
+               assert_same_contacts(expected_contacts, actual_contacts);
+       }
+
+       {
+               Document doc;
+               Contacts contacts = {{
+                               Contact{"john@example.com", "John Lennon",
+                                       Contact::Type::Bcc},
+                               Contact{"ringo@example.com", "Ringo",
+                                       Contact::Type::Bcc},
+                       }};
+               doc.add(Field::Id::Bcc, contacts);
+
+               TempDir tempdir;
+               auto db = Xapian::WritableDatabase(tempdir.path());
+               db.add_document(doc.xapian_document());
+
+               auto contacts2 = doc.contacts_value(Field::Id::Bcc);
+               assert_same_contacts(contacts, contacts2);
+       }
+
+}
+
+static void
+test_cc()
+{
+       Document doc;
+       doc.add(Field::Id::Cc, test_contacts);
+
+       Contacts expected_contacts = {{
+                       Contact{"paul@example.com", "Paul", Contact::Type::Cc},
+                       Contact{"george@example.com", "George",  Contact::Type::Cc}
+               }};
+       const auto actual_contacts = doc.contacts_value(Field::Id::Cc);
+
+       assert_same_contacts(expected_contacts, actual_contacts);
+}
+
+
+static void
+test_from()
+{
+       Document doc;
+       doc.add(Field::Id::From, test_contacts);
+
+       Contacts expected_contacts = {{
+                       Contact{"james@example.com", "James", Contact::Type::From},
+               }};
+       const auto actual_contacts = doc.contacts_value(Field::Id::From);
+
+       assert_same_contacts(expected_contacts, actual_contacts);
+}
+
+static void
+test_to()
+{
+       Document doc;
+       doc.add(Field::Id::To, test_contacts);
+
+       Contacts expected_contacts = {{
+               Contact{"lars@example.com", "Lars",  Contact::Type::To},
+               Contact{"kirk@example.com", "Kirk", Contact::Type::To},
+               Contact{"jason@example.com", "Jason",  Contact::Type::To}
+               }};
+       const auto actual_contacts = doc.contacts_value(Field::Id::To);
+
+       assert_same_contacts(expected_contacts, actual_contacts);
+}
+
+
+static void
+test_size()
+{
+       {
+               Document doc;
+               doc.add(Field::Id::Size, 12345);
+               g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,12345);
+       }
+
+       {
+               Document doc;
+               g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,0);
+       }
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/message/document/bcc", test_bcc);
+       g_test_add_func("/message/document/cc", test_cc);
+       g_test_add_func("/message/document/from", test_from);
+       g_test_add_func("/message/document/to", test_to);
+
+       g_test_add_func("/message/document/size", test_size);
+
+       return g_test_run();
+}
+#endif /*BUILD_TESTS*/
diff --git a/lib/message/mu-document.hh b/lib/message/mu-document.hh
new file mode 100644 (file)
index 0000000..9c923a1
--- /dev/null
@@ -0,0 +1,239 @@
+/** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_DOCUMENT_HH__
+#define MU_DOCUMENT_HH__
+
+#include <xapian.h>
+#include <utility>
+#include <string>
+#include <vector>
+#include "utils/mu-xapian-utils.hh"
+
+#include "mu-fields.hh"
+#include "mu-priority.hh"
+#include "mu-flags.hh"
+#include "mu-contact.hh"
+#include <utils/mu-option.hh>
+#include <utils/mu-sexp.hh>
+
+namespace Mu {
+
+/**
+ * A Document describes the information about a message that is
+ * or can be stored in the database.
+ *
+ */
+class Document {
+public:
+       /**
+        * Construct a message for a new Xapian Document
+        *
+        */
+       Document() {}
+
+       /**
+        * Construct a message document based on on existing Xapian document.
+        *
+        * @param doc
+        */
+       Document(const Xapian::Document& doc): xdoc_{doc} {}
+
+       /**
+        * Get a reference to the underlying Xapian document.
+        *
+        */
+       const Xapian::Document& xapian_document() const { return xdoc_; }
+
+       /**
+        * Get the doc-id for this document
+        *
+        * @return the docid
+        */
+       Xapian::docid docid() const { return xdoc_.get_docid(); }
+
+       /*
+        * updating a document with terms & values
+        */
+
+       /**
+        * Add a string value to the document
+        *
+        * @param field_id field id
+        * @param val string value
+        */
+       void    add(Field::Id field_id, const std::string& val);
+
+       /**
+        * Add a string-vec value to the document, if non-empty
+        *
+        * @param field_id field id
+        * @param val string-vec value
+        */
+       void    add(Field::Id field_id, const std::vector<std::string>& vals);
+
+
+       /**
+        * Add message-contacts to the document, if non-empty
+        *
+        * @param field_id field id
+        * @param contacts message contacts
+        */
+       void    add(Field::Id id, const Contacts& contacts);
+
+       /**
+        * Add some extra contacts with the given propname; this is useful for
+        * ":reply-to" and ":list-post" which don't have a Field::Id and are
+        * only present in the sexp, not in the terms/values
+        *
+        * @param propname property name (e.g.,. ":reply-to")
+        * @param contacts contacts for this property.
+        */
+       void add_extra_contacts(const std::string& propname,
+                               const Contacts& contacts);
+
+       /**
+        * Add an integer value to the document
+        *
+        * @param field_id field id
+        * @param val integer value
+        */
+       void    add(Field::Id field_id, int64_t val);
+
+       /**
+        * Add a message priority to the document
+        *
+        * @param prio priority
+        */
+       void    add(Priority prio);
+
+
+       /**
+        *  Add message flags to the document
+        *
+        * @param flags mesage flags.
+        */
+       void    add(Flags flags);
+
+       /**
+        * Remove values and terms for some field.
+        *
+        * @param field_id
+        */
+       void remove(Field::Id field_id);
+
+       /**
+        * Update the cached sexp from the sexp_list_
+        */
+       void update_cached_sexp();
+
+       /**
+        * Get the cached s-expression
+        *
+        * @return a string
+        */
+       std::string cached_sexp() const;
+
+       /**
+        * Get the cached s-expressionl useful for changing
+        * it (call update_sexp_cache() when done)
+        *
+        * @return the cache s-expression
+        */
+       Sexp::List& sexp_list();
+
+       /**
+        * Generically adds an optional value, if set, to the document
+        *
+        * @param id the field 0d
+        * @param an optional value
+        */
+       template<typename T> void add(Field::Id id, const Option<T>& val) {
+               if (val)
+                       add(id, val.value());
+       }
+
+       /*
+        * Retrieving values
+        */
+
+       /**
+        * Get a message-field as a string-value
+        *
+        * @param field_id id of the field to get.
+        *
+        * @return a string (empty if not found)
+        */
+       std::string string_value(Field::Id field_id) const noexcept {
+               return xapian_try([&]{
+                       return xdoc_.get_value(field_from_id(field_id).value_no());
+               }, std::string{});
+       }
+       /**
+        * Get a vec of string values.
+        *
+        * @param field_id id of the field to get
+        *
+        * @return a string list
+        */
+       std::vector<std::string> string_vec_value(Field::Id field_id) const noexcept;
+
+
+       /**
+        * Get an integer value
+        *
+        * @param field_id id of the field to get
+        *
+        * @return an integer or 0 if not found.
+        */
+       int64_t integer_value(Field::Id field_id) const noexcept;
+
+
+       /**
+        * Get contacts
+        *
+        * @param field_id id of the contacts field to get
+        *
+        * @return a contacts list
+        */
+       Contacts contacts_value(Field::Id id) const noexcept;
+
+       /**
+        * Get the priority
+        *
+        * @return the message priority
+        */
+       Priority priority_value() const noexcept;
+
+       /**
+        * Get the message flags
+        *
+        *
+        * @return flags
+        */
+       Flags    flags_value() const noexcept;
+
+private:
+       Xapian::Document        xdoc_;
+       Sexp::List              sexp_list_;
+
+};
+
+} // namepace Mu
+
+#endif /* MU_DOCUMENT_HH__ */
diff --git a/lib/message/mu-fields.cc b/lib/message/mu-fields.cc
new file mode 100644 (file)
index 0000000..6727e57
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-fields.hh"
+#include "mu-flags.hh"
+
+#include "utils/mu-test-utils.hh"
+
+using namespace Mu;
+
+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;
+
+       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)
+                       res[i] = g_ascii_tolower(res[i]);
+       } else
+               res += utf8_flatten(s);
+
+       if (G_UNLIKELY(res.size() > MaxTermLength))
+               res.erase(MaxTermLength);
+
+       return res;
+}
+
+/**
+ * compile-time checks
+ */
+constexpr bool
+validate_field_ids()
+{
+       for (auto id = 0U; id != Field::id_size(); ++id) {
+               const auto field_id = static_cast<Field::Id>(id);
+               if (field_from_id(field_id).id != field_id)
+                       return false;
+       }
+       return true;
+}
+
+constexpr bool
+validate_field_shortcuts()
+{
+#ifdef BUILD_TESTS
+       std::array<size_t, 26> no_dups = {0};
+#endif /*BUILD_TESTS*/
+       for (auto id = 0U; id != Field::id_size(); ++id) {
+               const auto field_id = static_cast<Field::Id>(id);
+               const auto shortcut = field_from_id(field_id).shortcut;
+               if (shortcut != 0 &&
+                   (shortcut < 'a' || shortcut > 'z'))
+                       return false;
+#ifdef BUILD_TESTS
+               if (shortcut != 0) {
+                       if (++no_dups[static_cast<size_t>(shortcut-'a')] > 1) {
+                               g_critical("shortcut '%c' is duplicated",
+                                          shortcut);
+                               return false;
+                       }
+               }
+#endif
+       }
+
+       return true;
+}
+
+
+constexpr /*static*/ bool
+validate_field_flags()
+{
+       for (auto&& field: Fields) {
+               /* - A field has at most one of Indexable, HasTerms, IsXapianBoolean and
+                  IsContact. */
+               size_t flagnum{};
+
+               if (field.is_indexable_term())
+                       ++flagnum;
+               if (field.is_boolean_term())
+                       ++flagnum;
+               if (field.is_normal_term())
+                       ++flagnum;
+
+               if (flagnum > 1) {
+                       //g_warning("invalid field %*.s", STR_V(field.name));
+                       return false;
+               }
+       }
+
+       return true;
+}
+
+/*
+ * tests... also build as runtime-tests, so we can get coverage info
+ */
+#ifdef BUILD_TESTS
+#define static_assert g_assert_true
+#endif /*BUILD_TESTS*/
+
+
+[[maybe_unused]]
+static void
+test_ids()
+{
+       static_assert(validate_field_ids());
+}
+
+[[maybe_unused]]
+static void
+test_shortcuts()
+{
+       static_assert(validate_field_shortcuts());
+}
+
+[[maybe_unused]]
+static void
+test_prefix()
+{
+       static_assert(field_from_id(Field::Id::Subject).xapian_prefix() == 'S');
+       static_assert(field_from_id(Field::Id::XBodyHtml).xapian_prefix() == 0);
+}
+
+[[maybe_unused]]
+static void
+test_field_flags()
+{
+       static_assert(validate_field_flags());
+}
+
+#ifdef BUILD_TESTS
+
+
+static void
+test_field_from_name()
+{
+       g_assert_true(field_from_name("s")->id == Field::Id::Subject);
+       g_assert_true(field_from_name("subject")->id == Field::Id::Subject);
+       g_assert_false(!!field_from_name("8"));
+       g_assert_false(!!field_from_name(""));
+
+       g_assert_true(field_from_name("").value_or(field_from_id(Field::Id::Bcc)).id ==
+                     Field::Id::Bcc);
+}
+
+
+static void
+test_xapian_term()
+{
+       using namespace std::string_literals;
+       using namespace std::literals;
+
+       assert_equal(field_from_id(Field::Id::Subject).xapian_term(""s), "S");
+       assert_equal(field_from_id(Field::Id::Subject).xapian_term("boo"s), "Sboo");
+
+       assert_equal(field_from_id(Field::Id::From).xapian_term('x'), "Fx");
+       assert_equal(field_from_id(Field::Id::To).xapian_term("boo"sv), "Tboo");
+
+       auto s1 = field_from_id(Field::Id::Subject).xapian_term(std::string(MaxTermLength - 1, 'x'));
+       auto s2 = field_from_id(Field::Id::Subject).xapian_term(std::string(MaxTermLength, 'x'));
+       g_assert_cmpuint(s1.length(), ==, s2.length());
+}
+
+int
+main(int argc, char* argv[])
+{
+       mu_test_init(&argc, &argv);
+
+       g_test_add_func("/message/fields/ids", test_ids);
+       g_test_add_func("/message/fields/shortcuts", test_shortcuts);
+       g_test_add_func("/message/fields/from-name", test_field_from_name);
+       g_test_add_func("/message/fields/prefix", test_prefix);
+       g_test_add_func("/message/fields/xapian-term", test_xapian_term);
+       g_test_add_func("/message/fields/flags", test_field_flags);
+
+       return g_test_run();
+}
+#endif /*BUILD_TESTS*/
diff --git a/lib/message/mu-fields.hh b/lib/message/mu-fields.hh
new file mode 100644 (file)
index 0000000..22b486b
--- /dev/null
@@ -0,0 +1,546 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_FIELDS_HH__
+#define MU_FIELDS_HH__
+
+#include <cstdint>
+#include <string_view>
+#include <algorithm>
+#include <array>
+#include <xapian.h>
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+
+namespace Mu {
+
+// Xapian does not like terms much longer than this
+constexpr auto MaxTermLength = 240;
+// http://article.gmane.org/gmane.comp.search.xapian.general/3656 */
+
+struct Field {
+       /**
+        * Field Ids.
+        *
+        * Note, the Ids are also used as indices in the Fields array,
+        * so their numerical values must be 0...Count.
+        *
+        */
+       enum struct Id {
+               Bcc = 0,        /**< Blind Carbon-Copy */
+               BodyText,       /**< Text body */
+               Cc,             /**< Carbon-Copy */
+               Changed,        /**< Last change time (think 'ctime') */
+               Date,           /**< Message date */
+               EmbeddedText,   /**< Embedded text in message */
+               File,           /**< Filename */
+               Flags,          /**< Message flags */
+               From,           /**< Message sender */
+               Maildir,        /**< Maildir path */
+               MailingList,    /**< Mailing list */
+               MessageId,      /**< Message Id */
+               MimeType,       /**< MIME-Type */
+               Path,           /**< File-system Path */
+               Priority,       /**< Message priority */
+               References,     /**< All references (incl. Reply-To:) */
+               Size,           /**< Message size (in bytes) */
+               Subject,        /**< Message subject */
+               Tags,           /**< Message Tags */
+               ThreadId,       /**< Thread Id */
+               To,             /**< To: recipient */
+               /*
+                * <private>
+                */
+               XBodyHtml,      /**< HTML Body */
+
+               _count_ /**< Number of FieldIds */
+       };
+
+       /**
+        * Get the number of Id values.
+        *
+        * @return the number.
+        */
+       static constexpr size_t id_size()
+       {
+               return static_cast<size_t>(Id::_count_);
+       }
+
+       constexpr Xapian::valueno value_no() const {
+               return static_cast<Xapian::valueno>(id);
+       }
+
+       /**
+        * Field types
+        *
+        */
+       enum struct Type {
+               String,         /**< String */
+               StringList,     /**< List of strings */
+               ContactList,    /**< List of contacts */
+               ByteSize,       /**< Size in bytes */
+               TimeT,          /**< A time_t value */
+               Integer,        /**< An integer */
+       };
+
+       constexpr bool is_string() const { return type == Type::String; }
+       constexpr bool is_string_list() const { return type == Type::StringList; }
+       constexpr bool is_byte_size() const { return type == Type::ByteSize; }
+       constexpr bool is_time_t() const { return type == Type::TimeT; }
+       constexpr bool is_integer() const { return type == Type::Integer; }
+       constexpr bool is_numerical() const { return is_byte_size() || is_time_t() || is_integer(); }
+
+       /**
+        * Field flags
+        * note: the differences for our purposes between a xapian field and a
+        * term: - there is only a single value for some item in per document
+        * (msg), ie.  one value containing the list of To: addresses - there
+        * can be multiple terms, each containing e.g. one of the To:
+        * addresses - searching uses terms, but to display some field, it
+        * must be in the value (at least when using MuMsgIter)
+        *
+        * Rules (build-time enforced):
+        * - A field has at most one of Indexable, HasTerms, IsXapianBoolean and IsContact.
+        */
+
+       enum struct Flag {
+               /*
+                * Different kind of terms; at most one is true,
+                * and cannot be combined with IsContact. Compile-time enforced.
+                */
+               NormalTerm    = 1 << 0,
+               /**< Field is a searchable term */
+               BooleanTerm   = 1 << 1,
+               /**< Field is a boolean search-term (i.e. at most one per message);
+                * wildcards do not work */
+               IndexableTerm = 1 << 2,
+               /**< Field has indexable text as term */
+               /*
+                * Contact flag cannot be combined with any of the term flags.
+                * This is compile-time enforced.
+                */
+               Contact = 1 << 10,
+               /**< field contains one or more e-mail-addresses */
+               Value   = 1 << 11,
+               /**< Field value is stored (so the literal value can be retrieved) */
+
+               Range      = 1 << 21,
+
+               IncludeInSexp = 1 << 24,
+               /**< whether to include this field in the cached sexp. */
+
+               /**< whether this is a range field (e.g., date, size)*/
+               Internal      = 1 << 26
+       };
+
+       constexpr bool any_of(Flag some_flag) const{
+               return (static_cast<int>(some_flag) & static_cast<int>(flags)) != 0;
+       }
+
+       constexpr bool is_indexable_term()      const { return any_of(Flag::IndexableTerm); }
+       constexpr bool is_boolean_term()        const { return any_of(Flag::BooleanTerm); }
+       constexpr bool is_normal_term()         const { return any_of(Flag::NormalTerm); }
+       constexpr bool is_searchable()          const { return is_indexable_term() ||
+                                                              is_boolean_term() ||
+                                                              is_normal_term(); }
+
+       constexpr bool is_value()               const { return any_of(Flag::Value); }
+       constexpr bool is_internal()            const { return any_of(Flag::Internal); }
+
+       constexpr bool is_contact()             const { return any_of(Flag::Contact); }
+       constexpr bool is_range()               const { return any_of(Flag::Range); }
+
+       constexpr bool include_in_sexp()        const { return any_of(Flag::IncludeInSexp);}
+
+       /**
+        * Field members
+        *
+        */
+       Id               id;            /**< Id of the message field */
+       Type             type;          /**< Type of the message field */
+       std::string_view name;          /**< Name of the message field */
+       std::string_view alias;         /**< Alternative name for the message field */
+       std::string_view description;   /**< Decription of the message field */
+       std::string_view example_query; /**< Example query */
+       char             shortcut;      /**< Shortcut for the message field; a..z */
+       Flag             flags;         /**< Flags */
+
+       /**
+        * Convenience / helpers
+        *
+        */
+
+       constexpr char xapian_prefix() const
+       { /* xapian uses uppercase shortcuts; toupper is not constexpr */
+               return shortcut == 0 ? 0 : shortcut - ('a' - 'A');
+       }
+
+       /**
+        * Get the xapian term; truncated to MaxTermLength and
+        * utf8-flattened.
+        *
+        * @param s
+        *
+        * @return the xapian term
+        */
+       std::string xapian_term(const std::string& s="") const;
+       std::string xapian_term(std::string_view sv) const {
+               return xapian_term(std::string{sv});
+       }
+       std::string xapian_term(char c) const {
+               return xapian_term(std::string(1, c));
+       }
+};
+
+MU_ENABLE_BITOPS(Field::Flag);
+
+/**
+ * Sequence of _all_ message fields
+ */
+static constexpr std::array<Field, Field::id_size()>
+    Fields = {
+       {
+           {
+               Field::Id::Bcc,
+               Field::Type::ContactList,
+               "bcc", {},
+               "Blind carbon-copy recipient",
+               "bcc:foo@example.com",
+               'h',
+               Field::Flag::Contact |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp |
+               Field::Flag::IndexableTerm,
+
+           },
+           {
+               Field::Id::BodyText,
+               Field::Type::String,
+               "body", {},
+               "Message plain-text body",
+               "body:capybara",
+               'b',
+               Field::Flag::IndexableTerm,
+           },
+           {
+               Field::Id::Cc,
+               Field::Type::ContactList,
+               "cc", {},
+               "Carbon-copy recipient",
+               "cc:quinn@example.com",
+               'c',
+               Field::Flag::Contact |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp |
+               Field::Flag::IndexableTerm,
+           },
+
+           {
+               Field::Id::Changed,
+               Field::Type::TimeT,
+               "changed", {},
+               "Last change time",
+               "changed:30M..",
+               'k',
+               Field::Flag::Value |
+               Field::Flag::Range |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::Date,
+               Field::Type::TimeT,
+               "date", {},
+               "Message date",
+               "date:20220101..20220505",
+               'd',
+               Field::Flag::Value |
+               Field::Flag::Range |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::EmbeddedText,
+               Field::Type::String,
+               "embed", {},
+               "Embedded text",
+               "embed:war OR embed:peace",
+               'e',
+               Field::Flag::IndexableTerm
+           },
+           {
+               Field::Id::File,
+               Field::Type::String,
+               "file", {},
+               "Attachment file name",
+               "file:/image\\.*.jpg/",
+               'j',
+               Field::Flag::BooleanTerm
+           },
+           {
+               Field::Id::Flags,
+               Field::Type::Integer,
+               "flags", "flag",
+               "Message properties",
+               "flag:unread AND flag:personal",
+               'g',
+               Field::Flag::BooleanTerm |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::From,
+               Field::Type::ContactList,
+               "from", {},
+               "Message sender",
+               "from:jimbo",
+               'f',
+               Field::Flag::Contact |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp |
+               Field::Flag::IndexableTerm,
+           },
+           {
+               Field::Id::Maildir,
+               Field::Type::String,
+               "maildir", {},
+               "Maildir path for message",
+               "maildir:/private/archive",
+               'm',
+               Field::Flag::BooleanTerm |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::MailingList,
+               Field::Type::String,
+               "list", {},
+               "Mailing list (List-Id:)",
+               "list:mu-discuss.example.com",
+               'v',
+               Field::Flag::BooleanTerm |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::MessageId,
+               Field::Type::String,
+               "message-id", "msgid",
+               "Message-Id",
+               "msgid:abc@123",
+               'i',
+               Field::Flag::BooleanTerm |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::MimeType,
+               Field::Type::String,
+               "mime", "mime-type",
+               "Attachment MIME-type",
+               "mime:image/jpeg",
+               'y',
+               Field::Flag::BooleanTerm
+           },
+           {
+               Field::Id::Path,
+               Field::Type::String,
+               "path", {},
+               "File system path to message",
+               "path:/a/b/Maildir/cur/msg:2,S",
+               'l',
+               Field::Flag::BooleanTerm |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::Priority,
+               Field::Type::Integer,
+               "priority", "prio",
+               "Priority",
+               "prio:high",
+               'p',
+               Field::Flag::BooleanTerm |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::References,
+               Field::Type::StringList,
+               "references", {},
+               "References to related messages",
+               {},
+               'r',
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::Size,
+               Field::Type::ByteSize,
+               "size", {},
+               "Message size in bytes",
+               "size:1M..5M",
+               'z',
+               Field::Flag::Value |
+               Field::Flag::Range |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::Subject,
+               Field::Type::String,
+               "subject", {},
+               "Message subject",
+               "subject:wombat",
+               's',
+               Field::Flag::Value |
+               Field::Flag::IndexableTerm |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::Tags,
+               Field::Type::StringList,
+               "tags", "tag",
+               "Message tags",
+               "tag:projectx",
+               'x',
+               Field::Flag::BooleanTerm |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp
+           },
+           {
+               Field::Id::ThreadId,
+               Field::Type::String,
+               "thread", {},
+               "Thread a message belongs to",
+               {},
+               'w',
+               Field::Flag::BooleanTerm |
+               Field::Flag::Value
+           },
+           {
+               Field::Id::To,
+               Field::Type::ContactList,
+               "to", {},
+               "Message recipient",
+               "to:flimflam@example.com",
+               't',
+               Field::Flag::Contact |
+               Field::Flag::Value |
+               Field::Flag::IncludeInSexp |
+               Field::Flag::IndexableTerm,
+           },
+
+           /* internal */
+           {
+               Field::Id::XBodyHtml,
+               Field::Type::String,
+               "htmlbody", {},
+               "Message html body",
+               {},
+               {},
+               Field::Flag::Internal
+           },
+       }};
+
+/*
+ * Convenience
+ */
+
+/**
+ * Get the message field for the given Id.
+ *
+ * @param id of the message field
+ *
+ * @return ref of the message field.
+ */
+constexpr const Field&
+field_from_id(Field::Id id)
+{
+       return Fields.at(static_cast<size_t>(id));
+}
+
+/**
+ * Invoke func for each message-field
+ *
+ * @param func some callable
+ */
+template <typename Func>
+constexpr void field_for_each(Func&& func) {
+       for (const auto& field: Fields)
+               func(field);
+}
+
+/**
+ * Find a message field that satisfies some predicate
+ *
+ * @param pred the predicate (a callable)
+ *
+ * @return a message-field id, or nullopt if not found.
+ */
+template <typename Pred>
+constexpr Option<Field> field_find_if(Pred&& pred) {
+       for (auto&& field: Fields)
+               if (pred(field))
+                       return field;
+       return Nothing;
+}
+
+/**
+ * Get the the message-field id for the given name or shortcut
+ *
+ * @param name_or_shortcut
+ *
+ * @return the message-field-id or nullopt.
+ */
+static inline
+Option<Field> field_from_shortcut(char shortcut) {
+       return field_find_if([&](auto&& field){
+               return field.shortcut == shortcut;
+       });
+}
+static inline
+Option<Field> field_from_name(const std::string& name) {
+       switch(name.length()) {
+       case 0:
+               return Nothing;
+       case 1:
+               return field_from_shortcut(name[0]);
+       default:
+               return field_find_if([&](auto&& field){
+                       return name == field.name || name == field.alias;
+               });
+       }
+}
+
+/**
+ * Get the Field::Id for some number, or nullopt if it does not match
+ *
+ * @param id an id number
+ *
+ * @return Field::Id  or nullopt
+ */
+static inline
+Option<Field> field_from_number(size_t id)
+{
+       if (id >= static_cast<size_t>(Field::Id::_count_))
+               return Nothing;
+       else
+               return field_from_id(static_cast<Field::Id>(id));
+}
+
+} // namespace Mu
+#endif /* MU_FIELDS_HH__ */
diff --git a/lib/message/mu-flags.cc b/lib/message/mu-flags.cc
new file mode 100644 (file)
index 0000000..549fa2e
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+/*
+ * implementation is almost completely in the header; here we just add some
+ * compile-time tests.
+ */
+
+#include "mu-flags.hh"
+
+using namespace Mu;
+
+std::string
+Mu::to_string(Flags flags)
+{
+       std::string str;
+
+       for (auto&& info: AllMessageFlagInfos)
+               if (any_of(info.flag & flags))
+                       str+=info.shortcut;
+
+       return str;
+}
+
+
+/*
+ * flags & flag-info
+ */
+constexpr bool
+validate_message_info_flags()
+{
+       for (auto id = 0U; id != AllMessageFlagInfos.size(); ++id) {
+               const auto flag = static_cast<Flags>(1 << id);
+               if (flag != AllMessageFlagInfos[id].flag)
+                       return false;
+       }
+       return true;
+}
+
+
+/*
+ * tests... also build as runtime-tests, so we can get coverage info
+ */
+#ifdef BUILD_TESTS
+#define static_assert g_assert_true
+#endif /*BUILD_TESTS*/
+
+[[maybe_unused]] static void
+test_basic()
+{
+       static_assert(AllMessageFlagInfos.size() ==
+                     __builtin_ctz(static_cast<unsigned>(Flags::_final_)));
+       static_assert(validate_message_info_flags());
+
+       static_assert(!!flag_info(Flags::Encrypted));
+       static_assert(!flag_info(Flags::None));
+       static_assert(!flag_info(static_cast<Flags>(0)));
+       static_assert(!flag_info(static_cast<Flags>(1<<AllMessageFlagInfos.size())));
+}
+
+/*
+ * flag_info
+ */
+[[maybe_unused]] static void
+test_flag_info()
+{
+       static_assert(flag_info('D')->flag == Flags::Draft);
+       static_assert(flag_info('l')->flag == Flags::MailingList);
+       static_assert(!flag_info('y'));
+
+       static_assert(flag_info("trashed")->flag == Flags::Trashed);
+       static_assert(flag_info("attach")->flag == Flags::HasAttachment);
+       static_assert(!flag_info("fnorb"));
+
+
+       static_assert(flag_info('D')->shortcut_lower() == 'd');
+       static_assert(flag_info('u')->shortcut_lower() == 'u');
+}
+
+/*
+ * flags_from_expr
+ */
+[[maybe_unused]] static void
+test_flags_from_expr()
+{
+       static_assert(flags_from_absolute_expr("SRP").value() ==
+                     (Flags::Seen | Flags::Replied | Flags::Passed));
+       static_assert(flags_from_absolute_expr("Faul").value() ==
+                     (Flags::Flagged | Flags::Unread |
+                      Flags::HasAttachment | Flags::MailingList));
+
+       static_assert(!flags_from_absolute_expr("DRT?"));
+       static_assert(flags_from_absolute_expr("DRT?", true/*ignore invalid*/).value() ==
+                     (Flags::Draft | Flags::Replied |
+                      Flags::Trashed));
+       static_assert(flags_from_absolute_expr("DFPNxulabcdef", true/*ignore invalid*/).value() ==
+                     (Flags::Draft|Flags::Flagged|Flags::Passed|
+                      Flags::New | Flags::Encrypted |
+                      Flags::Unread | Flags::MailingList | Flags::Calendar |
+                      Flags::HasAttachment));
+}
+
+
+/*
+ * flags_from_delta_expr
+ */
+[[maybe_unused]] static void
+test_flags_from_delta_expr()
+{
+       static_assert(flags_from_delta_expr(
+                             "+S-u-N", Flags::New|Flags::Unread).value() ==
+                     Flags::Seen);
+       static_assert(flags_from_delta_expr("+R+P-F", Flags::Seen).value() ==
+                     (Flags::Seen|Flags::Passed|Flags::Replied));
+       /* '-B' is invalid */
+       static_assert(!flags_from_delta_expr("+R+P-B", Flags::Seen));
+       /* '-B' is invalid, but ignore invalid */
+       static_assert(flags_from_delta_expr("+R+P-B", Flags::Seen, true) ==
+                     (Flags::Replied|Flags::Passed|Flags::Seen));
+       static_assert(flags_from_delta_expr("+F+T-S", Flags::None, true).value() ==
+                     (Flags::Flagged|Flags::Trashed));
+}
+
+/*
+ * flags_filter
+ */
+[[maybe_unused]] static void
+test_flags_filter()
+{
+       static_assert(flags_filter(flags_from_absolute_expr(
+                                                  "DFPNxulabcdef", true/*ignore invalid*/).value(),
+                                          MessageFlagCategory::Mailfile) ==
+                     (Flags::Draft|Flags::Flagged|Flags::Passed));
+}
+
+
+#ifdef BUILD_TESTS
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/message/flags/basic", test_basic);
+       g_test_add_func("/message/flags/flag-info", test_flag_info);
+       g_test_add_func("/message/flags/flags-from-absolute-expr",
+                       test_flags_from_expr);
+       g_test_add_func("/message/flags/flags-from-delta-expr",
+                       test_flags_from_delta_expr);
+       g_test_add_func("/message/flags/flags-filter",
+                       test_flags_filter);
+
+       return g_test_run();
+}
+#endif /*BUILD_TESTS*/
diff --git a/lib/message/mu-flags.hh b/lib/message/mu-flags.hh
new file mode 100644 (file)
index 0000000..0ac6496
--- /dev/null
@@ -0,0 +1,352 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_FLAGS_HH__
+#define MU_FLAGS_HH__
+
+#include <algorithm>
+#include <string_view>
+#include <array>
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+
+namespace Mu {
+
+enum struct Flags {
+       None = 0, /**< No flags */
+       /**
+        * next 6 are seen in the file-info part of maildir message file
+        * names, ie., in a name like "1234345346:2,<fileinfo>",
+        * <fileinfo> consists of zero or more of the following
+        * characters (in ascii order)
+        */
+       Draft   = 1 << 0, /**< A draft message */
+       Flagged = 1 << 1, /**< A flagged message */
+       Passed  = 1 << 2, /**< A passed (forwarded) message */
+       Replied = 1 << 3, /**< A replied message */
+       Seen    = 1 << 4, /**< A seen (read) message */
+       Trashed = 1 << 5, /**< A trashed message */
+
+       /**
+        * decides on cur/ or new/ in the maildir
+        */
+       New = 1 << 6, /**< A new message */
+
+       /**
+        * content flags -- not visible in the filename, but used for
+        * searching
+        */
+       Signed        = 1 << 7, /**< Cryptographically signed */
+       Encrypted     = 1 << 8, /**< Encrypted */
+       HasAttachment = 1 << 9, /**< Has an attachment */
+
+       Unread = 1 << 10, /**< Unread; pseudo-flag, only for queries, so we can
+                          * search for flag:unread, which is equivalent to
+                          * 'flag:new OR NOT flag:seen' */
+       /**
+        * other content flags
+        */
+       MailingList = 1 << 11, /**< A mailing-list message */
+       Personal    = 1 << 12, /**< A personal message (i.e., at least one of the
+                               * contact fields contains a personal address) */
+       Calendar    = 1 << 13, /**< A calendar invitation */
+       /*
+        * <private>
+        */
+       _final_ = 1 << 14
+};
+MU_ENABLE_BITOPS(Flags);
+
+/**
+ * Message flags category
+ *
+ */
+enum struct MessageFlagCategory {
+       None,     /**< Nothing */
+       Mailfile, /**< Flag for a message file */
+       Maildir,  /**< Flag for message file's location */
+       Content,  /**< Message content flag */
+       Pseudo    /**< Pseudo flag */
+};
+
+/**
+ * Info about invidual message flags
+ *
+ */
+struct MessageFlagInfo {
+
+       Flags                   flag;           /**< The message flag */
+       char                    shortcut;       /**< Shortcut character;
+                                                * tolower(shortcut) must be
+                                                * unique for all flags */
+       std::string_view        name;           /**< Name of the flag */
+       MessageFlagCategory     category;       /**< Flag category */
+       std::string_view        description;    /**< Description */
+
+       /**
+        * Get the lower-case version of shortcut
+        *
+        * @return lower-case shortcut
+        */
+       constexpr char shortcut_lower() const {
+               return shortcut >= 'A' && shortcut <= 'Z' ?
+                       shortcut +  ('a' - 'A') : shortcut;
+       }
+};
+
+/**
+ * Array of all flag information.
+ */
+constexpr std::array<MessageFlagInfo, 14> AllMessageFlagInfos = {{
+    MessageFlagInfo{Flags::Draft,        'D', "draft",         MessageFlagCategory::Mailfile,
+           "Draft (in progress)"
+    },
+    MessageFlagInfo{Flags::Flagged,     'F', "flagged",        MessageFlagCategory::Mailfile,
+           "User-flagged"
+    },
+    MessageFlagInfo{Flags::Passed,      'P', "passed",         MessageFlagCategory::Mailfile,
+           "Forwarded message"
+    },
+    MessageFlagInfo{Flags::Replied,     'R', "replied",        MessageFlagCategory::Mailfile,
+           "Replied-to"
+    },
+    MessageFlagInfo{Flags::Seen,        'S', "seen",           MessageFlagCategory::Mailfile,
+           "Viewed at least once"
+    },
+    MessageFlagInfo{Flags::Trashed,     'T', "trashed",        MessageFlagCategory::Mailfile,
+           "Marked for deletion"
+    },
+    MessageFlagInfo{Flags::New,                 'N', "new",            MessageFlagCategory::Maildir,
+           "New message"
+    },
+    MessageFlagInfo{Flags::Signed,      'z', "signed",         MessageFlagCategory::Content,
+           "Cryptographically signed"
+    },
+    MessageFlagInfo{Flags::Encrypted,   'x', "encrypted",      MessageFlagCategory::Content,
+           "Encrypted"
+    },
+    MessageFlagInfo{Flags::HasAttachment,'a', "attach",        MessageFlagCategory::Content,
+           "Has at least one attachment"
+    },
+
+    MessageFlagInfo{Flags::Unread,      'u', "unread",         MessageFlagCategory::Pseudo,
+           "New or not seen message"
+    },
+    MessageFlagInfo{Flags::MailingList,         'l', "list",           MessageFlagCategory::Content,
+           "Mailing list message"
+    },
+    MessageFlagInfo{Flags::Personal,    'q', "personal",       MessageFlagCategory::Content,
+           "Personal message"
+    },
+    MessageFlagInfo{Flags::Calendar,    'c', "calendar",       MessageFlagCategory::Content,
+           "Calendar invitation"
+    },
+}};
+
+
+/**
+ * Invoke some callable Func for each flag info
+ *
+ * @param func some callable
+ */
+template<typename Func>
+constexpr void flag_infos_for_each(Func&& func)
+{
+       for (auto&& info: AllMessageFlagInfos)
+               func(info);
+}
+
+/**
+ * Get flag info for some flag
+ *
+ * @param flag a singular flag
+ *
+ * @return the MessageFlagInfo, or Nothing in case of error.
+ */
+constexpr const Option<MessageFlagInfo>
+flag_info(Flags flag)
+{
+       constexpr auto upper = static_cast<unsigned>(Flags::_final_);
+       const auto     val   = static_cast<unsigned>(flag);
+
+       if (__builtin_popcount(val) != 1 || val >= upper)
+               return Nothing;
+
+       return AllMessageFlagInfos[static_cast<unsigned>(__builtin_ctz(val))];
+}
+
+/**
+ * Get flag info for some flag
+ *
+ * @param shortcut shortcut character
+ *
+ * @return the MessageFlagInfo
+ */
+constexpr const Option<MessageFlagInfo>
+flag_info(char shortcut)
+{
+       for (auto&& info : AllMessageFlagInfos)
+               if (info.shortcut == shortcut)
+                       return info;
+
+       return Nothing;
+}
+
+/**
+ * Get flag info for some flag
+ *
+ * @param name of the message-flag.
+ *
+ * @return the MessageFlagInfo
+ */
+constexpr const Option<MessageFlagInfo>
+flag_info(std::string_view name)
+{
+       for (auto&& info : AllMessageFlagInfos)
+               if (info.name == name)
+                       return info;
+
+       return Nothing;
+}
+
+/**
+ * There are two string-based expression types for flags:
+ * 1) 'absolute': replace the existing flags
+ * 2_ 'delta'  : flags as a delta of existing flags.
+ */
+
+/**
+ * Get the (OR'ed) flags corresponding to an expression.
+ *
+ * @param expr the expression (a sequence of flag shortcut characters)
+ * @param ignore_invalid if @true, ignore invalid flags, otherwise return
+ * nullopt if an invalid flag is encountered
+ *
+ * @return the (OR'ed) flags or Flags::None
+ */
+constexpr Option<Flags>
+flags_from_absolute_expr(std::string_view expr, bool ignore_invalid = false)
+{
+       Flags flags{Flags::None};
+
+       for (auto&& kar : expr) {
+               if (const auto& info{flag_info(kar)}; !info) {
+                       if (!ignore_invalid)
+                               return Nothing;
+               } else
+                       flags |= info->flag;
+       }
+
+       return flags;
+}
+
+/**
+ * Calculate flags from existing flags and a delta expression
+ *
+ * Update @p flags with the flags in @p expr, where @p exprt consists of the the
+ * normal flag shortcut characters, prefixed with either '+' or '-', which means
+ * resp. "add this flag" or "remove this flag".
+ *
+ * So, e.g. "-N+S" would unset the NEW flag and set the SEEN flag, without
+ * affecting other flags.
+ *
+ * @param expr delta expression
+ * @param flags existing flags
+ * @param ignore_invalid if @true, ignore invalid flags, otherwise return
+ * nullopt if an invalid flag is encountered
+ *
+ * @return new flags, or nullopt in case of error
+ */
+constexpr Option<Flags>
+flags_from_delta_expr(std::string_view expr, Flags flags,
+                     bool ignore_invalid = false)
+{
+       if (expr.size() % 2 != 0)
+               return Nothing;
+
+       for (auto u = 0U; u != expr.size(); u += 2) {
+               if (const auto& info{flag_info(expr[u + 1])}; !info) {
+                       if (!ignore_invalid)
+                               return Nothing;
+               } else {
+                       switch (expr[u]) {
+                       case '+': flags |= info->flag; break;
+                       case '-': flags &= ~info->flag; break;
+                       default:
+                               if (!ignore_invalid)
+                                       return Nothing;
+                               break;
+                       }
+               }
+       }
+
+       return flags;
+}
+
+/**
+ * Calculate the flags from either 'absolute' or 'delta' expressions
+ *
+ * @param expr a flag expression, either 'delta' or 'absolute'
+ * @param flags optional: existing flags or none. Required for delta.
+ *
+ * @return either messages flags or Nothing in case of error.
+ */
+constexpr Option<Flags>
+flags_from_expr(std::string_view        expr,
+               Option<Flags> flags = Nothing)
+{
+       if (expr.empty())
+               return Nothing;
+
+       if (expr[0] == '+' || expr[0] == '-')
+               return flags_from_delta_expr(
+                   expr, flags.value_or(Flags::None), true);
+       else
+               return flags_from_absolute_expr(expr, true);
+}
+
+/**
+ * Filter out flags which are not in the given category
+ *
+ * @param flags flags
+ * @param cat category
+ *
+ * @return filter flags
+ */
+constexpr Flags
+flags_filter(Flags flags, MessageFlagCategory cat)
+{
+       for (auto&& info : AllMessageFlagInfos)
+               if (info.category != cat)
+                       flags &= ~info.flag;
+       return flags;
+}
+
+/**
+ * Get a string representation of flags
+ *
+ * @param flags flags
+ *
+ * @return string as a sequence of message-flag shortcuts
+ */
+std::string to_string(Flags flags);
+
+} // namespace Mu
+
+#endif /* MU_FLAGS_HH__ */
diff --git a/lib/message/mu-message-file.cc b/lib/message/mu-message-file.cc
new file mode 100644 (file)
index 0000000..3c86c24
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb.bulk@gmail.com>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-message-file.hh"
+
+using namespace Mu;
+
+Result<std::string>
+Mu::maildir_from_path(const std::string& path, const std::string& root)
+{
+       const auto pos = path.find(root);
+       if (pos != 0 || path[root.length()] != '/')
+               return Err(Error{Error::Code::InvalidArgument,
+                               "root '%s' is not a root for path '%s'",
+                               root.c_str(), path.c_str()});
+
+       auto mdir{path.substr(root.length())};
+       auto slash{mdir.rfind('/')};
+
+       if (G_UNLIKELY(slash == std::string::npos) || slash < 4)
+               return Err(Error{Error::Code::InvalidArgument,
+                               "invalid path: %s", path.c_str()});
+       mdir.erase(slash);
+       auto subdir = mdir.data() + slash - 4;
+       if (G_UNLIKELY(strncmp(subdir, "/cur", 4) != 0 && strncmp(subdir, "/new", 4)))
+               return Err(Error::Code::InvalidArgument,
+                          "cannot find '/new' or '/cur' - invalid path: %s",
+                          path.c_str());
+
+       if (mdir.length() == 4)
+               return "/";
+
+       mdir.erase(mdir.length() - 4);
+
+       return Ok(std::move(mdir));
+}
+
+Mu::FileParts
+Mu::message_file_parts(const std::string& file)
+{
+       const auto pos{file.find_last_of(":!;")};
+
+       /* no suffix at all? */
+       if (pos == std::string::npos ||
+           pos > file.length() - 3 ||
+           file[pos + 1] != '2' ||
+           file[pos + 2] != ',')
+               return FileParts{ file, ':', {}};
+
+       return FileParts {
+               file.substr(0, pos),
+               file[pos],
+               file.substr(pos + 3)
+       };
+}
+
+Mu::Result<DirFile>
+Mu::base_message_dir_file(const std::string& path)
+{
+       constexpr auto newdir{ G_DIR_SEPARATOR_S "new"};
+
+       char *dirname{g_path_get_dirname(path.c_str())};
+       bool is_new{!!g_str_has_suffix(dirname, newdir)};
+
+       std::string mdir{dirname, ::strlen(dirname) - 4};
+       g_free(dirname);
+
+       char *basename{g_path_get_basename(path.c_str())};
+       std::string bname{basename};
+       g_free(basename);
+
+       return Ok(DirFile{std::move(mdir), std::move(bname), is_new});
+}
+
+Mu::Result<Mu::Flags>
+Mu::flags_from_path(const std::string& path)
+{      /*
+        * this gets us the source maildir filesystem path, the directory
+        * in which new/ & cur/ lives, and the source file
+        */
+       auto dirfile{base_message_dir_file(path)};
+       if (!dirfile)
+               return Err(std::move(dirfile.error()));
+
+       /* a message under new/ is just.. New. Filename is not considered */
+       if (dirfile->is_new)
+               return Ok(Flags::New);
+
+       /* it's cur/ message, so parse the file name */
+       const auto parts{message_file_parts(dirfile->file)};
+       auto flags{flags_from_absolute_expr(parts.flags_suffix,
+                                           true/*ignore invalid*/)};
+       if (!flags) {
+               /* LCOV_EXCL_START*/
+               return Err(Error{Error::Code::InvalidArgument,
+                               "invalid flags ('%s')", parts.flags_suffix.c_str()});
+               /* LCOV_EXCL_STOP*/
+       }
+
+       /* of course, only _file_ flags are allowed */
+       return Ok(flags_filter(flags.value(), MessageFlagCategory::Mailfile));
+}
+
+
+
+
+#ifdef BUILD_TESTS
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_maildir_from_path()
+{
+       std::array<std::tuple<std::string, std::string, std::string>, 1> test_cases = {{
+               { "/home/foo/Maildir/hello/cur/msg123", "/home/foo/Maildir", "/hello" }
+       }};
+
+       for(auto&& tcase: test_cases)  {
+               const auto res{maildir_from_path(std::get<0>(tcase), std::get<1>(tcase))};
+               assert_valid_result(res);
+               assert_equal(*res, std::get<2>(tcase));
+       }
+
+       g_assert_false(!!maildir_from_path("/home/foo/Maildir/cur/test1", "/home/bar"));
+       g_assert_false(!!maildir_from_path("/x", "/x/y"));
+       g_assert_false(!!maildir_from_path("/home/a/Maildir/b/xxx/test", "/home/a/Maildir"));
+}
+
+static void
+test_base_message_dir_file()
+{
+       struct TestCase {
+               const std::string path;
+               DirFile expected;
+       };
+       std::array<TestCase, 1> test_cases = {{
+                       { "/home/djcb/Maildir/foo/cur/msg:2,S",
+                         { "/home/djcb/Maildir/foo", "msg:2,S", false } }
+               }};
+       for(auto&& tcase: test_cases)  {
+               const auto res{base_message_dir_file(tcase.path)};
+               assert_valid_result(res);
+               assert_equal(res->dir, tcase.expected.dir);
+               assert_equal(res->file, tcase.expected.file);
+               g_assert_cmpuint(res->is_new, ==, tcase.expected.is_new);
+       }
+}
+
+static void
+test_flags_from_path()
+{
+       std::array<std::pair<std::string, Flags>, 5> test_cases = {{
+               {"/home/foo/Maildir/test/cur/123456:2,FSR",
+                (Flags::Replied | Flags::Seen | Flags::Flagged)},
+               {"/home/foo/Maildir/test/new/123456", Flags::New},
+               {/* NOTE: when in new/, the :2,.. stuff is ignored */
+                       "/home/foo/Maildir/test/new/123456:2,FR",
+                       Flags::New},
+               {"/home/foo/Maildir/test/cur/123456:2,DTP",
+                (Flags::Draft | Flags::Trashed | Flags::Passed)},
+               {"/home/foo/Maildir/test/cur/123456:2,S", Flags::Seen}
+       }};
+
+       for (auto&& tcase: test_cases) {
+               auto res{flags_from_path(tcase.first)};
+               assert_valid_result(res);
+               /* LCOV_EXCL_START*/
+               if (g_test_verbose()) {
+                       g_print("%s -> <%s>\n", tcase.first.c_str(),
+                               to_string(res.value()).c_str());
+                       g_assert_true(res.value() == tcase.second);
+               }
+               /*LCOV_EXCL_STOP*/
+       }
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       mu_test_init(&argc, &argv);
+
+       g_test_add_func("/message/file/maildir-from-path",
+                       test_maildir_from_path);
+       g_test_add_func("/message/file/base-message-dir-file",
+                       test_base_message_dir_file);
+       g_test_add_func("/message/file/flags-from-path", test_flags_from_path);
+
+       return g_test_run();
+}
+#endif /*BUILD_TESTS*/
diff --git a/lib/message/mu-message-file.hh b/lib/message/mu-message-file.hh
new file mode 100644 (file)
index 0000000..09a9ed3
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb.bulk@gmail.com>
+**
+** 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_MESSAGE_FILE_HH__
+#define MU_MESSAGE_FILE_HH__
+
+#include "mu-flags.hh"
+#include <utils/mu-result.hh>
+
+namespace Mu {
+
+/*
+ * The file-components, ie.
+ *     1631819685.fb7b279bbb0a7b66.evergrey:2,RS
+ *     => {
+ *       "1631819685.fb7b279bbb0a7b66.evergrey",
+ *       ':',
+ *       "2,",
+ *       "RS"
+ *     }
+ */
+struct FileParts {
+       std::string     base;   /**< basename */
+       char            separator; /**< separator */
+       std::string     flags_suffix; /**< suffix (with flags) */
+};
+
+/**
+ * Get the file-parts for some message-file
+ *
+ * @param file path to some message file (does not have to exist)
+ *
+ * @return FileParts for the message file
+ */
+FileParts message_file_parts(const std::string& file);
+
+
+struct DirFile {
+       std::string     dir;
+       std::string     file;
+       bool            is_new;
+};
+
+/**
+ * Get information about the message file componemts
+ *
+ * @param path message path
+ *
+ * @return the components for the message file or an error.
+ */
+Result<DirFile> base_message_dir_file(const std::string& path);
+
+
+
+/**
+ * Get the Maildir flags from the full path of a mailfile. The flags are as
+ * specified in http://cr.yp.to/proto/maildir.html, plus Flag::New for new
+ * messages, ie the ones that live in new/. The flags are logically OR'ed. Note
+ * that the file does not have to exist; the flags are based on the path only.
+ *
+ * @param pathname of a mailfile; it does not have to refer to an
+ * actual message
+ *
+ * @return the message flags or an error
+ */
+Result<Flags> flags_from_path(const std::string& pathname);
+
+/**
+ * get the maildir for a certain message path, ie, the path *before*
+ * cur/ or new/ and *after* the root.
+ *
+ * @param path path for some message
+ * @param root filesystem root for the maildir
+ *
+ * @return the maildir or an Error
+ */
+Result<std::string> maildir_from_path(const std::string& path,
+                                     const std::string& root);
+} // Mu
+
+
+#endif /* MU_MESSAGE_FILE_HH__ */
diff --git a/lib/message/mu-message-part.cc b/lib/message/mu-message-part.cc
new file mode 100644 (file)
index 0000000..8f0b763
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#include "mu-message-part.hh"
+#include "glibconfig.h"
+#include "mu-mime-object.hh"
+#include "utils/mu-utils.hh"
+#include <string>
+
+using namespace Mu;
+
+MessagePart::MessagePart(const Mu::MimeObject& obj):
+       mime_obj{std::make_unique<Mu::MimeObject>(obj)}
+{}
+
+MessagePart::MessagePart(const MessagePart& other):
+       MessagePart(*other.mime_obj)
+{}
+
+MessagePart::~MessagePart() = default;
+
+const MimeObject&
+MessagePart::mime_object() const noexcept
+{
+       return *mime_obj;
+}
+
+Option<std::string>
+MessagePart::cooked_filename() const noexcept
+{
+       // make a bit more pallatble.
+       auto cleanup = [](const std::string& name)->std::string {
+               std::string clean;
+               clean.reserve(name.length());
+               for (auto& c: name) {
+                       auto taboo{(::iscntrl(c) || c == G_DIR_SEPARATOR ||
+                                   c == ' ' || c == '\\' || c == ':')};
+                       clean += (taboo ? '-' : c);
+               }
+               if (clean.size() > 1 && clean[0] == '-')
+                       clean.erase(0, 1);
+
+               return clean;
+       };
+
+       // a MimePart... use the name if there is one.
+       if (mime_object().is_part())
+               return MimePart{mime_object()}.filename().map(cleanup);
+
+       // MimeMessagepart. Construct a name based on subject.
+       if (mime_object().is_message_part()) {
+               auto msg{MimeMessagePart{mime_object()}.get_message()};
+               if (!msg)
+                       return Nothing;
+               else
+                       return msg->subject()
+                               .map(cleanup)
+                               .value_or("no-subject") + ".eml";
+       }
+
+       return Nothing;
+}
+
+Option<std::string>
+MessagePart::raw_filename() const noexcept
+{
+       if (!mime_object().is_part())
+               return Nothing;
+       else
+               return MimePart{mime_object()}.filename();
+}
+
+
+
+Option<std::string>
+MessagePart::mime_type() const noexcept
+{
+       if (const auto ctype{mime_object().content_type()}; ctype)
+               return ctype->media_type() + "/" + ctype->media_subtype();
+       else
+               return Nothing;
+}
+
+Option<std::string>
+MessagePart::content_description() const noexcept
+{
+       if (!mime_object().is_part())
+               return Nothing;
+       else
+               return MimePart{mime_object()}.content_description();
+}
+
+size_t
+MessagePart::size() const noexcept
+{
+       if (!mime_object().is_part())
+               return 0;
+       else
+               return MimePart{mime_object()}.size();
+}
+
+bool
+MessagePart::is_attachment() const noexcept
+{
+       if (!mime_object().is_part())
+               return false;
+       else
+               return MimePart{mime_object()}.is_attachment();
+}
+
+
+Option<std::string>
+MessagePart::to_string() const noexcept
+{
+       if (mime_object().is_part())
+               return MimePart{mime_object()}.to_string();
+       else
+               return mime_object().to_string_opt();
+}
+
+
+
+Result<size_t>
+MessagePart::to_file(const std::string& path, bool overwrite) const noexcept
+{
+       if (!mime_object().is_part())
+               return Err(Error::Code::InvalidArgument,
+                          "not a part");
+       else
+               return MimePart{mime_object()}.to_file(path, overwrite);
+}
+
+bool
+MessagePart::is_signed() const noexcept
+{
+       return mime_object().is_multipart_signed();
+}
+
+bool
+MessagePart::is_encrypted() const noexcept
+{
+       return mime_object().is_multipart_encrypted();
+}
+
+bool /* heuristic */
+MessagePart::looks_like_attachment() const noexcept
+{
+       auto matches=[](const MimeContentType& ctype,
+                       const std::initializer_list<std::pair<const char*, const char*>>& ctypes) {
+               return std::find_if(ctypes.begin(), ctypes.end(), [&](auto&& item){
+                       return ctype.is_type(item.first, item.second); }) != ctypes.end();
+       };
+
+       const auto ctype{mime_object().content_type()};
+       if (!ctype)
+               return false; // no content-type: not an attachment.
+
+       // we consider some parts _not_ to be attachments regardless of disposition
+       if  (matches(*ctype,{{"application", "pgp-keys"}}))
+               return false;
+
+       // we consider some parts to be attachments regardless of disposition
+       if  (matches(*ctype,{{"image", "*"},
+                            {"audio", "*"},
+                            {"application", "*"},
+                            {"application", "x-patch"}}))
+               return true;
+
+       // otherwise, rely on the disposition
+       return is_attachment();
+}
diff --git a/lib/message/mu-message-part.hh b/lib/message/mu-message-part.hh
new file mode 100644 (file)
index 0000000..b955fc8
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#ifndef MU_MESSAGE_PART_HH__
+#define MU_MESSAGE_PART_HH__
+
+#include <string>
+#include <memory>
+#include <utils/mu-option.hh>
+#include <utils/mu-result.hh>
+
+namespace Mu {
+
+class MimeObject; // forward declaration; don't want to include for build-time
+                 // reasons.
+
+class MessagePart {
+public:
+       /**
+        * Construct MessagePart from a MimeObject
+        *
+        * @param obj
+        */
+       MessagePart(const MimeObject& obj);
+
+       /**
+        * Copy CTOR
+        *
+        * @param other
+        */
+       MessagePart(const MessagePart& other);
+
+       /**
+        * DTOR
+        *
+        */
+       ~MessagePart();
+
+
+       /**
+        * Get the underlying MimeObject; you need to include mu-mime-object.hh
+        * to do anything useful with it.
+        *
+        * @return reference to the mime-object
+        */
+       const MimeObject& mime_object() const noexcept;
+
+       /**
+        * Filename for the mime-part file. This is a "cooked" filename with
+        * unallowed characters removed. If there's no filename specified,
+        * construct one (such as in the case of MimeMessagePart).
+        *
+        * @see raw_filename()
+        *
+        * @return the name
+        */
+       Option<std::string> cooked_filename() const noexcept;
+
+       /**
+        * Name for the mime-part file, i.e., MimePart::filename
+        *
+        * @return the filename or Nothing if there is none
+        */
+       Option<std::string> raw_filename() const noexcept;
+
+       /**
+        * Mime-type for the mime-part (e.g. "text/plain")
+        *
+        * @return the mime-part or Nothing if there is none
+        */
+       Option<std::string> mime_type() const noexcept;
+
+
+       /**
+        * Get the content description for this part, or Nothing
+        *
+        * @return the content description
+        */
+       Option<std::string> content_description() const noexcept;
+
+       /**
+        * Get the length of the (unencoded) MIME-part.
+        *
+        * @return the size
+        */
+       size_t size() const noexcept;
+
+       /**
+        * Does this part have an "attachment" disposition? Otherwise it is
+        * "inline". Note that does *not* map 1:1 to a message's HasAttachment
+        * flag (which uses looks_like_attachment())
+        *
+        * @return true or false.
+        */
+       bool is_attachment() const noexcept;
+
+
+       /**
+        * Does this part appear to be an attachment from an end-users point of
+        * view? This uses some heuristics to guess. Some parts for which
+        * is_attachment() is true may not "really" be attachments, and
+        * vice-versa
+        *
+        * @return true or false.
+        */
+       bool looks_like_attachment() const noexcept;
+
+       /**
+        * Is this part signed?
+        *
+        * @return true or false
+        */
+       bool is_signed() const noexcept;
+
+
+       /**
+        * Is this part encrypted?
+        *
+        * @return true or false
+        */
+       bool is_encrypted() const noexcept;
+
+
+       /**
+        * Write (decoded) mime-part contents to string
+        *
+        * @return a string or nothing if there is no contemt
+        */
+       Option<std::string> to_string() const noexcept;
+
+       /**
+        * Write (decoded) mime part to a file
+        *
+        * @param path path to file
+        * @param overwrite whether to possibly overwrite
+        *
+        * @return size of file or or an error.
+        */
+       Result<size_t> to_file(const std::string& path, bool overwrite) const noexcept;
+
+       struct Private;
+private:
+       const std::unique_ptr<MimeObject> mime_obj;
+};
+
+} // namespace Mu
+
+#endif /* MU_MESSAGE_PART_HH__ */
diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc
new file mode 100644 (file)
index 0000000..a8ff0f5
--- /dev/null
@@ -0,0 +1,838 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#include "mu-message.hh"
+#include "gmime/gmime-references.h"
+#include "gmime/gmime-stream-mem.h"
+#include "mu-maildir.hh"
+
+#include <array>
+#include <string>
+#include <regex>
+#include <utils/mu-util.h>
+#include <utils/mu-utils.hh>
+#include <utils/mu-error.hh>
+#include <utils/mu-option.hh>
+
+#include <atomic>
+#include <mutex>
+#include <cstdlib>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gmime/gmime.h>
+
+#include "gmime/gmime-message.h"
+#include "mu-mime-object.hh"
+
+using namespace Mu;
+
+struct Message::Private {
+       Private(Message::Options options): opts{options} {}
+       Private(Message::Options options, Xapian::Document&& xdoc):
+               opts{options}, doc{std::move(xdoc)} {}
+
+       Message::Options                opts;
+       Document                        doc;
+       mutable Option<MimeMessage>     mime_msg;
+
+       Flags                           flags{};
+       Option<std::string>             mailing_list;
+       std::vector<Part>               parts;
+
+       ::time_t                        ctime{};
+
+       std::string                     cache_path;
+       /*
+        * we only need to index these, so we don't
+        * really need these copy if we re-arrange things
+        * a bit
+        */
+       Option<std::string> body_txt;
+       Option<std::string> body_html;
+       Option<std::string> embedded;
+};
+
+
+static void fill_document(Message::Private& priv);
+
+static Result<struct stat>
+get_statbuf(const std::string& path)
+{
+       if (!g_path_is_absolute(path.c_str()))
+               return Err(Error::Code::File, "path '%s' is not absolute",
+                          path.c_str());
+       if (::access(path.c_str(), R_OK) != 0)
+               return Err(Error::Code::File, "file @ '%s' is not readable",
+                          path.c_str());
+
+       struct stat statbuf{};
+       if (::stat(path.c_str(), &statbuf) < 0)
+               return Err(Error::Code::File, "cannot stat %s: %s", path.c_str(),
+                           g_strerror(errno));
+
+       if (!S_ISREG(statbuf.st_mode))
+               return Err(Error::Code::File, "not a regular file: %s", path.c_str());
+
+       return Ok(std::move(statbuf));
+}
+
+
+Message::Message(const std::string& path,  Message::Options opts):
+       priv_{std::make_unique<Private>(opts)}
+{
+       const auto statbuf{get_statbuf(path)};
+       if (!statbuf)
+               throw statbuf.error();
+
+       priv_->ctime = statbuf->st_ctime;
+
+       init_gmime();
+       if (auto msg{MimeMessage::make_from_file(path)}; !msg)
+               throw msg.error();
+       else
+               priv_->mime_msg = std::move(msg.value());
+
+       auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), NULL))};
+       if (xpath)
+               priv_->doc.add(Field::Id::Path, std::move(xpath.value()));
+
+       priv_->doc.add(Field::Id::Size, static_cast<int64_t>(statbuf->st_size));
+
+       // rest of the fields
+       fill_document(*priv_);
+}
+
+Message::Message(const std::string& text, const std::string& path,
+                Message::Options opts):
+       priv_{std::make_unique<Private>(opts)}
+{
+       if (text.empty())
+               throw Error{Error::Code::InvalidArgument, "text must not be empty"};
+
+       if (!path.empty()) {
+               auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), {}))};
+               if (xpath)
+                       priv_->doc.add(Field::Id::Path, std::move(xpath.value()));
+       }
+
+       priv_->ctime = ::time({});
+
+       priv_->doc.add(Field::Id::Size, static_cast<int64_t>(text.size()));
+
+       init_gmime();
+       if (auto msg{MimeMessage::make_from_text(text)}; !msg)
+               throw msg.error();
+       else
+               priv_->mime_msg = std::move(msg.value());
+
+       fill_document(*priv_);
+}
+
+
+Message::Message(Message&& other) noexcept
+{
+       *this = std::move(other);
+}
+
+Message&
+Message::operator=(Message&& other) noexcept
+{
+       if (this != &other)
+               priv_ = std::move(other.priv_);
+
+       return *this;
+}
+
+Message::Message(Xapian::Document&& doc):
+       priv_{std::make_unique<Private>(Message::Options::None, std::move(doc))}
+{}
+
+
+Message::~Message() = default;
+
+const Mu::Document&
+Message::document() const
+{
+       return priv_->doc;
+}
+
+
+unsigned
+Message::docid() const
+{
+       return priv_->doc.xapian_document().get_docid();
+}
+
+
+const Mu::Sexp::List&
+Message::to_sexp_list() const
+{
+       return priv_->doc.sexp_list();
+}
+
+void
+Message::update_cached_sexp()
+{
+       priv_->doc.update_cached_sexp();
+}
+
+Result<void>
+Message::set_maildir(const std::string& maildir)
+{
+       /* sanity check a little bit */
+
+       if (maildir.empty() ||
+           maildir.at(0) != '/' ||
+           (maildir.size() > 1 && maildir.at(maildir.length()-1) == '/'))
+               return Err(Error::Code::Message,
+                          "'%s' is not a valid maildir", maildir.c_str());
+
+       const auto path{document().string_value(Field::Id::Path)};
+       if (path == maildir || path.find(maildir) == std::string::npos)
+               return Err(Error::Code::Message,
+                          "'%s' is not a valid maildir for message @ %s",
+                          maildir.c_str(), path.c_str());
+
+       priv_->doc.remove(Field::Id::Maildir);
+       priv_->doc.add(Field::Id::Maildir, maildir);
+
+       return Ok();
+}
+
+void
+Message::set_flags(Flags flags)
+{
+       priv_->doc.remove(Field::Id::Flags);
+       priv_->doc.add(flags);
+}
+
+bool
+Message::load_mime_message(bool reload) const
+{
+       if (priv_->mime_msg && !reload)
+               return true;
+
+       const auto path{document().string_value(Field::Id::Path)};
+       if (auto mime_msg{MimeMessage::make_from_file(path)}; !mime_msg) {
+               g_warning("failed to load '%s': %s",
+                         path.c_str(), mime_msg.error().what());
+               return false;
+       } else {
+               priv_->mime_msg = std::move(mime_msg.value());
+               fill_document(*priv_);
+               return true;
+       }
+}
+
+void
+Message::unload_mime_message() const
+{
+       priv_->mime_msg = Nothing;
+}
+
+bool
+Message::has_mime_message() const
+{
+       return !!priv_->mime_msg;
+}
+
+
+static Priority
+get_priority(const MimeMessage& mime_msg)
+{
+       constexpr std::array<std::pair<std::string_view, Priority>, 10>
+               prio_alist = {{
+                       {"high",        Priority::High},
+                       {"1",           Priority::High},
+                       {"2",           Priority::High},
+
+                       {"normal",      Priority::Normal},
+                       {"3",           Priority::Normal},
+
+                       {"low",         Priority::Low},
+                       {"list",        Priority::Low},
+                       {"bulk",        Priority::Low},
+                       {"4",           Priority::Low},
+                       {"5",           Priority::Low}
+               }};
+
+       const auto opt_str = mime_msg.header("Precedence")
+               .disjunction(mime_msg.header("X-Priority"))
+               .disjunction(mime_msg.header("Importance"));
+
+       if (!opt_str)
+               return  Priority::Normal;
+
+       const auto it = seq_find_if(prio_alist, [&](auto&& item) {
+               return g_ascii_strncasecmp(item.first.data(), opt_str->c_str(),
+                                          item.first.size()) == 0; });
+
+       return it == prio_alist.cend() ? Priority::Normal : it->second;
+}
+
+
+/* see: http://does-not-exist.org/mail-archives/mutt-dev/msg08249.html */
+static std::vector<std::string>
+extract_tags(const MimeMessage& mime_msg)
+{
+       constexpr std::array<std::pair<const char*, char>, 3> tag_headers = {{
+               {"X-Label", ' '}, {"X-Keywords", ','}, {"Keywords", ' '}
+       }};
+       static const auto strip_rx{std::regex("^\\s+| +$|( )\\s+")};
+
+       std::vector<std::string> tags;
+       seq_for_each(tag_headers, [&](auto&& item) {
+               if (auto&& hdr = mime_msg.header(item.first); hdr) {
+                       for (auto&& tagval : split(*hdr, item.second)) {
+                               tags.emplace_back(
+                                       std::regex_replace(tagval, strip_rx, "$1"));
+                       }
+               }
+       });
+
+       return tags;
+}
+
+static Option<std::string>
+get_mailing_list(const MimeMessage& mime_msg)
+{
+       char *dechdr, *res;
+       const char *b, *e;
+
+       const auto hdr{mime_msg.header("List-Id")};
+       if (!hdr)
+               return {};
+
+       dechdr = g_mime_utils_header_decode_phrase(NULL, hdr->c_str());
+       if (!dechdr)
+               return {};
+
+       e = NULL;
+       b = ::strchr(dechdr, '<');
+       if (b)
+               e = strchr(b, '>');
+
+       if (b && e)
+               res = g_strndup(b + 1, e - b - 1);
+       else
+               res = g_strdup(dechdr);
+
+       g_free(dechdr);
+
+       return to_string_opt_gchar(std::move(res));
+}
+
+static void
+append_text(Option<std::string>& str, Option<std::string> app)
+{
+       if (!str)
+               str = app;
+       else if (app)
+               str.value() += app.value();
+}
+
+static void
+accumulate_text(const MimePart& part, Message::Private& info,
+               const MimeContentType& ctype)
+{
+       if (!ctype.is_type("text", "*"))
+               return; /* not a text type */
+
+       if (part.is_attachment())
+               append_text(info.embedded, part.to_string());
+       else if (ctype.is_type("text", "plain"))
+               append_text(info.body_txt, part.to_string());
+       else if (ctype.is_type("text", "html"))
+               append_text(info.body_html, part.to_string());
+}
+
+
+static bool /* heuristic */
+looks_like_attachment(const MimeObject& parent, const MessagePart& mpart)
+{
+       if (parent) { /* crypto multipart children are not considered attachments */
+               if (const auto parent_ctype{parent.content_type()}; parent_ctype) {
+                       if (parent_ctype->is_type("multipart", "signed") ||
+                           parent_ctype->is_type("multipart", "encrypted"))
+                               return false;
+               }
+       }
+
+       return mpart.looks_like_attachment();
+}
+
+
+static void
+process_part(const MimeObject& parent, const MimePart& part,
+            Message::Private& info, const MessagePart& mpart)
+{
+       const auto ctype{part.content_type()};
+       if (!ctype)
+               return;
+
+       // flag as calendar, if not already
+       if (none_of(info.flags & Flags::Calendar) &&
+           ctype->is_type("text", "calendar"))
+               info.flags |= Flags::Calendar;
+
+       // flag as attachment, if not already.
+       if (none_of(info.flags & Flags::HasAttachment) &&
+           looks_like_attachment(parent, mpart))
+               info.flags |= Flags::HasAttachment;
+
+       // if there are text parts, gather.
+       accumulate_text(part, info, *ctype);
+}
+
+
+static void
+process_message_part(const MimeMessagePart& msg_part,
+                    Message::Private& info)
+{
+       auto submsg{msg_part.get_message()};
+       if (!submsg)
+               return;
+
+       submsg->for_each([&](auto&& parent, auto&& child_obj) {
+
+               /* XXX: we only handle one level */
+
+               if (!child_obj.is_part())
+                       return;
+
+               const auto ctype{child_obj.content_type()};
+               if (!ctype || !ctype->is_type("text", "*"))
+                       return;
+
+               append_text(info.embedded, MimePart{child_obj}.to_string());
+       });
+}
+
+static void
+handle_object(const MimeObject& parent,
+             const MimeObject& obj, Message::Private& info);
+
+
+static void
+handle_encrypted(const MimeMultipartEncrypted& part, Message::Private& info)
+{
+       if (!any_of(info.opts & Message::Options::Decrypt)) {
+               /* just added to the list */
+               info.parts.emplace_back(part);
+               return;
+       }
+
+       const auto proto{part.content_type_parameter("protocol").value_or("unknown")};
+       const auto ctx = MimeCryptoContext::make(proto);
+       if (!ctx) {
+               g_warning("failed to create context for protocol <%s>",
+                         proto.c_str());
+               return;
+       }
+
+       auto res{part.decrypt(*ctx)};
+       if (!res) {
+               g_warning("failed to decrypt: %s", res.error().what());
+               return;
+       }
+
+       if (res->first.is_multipart()) {
+               MimeMultipart{res->first}.for_each(
+                       [&](auto&& parent, auto&& child_obj) {
+                               handle_object(parent, child_obj, info);
+                       });
+
+       } else
+               handle_object(part, res->first, info);
+}
+
+
+static void
+handle_object(const MimeObject& parent,
+             const MimeObject& obj, Message::Private& info)
+{
+       /* if it's an encrypted part we should decrypt, recurse */
+       if (obj.is_multipart_encrypted())
+               handle_encrypted(MimeMultipartEncrypted{obj}, info);
+       else if (obj.is_part() ||
+                obj.is_message_part() ||
+                obj.is_multipart_signed() ||
+                obj.is_multipart_encrypted())
+               info.parts.emplace_back(obj);
+
+       if (obj.is_part())
+               process_part(parent, obj, info, info.parts.back());
+       else if (obj.is_message_part())
+               process_message_part(obj, info);
+       else if (obj.is_multipart_signed())
+               info.flags |= Flags::Signed;
+       else if (obj.is_multipart_encrypted()) {
+               /* FIXME: An encrypted part might be signed at the same time.
+                *        In that case the signed flag is lost. */
+               info.flags |= Flags::Encrypted;
+       } else if (obj.is_mime_application_pkcs7_mime()) {
+               MimeApplicationPkcs7Mime smime(obj);
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+               // CompressedData, CertsOnly, Unknown
+               switch (smime.smime_type()) {
+               case Mu::MimeApplicationPkcs7Mime::SecureMimeType::SignedData:
+                       info.flags |= Flags::Signed;
+                       break;
+               case Mu::MimeApplicationPkcs7Mime::SecureMimeType::EnvelopedData:
+                       info.flags |= Flags::Encrypted;
+                       break;
+               default:
+                       break;
+               }
+#pragma GCC diagnostic pop
+       }
+}
+
+/**
+ * This message -- recursively walk through message, and initialize some
+ * other values that depend on another.
+ *
+ * @param mime_msg
+ * @param path
+ * @param info
+ */
+static void
+process_message(const MimeMessage& mime_msg, const std::string& path,
+               Message::Private& info)
+{
+       /* only have file-flags when there's a path. */
+       if (!path.empty()) {
+               info.flags = flags_from_path(path).value_or(Flags::None);
+               /* pseudo-flag --> unread means either NEW or NOT SEEN, just
+                * for searching convenience */
+               if (any_of(info.flags & Flags::New) || none_of(info.flags & Flags::Seen))
+                       info.flags |= Flags::Unread;
+       }
+
+       // parts
+       mime_msg.for_each([&](auto&& parent, auto&& child_obj) {
+               handle_object(parent, child_obj, info);
+       });
+
+       // get the mailing here, and use it do update flags, too.
+       info.mailing_list = get_mailing_list(mime_msg);
+       if (info.mailing_list)
+               info.flags |= Flags::MailingList;
+}
+
+static Mu::Result<std::string>
+calculate_sha256(const std::string& path)
+{
+       g_autoptr(GChecksum) checksum{g_checksum_new(G_CHECKSUM_SHA256)};
+
+       FILE *file{::fopen(path.c_str(), "r")};
+       if (!file)
+               return Err(Error{Error::Code::File, "failed to open %s: %s",
+                                       path.c_str(), ::strerror(errno)});
+
+       std::array<uint8_t, 4096> buf{};
+       while (true) {
+               const auto n = ::fread(buf.data(), 1, buf.size(), file);
+               if (n == 0)
+                       break;
+               g_checksum_update(checksum, buf.data(), n);
+       }
+
+       bool has_err = ::ferror(file) != 0;
+       ::fclose(file);
+
+       if (has_err)
+               return Err(Error{Error::Code::File, "failed to read %s", path.c_str()});
+
+       return Ok(g_checksum_get_string(checksum));
+}
+
+/**
+ * Get a fake-message-id for a message without one.
+ *
+ * @param path message path
+ *
+ * @return a fake message-id
+ */
+static std::string
+fake_message_id(const std::string& path)
+{
+       constexpr auto mu_suffix{"@mu.id"};
+
+       // not a very good message-id, only for testing.
+       if (path.empty() || ::access(path.c_str(), R_OK) != 0)
+               return format("%08x%s", g_str_hash(path.c_str()), mu_suffix);
+       if (const auto sha256_res{calculate_sha256(path)}; !sha256_res)
+               return format("%08x%s", g_str_hash(path.c_str()), mu_suffix);
+       else
+               return format("%s%s", sha256_res.value().c_str(), mu_suffix);
+}
+
+/* many of the doc.add(fiels ....) automatically update the sexp-list as well;
+ * however, there are some _extra_ values in the sexp-list that are not
+ * based on a field. So we add them here.
+ */
+
+
+
+static void
+doc_add_list_post(Document& doc, const MimeMessage& mime_msg)
+{
+       /* some mailing lists do not set the reply-to; see pull #1278. So for
+        * those cases, check the List-Post address and use that instead */
+
+       GMatchInfo* minfo;
+       GRegex*     rx;
+       const auto list_post{mime_msg.header("List-Post")};
+       if (!list_post)
+               return;
+
+       rx = g_regex_new("<?mailto:([a-z0-9!@#$%&'*+-/=?^_`{|}~]+)>?",
+                        G_REGEX_CASELESS, (GRegexMatchFlags)0, {});
+       g_return_if_fail(rx);
+
+       Contacts contacts;
+       if (g_regex_match(rx, list_post->c_str(), (GRegexMatchFlags)0, &minfo)) {
+               auto    address = (char*)g_match_info_fetch(minfo, 1);
+               contacts.push_back(Contact(address));
+               g_free(address);
+       }
+
+       g_match_info_free(minfo);
+       g_regex_unref(rx);
+
+       doc.add_extra_contacts(":list-post", contacts);
+}
+
+static void
+doc_add_reply_to(Document& doc, const MimeMessage& mime_msg)
+{
+       doc.add_extra_contacts(":reply-to", mime_msg.contacts(Contact::Type::ReplyTo));
+}
+
+static void
+fill_document(Message::Private& priv)
+{
+       /* hunt & gather info from message tree */
+       Document& doc{priv.doc};
+       MimeMessage& mime_msg{priv.mime_msg.value()};
+
+       const auto path{doc.string_value(Field::Id::Path)};
+       const auto refs{mime_msg.references()};
+        const auto& raw_message_id = mime_msg.message_id();
+        const auto message_id = raw_message_id.has_value() && !raw_message_id->empty()
+            ? *raw_message_id
+            : fake_message_id(path);
+
+       process_message(mime_msg, path, priv);
+
+       doc_add_list_post(doc, mime_msg); /* only in sexp */
+       doc_add_reply_to(doc, mime_msg);  /* only in sexp */
+
+       field_for_each([&](auto&& field) {
+               /* insist on expliclity handling each */
+#pragma GCC diagnostic push
+#pragma GCC diagnostic error "-Wswitch"
+               switch(field.id) {
+               case Field::Id::Bcc:
+                       doc.add(field.id, mime_msg.contacts(Contact::Type::Bcc));
+                       break;
+               case Field::Id::BodyText:
+                       doc.add(field.id, priv.body_txt);
+
+                       break;
+               case Field::Id::Cc:
+                       doc.add(field.id, mime_msg.contacts(Contact::Type::Cc));
+                       break;
+               case Field::Id::Changed:
+                       doc.add(field.id, priv.ctime);
+                       break;
+               case Field::Id::Date:
+                       doc.add(field.id, mime_msg.date());
+                       break;
+               case Field::Id::EmbeddedText:
+                       doc.add(field.id, priv.embedded);
+                       break;
+               case Field::Id::File:
+                       for (auto&& part: priv.parts)
+                               doc.add(field.id, part.raw_filename());
+                       break;
+               case Field::Id::Flags:
+                       doc.add(priv.flags);
+                       break;
+               case Field::Id::From:
+                       doc.add(field.id, mime_msg.contacts(Contact::Type::From));
+                       break;
+               case Field::Id::Maildir: /* already */
+                       break;
+               case Field::Id::MailingList:
+                       doc.add(field.id, priv.mailing_list);
+                       break;
+               case Field::Id::MessageId:
+                       doc.add(field.id, message_id);
+                       break;
+               case Field::Id::MimeType:
+                       for (auto&& part: priv.parts)
+                               doc.add(field.id, part.mime_type());
+                       break;
+               case Field::Id::Path: /* already */
+                       break;
+               case Field::Id::Priority:
+                       doc.add(get_priority(mime_msg));
+                       break;
+               case Field::Id::References:
+                       if (!refs.empty())
+                               doc.add(field.id, refs);
+                       break;
+               case Field::Id::Size: /* already */
+                       break;
+               case Field::Id::Subject:
+                       doc.add(field.id, mime_msg.subject());
+                       break;
+               case Field::Id::Tags:
+                       if (auto&& tags{extract_tags(mime_msg)}; !tags.empty())
+                               doc.add(field.id, tags);
+                       break;
+               case Field::Id::ThreadId:
+                       // either the oldest reference, or otherwise the message id
+                       doc.add(field.id, refs.empty() ? message_id : refs.at(0));
+                       break;
+               case Field::Id::To:
+                       doc.add(field.id, mime_msg.contacts(Contact::Type::To));
+                       break;
+                       /* internal fields */
+               case Field::Id::XBodyHtml:
+                       doc.add(field.id, priv.body_html);
+                       break;
+               /* LCOV_EXCL_START */
+               case Field::Id::_count_:
+               default:
+                       break;
+               /* LCOV_EXCL_STOP */
+               }
+#pragma GCC diagnostic pop
+
+       });
+}
+
+Option<std::string>
+Message::header(const std::string& header_field) const
+{
+       load_mime_message();
+       return priv_->mime_msg->header(header_field);
+}
+
+Option<std::string>
+Message::body_text() const
+{
+       load_mime_message();
+       return priv_->body_txt;
+}
+
+Option<std::string>
+Message::body_html() const
+{
+       load_mime_message();
+       return priv_->body_html;
+}
+
+Contacts
+Message::all_contacts() const
+{
+       Contacts contacts;
+
+       if (!load_mime_message())
+               return contacts; /* empty */
+
+       return priv_->mime_msg->contacts(Contact::Type::None); /* get all types */
+}
+
+const std::vector<Message::Part>&
+Message::parts() const
+{
+       if (!load_mime_message()) {
+               static std::vector<Message::Part> empty;
+               return empty;
+       }
+
+       return priv_->parts;
+}
+
+Result<std::string>
+Message::cache_path(Option<size_t> index) const
+{
+       /* create tmpdir for this message, if needed */
+       if (priv_->cache_path.empty()) {
+               GError *err{};
+               auto tpath{to_string_opt_gchar(g_dir_make_tmp("mu-cache-XXXXXX", &err))};
+               if (!tpath)
+                       return Err(Error::Code::File, &err, "failed to create temp dir");
+
+               priv_->cache_path = std::move(tpath.value());
+       }
+
+       if (index) {
+               GError *err{};
+               auto tpath = format("%s/%zu", priv_->cache_path.c_str(), *index);
+               if (g_mkdir(tpath.c_str(), 0700) != 0)
+                       return Err(Error::Code::File, &err,
+                                  "failed to create cache dir '%s'; err=%d",
+                                  tpath.c_str(), errno);
+               return Ok(std::move(tpath));
+       } else
+
+               return Ok(std::string{priv_->cache_path});
+}
+
+// for now this only remove stray '/' at the end
+std::string
+Message::sanitize_maildir(const std::string& mdir)
+{
+       if (mdir.size() > 1 && mdir.at(mdir.length()-1) == '/')
+               return mdir.substr(0, mdir.length() - 1);
+       else
+               return mdir;
+}
+
+Result<void>
+Message::update_after_move(const std::string& new_path,
+                          const std::string& new_maildir,
+                          Flags new_flags)
+{
+       if (auto statbuf{get_statbuf(new_path)}; !statbuf)
+               return Err(statbuf.error());
+       else
+               priv_->ctime = statbuf->st_ctime;
+
+       priv_->doc.remove(Field::Id::Path);
+       priv_->doc.remove(Field::Id::Changed);
+
+       priv_->doc.add(Field::Id::Path, new_path);
+       priv_->doc.add(Field::Id::Changed, priv_->ctime);
+
+       set_flags(new_flags);
+
+       if (const auto res = set_maildir(sanitize_maildir(new_maildir)); !res)
+               return res;
+
+       return Ok();
+}
diff --git a/lib/message/mu-message.hh b/lib/message/mu-message.hh
new file mode 100644 (file)
index 0000000..3f2e001
--- /dev/null
@@ -0,0 +1,481 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_MESSAGE_HH__
+#define MU_MESSAGE_HH__
+
+#include <memory>
+#include <string>
+#include <vector>
+#include "mu-contact.hh"
+#include "mu-priority.hh"
+#include "mu-flags.hh"
+#include "mu-fields.hh"
+#include "mu-document.hh"
+#include "mu-message-part.hh"
+#include "mu-message-file.hh"
+
+#include <xapian.h>
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-option.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-sexp.hh"
+
+namespace Mu {
+
+class Message {
+public:
+       enum struct Options {
+               None         = 0,       /**< Defaults */
+               Decrypt      = 1 << 0,  /**< Attempt to decrypt */
+               RetrieveKeys = 1 << 1,  /**< Auto-retrieve crypto keys (implies network
+                                          * access) */
+       };
+
+       /**
+        * Move CTOR
+        *
+        * @param some other message
+        */
+       Message(Message&& other) noexcept;
+
+
+       /**
+        * operator=
+        *
+        * @param other move some object object
+        *
+        * @return
+        */
+       Message& operator=(Message&& other) noexcept;
+
+       /**
+        * Construct a message based on a path
+        *
+        * @param path path to message
+        * @param opts options
+        *
+        * @return a message or an error
+        */
+       static Result<Message> make_from_path(const std::string& path,
+                                             Options opts={}) try {
+               return Ok(Message{path,opts});
+       } catch (Error& err) {
+               return Err(err);
+       }
+       /* LCOV_EXCL_START */
+       catch (...) {
+               return Err(Mu::Error(Error::Code::Message,
+                                    "failed to create message from path"));
+       }
+       /* LCOV_EXCL_STOP */
+
+       /**
+        * Construct a message based on a Xapian::Document
+        *
+        * @param doc a Mu Document
+        *
+        * @return a message or an error
+        */
+       static Result<Message> make_from_document(Xapian::Document&& doc) try {
+               return Ok(Message{std::move(doc)});
+       } catch (Error& err) {
+               return Err(err);
+       }
+       /* LCOV_EXCL_START */
+       catch (...) {
+               return Err(Mu::Error(Error::Code::Message,
+                                    "failed to create message from document"));
+       }
+       /* LCOV_EXCL_STOP */
+
+
+       /**
+        * Construct a message from a string. This is mostly useful for testing.
+        *
+        * @param text message text
+        * @param path path to message - optional; path does not have to exist.
+        * @param opts options
+        *
+        * @return a message or an error
+        */
+       static Result<Message> make_from_text(const std::string& text,
+                                             const std::string& path={},
+                                             Options opts={}) try {
+               return Ok(Message{text, path, opts});
+       } catch (Error& err) {
+               return Err(err);
+       }
+       /* LCOV_EXCL_START */
+       catch (...) {
+               return Err(Mu::Error(Error::Code::Message,
+                                    "failed to create message from text"));
+       }
+       /* LCOV_EXCL_STOP */
+
+       /**
+        * DTOR
+        */
+       ~Message();
+
+       /**
+        * Get the document.
+        *
+        *
+        * @return document
+        */
+       const Document& document() const;
+
+
+       /**
+        * Get the document-id, or 0 if non-existent.
+        *
+        * @return document id
+        */
+       unsigned docid() const;
+
+       /**
+        * Get the file system path of this message
+        *
+        * @return the path of this Message or NULL in case of error.
+        * the returned string should *not* be modified or freed.
+        */
+       std::string path() const { return document().string_value(Field::Id::Path); }
+
+       /**
+        * Get the sender (From:) of this message
+        *
+        * @return the sender(s) of this Message
+        */
+       Contacts from() const { return document().contacts_value(Field::Id::From); }
+
+       /**
+        * Get the recipient(s) (To:) for this message
+        *
+        * @return recipients
+        */
+       Contacts to() const { return document().contacts_value(Field::Id::To); }
+
+       /**
+        * Get the recipient(s) (Cc:) for this message
+        *
+        * @return recipients
+        */
+       Contacts cc() const { return document().contacts_value(Field::Id::Cc); }
+
+       /**
+        * Get the recipient(s) (Bcc:) for this message
+        *
+        * @return recipients
+        */
+       Contacts bcc() const { return document().contacts_value(Field::Id::Bcc); }
+
+       /**
+        * Get the maildir this message resides in; i.e., if the path is
+        * ~/Maildir/foo/bar/cur/msg, the maildir would typically be foo/bar
+        *
+        * This is determined when _storing_ the message (which uses
+        * set_maildir())
+        *
+        * @return the maildir requested or empty */
+       std::string maildir() const  { return document().string_value(Field::Id::Maildir); }
+
+       /**
+        * Set the maildir for this message. This is for use by the _store_ when
+        * it has determined the maildir for this message from the message's path and
+        * the root-maildir known by the store.
+        *
+        * @param maildir the maildir for this message
+        *
+        * @return Ok() or some error if the maildir is invalid
+        */
+       Result<void> set_maildir(const std::string& maildir);
+
+       /**
+        * Clean up the maildir. This is for internal use, but exposed for testing.
+        * For now cleaned-up means "stray trailing / removed".
+        *
+        * @param maildir some maildir
+        *
+        * @return a cleaned-up version
+        */
+       static std::string sanitize_maildir(const std::string& maildir);
+
+       /**
+        * Get the subject of this message
+        *
+        * @return the subject of this Message
+        */
+       std::string subject() const  { return document().string_value(Field::Id::Subject); }
+
+       /**
+        * Get the Message-Id of this message
+        *
+        * @return the Message-Id of this message (without the enclosing <>), or
+        * a fake message-id for messages that don't have them.
+        *
+        * For file-backed message, this fake message-id is based on a hash of the
+        * message contents. For non-file-backed (test) messages, some other value
+        * is concocted.
+        */
+       std::string message_id() const { return document().string_value(Field::Id::MessageId);}
+
+       /**
+        * get the mailing list for a message, i.e. the mailing-list
+        * identifier in the List-Id header.
+        *
+        * @return the mailing list id for this message (without the enclosing <>)
+        * or NULL in case of error or if there is none.
+        */
+       std::string mailing_list() const { return document().string_value(Field::Id::MailingList);}
+
+       /**
+        * get the message date/time (the Date: field) as time_t
+        *
+        * @return message date/time or 0 in case of error or if there
+        * is no such header.
+        */
+       ::time_t date() const {
+               return static_cast<::time_t>(document().integer_value(Field::Id::Date));
+       }
+
+       /**
+        * get the last change-time this message. For path/document-based
+        * messages this corresponds with the ctime of the underlying file; for
+        * the text-based ones (as used for testing) it is the creation time.
+        *
+        * @return last-change time or 0 if unknown
+        */
+       ::time_t changed() const {
+               return static_cast<::time_t>(document().integer_value(Field::Id::Changed));
+       }
+
+       /**
+        * get the flags for this message.
+        *
+        * @return the file/content flags
+        */
+       Flags flags() const { return document().flags_value(); }
+
+
+       /**
+        * Update the flags for this message. This is useful for flags
+        * that can only be determined after the message has been created already,
+        * such as the 'personal' flag.
+        *
+        * @param flags new flags.
+        */
+       void set_flags(Flags flags);
+
+       /**
+        * get the message priority for this message. The X-Priority, X-MSMailPriority,
+        * Importance and Precedence header are checked, in that order. if no known or
+        * explicit priority is set, Priority::Id::Normal is assumed
+        *
+        * @return the message priority
+        */
+       Priority priority() const { return document().priority_value(); }
+
+       /**
+        * get the file size in bytes of this message
+        *
+        * @return the filesize
+        */
+       size_t size() const { return static_cast<size_t>(document().integer_value(Field::Id::Size)); }
+
+       /**
+        * Get the (possibly empty) list of references (consisting of both the
+        * References and In-Reply-To fields), with the oldest first and the
+        * direct parent as the last one. Note, any reference (message-id) will
+        * appear at most once, duplicates and fake-message-id (see impls) are
+        * filtered out.
+        *
+        * @return a vec with the references for this msg.
+        */
+       std::vector<std::string> references() const {
+               return document().string_vec_value(Field::Id::References);
+       }
+
+       /**
+        * Get the thread-id for this message. This is the message-id of the
+        * oldest-known (grand) parent, or the message-id of this message if
+        * none.
+        *
+        * @return the thread id.
+        */
+       std::string thread_id() const {
+               return document().string_value(Field::Id::ThreadId);
+       }
+
+       /**
+        * get the list of tags (ie., X-Label)
+        *
+        * @param msg a valid MuMsg
+        *
+        * @return a list with the tags for this msg. Don't modify/free
+        */
+       std::vector<std::string> tags() const {
+               return document()
+                       .string_vec_value(Field::Id::Tags);
+       }
+
+       /**
+        * Get the cached s-expression for this message, or {} if not available.
+        *
+        * @return sexp or empty.
+        */
+       std::string cached_sexp() const {
+               return document().cached_sexp();
+       }
+
+       /*
+        * Convert to Sexp
+        */
+
+       /**
+        * Get the s-expression for this message. Stays valid as long
+        * as this message is.
+        *
+        * @return a Mu::Sexp::List representing the message.
+        */
+       const Mu::Sexp::List& to_sexp_list() const;
+       Mu::Sexp to_sexp() const {
+               return Sexp::make_list(Sexp::List(to_sexp_list()));
+       }
+
+       /**
+        * Update the cached sexp for this message which is stored in the
+        * document. This should be done immediately before storing it in the
+        * database.
+        *
+        */
+       void update_cached_sexp();
+
+       /*
+        * And some non-const message, for updating an existing
+        * message after a file-system move.
+        *
+        * @return Ok or an error.
+        */
+       Result<void> update_after_move(const std::string& new_path,
+                                      const std::string& new_maildir,
+                                      Flags new_flags);
+       /*
+        * Below require a file-backed message, which is a relatively slow
+        * if there isn't one already; see load_mime_message()
+        */
+
+       /**
+        * Get the text body
+        *
+        * @return text body
+        */
+       Option<std::string> body_text() const;
+
+       /**
+        * Get the HTML body
+        *
+        * @return text body
+        */
+       Option<std::string> body_html() const;
+
+       /**
+        * Get some message-header
+        *
+        * @param header_field name of the header
+        *
+        * @return the value (UTF-8), or Nothing.
+        */
+       Option<std::string> header(const std::string& header_field) const;
+
+
+       /**
+        * Get all contacts for this message.
+        *
+        * @return contacts
+        */
+       Contacts all_contacts() const;
+
+       /**
+        * Get information about MIME-parts in this message.
+        *
+        * @return mime-part info.
+        */
+       using Part = MessagePart;
+       const std::vector<Part>& parts() const;
+
+       /**
+        * Get the path to a cche directory for this message, which
+        * is useful for temporarily saving attachments
+        *
+        * @param index optionally, create <cache-path>/<index> instead;
+        * this is useful for having part-specific subdirectories.
+        *
+        * @return path to a (created) cache directory, or an error.
+        */
+       Result<std::string> cache_path(Option<size_t> index={}) const;
+
+
+       /**
+        * Load the GMime (file) message (for a database-backed message),
+        * if not already (but see @param reload).
+        *
+        * Affects cached-state only, so we still mark this as 'const'
+        *
+        * @param reload whether to force reloading (even if already)
+        *
+        * @return true if loading worked; false otherwise.
+        */
+       bool load_mime_message(bool reload=false) const;
+
+       /**
+        * Clear the GMime message.
+        *
+        * Affects cached-state only, so we still mark this as 'const'
+        */
+       void unload_mime_message() const;
+
+       /**
+        * Has a (file-base) GMime message been loaded?
+        *
+        *
+        * @return true or false
+        */
+       bool has_mime_message() const;
+
+       struct Private;
+
+       /*
+        * Usually the make_ builders are better to create a message, but in
+        * some special cases, we need a heap-allocated message... */
+
+       Message(Xapian::Document&& xdoc);
+       Message(const std::string& path, Options opts);
+
+private:
+       Message(const std::string& str, const std::string& path, Options opt);
+
+       std::unique_ptr<Private> priv_;
+
+}; // Message
+MU_ENABLE_BITOPS(Message::Options);
+
+} // Mu
+#endif /* MU_MESSAGE_HH__ */
diff --git a/lib/message/mu-mime-object.cc b/lib/message/mu-mime-object.cc
new file mode 100644 (file)
index 0000000..17b56f0
--- /dev/null
@@ -0,0 +1,790 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#include "mu-mime-object.hh"
+#include "gmime/gmime-message.h"
+#include "utils/mu-utils.hh"
+#include <mutex>
+#include <regex>
+#include <fcntl.h>
+#include <errno.h>
+
+using namespace Mu;
+
+
+\f
+/* note, we do the gmime initialization here rather than in mu-runtime, because this way
+ * we don't need mu-runtime for simple cases -- such as our unit tests. Also note that we
+ * need gmime init even for the doc backend, as we use the address parsing functions also
+ * there. */
+
+void
+Mu::init_gmime(void)
+{
+       // fast path.
+       static bool gmime_initialized = false;
+       if (gmime_initialized)
+               return;
+
+       static std::mutex gmime_lock;
+       std::lock_guard lock (gmime_lock);
+       if (gmime_initialized)
+               return; // already
+
+       g_debug("initializing gmime %u.%u.%u",
+               gmime_major_version,
+               gmime_minor_version,
+               gmime_micro_version);
+
+       g_mime_init();
+       gmime_initialized = true;
+
+       std::atexit([] {
+               g_debug("shutting down gmime");
+               g_mime_shutdown();
+               gmime_initialized = false;
+       });
+}
+
+
+std::string
+Mu::address_rfc2047(const Contact& contact)
+{
+       init_gmime();
+
+       InternetAddress *addr =
+               internet_address_mailbox_new(contact.name.c_str(),
+                                            contact.email.c_str());
+
+       std::string encoded = to_string_gchar(
+               internet_address_to_string(addr, {}, true));
+
+       g_object_unref(addr);
+
+       return encoded;
+}
+
+\f
+/*
+ * MimeObject
+ */
+
+Option<std::string>
+MimeObject::header(const std::string& hdr) const noexcept
+{
+       if (auto val{g_mime_object_get_header(self(), hdr.c_str())}; !val)
+               return Nothing;
+       else if (!g_utf8_validate(val, -1, {}))
+               return utf8_clean(val);
+       else
+               return std::string{val};
+}
+
+
+std::vector<std::pair<std::string, std::string>>
+MimeObject::headers() const noexcept
+{
+       GMimeHeaderList *lst;
+
+       lst = g_mime_object_get_header_list(self()); /* _not_ owned */
+       if (!lst)
+               return {};
+
+       std::vector<std::pair<std::string, std::string>> hdrs;
+       const auto hdr_num{g_mime_header_list_get_count(lst)};
+
+       for (int i = 0; i != hdr_num; ++i) {
+               GMimeHeader *hdr{g_mime_header_list_get_header_at(lst, i)};
+               if (!hdr) /* ^^^ _not_ owned */
+                       continue;
+               const auto name{g_mime_header_get_name(hdr)};
+               const auto val{g_mime_header_get_value(hdr)};
+               if (!name || !val)
+                       continue;
+               hdrs.emplace_back(name, val);
+       }
+
+       return hdrs;
+}
+
+
+
+Result<size_t>
+MimeObject::write_to_stream(const MimeFormatOptions& f_opts,
+                           MimeStream& stream) const
+{
+       auto written = g_mime_object_write_to_stream(self(), f_opts.get(),
+                                                    GMIME_STREAM(stream.object()));
+       if (written < 0)
+               return Err(Error::Code::File, "failed to write mime-object to stream");
+       else
+               return Ok(static_cast<size_t>(written));
+}
+
+Option<std::string>
+MimeObject::to_string_opt() const noexcept
+{
+       auto stream{MimeStream::make_mem()};
+       if (!stream) {
+               g_warning("failed to create mem stream");
+               return Nothing;
+       }
+
+       const auto written = g_mime_object_write_to_stream(
+               self(), {}, GMIME_STREAM(stream.object()));
+       if (written < 0) {
+               g_warning("failed to write object to stream");
+               return Nothing;
+       }
+
+       std::string buffer;
+       buffer.resize(written + 1);
+       stream.reset();
+
+       auto bytes{g_mime_stream_read(GMIME_STREAM(stream.object()),
+                                     buffer.data(), written)};
+       if (bytes < 0)
+               return Nothing;
+
+       buffer.data()[written]='\0';
+       buffer.resize(written);
+
+       return buffer;
+}
+
+\f
+/*
+ * MimeCryptoContext
+ */
+
+Result<size_t>
+MimeCryptoContext::import_keys(MimeStream& stream)
+{
+       GError *err{};
+       auto res = g_mime_crypto_context_import_keys(
+               self(), GMIME_STREAM(stream.object()), &err);
+
+       if (res < 0)
+               return Err(Error::Code::File, &err,
+                          "error importing keys");
+
+       return Ok(static_cast<size_t>(res));
+}
+
+void
+MimeCryptoContext::set_request_password(PasswordRequestFunc pw_func)
+{
+       static auto request_func = pw_func;
+
+       g_mime_crypto_context_set_request_password(
+               self(),
+               [](GMimeCryptoContext *ctx,
+                  const char *user_id,
+                  const char *prompt,
+                  gboolean reprompt,
+                  GMimeStream *response,
+                  GError **err) -> gboolean {
+                       MimeStream mstream{MimeStream::make_from_stream(response)};
+
+                       auto res = request_func(MimeCryptoContext(ctx),
+                                               std::string{user_id ? user_id : ""},
+                                               std::string{prompt ? prompt : ""},
+                                               !!reprompt,
+                                               mstream);
+                       if (res)
+                               return TRUE;
+
+                       res.error().fill_g_error(err);
+                       return FALSE;
+               });
+
+}
+
+Result<void>
+MimeCryptoContext::setup_gpg_test(const std::string& testpath)
+{
+       /* setup clean environment for testing; inspired by gmime */
+
+       g_setenv ("GNUPGHOME", format("%s/.gnupg", testpath.c_str()).c_str(), 1);
+
+       /* disable environment variables that gpg-agent uses for pinentry */
+       g_unsetenv ("DBUS_SESSION_BUS_ADDRESS");
+       g_unsetenv ("DISPLAY");
+       g_unsetenv ("GPG_TTY");
+
+       if (g_mkdir_with_parents((testpath + "/.gnupg").c_str(), 0700) != 0)
+               return Err(Error::Code::File,
+                          "failed to create gnupg dir; err=%d", errno);
+
+       auto write_gpgfile=[&](const std::string& fname, const std::string& data)
+               -> Result<void> {
+
+               GError *err{};
+               std::string path{format("%s/%s", testpath.c_str(), fname.c_str())};
+               if (!g_file_set_contents(path.c_str(), data.c_str(), data.size(), &err))
+                       return Err(Error::Code::File, &err,
+                                  "failed to write %s", path.c_str());
+               else
+                       return Ok();
+       };
+
+       // some more elegant way?
+       if (auto&& res = write_gpgfile("gpg.conf", "pinentry-mode loopback\n"); !res)
+               return res;
+       if (auto&& res = write_gpgfile("gpgsm.conf", "disable-crl-checks\n"))
+               return res;
+
+       return Ok();
+}
+
+\f
+/*
+ * MimeMessage
+ */
+
+
+
+static Result<MimeMessage>
+make_from_stream(GMimeStream* &&stream/*consume*/)
+{
+       init_gmime();
+       GMimeParser *parser{g_mime_parser_new_with_stream(stream)};
+       g_object_unref(stream);
+       if (!parser)
+               return Err(Error::Code::Message, "cannot create mime parser");
+
+       GMimeMessage *gmime_msg{g_mime_parser_construct_message(parser, NULL)};
+       g_object_unref(parser);
+       if (!gmime_msg)
+               return Err(Error::Code::Message, "message seems invalid");
+
+       auto mime_msg{MimeMessage{std::move(G_OBJECT(gmime_msg))}};
+       g_object_unref(gmime_msg);
+
+       return Ok(std::move(mime_msg));
+}
+
+Result<MimeMessage>
+MimeMessage::make_from_file(const std::string& path)
+{
+       GError* err{};
+       init_gmime();
+       if (auto&& stream{g_mime_stream_file_open(path.c_str(), "r", &err)}; !stream)
+               return Err(Error::Code::Message, &err,
+                          "failed to open stream for %s", path.c_str());
+       else
+               return make_from_stream(std::move(stream));
+}
+
+Result<MimeMessage>
+MimeMessage::make_from_text(const std::string& text)
+{
+       init_gmime();
+       if (auto&& stream{g_mime_stream_mem_new_with_buffer(
+                               text.c_str(), text.length())}; !stream)
+               return Err(Error::Code::Message,
+                          "failed to open stream for string");
+       else
+               return make_from_stream(std::move(stream));
+}
+
+Option<int64_t>
+MimeMessage::date() const noexcept
+{
+       GDateTime *dt{g_mime_message_get_date(self())};
+       if (!dt)
+               return Nothing;
+       else
+               return g_date_time_to_unix(dt);
+}
+
+constexpr Option<GMimeAddressType>
+address_type(Contact::Type ctype)
+{
+       switch(ctype) {
+       case Contact::Type::Bcc:
+               return GMIME_ADDRESS_TYPE_BCC;
+       case Contact::Type::Cc:
+               return GMIME_ADDRESS_TYPE_CC;
+       case Contact::Type::From:
+               return GMIME_ADDRESS_TYPE_FROM;
+       case Contact::Type::To:
+               return GMIME_ADDRESS_TYPE_TO;
+       case Contact::Type::ReplyTo:
+               return GMIME_ADDRESS_TYPE_REPLY_TO;
+       case Contact::Type::Sender:
+               return GMIME_ADDRESS_TYPE_SENDER;
+       case Contact::Type::None:
+       default:
+               return Nothing;
+       }
+}
+
+static Mu::Contacts
+all_contacts(const MimeMessage& msg)
+{
+       Contacts contacts;
+
+       for (auto&& cctype: {
+                       Contact::Type::Sender,
+                       Contact::Type::From,
+                       Contact::Type::ReplyTo,
+                       Contact::Type::To,
+                       Contact::Type::Cc,
+                       Contact::Type::Bcc
+               }) {
+               auto addrs{msg.contacts(cctype)};
+               std::move(addrs.begin(), addrs.end(),
+                         std::back_inserter(contacts));
+       }
+
+       return contacts;
+}
+
+Mu::Contacts
+MimeMessage::contacts(Contact::Type ctype) const noexcept
+{
+       /* special case: get all */
+       if (ctype == Contact::Type::None)
+               return all_contacts(*this);
+
+       const auto atype{address_type(ctype)};
+       if (!atype)
+               return {};
+
+       auto addrs{g_mime_message_get_addresses(self(), *atype)};
+       if (!addrs)
+               return {};
+
+       const auto msgtime{date().value_or(0)};
+
+       Contacts contacts;
+       auto lst_len{internet_address_list_length(addrs)};
+       contacts.reserve(lst_len);
+       for (auto i = 0; i != lst_len; ++i) {
+
+               auto&& addr{internet_address_list_get_address(addrs, i)};
+               const auto name{internet_address_get_name(addr)};
+
+               if (G_UNLIKELY(!INTERNET_ADDRESS_IS_MAILBOX(addr)))
+                       continue;
+
+               const auto email{internet_address_mailbox_get_addr (
+                               INTERNET_ADDRESS_MAILBOX(addr))};
+               if (G_UNLIKELY(!email))
+                       continue;
+
+               contacts.emplace_back(email, name ? name : "", ctype, msgtime);
+       }
+
+       return contacts;
+}
+
+/*
+ * references() returns the concatenation of the References and In-Reply-To
+ * message-ids (in that order). Duplicates are removed.
+ *
+ * The _first_ one in the list determines the thread-id for the message.
+ */
+std::vector<std::string>
+MimeMessage::references() const noexcept
+{
+       // is ref already in the list? O(n) but with small n.
+       auto is_dup = [](auto&& seq, const std::string& ref) {
+               return seq_some(seq, [&](auto&& str) { return ref == str; });
+       };
+
+       auto is_fake = [](auto&& msgid) {
+               // this is bit ugly; protonmail injects fake References which
+               // can otherwise screw up threading.
+               if (g_str_has_suffix(msgid, "protonmail.internalid"))
+                       return true;
+               /* ... */
+               return false;
+       };
+
+       std::vector<std::string> refs;
+       for (auto&& ref_header: { "References", "In-reply-to" }) {
+
+               auto hdr{header(ref_header)};
+               if (!hdr)
+                       continue;
+
+               GMimeReferences *mime_refs{g_mime_references_parse({}, hdr->c_str())};
+               refs.reserve(refs.size() + g_mime_references_length(mime_refs));
+
+               for (auto i = 0; i != g_mime_references_length(mime_refs); ++i) {
+                       const auto msgid{g_mime_references_get_message_id(mime_refs, i)};
+                       if (msgid && !is_dup(refs, msgid) && !is_fake(msgid))
+                               refs.emplace_back(msgid);
+               }
+               g_mime_references_free(mime_refs);
+       }
+
+       return refs;
+}
+
+void
+MimeMessage::for_each(const ForEachFunc& func) const noexcept
+{
+       struct CallbackData { const ForEachFunc& func; };
+       CallbackData cbd{func};
+
+       g_mime_message_foreach(
+               self(),
+               [] (GMimeObject *parent, GMimeObject *part, gpointer user_data) {
+                       auto cb_data{reinterpret_cast<CallbackData*>(user_data)};
+                       cb_data->func(MimeObject{parent}, MimeObject{part});
+               }, &cbd);
+}
+
+
+\f
+/*
+ * MimePart
+ */
+size_t
+MimePart::size() const noexcept
+{
+       auto wrapper{g_mime_part_get_content(self())};
+       if (!wrapper) {
+               g_warning("failed to get content wrapper");
+               return 0;
+       }
+
+       auto stream{g_mime_data_wrapper_get_stream(wrapper)};
+       if (!stream) {
+               g_warning("failed to get stream");
+               return 0;
+       }
+
+       return static_cast<size_t>(g_mime_stream_length(stream));
+}
+Option<std::string>
+MimePart::to_string() const noexcept
+{
+       /*
+        * easy case: text. this automatically handles conversion to utf-8.
+        */
+       if (GMIME_IS_TEXT_PART(self())) {
+               if (char* txt{g_mime_text_part_get_text(GMIME_TEXT_PART(self()))}; !txt)
+                       return Nothing;
+               else
+                       return to_string_gchar(std::move(txt)/*consumes*/);
+       }
+
+       /*
+        * harder case: read from stream manually
+        */
+       GMimeDataWrapper *wrapper{g_mime_part_get_content(self())};
+       if (!wrapper) { /* this happens with invalid mails */
+               g_debug("failed to create data wrapper");
+               return Nothing;
+       }
+
+       GMimeStream *stream{g_mime_stream_mem_new()};
+       if (!stream) {
+               g_warning("failed to create mem stream");
+               return Nothing;
+       }
+
+       ssize_t buflen{g_mime_data_wrapper_write_to_stream(wrapper, stream)};
+       if (buflen <= 0) { /* empty buffer, not an error */
+               g_object_unref(stream);
+               return Nothing;
+       }
+
+       std::string buffer;
+       buffer.resize(buflen + 1);
+       g_mime_stream_reset(stream);
+
+       auto bytes{g_mime_stream_read(stream, buffer.data(), buflen)};
+       g_object_unref(stream);
+       if (bytes < 0)
+               return Nothing;
+
+       buffer.data()[bytes]='\0';
+       buffer.resize(buflen);
+
+       return buffer;
+
+}
+
+
+
+Result<size_t>
+MimePart::to_file(const std::string& path, bool overwrite) const noexcept
+{
+       MimeDataWrapper wrapper{g_mime_part_get_content(self())};
+       if (!wrapper)  /* this happens with invalid mails */
+               return Err(Error::Code::File, "failed to create data wrapper");
+
+       GError *err{};
+       auto strm{g_mime_stream_fs_open(path.c_str(),
+                                       O_WRONLY | O_CREAT | O_TRUNC |(overwrite ? 0 : O_EXCL),
+                                       S_IRUSR|S_IWUSR,
+                                       &err)};
+       if (!strm)
+               return Err(Error::Code::File, &err, "failed to open '%s'", path.c_str());
+
+       MimeStream stream{MimeStream::make_from_stream(strm)};
+       ssize_t written{g_mime_data_wrapper_write_to_stream(
+                       GMIME_DATA_WRAPPER(wrapper.object()),
+                       GMIME_STREAM(stream.object()))};
+
+       if (written < 0) {
+               return Err(Error::Code::File, &err,
+                          "failed to write to '%s'", path.c_str());
+       }
+
+       return Ok(static_cast<size_t>(written));
+}
+
+
+
+
+void
+MimeMultipart::for_each(const ForEachFunc& func) const noexcept
+{
+       struct CallbackData { const ForEachFunc& func; };
+       CallbackData cbd{func};
+
+       g_mime_multipart_foreach(
+               self(),
+               [] (GMimeObject *parent, GMimeObject *part, gpointer user_data) {
+                       auto cb_data{reinterpret_cast<CallbackData*>(user_data)};
+                       cb_data->func(MimeObject{parent}, MimeObject{part});
+               }, &cbd);
+}
+
+
+/*
+ * we need to be able to pass a crypto-context to the verify(), but
+ * g_mime_multipart_signed_verify() doesn't offer that anymore in GMime 3.x.
+ *
+ * So, add that by reimplementing it a bit (follow the upstream impl)
+ */
+
+
+static bool
+mime_types_equal (const std::string& mime_type, const std::string& official_type)
+{
+       if (g_ascii_strcasecmp(mime_type.c_str(), official_type.c_str()) == 0)
+               return true;
+
+       const auto slash_pos = official_type.find("/");
+       if (slash_pos == std::string::npos || slash_pos == 0)
+               return false;
+
+       /* If the official mime-type's subtype already begins with "x-", then there's
+        * nothing else to check. */
+       const auto subtype{official_type.substr(slash_pos + 1)};
+       if (g_ascii_strncasecmp (subtype.c_str(), "x-", 2) == 0)
+               return false;
+       const auto supertype{official_type.substr(0, slash_pos - 1)};
+       const auto xtype{official_type.substr(0, slash_pos - 1) + "x-" + subtype};
+
+       /* Check if the "x-" version of the official mime-type matches the
+        * supplied mime-type. For example, if the official mime-type is
+        * "application/pkcs7-signature", then we also want to match
+        * "application/x-pkcs7-signature". */
+       return g_ascii_strcasecmp(mime_type.c_str(), xtype.c_str()) == 0;
+}
+
+
+/**
+ * A bit of a monster, this impl.
+ *
+ * It's the transliteration of the g_mime_multipart_signed_verify() which
+ * adds the feature of passing in the CryptoContext.
+ *
+ */
+Result<std::vector<MimeSignature>>
+MimeMultipartSigned::verify(const MimeCryptoContext& ctx, VerifyFlags vflags) const noexcept
+{
+       if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2)
+               return Err(Error::Code::Crypto, "cannot verify, not enough subparts");
+
+       const auto proto{content_type_parameter("protocol")};
+       const auto sign_proto{ctx.signature_protocol()};
+
+       if (!proto || !sign_proto || !mime_types_equal(*proto, *sign_proto))
+               return Err(Error::Code::Crypto, "unsupported protocol " +
+                          proto.value_or("<unknown>"));
+
+       const auto sig{signed_signature_part()};
+       const auto content{signed_content_part()};
+       if (!sig || !content)
+               return Err(Error::Code::Crypto, "cannot find part");
+
+       const auto sig_mime_type{sig->mime_type()};
+       if (!sig || !mime_types_equal(sig_mime_type.value_or("<none>"), *sign_proto))
+               return Err(Error::Code::Crypto, "failed to find matching signature part");
+
+       MimeFormatOptions fopts{g_mime_format_options_new()};
+       g_mime_format_options_set_newline_format(fopts.get(), GMIME_NEWLINE_FORMAT_DOS);
+
+       MimeStream stream{MimeStream::make_mem()};
+       if (auto&& res = content->write_to_stream(fopts, stream); !res)
+               return Err(res.error());
+       stream.reset();
+
+       MimeDataWrapper wrapper{g_mime_part_get_content(GMIME_PART(sig->object()))};
+       MimeStream sigstream{MimeStream::make_mem()};
+       if (auto&& res = wrapper.write_to_stream(sigstream); !res)
+               return Err(res.error());
+       sigstream.reset();
+
+       GError *err{};
+       GMimeSignatureList *siglist{g_mime_crypto_context_verify(
+                       GMIME_CRYPTO_CONTEXT(ctx.object()),
+                       static_cast<GMimeVerifyFlags>(vflags),
+                       GMIME_STREAM(stream.object()),
+                       GMIME_STREAM(sigstream.object()),
+                       {},
+                       &err)};
+       if (!siglist)
+               return Err(Error::Code::Crypto, &err, "failed to verify");
+
+       std::vector<MimeSignature> sigs;
+       for (auto i = 0;
+            i != g_mime_signature_list_length(siglist); ++i) {
+               GMimeSignature *msig = g_mime_signature_list_get_signature(siglist, i);
+               sigs.emplace_back(MimeSignature(msig));
+       }
+       g_object_unref(siglist);
+
+       return sigs;
+}
+
+
+std::vector<MimeCertificate>
+MimeDecryptResult::recipients() const noexcept
+{
+       GMimeCertificateList *lst{g_mime_decrypt_result_get_recipients(self())};
+       if (!lst)
+               return {};
+
+       std::vector<MimeCertificate> certs;
+       for (int i = 0; i != g_mime_certificate_list_length(lst); ++i)
+               certs.emplace_back(
+                       MimeCertificate(
+                               g_mime_certificate_list_get_certificate(lst, i)));
+
+       return certs;
+}
+
+std::vector<MimeSignature>
+MimeDecryptResult::signatures() const noexcept
+{
+       GMimeSignatureList *lst{g_mime_decrypt_result_get_signatures(self())};
+       if (!lst)
+               return {};
+
+       std::vector<MimeSignature> sigs;
+       for (auto i = 0; i != g_mime_signature_list_length(lst); ++i) {
+               GMimeSignature *sig = g_mime_signature_list_get_signature(lst, i);
+               sigs.emplace_back(MimeSignature(sig));
+       }
+
+       return sigs;
+}
+/**
+ * Like verify, a bit of a monster, this impl.
+ *
+ * It's the transliteration of the g_mime_multipart_encrypted_decrypt() which
+ * adds the feature of passing in the CryptoContext.
+ *
+ */
+
+Mu::Result<MimeMultipartEncrypted::Decrypted>
+MimeMultipartEncrypted::decrypt(const MimeCryptoContext& ctx, DecryptFlags dflags,
+                               const std::string& session_key) const noexcept
+{
+       if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2)
+               return Err(Error::Code::Crypto, "cannot decrypted, not enough subparts");
+
+       const auto proto{content_type_parameter("protocol")};
+       const auto enc_proto{ctx.encryption_protocol()};
+
+       if (!proto || !enc_proto || !mime_types_equal(*proto, *enc_proto))
+               return Err(Error::Code::Crypto, "unsupported protocol " +
+                          proto.value_or("<unknown>"));
+
+       const auto version{encrypted_version_part()};
+       const auto encrypted{encrypted_content_part()};
+       if (!version || !encrypted)
+               return Err(Error::Code::Crypto, "cannot find part");
+
+       if (!mime_types_equal(version->mime_type().value_or(""), proto.value()))
+               return Err(Error::Code::Crypto,
+                          "cannot decrypt; unexpected version content-type '%s' != '%s'",
+                          version->mime_type().value_or("").c_str(),
+                          proto.value().c_str());
+
+       if (!mime_types_equal(encrypted->mime_type().value_or(""), "application/octet-stream"))
+               return Err(Error::Code::Crypto,
+                          "cannot decrypt; unexpected encrypted content-type '%s'",
+                          encrypted->mime_type().value_or("").c_str());
+
+       const auto content{encrypted->content()};
+       auto ciphertext{MimeStream::make_mem()};
+       content.write_to_stream(ciphertext);
+       ciphertext.reset();
+
+       auto stream{MimeStream::make_mem()};
+       auto filtered{MimeStream::make_filtered(stream)};
+       auto filter{g_mime_filter_dos2unix_new(FALSE)};
+       g_mime_stream_filter_add(GMIME_STREAM_FILTER(filtered.object()),
+                                filter);
+       g_object_unref(filter);
+
+       GError *err{};
+       GMimeDecryptResult *dres =
+               g_mime_crypto_context_decrypt(GMIME_CRYPTO_CONTEXT(ctx.object()),
+                                             static_cast<GMimeDecryptFlags>(dflags),
+                                             session_key.empty() ?
+                                             NULL : session_key.c_str(),
+                                             GMIME_STREAM(ciphertext.object()),
+                                             GMIME_STREAM(filtered.object()),
+                                             &err);
+       if (!dres)
+               return Err(Error::Code::Crypto, &err, "decryption failed");
+
+       filtered.flush();
+       stream.reset();
+
+       auto parser{g_mime_parser_new()};
+       g_mime_parser_init_with_stream(parser, GMIME_STREAM(stream.object()));
+
+       auto decrypted{g_mime_parser_construct_part(parser, NULL)};
+       g_object_unref(parser);
+       if (!decrypted) {
+               g_object_unref(dres);
+               return Err(Error::Code::Crypto, "failed to parse decrypted part");
+       }
+
+       Decrypted result = { MimeObject{decrypted}, MimeDecryptResult{dres} };
+
+       g_object_unref(decrypted);
+       g_object_unref(dres);
+
+       return Ok(std::move(result));
+}
diff --git a/lib/message/mu-mime-object.hh b/lib/message/mu-mime-object.hh
new file mode 100644 (file)
index 0000000..56a4b56
--- /dev/null
@@ -0,0 +1,1377 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_MIME_OBJECT_HH__
+#define MU_MIME_OBJECT_HH__
+
+#include <stdexcept>
+#include <string>
+#include <functional>
+#include <array>
+#include <vector>
+#include <gmime/gmime.h>
+#include "gmime/gmime-application-pkcs7-mime.h"
+#include "gmime/gmime-crypto-context.h"
+#include "utils/mu-option.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+#include "mu-contact.hh"
+
+namespace Mu {
+
+/* non-GObject types */
+
+using MimeFormatOptions = deletable_unique_ptr<GMimeFormatOptions, g_mime_format_options_free>;
+
+/**
+ * Initialize gmime (idempotent)
+ *
+ */
+void init_gmime(void);
+
+
+/**
+ * Get a RFC2047-compatible address for the given contact
+ *
+ * @param contact a contact
+ *
+ * @return an address string
+ */
+std::string address_rfc2047(const Contact& contact);
+
+class Object {
+public:
+       /**
+        * Default CTOR
+        *
+        */
+       Object() noexcept: self_{}  {}
+
+       /**
+        * Create an object from a GObject
+        *
+        * @param obj a gobject. A ref is added.
+        */
+       Object(GObject* &&obj): self_{G_OBJECT(g_object_ref(obj))} {
+               if (!G_IS_OBJECT(obj))
+                       throw std::runtime_error("not a g-object");
+       }
+
+       /**
+        * Copy CTOR
+        *
+        * @param other some other Object
+        */
+       Object(const Object& other) noexcept { *this = other; }
+
+       /**
+        * Move CTOR
+        *
+        * @param other some other Object
+        */
+       Object(Object&& other) noexcept { *this = std::move(other); }
+
+       /**
+        * operator=
+        *
+        * @param other copy some other object
+        *
+        * @return *this
+        */
+       Object& operator=(const Object& other) noexcept {
+
+               if (this != &other) {
+                       auto oldself = self_;
+                       self_ = other.self_ ?
+                               G_OBJECT(g_object_ref(other.self_)) : nullptr;
+                       if (oldself)
+                               g_object_unref(oldself);
+               }
+               return *this;
+       }
+
+       /**
+        * operator=
+        *
+        * @param other move some object object
+        *
+        * @return
+        */
+       Object& operator=(Object&& other) noexcept {
+
+               if (this != &other) {                   auto oldself = self_;
+                       self_ = other.self_;
+                       other.self_ = nullptr;
+                       if (oldself)
+                               g_object_unref(oldself);
+               }
+               return *this;
+       }
+
+       /**
+        * DTOR
+        */
+       virtual ~Object() {
+               if (self_) {
+                       g_object_unref(self_);
+               }
+       }
+
+       /**
+        * operator bool
+        *
+        * @return true if object wraps a GObject, false otherwise
+        */
+       operator bool() const noexcept { return !!self_; }
+
+       /**
+        * Get a ptr to the underlying GObject
+        *
+        * @return GObject or NULL
+        */
+       GObject* object() const { return self_; }
+
+
+       /**
+        * Unref the object
+        *
+        */
+       void unref() noexcept {
+               g_object_unref(self_);
+       }
+
+
+       /**
+        * Ref the object
+        *
+        */
+       void ref() noexcept {
+               g_object_ref(self_);
+       }
+
+
+private:
+       mutable GObject *self_{};
+};
+
+
+
+
+
+\f
+/**
+ * Thin wrapper around a GMimeContentType
+ *
+ */
+struct MimeContentType: public Object {
+
+       MimeContentType(GMimeContentType *ctype) : Object{G_OBJECT(ctype)} {
+               if (!GMIME_IS_CONTENT_TYPE(self()))
+                       throw std::runtime_error("not a content-type");
+       }
+       std::string media_type() const noexcept {
+               return g_mime_content_type_get_media_type(self());
+       }
+       std::string media_subtype() const noexcept {
+               return g_mime_content_type_get_media_subtype(self());
+       }
+
+       Option<std::string> mime_type() const noexcept {
+               return to_string_opt_gchar(g_mime_content_type_get_mime_type(self()));
+       }
+
+       bool is_type(const std::string& type, const std::string& subtype) const {
+               return g_mime_content_type_is_type(self(), type.c_str(),
+                                                  subtype.c_str());
+       }
+private:
+       GMimeContentType* self() const {
+               return reinterpret_cast<GMimeContentType*>(object());
+       }
+};
+
+
+
+
+\f
+/**
+ * Thin wrapper around a GMimeStream
+ *
+ */
+struct MimeStream: public Object {
+
+       ssize_t write(const char* buf, ::size_t size) {
+               return g_mime_stream_write(self(), buf, size);
+       }
+
+       bool reset() {
+               return g_mime_stream_reset(self()) < 0 ? false : true;
+       }
+
+       bool flush() {
+               return g_mime_stream_flush(self()) < 0 ? false : true;
+       }
+
+       static MimeStream make_mem() {
+               MimeStream mstream{g_mime_stream_mem_new()};
+               mstream.unref(); /* remove extra ref */
+               return mstream;
+       }
+
+       static MimeStream make_filtered(MimeStream& stream) {
+               MimeStream mstream{g_mime_stream_filter_new(stream.self())};
+               mstream.unref(); /* remove extra refs */
+               return mstream;
+       }
+
+       static MimeStream make_from_stream(GMimeStream *strm) {
+               MimeStream mstream{strm};
+               mstream.unref(); /* remove extra ref */
+               return mstream;
+       }
+
+private:
+       MimeStream(GMimeStream *stream): Object(G_OBJECT(stream)) {
+               if (!GMIME_IS_STREAM(self()))
+                       throw std::runtime_error("not a mime-stream");
+       };
+
+       GMimeStream* self() const {
+               return reinterpret_cast<GMimeStream*>(object());
+       }
+};
+
+template<typename S, typename T>
+constexpr Option<std::string_view> to_string_view_opt(const S& seq, T t) {
+       auto&& it = seq_find_if(seq, [&](auto&& item){return item.first == t;});
+       if (it == seq.cend())
+               return Nothing;
+       else
+               return it->second;
+}
+
+\f
+/**
+ * Thin wrapper around a GMimeDataWrapper
+ *
+ */
+struct MimeDataWrapper: public Object {
+       MimeDataWrapper(GMimeDataWrapper *wrapper): Object(G_OBJECT(wrapper)) {
+               if (!GMIME_IS_DATA_WRAPPER(self()))
+                       throw std::runtime_error("not a data-wrapper");
+       };
+
+       Result<size_t> write_to_stream(MimeStream& stream) const {
+               if (auto&& res = g_mime_data_wrapper_write_to_stream(
+                           self(), GMIME_STREAM(stream.object())) ; res < 0)
+                       return Err(Error::Code::Message, "failed to write to stream");
+               else
+                       return Ok(static_cast<size_t>(res));
+       }
+
+private:
+       GMimeDataWrapper* self() const {
+               return reinterpret_cast<GMimeDataWrapper*>(object());
+       }
+};
+
+
+\f
+/**
+ * Thin wrapper around a GMimeCertifcate
+ *
+ */
+struct MimeCertificate: public Object {
+       MimeCertificate(GMimeCertificate *cert) : Object{G_OBJECT(cert)} {
+               if (!GMIME_IS_CERTIFICATE(self()))
+                       throw std::runtime_error("not a certificate");
+       }
+
+       enum struct PubkeyAlgo {
+               Default = GMIME_PUBKEY_ALGO_DEFAULT,
+               Rsa     = GMIME_PUBKEY_ALGO_RSA,
+               RsaE    = GMIME_PUBKEY_ALGO_RSA_E,
+               RsaS    = GMIME_PUBKEY_ALGO_RSA_S,
+               ElgE    = GMIME_PUBKEY_ALGO_ELG_E,
+               Dsa     = GMIME_PUBKEY_ALGO_DSA,
+               Ecc     = GMIME_PUBKEY_ALGO_ECC,
+               Elg     = GMIME_PUBKEY_ALGO_ELG,
+               EcDsa   = GMIME_PUBKEY_ALGO_ECDSA,
+               EcDh    = GMIME_PUBKEY_ALGO_ECDH,
+               EdDsa   = GMIME_PUBKEY_ALGO_EDDSA,
+       };
+
+       enum struct DigestAlgo {
+               Default      = GMIME_DIGEST_ALGO_DEFAULT,
+               Md5          = GMIME_DIGEST_ALGO_MD5,
+               Sha1         = GMIME_DIGEST_ALGO_SHA1,
+               RipEmd160    = GMIME_DIGEST_ALGO_RIPEMD160,
+               Md2          = GMIME_DIGEST_ALGO_MD2,
+               Tiger192     = GMIME_DIGEST_ALGO_TIGER192,
+               Haval5160    = GMIME_DIGEST_ALGO_HAVAL5160,
+               Sha256       = GMIME_DIGEST_ALGO_SHA256,
+               Sha384       = GMIME_DIGEST_ALGO_SHA384,
+               Sha512       = GMIME_DIGEST_ALGO_SHA512,
+               Sha224       = GMIME_DIGEST_ALGO_SHA224,
+               Md4          = GMIME_DIGEST_ALGO_MD4,
+               Crc32        = GMIME_DIGEST_ALGO_CRC32,
+               Crc32Rfc1510 = GMIME_DIGEST_ALGO_CRC32_RFC1510,
+               Crc32Rfc2440 = GMIME_DIGEST_ALGO_CRC32_RFC2440,
+       };
+
+       enum struct Trust {
+               Unknown       = GMIME_TRUST_UNKNOWN,
+               Undefined     = GMIME_TRUST_UNDEFINED,
+               Never         = GMIME_TRUST_NEVER,
+               Marginal      = GMIME_TRUST_MARGINAL,
+               TrustFull     = GMIME_TRUST_FULL,
+               TrustUltimate = GMIME_TRUST_ULTIMATE,
+       };
+
+       enum struct Validity {
+               Unknown   = GMIME_VALIDITY_UNKNOWN,
+               Undefined = GMIME_VALIDITY_UNDEFINED,
+               Never     = GMIME_VALIDITY_NEVER,
+               Marginal  = GMIME_VALIDITY_MARGINAL,
+               Full      = GMIME_VALIDITY_FULL,
+               Ultimate  = GMIME_VALIDITY_ULTIMATE,
+       };
+
+       PubkeyAlgo pubkey_algo() const {
+               return static_cast<PubkeyAlgo>(
+                       g_mime_certificate_get_pubkey_algo(self()));
+       }
+
+       DigestAlgo digest_algo() const {
+               return static_cast<DigestAlgo>(
+                       g_mime_certificate_get_digest_algo(self()));
+       }
+
+       Validity id_validity() const {
+               return static_cast<Validity>(
+                       g_mime_certificate_get_id_validity(self()));
+       }
+
+       Trust trust() const {
+               return static_cast<Trust>(
+                       g_mime_certificate_get_trust(self()));
+       }
+
+       Option<std::string> issuer_serial() const {
+               return to_string_opt(g_mime_certificate_get_issuer_serial(self()));
+       }
+       Option<std::string> issuer_name() const {
+               return to_string_opt(g_mime_certificate_get_issuer_name(self()));
+       }
+
+       Option<std::string> fingerprint() const {
+               return to_string_opt(g_mime_certificate_get_fingerprint(self()));
+       }
+
+       Option<std::string> key_id() const {
+               return to_string_opt(g_mime_certificate_get_key_id(self()));
+       }
+
+
+       Option<std::string> name() const {
+               return to_string_opt(g_mime_certificate_get_name(self()));
+       }
+
+       Option<std::string> user_id() const {
+               return to_string_opt(g_mime_certificate_get_user_id(self()));
+       }
+
+       Option<::time_t> created() const {
+               if (auto t = g_mime_certificate_get_created(self()); t >= 0)
+                       return t;
+               else
+                       return Nothing;
+       }
+
+       Option<::time_t> expires() const {
+               if (auto t = g_mime_certificate_get_expires(self()); t >= 0)
+                       return t;
+               else
+                       return Nothing;
+       }
+
+private:
+       GMimeCertificate* self() const {
+               return reinterpret_cast<GMimeCertificate*>(object());
+       }
+};
+
+constexpr std::array<std::pair<MimeCertificate::PubkeyAlgo, std::string_view>, 11>
+AllPubkeyAlgos = {{
+               { MimeCertificate::PubkeyAlgo::Default, "default"},
+               { MimeCertificate::PubkeyAlgo::Rsa,     "rsa"},
+               { MimeCertificate::PubkeyAlgo::RsaE,    "rsa-encryption-only"},
+               { MimeCertificate::PubkeyAlgo::RsaS,    "rsa-signing-only"},
+               { MimeCertificate::PubkeyAlgo::ElgE,    "el-gamal-encryption-only"},
+               { MimeCertificate::PubkeyAlgo::Dsa,     "dsa"},
+               { MimeCertificate::PubkeyAlgo::Ecc,     "elliptic curve"},
+               { MimeCertificate::PubkeyAlgo::Elg,     "el-gamal"},
+               { MimeCertificate::PubkeyAlgo::EcDsa,   "elliptic-curve+dsa"},
+               { MimeCertificate::PubkeyAlgo::EcDh,    "elliptic-curve+diffie-helman"},
+               { MimeCertificate::PubkeyAlgo::EdDsa,   "elliptic-curve+dsa-2"}
+               }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::PubkeyAlgo algo) {
+       return to_string_view_opt(AllPubkeyAlgos, algo);
+}
+
+constexpr std::array<std::pair<MimeCertificate::DigestAlgo, std::string_view>, 15>
+AllDigestAlgos = {{
+               { MimeCertificate::DigestAlgo::Default,         "default"},
+               { MimeCertificate::DigestAlgo::Md5,             "md5"},
+               { MimeCertificate::DigestAlgo::Sha1,            "sha1"},
+               { MimeCertificate::DigestAlgo::RipEmd160,       "ripemd-160"},
+               { MimeCertificate::DigestAlgo::Md2,             "md2"},
+               { MimeCertificate::DigestAlgo::Tiger192,        "tiger-192"},
+               { MimeCertificate::DigestAlgo::Haval5160,       "haval-5-160"},
+               { MimeCertificate::DigestAlgo::Sha256,          "sha-256"},
+               { MimeCertificate::DigestAlgo::Sha384,          "sha-384"},
+               { MimeCertificate::DigestAlgo::Sha512,          "sha-512"},
+               { MimeCertificate::DigestAlgo::Sha224,          "sha-224"},
+               { MimeCertificate::DigestAlgo::Md4,             "md4"},
+               { MimeCertificate::DigestAlgo::Crc32,           "crc32"},
+               { MimeCertificate::DigestAlgo::Crc32Rfc1510,    "crc32-rfc1510"},
+               { MimeCertificate::DigestAlgo::Crc32Rfc2440,    "crc32-rfc2440"},
+       }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::DigestAlgo algo) {
+       return to_string_view_opt(AllDigestAlgos, algo);
+}
+
+constexpr std::array<std::pair<MimeCertificate::Trust, std::string_view>, 6>
+AllTrusts = {{
+               { MimeCertificate::Trust::Unknown,      "unknown" },
+               { MimeCertificate::Trust::Undefined,    "undefined" },
+               { MimeCertificate::Trust::Never,        "never" },
+               { MimeCertificate::Trust::Marginal,     "marginal" },
+               { MimeCertificate::Trust::TrustFull,    "trust-full" },
+               { MimeCertificate::Trust::TrustUltimate,"trust-ultimate" },
+       }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::Trust trust) {
+       return to_string_view_opt(AllTrusts, trust);
+}
+
+constexpr std::array<std::pair<MimeCertificate::Validity, std::string_view>, 6>
+AllValidities = {{
+               { MimeCertificate::Validity::Unknown,   "unknown" },
+               { MimeCertificate::Validity::Undefined, "undefined" },
+               { MimeCertificate::Validity::Never,     "never" },
+               { MimeCertificate::Validity::Marginal,  "marginal" },
+               { MimeCertificate::Validity::Full,      "full" },
+               { MimeCertificate::Validity::Ultimate,  "ultimate" },
+       }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::Validity val) {
+       return to_string_view_opt(AllValidities, val);
+}
+
+
+\f
+/**
+ * Thin wrapper around a GMimeSignature
+ *
+ */
+struct MimeSignature: public Object {
+       MimeSignature(GMimeSignature *sig) : Object{G_OBJECT(sig)} {
+               if (!GMIME_IS_SIGNATURE(self()))
+                       throw std::runtime_error("not a signature");
+       }
+
+       /**
+        * Signature status
+        *
+        */
+       enum struct Status {
+               Valid        = GMIME_SIGNATURE_STATUS_VALID,
+               Green        = GMIME_SIGNATURE_STATUS_GREEN,
+               Red          = GMIME_SIGNATURE_STATUS_RED,
+               KeyRevoked   = GMIME_SIGNATURE_STATUS_KEY_REVOKED,
+               KeyExpired   = GMIME_SIGNATURE_STATUS_KEY_EXPIRED,
+               SigExpired   = GMIME_SIGNATURE_STATUS_SIG_EXPIRED,
+               KeyMissing   = GMIME_SIGNATURE_STATUS_KEY_MISSING,
+               CrlMissing   = GMIME_SIGNATURE_STATUS_CRL_MISSING,
+               CrlTooOld    = GMIME_SIGNATURE_STATUS_CRL_TOO_OLD,
+               BadPolicy    = GMIME_SIGNATURE_STATUS_BAD_POLICY,
+               SysError     = GMIME_SIGNATURE_STATUS_SYS_ERROR,
+               TofuConflict = GMIME_SIGNATURE_STATUS_TOFU_CONFLICT
+       };
+
+       Status status() const { return static_cast<Status>(
+                       g_mime_signature_get_status(self())); }
+
+       ::time_t created() const { return g_mime_signature_get_created(self()); }
+       ::time_t expires() const { return g_mime_signature_get_expires(self()); }
+
+
+       const MimeCertificate certificate() const {
+               return MimeCertificate{g_mime_signature_get_certificate(self())};
+       }
+
+private:
+       GMimeSignature* self() const {
+               return reinterpret_cast<GMimeSignature*>(object());
+       }
+};
+
+constexpr std::array<std::pair<MimeSignature::Status, std::string_view>, 12>
+AllMimeSignatureStatuses= {{
+               { MimeSignature::Status::Valid,         "valid" },
+               { MimeSignature::Status::Green,         "green" },
+               { MimeSignature::Status::Red,           "red" },
+               { MimeSignature::Status::KeyRevoked,    "key-revoked" },
+               { MimeSignature::Status::KeyExpired,    "key-expired" },
+               { MimeSignature::Status::SigExpired,    "sig-expired" },
+               { MimeSignature::Status::KeyMissing,    "key-missing" },
+               { MimeSignature::Status::CrlMissing,    "crl-missing" },
+               { MimeSignature::Status::CrlTooOld,     "crl-too-old" },
+               { MimeSignature::Status::BadPolicy,     "bad-policy" },
+               { MimeSignature::Status::SysError,      "sys-error" },
+               { MimeSignature::Status::TofuConflict,  "tofu-confict" },
+       }};
+MU_ENABLE_BITOPS(MimeSignature::Status);
+
+static inline std::string to_string(MimeSignature::Status status) {
+       std::string str;
+       for (auto&& item: AllMimeSignatureStatuses) {
+               if (none_of(item.first & status))
+                       continue;
+               if (!str.empty())
+                       str += ", ";
+               str += item.second;
+       }
+       if (str.empty())
+               str = "none";
+
+       return str;
+}
+
+
+
+\f
+/**
+* Thin wrapper around a GMimeDecryptResult
+ *
+ */
+struct MimeDecryptResult: public Object {
+       MimeDecryptResult (GMimeDecryptResult *decres) : Object{G_OBJECT(decres)} {
+               if (!GMIME_IS_DECRYPT_RESULT(self()))
+                       throw std::runtime_error("not a decrypt-result");
+       }
+
+       std::vector<MimeCertificate> recipients() const noexcept;
+       std::vector<MimeSignature>   signatures() const noexcept;
+
+       enum struct CipherAlgo {
+               Default     = GMIME_CIPHER_ALGO_DEFAULT,
+               Idea        = GMIME_CIPHER_ALGO_IDEA,
+               Des3        = GMIME_CIPHER_ALGO_3DES,
+               Cast5       = GMIME_CIPHER_ALGO_CAST5,
+               Blowfish    = GMIME_CIPHER_ALGO_BLOWFISH,
+               Aes         = GMIME_CIPHER_ALGO_AES,
+               Aes192      = GMIME_CIPHER_ALGO_AES192,
+               Aes256      = GMIME_CIPHER_ALGO_AES256,
+               TwoFish     = GMIME_CIPHER_ALGO_TWOFISH,
+               Camellia128 = GMIME_CIPHER_ALGO_CAMELLIA128,
+               Camellia192 = GMIME_CIPHER_ALGO_CAMELLIA192,
+               Camellia256 = GMIME_CIPHER_ALGO_CAMELLIA256
+       };
+
+       CipherAlgo cipher() const noexcept {
+               return static_cast<CipherAlgo>(
+                       g_mime_decrypt_result_get_cipher(self()));
+       }
+
+       using DigestAlgo = MimeCertificate::DigestAlgo;
+       DigestAlgo mdc() const noexcept {
+               return static_cast<DigestAlgo>(
+                       g_mime_decrypt_result_get_mdc(self()));
+       }
+
+       Option<std::string> session_key() const noexcept {
+               return to_string_opt(g_mime_decrypt_result_get_session_key(self()));
+       }
+
+private:
+       GMimeDecryptResult* self() const {
+               return reinterpret_cast<GMimeDecryptResult*>(object());
+       }
+};
+
+constexpr std::array<std::pair<MimeDecryptResult::CipherAlgo, std::string_view>, 12>
+AllCipherAlgos= {{
+               {MimeDecryptResult::CipherAlgo::Default,        "default"},
+               {MimeDecryptResult::CipherAlgo::Idea,           "idea"},
+               {MimeDecryptResult::CipherAlgo::Des3,           "3des"},
+               {MimeDecryptResult::CipherAlgo::Cast5,          "cast5"},
+               {MimeDecryptResult::CipherAlgo::Blowfish,       "blowfish"},
+               {MimeDecryptResult::CipherAlgo::Aes,            "aes"},
+               {MimeDecryptResult::CipherAlgo::Aes192,         "aes192"},
+               {MimeDecryptResult::CipherAlgo::Aes256,         "aes256"},
+               {MimeDecryptResult::CipherAlgo::TwoFish,        "twofish"},
+               {MimeDecryptResult::CipherAlgo::Camellia128,    "camellia128"},
+               {MimeDecryptResult::CipherAlgo::Camellia192,    "camellia192"},
+               {MimeDecryptResult::CipherAlgo::Camellia256,    "camellia256"},
+       }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeDecryptResult::CipherAlgo algo) {
+       return to_string_view_opt(AllCipherAlgos, algo);
+}
+
+\f
+/**
+ * Thin wrapper around a GMimeCryptoContext
+ *
+ */
+struct MimeCryptoContext : public Object {
+
+       /**
+        * Make a new PGP crypto context.
+        *
+        * For 'test-mode', pass a test-path; in this mode GPG will be setup
+        * in an isolated mode so it does not affect normal usage.
+        *
+        * @param testpath (for unit-tests) pass a path to an existing dir to
+        * create a pgp setup. For normal use, leave empty.
+        *
+        * @return A MimeCryptoContext or an error
+        */
+       static Result<MimeCryptoContext>
+       make_gpg(const std::string& testpath={}) try {
+               if (!testpath.empty()) {
+                       if (auto&& res = setup_gpg_test(testpath); !res)
+                               return Err(res.error());
+               }
+               MimeCryptoContext ctx(g_mime_gpg_context_new());
+               ctx.unref(); /* remove extra ref */
+               return Ok(std::move(ctx));
+       } catch (...) {
+               return Err(Error::Code::Crypto, "failed to create crypto context");
+       }
+
+       static Result<MimeCryptoContext>
+       make(const std::string& protocol) {
+               auto ctx = g_mime_crypto_context_new(protocol.c_str());
+               if (!ctx)
+                       return Err(Error::Code::Crypto, "unsupported protocol " + protocol);
+               MimeCryptoContext mctx{ctx};
+               mctx.unref(); /* remove extra ref */
+               return Ok(std::move(mctx));
+       }
+
+       Option<std::string> encryption_protocol() const noexcept {
+               return to_string_opt(g_mime_crypto_context_get_encryption_protocol(self()));
+       }
+       Option<std::string> signature_protocol() const noexcept {
+               return to_string_opt(g_mime_crypto_context_get_signature_protocol(self()));
+       }
+       Option<std::string> key_exchange_protocol() const noexcept {
+               return to_string_opt(g_mime_crypto_context_get_key_exchange_protocol(self()));
+       }
+
+       /**
+        * Imports a stream of keys/certificates contained within stream into
+        * the key/certificate database controlled by @this.
+        *
+        * @param stream
+        *
+        * @return number of keys imported, or an error.
+        */
+       Result<size_t> import_keys(MimeStream& stream);
+
+       /**
+        * Prototype for a request-password function.
+        *
+        * @param ctx the MimeCryptoContext making the request
+        * @param user_id the user_id of the password being requested
+        * @param prompt a string containing some helpful context for the prompt
+        * @param reprompt true if this password request is a reprompt due to a
+        * previously bad password response
+        * @param response a stream for the application to write the password to
+        * (followed by a newline '\n' character)
+        *
+        * @return nothing (Ok) or an error,
+        */
+       using PasswordRequestFunc =
+               std::function<Result<void>(
+               const MimeCryptoContext& ctx,
+                       const std::string& user_id,
+                       const std::string& prompt,
+                       bool reprompt,
+                       MimeStream& response)>;
+       /**
+        * Set a function to request a password.
+        *
+        * @param pw_func password function.
+        */
+       void set_request_password(PasswordRequestFunc pw_func);
+
+
+private:
+       MimeCryptoContext(GMimeCryptoContext *ctx): Object{G_OBJECT(ctx)} {
+               if (!GMIME_IS_CRYPTO_CONTEXT(self()))
+                       throw std::runtime_error("not a crypto-context");
+       }
+
+       static Result<void> setup_gpg_test(const std::string& testpath);
+
+       GMimeCryptoContext* self() const {
+               return reinterpret_cast<GMimeCryptoContext*>(object());
+       }
+};
+
+\f
+/**
+ * Thin wrapper around a GMimeObject
+ *
+ */
+class MimeObject: public Object {
+public:
+       /**
+        * Construct a new MimeObject. Take a ref on the obj
+        *
+        * @param mime_part mime-part pointer
+        */
+       MimeObject(const Object& obj): Object{obj}  {
+               if (!GMIME_IS_OBJECT(self()))
+                       throw std::runtime_error("not a mime-object");
+       }
+       MimeObject(GMimeObject *mobj): Object{G_OBJECT(mobj)}  {
+               if (mobj && !GMIME_IS_OBJECT(self()))
+                       throw std::runtime_error("not a mime-object");
+       }
+
+       /**
+        * Get a header from the MimeObject
+        *
+        * @param header the header to retrieve
+        *
+        * @return header value (UTF-8) or Nothing
+        */
+       Option<std::string> header(const std::string& header) const noexcept;
+
+
+       /**
+        * Get all headers as pairs of name, value
+        *
+        * @return all headers
+        */
+       std::vector<std::pair<std::string, std::string>> headers() const noexcept;
+
+
+       /**
+        * Get the content type
+        *
+        * @return  the content-type or Nothing
+        */
+       Option<MimeContentType> content_type() const noexcept {
+               auto ct{g_mime_object_get_content_type(self())};
+               if (!ct)
+                       return Nothing;
+               else
+                       return MimeContentType(ct);
+       }
+
+       Option<std::string> mime_type() const noexcept {
+               if (auto ct = content_type(); !ct)
+                       return Nothing;
+               else
+                       return ct->mime_type();
+       }
+
+       /**
+        * Get the content-type parameter
+        *
+        * @param param name of parameter
+        *
+        * @return the value of the parameter, or Nothing
+        */
+       Option<std::string> content_type_parameter(const std::string& param) const noexcept {
+               return Mu::to_string_opt(
+                       g_mime_object_get_content_type_parameter(self(), param.c_str()));
+       }
+
+       /**
+        * Write this MimeObject to some stream
+        *
+        * @param f_opts formatting options
+        * @param stream the stream
+        *
+        * @return the number or bytes written or an error
+        */
+       Result<size_t> write_to_stream(const MimeFormatOptions& f_opts,
+                                      MimeStream& stream) const;
+       /**
+        * Write the object to a string.
+        *
+        * @return
+        */
+       Option<std::string> to_string_opt() const noexcept;
+
+       /*
+        * subtypes.
+        */
+
+       /**
+        * Is this a MimePart?
+        *
+        * @return true or false
+        */
+       bool is_part()         const { return GMIME_IS_PART(self()); }
+
+       /**
+        * Is this a MimeMultiPart?
+        *
+        * @return true or false
+        */
+       bool is_multipart()    const { return GMIME_IS_MULTIPART(self());}
+
+       /**
+        * Is this a MimeMultiPart?
+        *
+        * @return true or false
+        */
+       bool is_multipart_encrypted()    const {
+               return GMIME_IS_MULTIPART_ENCRYPTED(self());
+       }
+
+       /**
+        * Is this a MimeMultiPart?
+        *
+        * @return true or false
+        */
+       bool is_multipart_signed()    const {
+               return GMIME_IS_MULTIPART_SIGNED(self());
+       }
+
+       /**
+        * Is this a MimeMessage?
+        *
+        * @return true or false
+        */
+       bool is_message()      const { return GMIME_IS_MESSAGE(self());}
+
+       /**
+        * Is this a MimeMessagePart?
+        *
+        * @return true orf alse
+        */
+       bool is_message_part() const { return GMIME_IS_MESSAGE_PART(self());}
+
+       /**
+        * Is this a MimeApplicationpkcs7Mime?
+        *
+        * @return true orf alse
+        */
+       bool is_mime_application_pkcs7_mime() const {
+               return GMIME_IS_APPLICATION_PKCS7_MIME(self());
+       }
+
+       /**
+        * Callback for for_each(). See GMimeObjectForEachFunc.
+        *
+        */
+       using ForEachFunc = std::function<void(const MimeObject& parent,
+               const MimeObject& part)>;
+
+private:
+       GMimeObject* self() const {
+               return reinterpret_cast<GMimeObject*>(object());
+       }
+};
+
+\f
+/**
+ * Thin wrapper around a GMimeMessage
+ *
+ */
+class MimeMessage: public MimeObject {
+public:
+       /**
+        * Construct a MimeMessage
+        *
+        * @param obj an Object of the right type
+        */
+       MimeMessage(const Object& obj): MimeObject(obj) {
+               if (!is_message())
+                       throw std::runtime_error("not a mime-message");
+       }
+
+       /**
+        * Make a MimeMessage from a file
+        *
+        * @param path path to the file
+        *
+        * @return a MimeMessage or an error.
+        */
+       static Result<MimeMessage> make_from_file (const std::string& path);
+
+       /**
+        * Make a MimeMessage from a string
+        *
+        * @param path path to the file
+        *
+        * @return a MimeMessage or an error.
+        */
+       static Result<MimeMessage> make_from_text (const std::string& text);
+
+       /**
+        * Get the contacts of a given type, or None for _all_
+        *
+        * @param ctype contact type
+        *
+        * @return contacts
+        */
+       Contacts contacts(Contact::Type ctype) const noexcept;
+
+       /**
+        * Gets the message-id if it exists, or nullopt otherwise.
+        *
+        * @return string or nullopt
+        */
+       Option<std::string> message_id() const noexcept {
+               return Mu::to_string_opt(g_mime_message_get_message_id(self()));
+       }
+
+       /**
+        * Gets the message-id if it exists, or nullopt otherwise.
+        *
+        * @return string or nullopt
+        */
+       Option<std::string> subject() const noexcept {
+               return Mu::to_string_opt(g_mime_message_get_subject(self()));
+       }
+
+       /**
+        * Gets the date if it exists, or nullopt otherwise.
+        *
+        * @return a time_t value (expressed as a 64-bit number) or nullopt
+        */
+       Option<int64_t> date() const noexcept;
+
+
+       /**
+        * Get the references for this message (including in-reply-to), in the
+        * order of older..newer; the first one would the oldest parent, and
+        * in-reply-to would be the last one (if any). These are de-duplicated,
+        * and known-fake references removed (see implementation)
+        *
+        * @return references.
+        */
+       std::vector<std::string> references() const noexcept;
+
+
+       /**
+        * Recursively apply func tol all parts of this message
+        *
+        * @param func a function
+        */
+       void for_each(const ForEachFunc& func) const noexcept;
+
+private:
+       GMimeMessage* self() const {
+               return reinterpret_cast<GMimeMessage*>(object());
+       }
+};
+\f
+/**
+ * Thin wrapper around a GMimePart.
+ *
+ */
+class MimePart: public MimeObject {
+public:
+       /**
+        * Construct a MimePart
+        *
+        * @param obj an Object of the right type
+        */
+       MimePart(const Object& obj): MimeObject(obj) {
+               if (!is_part())
+                       throw std::runtime_error("not a mime-part");
+       }
+
+       /**
+        * Determines whether or not the part is an attachment based on the
+        * value of the Content-Disposition header.
+        *
+        * @return true or false
+        */
+       bool is_attachment() const noexcept {
+               return g_mime_part_is_attachment(self());
+       }
+
+       /**
+        * Gets the value of the Content-Description for this mime part
+        * if it exists, or nullopt otherwise.
+        *
+        * @return string or nullopt
+        */
+       Option<std::string> content_description() const noexcept {
+               return Mu::to_string_opt(g_mime_part_get_content_description(self()));
+       }
+
+       /**
+        * Gets the value of the Content-Id for this mime part
+        * if it exists, or nullopt otherwise.
+        *
+        * @return string or nullopt
+        */
+       Option<std::string> content_id() const noexcept {
+               return Mu::to_string_opt(g_mime_part_get_content_id(self()));
+       }
+
+       /**
+        * Gets the value of the Content-Md5 header for this mime part
+        * if it exists, or nullopt otherwise.
+        *
+        * @return string or nullopt
+        */
+       Option<std::string> content_md5() const noexcept {
+               return Mu::to_string_opt(g_mime_part_get_content_md5(self()));
+
+       }
+
+       /**
+        * Verify the content md5 for the specified mime part. Returns false if
+        * the mime part does not contain a Content-MD5.
+        *
+        * @return true or false
+        */
+       bool verify_content_md5() const noexcept {
+               return g_mime_part_verify_content_md5(self());
+       }
+
+       /**
+        * Gets the value of the Content-Location for this mime part if it
+        * exists, or nullopt otherwise.
+        *
+        * @return string or nullopt
+        */
+       Option<std::string> content_location() const noexcept {
+               return Mu::to_string_opt(g_mime_part_get_content_location(self()));
+       }
+
+
+       MimeDataWrapper content() const noexcept {
+               return MimeDataWrapper{g_mime_part_get_content(self())};
+       }
+
+       /**
+        * Gets the filename for this mime part if it exists, or nullopt
+        * otherwise.
+        *
+        * @return string or nullopt
+        */
+       Option<std::string> filename() const noexcept {
+               return Mu::to_string_opt(g_mime_part_get_filename(self()));
+       }
+
+       /**
+        * Size of content, in bytes
+        *
+        * @return size
+        */
+       size_t size() const noexcept;
+
+       /**
+        * Get as UTF-8 string
+        *
+        * @return a string, or NULL.
+        */
+       Option<std::string> to_string() const noexcept;
+
+
+       /**
+        * Write part to a file
+        *
+        * @param path path to file
+        * @param overwrite if true, overwrite existing file, if it \18bqexists
+        *
+        * @return size of the wrtten file, or an error.
+        */
+       Result<size_t> to_file(const std::string& path, bool overwrite)
+               const noexcept;
+
+
+       /**
+        * Types of Content Encoding.
+        *
+        */
+       enum struct ContentEncoding {
+               Default         = GMIME_CONTENT_ENCODING_DEFAULT,
+               SevenBit        = GMIME_CONTENT_ENCODING_7BIT,
+               EightBit        = GMIME_CONTENT_ENCODING_8BIT,
+               Binary          = GMIME_CONTENT_ENCODING_BINARY,
+               Base64          = GMIME_CONTENT_ENCODING_BASE64,
+               QuotedPrintable = GMIME_CONTENT_ENCODING_QUOTEDPRINTABLE,
+               UuEncode        = GMIME_CONTENT_ENCODING_UUENCODE
+       };
+
+       /**
+        * Gets the content encoding of the mime part.
+        *
+        * @return the content encoding
+        */
+       ContentEncoding content_encoding() const noexcept {
+               const auto enc{g_mime_part_get_content_encoding(self())};
+               g_return_val_if_fail(enc <= GMIME_CONTENT_ENCODING_UUENCODE,
+                                    ContentEncoding::Default);
+               return static_cast<ContentEncoding>(enc);
+       }
+
+
+       /**
+        * Types of OpenPGP data
+        *
+        */
+       enum struct OpenPGPData {
+               None       = GMIME_OPENPGP_DATA_NONE,
+               Encrypted  = GMIME_OPENPGP_DATA_ENCRYPTED,
+               Signed     = GMIME_OPENPGP_DATA_SIGNED,
+               PublicKey  = GMIME_OPENPGP_DATA_PUBLIC_KEY,
+               PrivateKey = GMIME_OPENPGP_DATA_PRIVATE_KEY,
+       };
+
+       /**
+        * Gets whether or not (and what type) of OpenPGP data is contained
+        *
+        * @return OpenGPGData
+        */
+       OpenPGPData openpgp_data() const noexcept {
+               const auto data{g_mime_part_get_openpgp_data(self())};
+               g_return_val_if_fail(data <= GMIME_OPENPGP_DATA_PRIVATE_KEY,
+                                    OpenPGPData::None);
+               return static_cast<OpenPGPData>(data);
+       }
+
+private:
+       GMimePart* self() const {
+               return reinterpret_cast<GMimePart*>(object());
+       }
+};
+
+
+\f
+/**
+ * Thin wrapper around a GMimeMessagePart.
+ *
+ */
+class MimeMessagePart: public MimeObject {
+public:
+       /**
+        * Construct a MimeMessagePart
+        *
+        * @param obj an Object of the right type
+        */
+       MimeMessagePart(const Object& obj): MimeObject(obj) {
+               if (!is_message_part())
+                       throw std::runtime_error("not a mime-message-part");
+       }
+
+       /**
+        * Get the MimeMessage for this MimeMessagePart.
+        *
+        * @return the MimeMessage or Nothing
+        */
+       Option<MimeMessage> get_message() const {
+               auto msg{g_mime_message_part_get_message(self())};
+               if (msg)
+                       return MimeMessage(Object(G_OBJECT(msg)));
+               else
+                       return Nothing;
+       }
+private:
+       GMimeMessagePart* self() const {
+               return reinterpret_cast<GMimeMessagePart*>(object());
+       }
+
+};
+\f/**
+ * Thin wrapper around a GMimeApplicationPkcs7Mime
+ *
+ */
+class MimeApplicationPkcs7Mime: public MimePart {
+public:
+       /**
+        * Construct a MimeApplicationPkcs7Mime
+        *
+        * @param obj an Object of the right type
+        */
+       MimeApplicationPkcs7Mime(const Object& obj): MimePart(obj) {
+               if (!is_mime_application_pkcs7_mime())
+                       throw std::runtime_error("not a mime-application-pkcs7-mime");
+       }
+
+       enum struct SecureMimeType {
+               CompressedData = GMIME_SECURE_MIME_TYPE_COMPRESSED_DATA,
+               EnvelopedData  = GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA,
+               SignedData     = GMIME_SECURE_MIME_TYPE_SIGNED_DATA,
+               CertsOnly      = GMIME_SECURE_MIME_TYPE_CERTS_ONLY,
+               Unknown        = GMIME_SECURE_MIME_TYPE_UNKNOWN
+       };
+
+       SecureMimeType smime_type() const {
+               return static_cast<SecureMimeType>(
+                       g_mime_application_pkcs7_mime_get_smime_type(self()));
+       }
+
+private:
+       GMimeApplicationPkcs7Mime* self() const {
+               return reinterpret_cast<GMimeApplicationPkcs7Mime*>(object());
+       }
+};
+
+\f
+/**
+ * Thin wrapper around a GMimeMultiPart
+ *
+ */
+class MimeMultipart: public MimeObject {
+public:
+       /**
+        * Construct a MimeMultipart
+        *
+        * @param obj an Object of the right type
+        */
+       MimeMultipart(const Object& obj): MimeObject(obj) {
+               if (!is_multipart())
+                       throw std::runtime_error("not a mime-multipart");
+       }
+
+       Option<MimePart> signed_content_part() const {
+               return part(GMIME_MULTIPART_SIGNED_CONTENT);
+       }
+
+       Option<MimePart> signed_signature_part() const {
+               return part(GMIME_MULTIPART_SIGNED_SIGNATURE);
+       }
+
+       Option<MimePart> encrypted_version_part() const {
+               return part(GMIME_MULTIPART_ENCRYPTED_VERSION);
+       }
+
+       Option<MimePart> encrypted_content_part() const {
+               return part(GMIME_MULTIPART_ENCRYPTED_CONTENT);
+       }
+
+       /**
+        * Recursively apply func to all parts
+        *
+        * @param func a function
+        */
+       void for_each(const ForEachFunc& func) const noexcept;
+
+private:
+
+       Option<MimePart> part(int index) const {
+               if (MimeObject mobj{g_mime_multipart_get_part(self(),index)}; !mobj)
+                       return Nothing;
+               else
+                       return mobj;
+       }
+
+       GMimeMultipart* self() const {
+               return reinterpret_cast<GMimeMultipart*>(object());
+       }
+};
+
+\f
+/**
+ * Thin wrapper around a GMimeMultiPartEncrypted
+ *
+ */
+class MimeMultipartEncrypted: public MimeMultipart {
+public:
+       /**
+        * Construct a MimeMultipartEncrypted
+        *
+        * @param obj an Object of the right type
+        */
+       MimeMultipartEncrypted(const Object& obj): MimeMultipart(obj) {
+               if (!is_multipart_encrypted())
+                       throw std::runtime_error("not a mime-multipart-encrypted");
+       }
+
+       enum struct DecryptFlags {
+               None                          = GMIME_DECRYPT_NONE,
+               ExportSessionKey              = GMIME_DECRYPT_EXPORT_SESSION_KEY,
+               NoVerify                      = GMIME_DECRYPT_NO_VERIFY,
+               EnableKeyserverLookups        = GMIME_DECRYPT_ENABLE_KEYSERVER_LOOKUPS,
+               EnableOnlineCertificateChecks = GMIME_DECRYPT_ENABLE_ONLINE_CERTIFICATE_CHECKS
+       };
+
+       using Decrypted = std::pair<MimeObject, MimeDecryptResult>;
+       Result<Decrypted> decrypt(const MimeCryptoContext& ctx,
+                                 DecryptFlags flags=DecryptFlags::None,
+                                 const std::string& session_key = {}) const noexcept;
+
+private:
+       GMimeMultipartEncrypted* self() const {
+               return reinterpret_cast<GMimeMultipartEncrypted*>(object());
+       }
+};
+
+MU_ENABLE_BITOPS(MimeMultipartEncrypted::DecryptFlags);
+
+\f
+/**
+ * Thin wrapper around a GMimeMultiPartSigned
+ *
+ */
+class MimeMultipartSigned: public MimeMultipart {
+public:
+       /**
+        * Construct a MimeMultipartSigned
+        *
+        * @param obj an Object of the right type
+        */
+       MimeMultipartSigned(const Object& obj): MimeMultipart(obj) {
+               if (!is_multipart_signed())
+                       throw std::runtime_error("not a mime-multipart-signed");
+       }
+
+       enum struct VerifyFlags {
+               None                          = GMIME_VERIFY_NONE,
+               EnableKeyserverLookups        = GMIME_VERIFY_ENABLE_KEYSERVER_LOOKUPS,
+               EnableOnlineCertificateChecks = GMIME_VERIFY_ENABLE_ONLINE_CERTIFICATE_CHECKS
+       };
+
+       // Result<std::vector<MimeSignature>> verify(VerifyFlags vflags=VerifyFlags::None) const noexcept;
+
+       Result<std::vector<MimeSignature>> verify(const MimeCryptoContext& ctx,
+                                                 VerifyFlags vflags=VerifyFlags::None) const noexcept;
+
+private:
+       GMimeMultipartSigned* self() const {
+               return reinterpret_cast<GMimeMultipartSigned*>(object());
+       }
+};
+
+
+MU_ENABLE_BITOPS(MimeMultipartSigned::VerifyFlags);
+
+} // namespace Mu
+
+
+#endif /* MU_MIME_OBJECT_HH__ */
diff --git a/lib/message/mu-priority.cc b/lib/message/mu-priority.cc
new file mode 100644 (file)
index 0000000..9b57cea
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-priority.hh"
+
+using namespace Mu;
+
+std::string
+Mu::to_string(Priority prio)
+{
+       return std::string{priority_name(prio)};
+}
+
+/*
+ * tests... also build as runtime-tests, so we can get coverage info
+ */
+#ifdef BUILD_TESTS
+#include <glib.h>
+#define static_assert g_assert_true
+#endif /*BUILD_TESTS*/
+
+[[maybe_unused]] static void
+test_priority_to_char()
+{
+       static_assert(to_char(Priority::Low) == 'l');
+       static_assert(to_char(Priority::Normal) == 'n');
+       static_assert(to_char(Priority::High) == 'h');
+}
+
+[[maybe_unused]] static void
+test_priority_from_char()
+{
+       static_assert(priority_from_char('l') == Priority::Low);
+       static_assert(priority_from_char('n') == Priority::Normal);
+       static_assert(priority_from_char('h') == Priority::High);
+       static_assert(priority_from_char('x') == Priority::Normal);
+}
+
+[[maybe_unused]] static void
+test_priority_name()
+{
+       static_assert(priority_name(Priority::Low) == "low");
+       static_assert(priority_name(Priority::Normal) == "normal");
+       static_assert(priority_name(Priority::High) == "high");
+}
+
+
+#ifdef BUILD_TESTS
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/message/priority/to-char", test_priority_to_char);
+       g_test_add_func("/message/priority/from-char", test_priority_from_char);
+       g_test_add_func("/message/priority/name", test_priority_name);
+
+       return g_test_run();
+}
+#endif /*BUILD_TESTS*/
diff --git a/lib/message/mu-priority.hh b/lib/message/mu-priority.hh
new file mode 100644 (file)
index 0000000..af76ece
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_PRIORITY_HH__
+#define MU_PRIORITY_HH__
+
+#include <array>
+#include <string>
+#include <string_view>
+#include "mu-fields.hh"
+
+namespace Mu {
+/**
+ * Message priorities
+ *
+ */
+
+/**
+ * The priority ids
+ *
+ */
+enum struct Priority : char {
+       Low    = 'l', /**< Low priority */
+       Normal = 'n', /**< Normal priority */
+       High   = 'h', /**< High priority */
+};
+
+/**
+ * Sequence of all message priorities.
+ */
+static constexpr std::array<Priority, 3> AllMessagePriorities = {
+    Priority::Low, Priority::Normal, Priority::High};
+
+/**
+ * Get the char for some priority
+ *
+ * @param id an id
+ *
+ * @return the char
+ */
+constexpr char
+to_char(Priority prio)
+{
+       return static_cast<char>(prio);
+}
+
+/**
+ * Get the priority for some character; unknown onws
+ * become Normal.
+ *
+ * @param c some character
+ */
+constexpr Priority
+priority_from_char(char c)
+{
+       switch (c) {
+       case 'l':
+               return Priority::Low;
+       case 'h':
+               return Priority::High;
+       case 'n':
+       default:
+               return Priority::Normal;
+       }
+}
+
+/**
+ * Get the name for a given priority
+ *
+ * @return the name
+ */
+constexpr std::string_view
+priority_name(Priority prio)
+{
+       switch (prio) {
+       case Priority::Low:
+               return "low";
+       case Priority::High:
+               return "high";
+       case Priority::Normal:
+       default:
+               return "normal";
+       }
+}
+
+/**
+ * Get the name for a given priority (backward compatibility)
+ *
+ * @return the name
+ */
+constexpr const char*
+priority_name_c_str(Priority prio)
+{
+       switch (prio) {
+       case Priority::Low: return "low";
+       case Priority::High: return "high";
+       case Priority::Normal:
+       default: return "normal";
+       }
+}
+
+/**
+ * Get a the message priority as a string
+ *
+ * @param prio priority
+ *
+ * @return a string
+ */
+std::string to_string(Priority prio);
+
+} // namespace Mu
+
+#endif /*MU_PRIORITY_HH_*/
diff --git a/lib/message/test-mu-message.cc b/lib/message/test-mu-message.cc
new file mode 100644 (file)
index 0000000..a96b44b
--- /dev/null
@@ -0,0 +1,1068 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "utils/mu-test-utils.hh"
+#include "mu-message.hh"
+#include "mu-mime-object.hh"
+#include <glib.h>
+#include <regex>
+
+using namespace Mu;
+
+/*
+ * test message 1
+ */
+
+static void
+test_message_mailing_list()
+{
+       constexpr const char *test_message_1 =
+R"(Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+  by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F
+  for <xxxx@localhost>; Mon,  4 Aug 2008 21:49:34 +0300 (EEST)
+Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: anon@example.com
+To: sqlite-dev@sqlite.org
+Mime-Version: 1.0 (Apple Message framework v926)
+Date: Mon, 4 Aug 2008 11:40:49 +0200
+X-Mailer: Apple Mail (2.926)
+Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: sqlite-dev-bounces@sqlite.org
+Content-Length: 639
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch
+statement you can use something like:
+goto * instructions[pOp->opcode];
+)";
+       auto message{Message::make_from_text(
+                       test_message_1,
+                       "/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S")};
+       g_assert_true(!!message);
+       assert_equal(message->path(),
+                    "/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S");
+       g_assert_true(message->maildir().empty());
+
+       g_assert_true(message->bcc().empty());
+
+       g_assert_true(!message->body_html());
+       assert_equal(message->body_text().value_or(""),
+R"(Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch
+statement you can use something like:
+goto * instructions[pOp->opcode];
+)");
+       g_assert_true(message->cc().empty());
+       g_assert_cmpuint(message->date(), ==, 1217842849);
+       g_assert_true(message->flags() == (Flags::MailingList | Flags::Seen));
+
+       const auto from{message->from()};
+       g_assert_cmpuint(from.size(),==,1);
+       assert_equal(from.at(0).name, "");
+       assert_equal(from.at(0).email, "anon@example.com");
+
+       assert_equal(message->mailing_list(), "sqlite-dev.sqlite.org");
+       assert_equal(message->message_id(),
+                    "83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net");
+
+       g_assert_true(message->priority() == Priority::Low);
+       g_assert_cmpuint(message->size(),==,::strlen(test_message_1));
+
+       /* text-based message use time({}) as their changed-time */
+       g_assert_cmpuint(::time({}) - message->changed(), >=, 0);
+       g_assert_cmpuint(::time({}) - message->changed(), <=, 2);
+
+       g_assert_true(message->references().empty());
+
+       assert_equal(message->subject(),
+                    "[sqlite-dev] VM optimization inside sqlite3VdbeExec");
+
+       const auto to{message->to()};
+       g_assert_cmpuint(to.size(),==,1);
+       assert_equal(to.at(0).name, "");
+       assert_equal(to.at(0).email, "sqlite-dev@sqlite.org");
+
+       assert_equal(message->header("X-Mailer").value_or(""), "Apple Mail (2.926)");
+
+       auto all_contacts{message->all_contacts()};
+       g_assert_cmpuint(all_contacts.size(), ==, 4);
+       seq_sort(all_contacts, [](auto&& c1, auto&& c2){return c1.email < c2.email; });
+       assert_equal(all_contacts[0].email, "anon@example.com");
+       assert_equal(all_contacts[1].email, "sqlite-dev-bounces@sqlite.org");
+       assert_equal(all_contacts[2].email, "sqlite-dev@sqlite.org");
+       assert_equal(all_contacts[3].email, "sqlite-dev@sqlite.org");
+}
+
+
+static void
+test_message_attachments(void)
+{
+       constexpr const char* msg_text =
+R"(Return-Path: <foo@example.com>
+Received: from pop.gmail.com [256.85.129.309]
+       by evergrey with POP3 (fetchmail-6.4.29)
+       for <djcb@localhost> (single-drop); Thu, 24 Mar 2022 20:12:40 +0200 (EET)
+Sender: "Foo, Example" <foo@example.com>
+User-agent: mu4e 1.7.11; emacs 29.0.50
+From: "Foo Example" <foo@example.com>
+To: bar@example.com
+Subject: =?utf-8?B?w6R0dMOkY2htZcOxdHM=?=
+Date: Thu, 24 Mar 2022 20:04:39 +0200
+Organization: ACME Inc.
+Message-Id: <3144HPOJ0VC77.3H1XTAG2AMTLH@"@WILSONB.COM>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+
+Hello,
+--=-=-=
+Content-Type: image/jpeg
+Content-Disposition: attachment; filename=file-01.bin
+Content-Transfer-Encoding: base64
+
+AAECAw==
+--=-=-=
+Content-Type: audio/ogg
+Content-Disposition: inline; filename=/tmp/file-02.bin
+Content-Transfer-Encoding: base64
+
+BAUGBw==
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: attachment;
+ filename="message.eml"
+
+From: "Fnorb" <fnorb@example.com>
+To: Bob <bob@example.com>
+Subject: news for you
+Date: Mon, 28 Mar 2022 22:53:26 +0300
+
+Attached message!
+
+--=-=-=
+Content-Type: text/plain
+
+World!
+--=-=-=--
+)";
+
+       auto message{Message::make_from_text(msg_text)};
+       g_assert_true(!!message);
+       g_assert_true(message->has_mime_message());
+       g_assert_true(message->path().empty());
+
+       g_assert_true(message->bcc().empty());
+       g_assert_true(!message->body_html());
+       assert_equal(message->body_text().value_or(""), R"(Hello,World!)");
+
+       g_assert_true(message->cc().empty());
+       g_assert_cmpuint(message->date(), ==, 1648145079);
+       /* no Flags::Unread since it's a message without path */
+       g_assert_true(message->flags() == (Flags::HasAttachment));
+
+       const auto from{message->from()};
+       g_assert_cmpuint(from.size(),==,1);
+       assert_equal(from.at(0).name, "Foo Example");
+       assert_equal(from.at(0).email, "foo@example.com");
+
+       // problem case: https://github.com/djcb/mu/issues/2232o
+       assert_equal(message->message_id(),
+               "3144HPOJ0VC77.3H1XTAG2AMTLH@\"@WILSONB.COM");
+
+       g_assert_true(message->path().empty());
+       g_assert_true(message->priority() == Priority::Normal);
+       g_assert_cmpuint(message->size(),==,::strlen(msg_text));
+
+       /* text-based message use time({}) as their changed-time */
+       g_assert_cmpuint(::time({}) - message->changed(), >=, 0);
+       g_assert_cmpuint(::time({}) - message->changed(), <=, 2);
+
+       assert_equal(message->subject(), "ättächmeñts");
+
+       const auto cache_path{message->cache_path()};
+       g_assert_true(!!cache_path);
+
+       g_assert_cmpuint(message->parts().size(),==,5);
+       {
+               auto&& part{message->parts().at(0)};
+               g_assert_false(!!part.raw_filename());
+               assert_equal(part.mime_type().value(), "text/plain");
+               assert_equal(part.to_string().value(), "Hello,");
+       }
+       {
+               auto&& part{message->parts().at(1)};
+               assert_equal(part.raw_filename().value(), "file-01.bin");
+               assert_equal(part.mime_type().value(), "image/jpeg");
+               // file consists of 4 bytes 0...3
+               g_assert_cmpuint(part.to_string()->at(0), ==, 0);
+               g_assert_cmpuint(part.to_string()->at(1), ==, 1);
+               g_assert_cmpuint(part.to_string()->at(2), ==, 2);
+               g_assert_cmpuint(part.to_string()->at(3), ==, 3);
+       }
+       {
+               auto&& part{message->parts().at(2)};
+               assert_equal(part.raw_filename().value(), "/tmp/file-02.bin");
+               assert_equal(part.cooked_filename().value(), "tmp-file-02.bin");
+               assert_equal(part.mime_type().value(), "audio/ogg");
+               // file consistso of 4 bytes 4..7
+               assert_equal(part.to_string().value(), "\004\005\006\007");
+               const auto fpath{*cache_path + part.cooked_filename().value()};
+               const auto res = part.to_file(fpath, true);
+
+               g_assert_cmpuint(*res,==,4);
+               g_assert_cmpuint(::access(fpath.c_str(), R_OK), ==, 0);
+       }
+
+       {
+               auto&& part{message->parts().at(3)};
+               g_assert_true(part.mime_type() == "message/rfc822");
+       }
+
+       {
+               auto&& part{message->parts().at(4)};
+               g_assert_false(!!part.raw_filename());
+               g_assert_true(!!part.mime_type());
+               assert_equal(part.mime_type().value(), "text/plain");
+               assert_equal(part.to_string().value(), "World!");
+       }
+}
+
+
+/*
+ * some test keys.
+ */
+
+constexpr std::string_view  pub_key =
+R"(-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEYlbaNhYJKwYBBAHaRw8BAQdAEgxZnlN3mIwqV89zchjFlEby8OgrbrkT+yRN
+hQhc+A+0LU11IFRlc3QgKG11IHRlc3Rpbmcga2V5KSA8bXVAZGpjYnNvZnR3YXJl
+Lm5sPoiUBBMWCgA8FiEE/HZRT+2bPjARz29Cw7FsU49t3vAFAmJW2jYCGwMFCwkI
+BwIDIgIBBhUKCQgLAgQWAgMBAh4HAheAAAoJEMOxbFOPbd7wJ2kBAIGmUDWYEPtn
+qYTwhZIdZtTa4KJ3UdtTqey9AnxJ9mzAAQDRJOoVppj5wW2xRhgYP+ysN2iBUYGE
+MhahOcNgxodbCLg4BGJW2jYSCisGAQQBl1UBBQEBB0D4Sp+GTVre7Cx5a8D3SwLJ
+/bRAVGDwqI7PL9B/cMmCTwMBCAeIeAQYFgoAIBYhBPx2UU/tmz4wEc9vQsOxbFOP
+bd7wBQJiVto2AhsMAAoJEMOxbFOPbd7w1tYA+wdfYCcwOP0QoNZZz2Yk12YkDk2R
+FsRrZZpb0GKC/a2VAP4qFceeSegcUCBTQaoeFE9vq9XiUVOO98QI8r9C8QwvBw==
+=jM/g
+-----END PGP PUBLIC KEY BLOCK-----
+)";
+
+constexpr std::string_view priv_key = // "test1234"
+R"(-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+lIYEYlbaNhYJKwYBBAHaRw8BAQdAEgxZnlN3mIwqV89zchjFlEby8OgrbrkT+yRN
+hQhc+A/+BwMCz6T2uBpk6a7/rXyE7C1bRbGjP6YSFcyRFz8VRV3Xlm7z6rdbdKZr
+8R15AtLvXA4DOK5GiZRB2VbIxi8B9CtZ9qQx6YbQPkAmRzISGAjECrQtTXUgVGVz
+dCAobXUgdGVzdGluZyBrZXkpIDxtdUBkamNic29mdHdhcmUubmw+iJQEExYKADwW
+IQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbaNgIbAwULCQgHAgMiAgEGFQoJCAsC
+BBYCAwECHgcCF4AACgkQw7FsU49t3vAnaQEAgaZQNZgQ+2ephPCFkh1m1NrgondR
+21Op7L0CfEn2bMABANEk6hWmmPnBbbFGGBg/7Kw3aIFRgYQyFqE5w2DGh1sInIsE
+YlbaNhIKKwYBBAGXVQEFAQEHQPhKn4ZNWt7sLHlrwPdLAsn9tEBUYPCojs8v0H9w
+yYJPAwEIB/4HAwI9MZDWcsoiJ/9oV5DRiAedeo3Ta/1M+aKfeNV36Ch1VGLwQF3E
+V77qIrJlsT8CwOZHWUksUBENvG3ak3vd84awHHaHoTmoFwtISfvQrFK0iHgEGBYK
+ACAWIQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbaNgIbDAAKCRDDsWxTj23e8NbW
+APsHX2AnMDj9EKDWWc9mJNdmJA5NkRbEa2WaW9Bigv2tlQD+KhXHnknoHFAgU0Gq
+HhRPb6vV4lFTjvfECPK/QvEMLwc=
+=w1Nc
+-----END PGP PRIVATE KEY BLOCK-----
+)";
+
+
+static void
+test_message_signed(void)
+{
+       constexpr const char *msgtext =
+R"(Return-Path: <diggler@gmail.com>
+From: Mu Test <mu@djcbsoftware.nl>
+To: Mu Test <mu@djcbsoftware.nl>
+Subject: boo
+Date: Wed, 13 Apr 2022 17:19:08 +0300
+Message-ID: <878rs9ysin.fsf@djcbsoftware.nl>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-=";
+       micalg=pgp-sha512; protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+
+Sapperdeflap
+
+--=-=-=
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iIkEARYKADEWIQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbcLhMcbXVAZGpjYnNv
+ZnR3YXJlLm5sAAoJEMOxbFOPbd7waIkA/jK1oY7OL8vrDoubNYxamy8HHmwtvO01
+Q46aYjxe0As6AP90bcAZ3dcn5RcTJaM0UhZssguawZ+tnriD3+5DPkMMCg==
+=e32+
+-----END PGP SIGNATURE-----
+--=-=-=--
+)";
+       TempDir tempdir;
+       auto ctx{MimeCryptoContext::make_gpg(tempdir.path())};
+       g_assert_true(!!ctx);
+
+       auto stream{MimeStream::make_mem()};
+       stream.write(pub_key.data(), pub_key.size());
+       stream.reset();
+
+       auto imported = ctx->import_keys(stream);
+       g_assert_cmpuint(*imported, ==, 1);
+
+       auto message{Message::make_from_text(
+                       msgtext,
+                       "/home/test/Maildir/inbox/cur/1649279777.107710_1.mindcrime:2,RS")};
+       g_assert_true(!!message);
+
+       g_assert_true(message->bcc().empty());
+       assert_equal(message->body_text().value_or(""), "Sapperdeflap\n");
+       g_assert_true(message->flags() == (Flags::Signed|Flags::Seen|Flags::Replied));
+
+       size_t n{};
+       for (auto&& part: message->parts()) {
+               if (!part.is_signed())
+                       continue;
+
+               const auto& mobj{part.mime_object()};
+               if (!mobj.is_multipart_signed())
+                       continue;
+
+               const auto mpart{MimeMultipartSigned(mobj)};
+               const auto sigs{mpart.verify(*ctx)};
+               if (!sigs)
+                       g_warning("%s", sigs.error().what());
+
+               g_assert_true(!!sigs);
+               g_assert_cmpuint(sigs->size(), ==, 1);
+               ++n;
+       }
+
+       g_assert_cmpuint(n, ==, 1);
+}
+
+
+static void
+test_message_signed_encrypted(void)
+{
+       constexpr const char *msgtext =
+R"(From: "Mu Test" <mu@djcbsoftware.nl>
+To: mu@djcbsoftware.nl
+Subject: encrypted and signed
+Date: Wed, 13 Apr 2022 17:32:30 +0300
+Message-ID: <87lew9xddt.fsf@djcbsoftware.nl>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DeEerj6WhdZASAQdAKdZwmugAlQA8c06Q5iQw4rwSADgfEWBTWlI6tDw7hEAw
+0qSSeeQbA802qjG5TesaDVbFoPp1gOESt67HkJBABj9niwZLnjbzVRXKFoPTYabu
+1MBWAQkCEO6kS0N73XQeJ9+nDkUacRX6sSgVM0j+nRdCGcrCQ8MOfLd9KUUBxpXy
+r/rIBMpZGOIpKJnoZ2x75VsQIp/ADHLe9zzXVe0tkahXJqvLo26w3gn4NSEIEDp6
+4T/zMZImqGrENaixNmRiRSAnwPkLt95qJGOIqYhuW3X6hMRZyU4zDNwkAvnK+2Fv
+Wjd+EmiFzh5tvCmPOSj556YFMV7UpFWO9VznXX/T5+f4i+95Lsm9Uotv/SiNtNQG
+DPU3wiL347SzmPFXckknjlzSzDL1XbdbHdmoJs0uNnbaZxRwhkuTYbLHdpBZrBgR
+C0bdoCx44QVU8HaZ2x91h3GoM/0q5bqM/rvCauwbokiJgAUrznecNPY=
+=Ado7
+-----END PGP MESSAGE-----
+--=-=-=--
+)";
+       TempDir tempdir;
+       auto ctx{MimeCryptoContext::make_gpg(tempdir.path())};
+       g_assert_true(!!ctx);
+
+       /// test1234
+       // ctx->set_request_password([](const MimeCryptoContext& ctx,
+       //                           const std::string& user_id,
+       //                           const std::string& prompt,
+       //                           bool reprompt,
+       //                           MimeStream& response)->Result<void> {
+       //                                return Err(Error::Code::Internal, "boo");
+       //                                //return Ok();
+       //                        });
+
+       {
+               auto stream{MimeStream::make_mem()};
+               stream.write(priv_key.data(), priv_key.size());
+               stream.write(pub_key.data(), pub_key.size());
+               stream.reset();
+
+
+               g_assert_cmpint(ctx->import_keys(stream).value_or(-1),==,1);
+       }
+
+       auto message{Message::make_from_text(
+                       msgtext,
+                       "/home/test/Maildir/inbox/cur/1649279888.107710_1.mindcrime:2,FS")};
+       g_assert_true(!!message);
+       g_assert_true(message->flags() == (Flags::Encrypted|Flags::Seen|Flags::Flagged));
+
+       size_t n{};
+       for (auto&& part: message->parts()) {
+
+               if (!part.is_encrypted())
+                       continue;
+
+               g_assert_false(!!part.content_description());
+               g_assert_false(part.is_attachment());
+               g_assert_cmpuint(part.size(),==,0);
+
+               const auto& mobj{part.mime_object()};
+               if (!mobj.is_multipart_encrypted())
+                       continue;
+
+               /* FIXME: make this work without user having to
+                * type password */
+
+               // const auto mpart{MimeMultipartEncrypted(mobj)};
+               // const auto decres = mpart.decrypt(*ctx);
+               // assert_valid_result(decres);
+
+               ++n;
+       }
+
+       g_assert_cmpuint(n, ==, 1);
+}
+
+
+static void
+test_message_multipart_mixed_rfc822(void)
+{
+       constexpr const char *msgtext =
+R"(Content-Type: multipart/mixed;
+       boundary="Multipart_Tue_Sep__2_15:42:35_2014-1"
+
+--Multipart_Tue_Sep__2_15:42:35_2014-1
+Content-Type: message/rfc822
+)";
+       auto message{Message::make_from_text(msgtext)};
+       g_assert_true(!!message);
+
+       g_assert_true(message->cached_sexp().empty());
+}
+
+
+static void
+test_message_detect_attachment(void)
+{
+       constexpr const char *msgtext =
+R"(From: "DUCK, Donald" <donald@example.com>
+Date: Tue, 3 May 2022 10:26:26 +0300
+Message-ID: <SADKLAJCLKDJLAS-xheQjE__+hS-3tff=pTYpMUyGiJwNGF_DA@mail.gmail.com>
+Subject: =?Windows-1252?Q?Purkuty=F6urakka?=
+To: Hello <moika@example.com>
+Content-Type: multipart/mixed; boundary="000000000000e687ed05de166d71"
+
+--000000000000e687ed05de166d71
+Content-Type: multipart/alternative; boundary="000000000000e687eb05de166d6f"
+
+--000000000000e687eb05de166d6f
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+fyi
+
+---------- Forwarded message ---------
+From: Fooish Bar <foobar@example.com>
+Date: Tue, 3 May 2022 at 08:59
+Subject: Ty=C3=B6t
+To: "DUCK, Donald" <donald@example.com>
+
+Moi,
+
+--
+
+--000000000000e687eb05de166d6f
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+abc
+
+--000000000000e687eb05de166d6f--
+--000000000000e687ed05de166d71
+Content-Type: application/pdf;
+       name="test1.pdf"
+Content-Disposition: attachment;
+       filename="test2.pdf"
+Content-Transfer-Encoding: base64
+Content-ID: <18088cfd4bc5517c6321>
+X-Attachment-Id: 18088cfd4bc5517c6321
+
+JVBERi0xLjcKJeLjz9MKNyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDEgMCBSIC9MYXN0
+TW9kaWZpZWQgKEQ6MjAyMjA1MDMwODU3MzYrMDMnMDAnKSAvUmVzb3VyY2VzIDIgMCBSIC9NZWRp
+cmVmCjM1NjE4CiUlRU9GCg==
+--000000000000e687ed05de166d71--
+)";
+       auto message{Message::make_from_text(msgtext)};
+       g_assert_true(!!message);
+
+       g_assert_true(message->path().empty());
+
+       g_assert_true(message->bcc().empty());
+       assert_equal(message->subject(), "Purkutyöurakka");
+       assert_equal(message->body_html().value_or(""), "abc\n");
+       assert_equal(message->body_text().value_or(""),
+                    R"(fyi
+
+---------- Forwarded message ---------
+From: Fooish Bar <foobar@example.com>
+Date: Tue, 3 May 2022 at 08:59
+Subject: Työt
+To: "DUCK, Donald" <donald@example.com>
+
+Moi,
+
+--
+)");
+       g_assert_true(message->cc().empty());
+       g_assert_cmpuint(message->date(), ==, 1651562786);
+       g_assert_true(message->flags() == (Flags::HasAttachment));
+
+       g_assert_cmpuint(message->parts().size(), ==, 3);
+
+       for (auto&& part: message->parts())
+               g_info("%s %s",
+                      part.is_attachment() ? "yes" : "no",
+                      part.mime_type().value_or("boo").c_str());
+}
+
+
+static void
+test_message_calendar(void)
+{
+       constexpr const char *msgtext =
+R"(MIME-Version: 1.0
+From: William <william@example.com>
+To: Billy <billy@example.com>
+Date: Thu, 9 Jan 2014 11:09:34 +0100
+Subject: Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30
+ (william@example.com)
+Thread-Topic: Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30
+ (william@example.com)
+Thread-Index: Ac8NIuske7OtG01VRpukb/bHE7SVHg==
+Message-ID: <001a11c3440066ee0b04ef86cea8@google.com>
+Accept-Language: en-US
+Content-Language: en-US
+X-MS-Exchange-Organization-AuthAs: Anonymous
+X-MS-Has-Attach: yes
+Content-Type: multipart/mixed;
+       boundary="_004_001a11c3440066ee0b04ef86cea8googlecom_"
+
+--_004_001a11c3440066ee0b04ef86cea8googlecom_
+Content-Type: multipart/alternative;
+       boundary="_002_001a11c3440066ee0b04ef86cea8googlecom_"
+
+--_002_001a11c3440066ee0b04ef86cea8googlecom_
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+
+PGh0bWw+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIgY29udGVudD0i
+dGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjxtZXRhIG5hbWU9IkdlbmVyYXRvciIgY29udGVu
+dD0iTWljcm9zb2Z0IEV4Y2hhbmdlIFNlcnZlciI+DQo8IS0tIGNvbnZlcnRlZCBmcm9tIHJ0ZiAt
+LT4NCjxzdHlsZT48IS0tIC5FbWFpbFF1b3RlIHsgbWFyZ2luLWxlZnQ6IDFwdDsgcGFkZGluZy1s
+ZWZ0OiA0cHQ7IGJvcmRlci1sZWZ0OiAjODAwMDAwIDJweCBzb2xpZDsgfSAtLT48L3N0eWxlPg0K
+PC9oZWFkPg0KPGJvZHk+DQo8Zm9udCBmYWNlPSJUaW1lcyBOZXcgUm9tYW4iIHNpemU9IjMiPjxh
+IG5hbWU9IkJNX0JFR0lOIj48L2E+DQo8dGFibGUgYm9yZGVyPSIxIiB3aWR0aD0iNzM0IiBzdHls
+ZT0iYm9yZGVyOjEgc29saWQ7IGJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTsgbWFyZ2luLWxlZnQ6
+IDJwdDsgIj4NCjx0cj4NCjx0ZD48Zm9udCBzaXplPSIxIj48YSBocmVmPSJodHRwczovL3d3dy5n
+b29nbGUuY29tL2NhbGVuZGFyL2V2ZW50P2FjdGlvbj1WSUVXJmFtcDtlaWQ9YzNOemNXUXhjRGxs
+Ym1VeU0ySnZNbWsyYjNOeU56ZG5jRzhnWkdwallrQmthbU5pYzI5bWRIZGhjbVV1Ym13JmFtcDt0
+b2s9TWpZamQybHNiR2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016
+QTJaRFV3TldVMlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nh
+b19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT5tb3JlDQpkZXRh
+aWxzIMK7PC91PjwvZm9udD48L2E+PGJyPg0KDQo8ZGl2IHN0eWxlPSJtYXJnaW4tYm90dG9tOiAx
+NHB0OyAiPjxmb250IGZhY2U9IkFyaWFsLCBzYW5zLXNlcmlmIiBzaXplPSIyIiBjb2xvcj0iIzIy
+MjIyMiI+PGI+SEVMTE8sPC9iPjwvZm9udD48L2Rpdj4NCjxkaXY+PGZvbnQgc2l6ZT0iMSIgY29s
+b3I9IiMyMjIyMjIiPjxicj4NCg0KSSBBTSBERVNNT05EIFdJTExJQU1TIEFORCBNWSBMSVRUTEUg
+U0lTVEVSIElTIEdMT1JJQSwgT1VSIEZBVEhFUiBPV05TIEEgTElNSVRFRCBPRiBDT0NPQSBBTkQg
+R09MRCBCVVNJTkVTUyBJTiBSRVBVQkxJUVVFIERVIENPTkdPLiBBRlRFUiBISVMgVFJJUCBUTyBD
+T1RFIERJVk9JUkUgVE8gTkVHT1RJQVRFIE9OIENPQ09BIEFORCBHT0xEIEJVU0lORVNTIEhFIFdB
+TlRFRCBUTyBJTlZFU1QgSU4gQUJST0FELiA8L2ZvbnQ+PC9kaXY+DQo8ZGl2IHN0eWxlPSJtYXJn
+aW4tdG9wOiAxNHB0OyBtYXJnaW4tYm90dG9tOiAxNHB0OyAiPjxmb250IHNpemU9IjMiPk9ORSBX
+RUVLIEhFIENBTUUgQkFDSyBGUk9NIEhJUyBUUklQIFRPIEFCSURKQU4gSEUgSEFEIEEgTU9UT1Ig
+QUNDSURFTlQgV0lUSCBPVVIgTU9USEVSIFdISUNIIE9VUiBNT1RIRVIgRElFRCBJTlNUQU5UTFkg
+QlVUIE9VUiBGQVRIRVIgRElFRCBBRlRFUiBGSVZFIERBWVMgSU4gQSBQUklWQVRFIEhPU1BJVEFM
+IElOIE9VUiBDT1VOVFJZLg0KSVQgV0FTIExJS0UgT1VSIEZBVEhFUiBLTkVXIEhFIFdBUyBHT0lO
+RyBUTyBESUUgTUFZIEhJUyBHRU5UTEUgU09VTCBSRVNUIElOIFBSRUZFQ1QgUEVBQ0UuIDwvZm9u
+dD48L2Rpdj4NCjxkaXYgc3R5bGU9Im1hcmdpbi10b3A6IDE0cHQ7IG1hcmdpbi1ib3R0b206IDE0
+cHQ7ICI+PGZvbnQgc2l6ZT0iMyI+SEUgRElTQ0xPU0VEIFRPIE1FIEFTIFRIRSBPTkxZIFNPTiBU
+SEFUIEhFIERFUE9TSVRFRCBUSEUgU1VNIE9GIChVU0QgJCAxMCw1MDAsMDAwKSBJTlRPIEEgQkFO
+SyBJTiBBQklESkFOIFRIQVQgVEhFIE1PTkVZIFdBUyBNRUFOVCBGT1IgSElTIENPQ09BIEFORCBH
+T0xEIEJVU0lORVNTIEhFIFdBTlRFRCBUTyBFU1RBQkxJU0ggSU4NCkFCUk9BRC5XRSBBUkUgU09M
+SUNJVElORyBGT1IgWU9VUiBIRUxQIFRPIFRSQU5TRkVSIFRISVMgTU9ORVkgSU5UTyBZT1VSIEFD
+Q09VTlQgSU4gWU9VUiBDT1VOVFJZIEZPUiBPVVIgSU5WRVNUTUVOVC4gPC9mb250PjwvZGl2Pg0K
+PGRpdiBzdHlsZT0ibWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9u
+dCBzaXplPSIzIj5QTEVBU0UgRk9SIFNFQ1VSSVRZIFJFQVNPTlMsSSBBRFZJQ0UgWU9VIFJFUExZ
+IFVTIFRIUk9VR0ggT1VSIFBSSVZBVEUgRU1BSUw6IDxhIGhyZWY9Im1haWx0bzp3aWxsaWFtc2Rl
+c21vbmQxMDdAeWFob28uY29tLnZuIj48Zm9udCBjb2xvcj0iIzAwMDBGRiI+PHU+d2lsbGlhbXNk
+ZXNtb25kMTA3QHlhaG9vLmNvbS52bjwvdT48L2ZvbnQ+PC9hPg0KRk9SIE1PUkUgREVUQUlMUy4g
+PC9mb250PjwvZGl2Pg0KPGRpdiBzdHlsZT0ibWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRv
+bTogMTRwdDsgIj48Zm9udCBzaXplPSIzIj5SRUdBUkRTLiA8L2ZvbnQ+PC9kaXY+DQo8ZGl2IHN0
+eWxlPSJtYXJnaW4tdG9wOiAxNHB0OyBtYXJnaW4tYm90dG9tOiAxNHB0OyAiPjxmb250IHNpemU9
+IjMiPkRFU01PTkQgL0dMT1JJQSBXSUxMSUFNUy48L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp
+emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp
+emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp
+emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp
+emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8dGFibGUgYm9yZGVy
+PSIxIiB3aWR0aD0iNzM0IiBzdHlsZT0iYm9yZGVyOjEgc29saWQ7IGJvcmRlci1jb2xsYXBzZTpj
+b2xsYXBzZTsgbWFyZ2luLWxlZnQ6IDJwdDsgIj4NCjxjb2wgd2lkdGg9IjM2NSI+DQo8Y29sIHdp
+ZHRoPSIzNjkiPg0KPHRyPg0KPHRkPjxmb250IHNpemU9IjMiPjxpPldoZW48L2k+PC9mb250Pjwv
+dGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJpYWwsIHNhbnMtc2VyaWYiIHNpemU9IjEiIGNvbG9yPSIj
+MjIyMjIyIj5UaHUgOSBKYW4gMjAxNCAwODozMCDigJMgMDk6MzAgPGZvbnQgY29sb3I9IiM4ODg4
+ODgiPlNhbyBQYXVsbzwvZm9udD48L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8dHI+DQo8dGQ+PGZvbnQg
+c2l6ZT0iMyI+PGk+Q2FsZW5kYXI8L2k+PC9mb250PjwvdGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJp
+YWwsIHNhbnMtc2VyaWYiIHNpemU9IjEiIGNvbG9yPSIjMjIyMjIyIj53aWxsaWFtc19kMjBAZ2xv
+Ym9tYWlsLmNvbTwvZm9udD48L3RkPg0KPC90cj4NCjx0cj4NCjx0ZD48Zm9udCBzaXplPSIzIj48
+aT5XaG88L2k+PC9mb250PjwvdGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJpYWwsIHNhbnMtc2VyaWYi
+IHNpemU9IjEiIGNvbG9yPSIjMjIyMjIyIj4oR3Vlc3QgbGlzdCBoYXMgYmVlbiBoaWRkZW4gYXQg
+b3JnYW5pc2VyJ3MgcmVxdWVzdCk8L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8L3RhYmxlPg0KPGRpdiBz
+dHlsZT0ibWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9udCBzaXplPSIxIiBjb2xvcj0iIzg4ODg4
+OCI+R29pbmc/Jm5ic3A7Jm5ic3A7IDxhIGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vY2Fs
+ZW5kYXIvZXZlbnQ/YWN0aW9uPVJFU1BPTkQmYW1wO2VpZD1jM056Y1dReGNEbGxibVV5TTJKdk1t
+azJiM055TnpkbmNHOGdaR3BqWWtCa2FtTmljMjltZEhkaGNtVXVibXcmYW1wO3JzdD0xJmFtcDt0
+b2s9TWpZamQybHNiR2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016
+QTJaRFV3TldVMlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nh
+b19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT48Yj5ZZXM8L2I+
+PC91PjwvZm9udD48L2E+PGZvbnQgY29sb3I9IiMyMjIyMjIiPjxiPg0KLSA8L2I+PC9mb250Pjxh
+IGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVJFU1BP
+TkQmYW1wO2VpZD1jM056Y1dReGNEbGxibVV5TTJKdk1tazJiM055TnpkbmNHOGdaR3BqWWtCa2Ft
+TmljMjltZEhkaGNtVXVibXcmYW1wO3JzdD0zJmFtcDt0b2s9TWpZamQybHNiR2xoYlhOZlpESXdR
+R2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016QTJaRFV3TldVMlltWXhOamRqTm1ZMVlU
+VXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nhb19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxm
+b250IGNvbG9yPSIjMjIwMENDIj48dT48Yj5NYXliZTwvYj48L3U+PC9mb250PjwvYT48Zm9udCBj
+b2xvcj0iIzIyMjIyMiI+PGI+DQotIDwvYj48L2ZvbnQ+PGEgaHJlZj0iaHR0cHM6Ly93d3cuZ29v
+Z2xlLmNvbS9jYWxlbmRhci9ldmVudD9hY3Rpb249UkVTUE9ORCZhbXA7ZWlkPWMzTnpjV1F4Y0Rs
+bGJtVXlNMkp2TW1rMmIzTnlOemRuY0c4Z1pHcGpZa0JrYW1OaWMyOW1kSGRoY21VdWJtdyZhbXA7
+cnN0PTImYW1wO3Rvaz1NallqZDJsc2JHbGhiWE5mWkRJd1FHZHNiMkp2YldGcGJDNWpiMjFqTXpj
+MllUaGtZbUZrTXpBMlpEVXdOV1UyWW1ZeE5qZGpObVkxWVRVeE5tSmpNakU1TjJZMyZhbXA7Y3R6
+PUFtZXJpY2EvU2FvX1BhdWxvJmFtcDtobD1lbl9HQiI+PGZvbnQgY29sb3I9IiMyMjAwQ0MiPjx1
+PjxiPk5vPC9iPjwvdT48L2ZvbnQ+PC9hPjxmb250IGNvbG9yPSIjMjIyMjIyIj4mbmJzcDsmbmJz
+cDsmbmJzcDsNCjwvZm9udD48YSBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFy
+L2V2ZW50P2FjdGlvbj1WSUVXJmFtcDtlaWQ9YzNOemNXUXhjRGxsYm1VeU0ySnZNbWsyYjNOeU56
+ZG5jRzhnWkdwallrQmthbU5pYzI5bWRIZGhjbVV1Ym13JmFtcDt0b2s9TWpZamQybHNiR2xoYlhO
+ZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016QTJaRFV3TldVMlltWXhOamRq
+Tm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nhb19QYXVsbyZhbXA7aGw9ZW5f
+R0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT5tb3JlDQpvcHRpb25zIMK7PC91PjwvZm9udD48
+L2E+PC9mb250PjwvZGl2Pg0KPC9mb250PjwvdGQ+DQo8L3RyPg0KPHRyPg0KPHRkIHN0eWxlPSJi
+YWNrZ3JvdW5kLWNvbG9yOiAjRjZGNkY2OyAiPjxmb250IHNpemU9IjMiPkludml0YXRpb24gZnJv
+bSA8YSBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFyLyI+PGZvbnQgY29sb3I9
+IiMwMDAwRkYiPjx1Pkdvb2dsZSBDYWxlbmRhcjwvdT48L2ZvbnQ+PC9hPg0KPGRpdiBzdHlsZT0i
+bWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9udCBzaXplPSIzIj5Z
+b3UgYXJlIHJlY2VpdmluZyB0aGlzIGNvdXJ0ZXN5IGVtYWlsIGF0IHRoZSBhY2NvdW50IGRqY2JA
+ZGpjYnNvZnR3YXJlLm5sIGJlY2F1c2UgeW91IGFyZSBhbiBhdHRlbmRlZSBvZiB0aGlzIGV2ZW50
+LjwvZm9udD48L2Rpdj4NCjxkaXYgc3R5bGU9Im1hcmdpbi10b3A6IDE0cHQ7IG1hcmdpbi1ib3R0
+b206IDE0cHQ7ICI+PGZvbnQgc2l6ZT0iMyI+VG8gc3RvcCByZWNlaXZpbmcgZnV0dXJlIG5vdGlm
+aWNhdGlvbnMgZm9yIHRoaXMgZXZlbnQsIGRlY2xpbmUgdGhpcyBldmVudC4gQWx0ZXJuYXRpdmVs
+eSwgeW91IGNhbiBzaWduIHVwIGZvciBhIEdvb2dsZSBhY2NvdW50IGF0DQo8YSBocmVmPSJodHRw
+czovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFyLyI+aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9jYWxl
+bmRhci88L2E+IGFuZCBjb250cm9sIHlvdXIgbm90aWZpY2F0aW9uIHNldHRpbmdzIGZvciB5b3Vy
+IGVudGlyZSBjYWxlbmRhci48L2ZvbnQ+PC9kaXY+DQo8L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8L3Rh
+YmxlPg0KPC9mb250Pg0KPC9ib2R5Pg0KPC9odG1sPg0K
+
+--_002_001a11c3440066ee0b04ef86cea8googlecom_
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20140109T103000Z
+DTEND:20140109T113000Z
+DTSTAMP:20140109T100934Z
+ORGANIZER;CN=William:mailto:william@example.com
+UID:sssqd1p9ene23bo2i6osr77gpo@google.com
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
+ TRUE;CN=billy@example.com;X-NUM-GUESTS=0:mailto:billy@example.com
+CREATED:20140109T100932Z
+DESCRIPTION:\nI AM DESMOND WILLIAMS AND MY LITTLE SISTER IS GLORIA\, OUR FA
+ THER OWNS A LIMITED OF COCOA AND GOLD BUSINESS IN REPUBLIQUE DU CONGO. AFTE
+ R HIS TRIP TO COTE DIVOIRE TO NEGOTIATE ON COCOA AND GOLD BUSINESS HE WANTE
+ D TO INVEST IN ABROAD. \n\nONE WEEK HE CAME BACK FROM HIS TRIP TO ABIDJAN H
+ E HAD A MOTOR ACCIDENT WITH OUR MOTHER WHICH OUR MOTHER DIED INSTANTLY BUT
+ OUR FATHER DIED AFTER FIVE DAYS IN A PRIVATE HOSPITAL IN OUR COUNTRY. IT WA
+ S LIKE OUR FATHER KNEW HE WAS GOING TO DIE MAY HIS GENTLE SOUL REST IN PREF
+ ECT PEACE. \n\nHE DISCLOSED TO ME AS THE ONLY SON THAT HE DEPOSITED THE SUM
+  OF (USD $ 10\,500\,000) INTO A BANK IN ABIDJAN THAT THE MONEY WAS MEANT FO
+ R HIS COCOA AND GOLD BUSINESS HE WANTED TO ESTABLISH IN ABROAD.WE ARE SOLIC
+ ITING FOR YOUR HELP TO TRANSFER THIS MONEY INTO YOUR ACCOUNT IN YOUR COUNTR
+ Y FOR OUR INVESTMENT. \n\nPLEASE FOR SECURITY REASONS\,I ADVICE YOU REPLY U
+ S THROUGH OUR PRIVATE EMAIL FOR MORE DETAI
+ LS. \n\nREGARDS. \n\nDESMOND /GLORIA WILLIAMS.\nView your event at http://w
+ ww.google.com/calendar/event?action=VIEW&eid=c3NzcWQxcDllbmUyM2JvMmk2b3NyNz
+ dncG8gZGpjYkBkamNic29mdHdhcmUubmw&tok=MjYjd2lsbGlhbXNfZDIwQGdsb2JvbWFpbC5jb
+ 21jMzc2YThkYmFkMzA2ZDUwNWU2YmYxNjdjNmY1YTUxNmJjMjE5N2Y3&ctz=America/Sao_Pau
+ lo&hl=en_GB.
+LAST-MODIFIED:20140109T100932Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:HELLO\,
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--_002_001a11c3440066ee0b04ef86cea8googlecom_--
+
+--_004_001a11c3440066ee0b04ef86cea8googlecom_
+Content-Type: application/ics; name="invite.ics"
+Content-Description: invite.ics
+Content-Disposition: attachment; filename="invite.ics"; size=2029;
+       creation-date="Thu, 09 Jan 2014 10:09:44 GMT";
+       modification-date="Thu, 09 Jan 2014 10:09:44 GMT"
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw
+LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT
+VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMTA5VDEwMzAwMFoNCkRURU5EOjIwMTQwMTA5
+VDExMzAwMFoNCkRUU1RBTVA6MjAxNDAxMDlUMTAwOTM0Wg0KT1JHQU5JWkVSO0NOPVdpbGxpYW1z
+IFdpbGxpYW1zOm1haWx0bzp3aWxsaWFtc19kMjBAZ2xvYm9tYWlsLmNvbQ0KVUlEOnNzc3FkMXA5
+ZW5lMjNibzJpNm9zcjc3Z3BvQGdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM
+O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7
+Q049ZGpjYkBkamNic29mdHdhcmUubmw7WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmRqY2JAZGpjYnNv
+ZnR3YXJlLm5sDQpDUkVBVEVEOjIwMTQwMTA5VDEwMDkzMloNCkRFU0NSSVBUSU9OOlxuSSBBTSBE
+RVNNT05EIFdJTExJQU1TIEFORCBNWSBMSVRUTEUgU0lTVEVSIElTIEdMT1JJQVwsIE9VUiBGQQ0K
+IFRIRVIgT1dOUyBBIExJTUlURUQgT0YgQ09DT0EgQU5EIEdPTEQgQlVTSU5FU1MgSU4gUkVQVUJM
+SVFVRSBEVSBDT05HTy4gQUZURQ0KIFIgSElTIFRSSVAgVE8gQ09URSBESVZPSVJFIFRPIE5FR09U
+SUFURSBPTiBDT0NPQSBBTkQgR09MRCBCVVNJTkVTUyBIRSBXQU5URQ0KIEQgVE8gSU5WRVNUIElO
+IEFCUk9BRC4gXG5cbk9ORSBXRUVLIEhFIENBTUUgQkFDSyBGUk9NIEhJUyBUUklQIFRPIEFCSURK
+QU4gSA0KIEUgSEFEIEEgTU9UT1IgQUNDSURFTlQgV0lUSCBPVVIgTU9USEVSIFdISUNIIE9VUiBN
+T1RIRVIgRElFRCBJTlNUQU5UTFkgQlVUIA0KIE9VUiBGQVRIRVIgRElFRCBBRlRFUiBGSVZFIERB
+WVMgSU4gQSBQUklWQVRFIEhPU1BJVEFMIElOIE9VUiBDT1VOVFJZLiBJVCBXQQ0KIFMgTElLRSBP
+VVIgRkFUSEVSIEtORVcgSEUgV0FTIEdPSU5HIFRPIERJRSBNQVkgSElTIEdFTlRMRSBTT1VMIFJF
+U1QgSU4gUFJFRg0KIEVDVCBQRUFDRS4gXG5cbkhFIERJU0NMT1NFRCBUTyBNRSBBUyBUSEUgT05M
+WSBTT04gVEhBVCBIRSBERVBPU0lURUQgVEhFIFNVTQ0KICBPRiAoVVNEICQgMTBcLDUwMFwsMDAw
+KSBJTlRPIEEgQkFOSyBJTiBBQklESkFOIFRIQVQgVEhFIE1PTkVZIFdBUyBNRUFOVCBGTw0KIFIg
+SElTIENPQ09BIEFORCBHT0xEIEJVU0lORVNTIEhFIFdBTlRFRCBUTyBFU1RBQkxJU0ggSU4gQUJS
+T0FELldFIEFSRSBTT0xJQw0KIElUSU5HIEZPUiBZT1VSIEhFTFAgVE8gVFJBTlNGRVIgVEhJUyBN
+T05FWSBJTlRPIFlPVVIgQUNDT1VOVCBJTiBZT1VSIENPVU5UUg0KIFkgRk9SIE9VUiBJTlZFU1RN
+RU5ULiBcblxuUExFQVNFIEZPUiBTRUNVUklUWSBSRUFTT05TXCxJIEFEVklDRSBZT1UgUkVQTFkg
+VQ0KIFMgVEhST1VHSCBPVVIgUFJJVkFURSBFTUFJTDogd2lsbGlhbXNkZXNtb25kMTA3QHlhaG9v
+LmNvbS52biBGT1IgTU9SRSBERVRBSQ0KIExTLiBcblxuUkVHQVJEUy4gXG5cbkRFU01PTkQgL0dM
+T1JJQSBXSUxMSUFNUy5cblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vdw0KIHd3Lmdvb2dsZS5j
+b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPWMzTnpjV1F4Y0RsbGJtVXlNMkp2TW1r
+MmIzTnlOeg0KIGRuY0c4Z1pHcGpZa0JrYW1OaWMyOW1kSGRoY21VdWJtdyZ0b2s9TWpZamQybHNi
+R2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYg0KIDIxak16YzJZVGhrWW1Ga016QTJaRFV3TldV
+MlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmY3R6PUFtZXJpY2EvU2FvX1BhdQ0KIGxvJmhs
+PWVuX0dCLg0KTEFTVC1NT0RJRklFRDoyMDE0MDEwOVQxMDA5MzJaDQpMT0NBVElPTjoNClNFUVVF
+TkNFOjANClNUQVRVUzpDT05GSVJNRUQNClNVTU1BUlk6SEVMTE9cLA0KVFJBTlNQOk9QQVFVRQ0K
+RU5EOlZFVkVOVA0KRU5EOlZDQUxFTkRBUg0K
+
+--_004_001a11c3440066ee0b04ef86cea8googlecom_--
+
+)";
+       auto message{Message::make_from_text(
+                       msgtext,
+                       "/home/test/Maildir/inbox/cur/162342449279256.107710_1.evergrey:2,PSp")};
+       g_assert_true(!!message);
+       assert_equal(message->subject(),
+                    "Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 (william@example.com)");
+       g_assert_true(message->flags() == (Flags::Passed|Flags::Seen|
+                                          Flags::HasAttachment|Flags::Calendar));
+}
+
+
+static void
+test_message_references()
+{
+       constexpr auto msgtext =
+R"(Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=utf-8
+References: <YuvYh1JbE3v+abd5@kili>
+ <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+ <T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid>
+To: "Robin Murphy" <robin.murphy@arm.com>
+Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com>
+From: "Dan Carpenter" <dan.carpenter@oracle.com>
+Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs
+List-Id: <kernel-janitors.vger.kernel.org>
+Date: Fri, 5 Aug 2022 09:37:02 +0300
+In-Reply-To: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+Precedence: bulk
+Message-Id: <20220805063702.GH3438@kadam>
+
+On Thu, Aug 04, 2022 at 05:31:39PM +0100, Robin Murphy wrote:
+> On 04/08/2022 3:32 pm, Dan Carpenter wrote:
+> > There are two issues here:
+)";
+       auto message{Message::make_from_text(
+                       msgtext,
+                       "/home/test/Maildir/inbox/cur/162342449279256.88888_1.evergrey:2,S")};
+       g_assert_true(!!message);
+       assert_equal(message->subject(),
+                    "Re: [PATCH] iommu/omap: fix buffer overflow in debugfs");
+       g_assert_true(message->priority() == Priority::Low);
+
+       /*
+        * "90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com" is seen both in
+        * references and in-reply-to; in the de-duplication, the first one wins.
+        */
+       std::vector<std::string> expected_refs = {
+               "YuvYh1JbE3v+abd5@kili",
+               "90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com",
+               /* protonmail.internalid is fake and removed */
+               // "T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_"
+               // "xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid"
+       };
+
+       assert_equal_seq_str(expected_refs, message->references());
+}
+
+
+static void
+test_message_outlook_body()
+{
+       constexpr auto msgtext =
+R"x(Received: from vu-ex2.activedir.vu.lt (172.16.159.219) by
+ vu-ex1.activedir.vu.lt (172.16.159.218) with Microsoft SMTP Server
+ (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1118.9
+ via Mailbox Transport; Fri, 27 May 2022 11:40:05 +0300
+Received: from vu-ex2.activedir.vu.lt (172.16.159.219) by
+ vu-ex2.activedir.vu.lt (172.16.159.219) with Microsoft SMTP Server
+ (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
+ 15.2.1118.9; Fri, 27 May 2022 11:40:05 +0300
+Received: from vu-ex2.activedir.vu.lt ([172.16.159.219]) by
+ vu-ex2.activedir.vu.lt ([172.16.159.219]) with mapi id 15.02.1118.009; Fri,
+ 27 May 2022 11:40:05 +0300
+From: =?windows-1257?Q?XXXXXXXXXX= <XXXXXXXXXX>
+To:  <XXXXXXXXXX@XXXXXXXXXX.com>
+Subject: =?windows-1257?Q?Pra=F0ymas?=
+Thread-Topic: =?windows-1257?Q?Pra=F0ymas?=
+Thread-Index: AQHYcaRi3ejPSLxkl0uTFDto7z2OcA==
+Date: Fri, 27 May 2022 11:40:05 +0300
+Message-ID: <5c2cd378af634e929a6cc69da1e66b9d@XX.vu.lt>
+Accept-Language: en-US, lt-LT
+Content-Language: en-US
+X-MS-Has-Attach:
+Content-Type: text/html; charset="windows-1257"
+Content-Transfer-Encoding: quoted-printable
+MIME-Version: 1.0
+X-TUID: 1vFQ9RPwwg/u
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dwindows-1=
+257">
+<style type=3D"text/css" style=3D"display:none;"><!-- P {margin-top:0;margi=
+n-bottom:0;} --></style>
+</head>
+<body dir=3D"ltr">
+<div id=3D"divtagdefaultwrapper" style=3D"font-size:12pt;color:#000000;font=
+-family:Calibri,Helvetica,sans-serif;" dir=3D"ltr">
+<p>Laba diena visiems,</p>
+<p>Trumpai.</p>
+<p>D=EBl leidimo ar neleidimo ginti darb=E0: ed=EBstytojo paskyroje spaud=
+=FEiate ikon=E0 &quot;ra=F0to darbai&quot;, atidar=E6 susiraskite =E1ra=F0=
+=E0 &quot;tvirtinti / netvirtinti&quot;, pa=FEym=EBkite vien=E0 i=F0 j=F8.&=
+nbsp;</p>
+<p><br>
+</p>
+<p>=D0=E1 darb=E0 privalu atlikti, kad paskui nekilt=F8 problem=F8 studentu=
+i =E1vedant =E1vertinim=E0.</p>
+<p><br>
+</p>
+<p>Jei neleid=FEiate ginti darbo, pra=F0au informuoti mane ir komisijos sek=
+retori=F8.&nbsp;&nbsp;</p>
+<p><br>
+</p>
+<p>Vis=E0 tolesn=E6 informacij=E0 atsi=F8siu artimiausiu metu (stengsiuosi =
+=F0iandien vakare).</p>
+<p><br>
+</p>
+<p>Pagarbiai.</p>
+<p><br>
+</p>
+<p><br>
+</p>
+<div id=3D"Signature">
+<div id=3D"divtagdefaultwrapper" dir=3D"ltr" style=3D"font-family: Calibri,=
+ Helvetica, sans-serif, EmojiFont, &quot;Apple Color Emoji&quot;, &quot;Seg=
+oe UI Emoji&quot;, NotoColorEmoji, &quot;Segoe UI Symbol&quot;, &quot;Andro=
+id Emoji&quot;, EmojiSymbols;">
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><br>
+</p>
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><br>
+</p>
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><br>
+</p>
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><span style=3D"font-size:10pt=
+; background-color:rgb(255,255,255); color:rgb(0,111,201)"><br>
+</span></p>
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><span style=3D"font-size:10pt=
+; background-color:rgb(255,255,255); color:rgb(0,111,201)">XXXXXXXXXX</span></p>
+<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px"><=
+/span></font></p>
+<span style=3D"font-size:10pt; background-color:rgb(255,255,255); color:rgb=
+(0,111,201); font-size:10pt"></span>
+<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p>
+<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p>
+<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p>
+<p style=3D""><br>
+</p>
+<p style=3D""><br>
+</p>
+</div>
+</div>
+</div>
+</body>
+</html>
+)x";
+       g_test_bug("2349");
+
+       auto message{Message::make_from_text(
+                       msgtext,
+                       "/home/test/Maildir/inbox/cur/162342449279256.77777_1.evergrey:2,S")};
+       g_assert_true(!!message);
+
+       assert_equal(message->subject(), "Prašymas");
+       g_assert_true(message->priority() == Priority::Normal);
+
+       g_assert_false(!!message->body_text());
+       g_assert_true(!!message->body_html());
+       g_assert_cmpuint(message->body_html()->find("<p>Pagarbiai.</p>"), ==, 935);
+}
+
+
+static void
+test_message_message_id()
+{
+       constexpr const auto msg1 =
+R"(From: "Mu Test" <mu@djcbsoftware.nl>
+To: mu@djcbsoftware.nl
+Message-ID: <87lew9xddt.fsf@djcbsoftware.nl>
+
+abc
+)";
+
+       constexpr const auto msg2 =
+R"(From: "Mu Test" <mu@djcbsoftware.nl>
+To: mu@djcbsoftware.nl
+
+abc
+)";
+
+       constexpr const auto msg3 =
+R"(From: "Mu Test" <mu@djcbsoftware.nl>
+To: mu@djcbsoftware.nl
+Message-ID:
+
+abc
+)";
+
+       const auto m1{Message::make_from_text(msg1, "/foo/cur/m123:2,S")};
+       assert_valid_result(m1);
+
+       const auto m2{Message::make_from_text(msg2, "/foo/cur/m456:2,S")};
+       assert_valid_result(m2);
+       const auto m3{Message::make_from_text(msg3, "/foo/cur/m789:2,S")};
+       assert_valid_result(m3);
+
+       assert_equal(m1->message_id(), "87lew9xddt.fsf@djcbsoftware.nl");
+
+       /* both with absent and empty message-id, generate "random" fake one,
+        * which must end in @mu.id */
+       g_assert_true(g_str_has_suffix(m2->message_id().c_str(), "@mu.id"));
+       g_assert_true(g_str_has_suffix(m3->message_id().c_str(), "@mu.id"));
+}
+
+
+static void
+test_message_fail ()
+{
+       {
+               const auto msg = Message::make_from_path("/root/non-existent-path-12345");
+               g_assert_false(!!msg);
+       }
+
+       {
+               const auto msg = Message::make_from_text("", "");
+               g_assert_false(!!msg);
+       }
+}
+
+static void
+test_message_sanitize_maildir()
+{
+       assert_equal(Message::sanitize_maildir("/"), "/");
+       assert_equal(Message::sanitize_maildir("/foo/bar"), "/foo/bar");
+       assert_equal(Message::sanitize_maildir("/foo/bar/cuux/"), "/foo/bar/cuux");
+}
+
+int
+main(int argc, char* argv[])
+{
+       mu_test_init(&argc, &argv);
+
+       g_test_add_func("/message/message/mailing-list",
+                       test_message_mailing_list);
+       g_test_add_func("/message/message/attachments",
+                       test_message_attachments);
+       g_test_add_func("/message/message/signed",
+                       test_message_signed);
+       g_test_add_func("/message/message/signed-encrypted",
+                       test_message_signed_encrypted);
+       g_test_add_func("/message/message/multipart-mixed-rfc822",
+                       test_message_multipart_mixed_rfc822);
+       g_test_add_func("/message/message/detect-attachment",
+                       test_message_detect_attachment);
+       g_test_add_func("/message/message/calendar",
+                       test_message_calendar);
+       g_test_add_func("/message/message/references",
+                       test_message_references);
+       g_test_add_func("/message/message/outlook-body",
+                       test_message_outlook_body);
+       g_test_add_func("/message/message/message-id",
+                       test_message_message_id);
+       g_test_add_func("/message/message/fail",
+                       test_message_fail);
+       g_test_add_func("/message/message/sanitize-maildir",
+                       test_message_sanitize_maildir);
+
+       return g_test_run();
+}
diff --git a/lib/mu-bookmarks.cc b/lib/mu-bookmarks.cc
new file mode 100644 (file)
index 0000000..1865b64
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+** Copyright (C) 2010-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include <glib.h>
+#include "mu-bookmarks.hh"
+
+#define MU_BOOKMARK_GROUP "mu"
+
+struct MuBookmarks {
+       char*       _bmpath;
+       GHashTable* _hash;
+};
+
+static void
+fill_hash(GHashTable* hash, GKeyFile* kfile)
+{
+       gchar **keys, **cur;
+
+       keys = g_key_file_get_keys(kfile, MU_BOOKMARK_GROUP, NULL, NULL);
+       if (!keys)
+               return;
+
+       for (cur = keys; *cur; ++cur) {
+               gchar* val;
+               val = g_key_file_get_string(kfile, MU_BOOKMARK_GROUP, *cur, NULL);
+               if (val)
+                       g_hash_table_insert(hash, *cur, val);
+       }
+
+       /* don't use g_strfreev, because we put them in the hash table;
+        * only free the gchar** itself */
+       g_free(keys);
+}
+
+static GHashTable*
+create_hash_from_key_file(const gchar* bmpath)
+{
+       GKeyFile*   kfile;
+       GHashTable* hash;
+
+       kfile = g_key_file_new();
+
+       if (!g_key_file_load_from_file(kfile, bmpath, G_KEY_FILE_NONE, NULL)) {
+               g_key_file_free(kfile);
+               return NULL;
+       }
+
+       hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+       fill_hash(hash, kfile);
+
+       g_key_file_free(kfile);
+
+       return hash;
+}
+
+MuBookmarks*
+mu_bookmarks_new(const gchar* bmpath)
+{
+       MuBookmarks* bookmarks;
+       GHashTable*  hash;
+
+       g_return_val_if_fail(bmpath, NULL);
+
+       hash = create_hash_from_key_file(bmpath);
+       if (!hash)
+               return NULL;
+
+       bookmarks = g_new(MuBookmarks, 1);
+
+       bookmarks->_bmpath = g_strdup(bmpath);
+       bookmarks->_hash   = hash;
+
+       return bookmarks;
+}
+
+void
+mu_bookmarks_destroy(MuBookmarks* bm)
+{
+       if (!bm)
+               return;
+
+       g_free(bm->_bmpath);
+       g_hash_table_destroy(bm->_hash);
+       g_free(bm);
+}
+
+const gchar*
+mu_bookmarks_lookup(MuBookmarks* bm, const gchar* name)
+{
+       g_return_val_if_fail(bm, NULL);
+       g_return_val_if_fail(name, NULL);
+
+       return (const char*)g_hash_table_lookup(bm->_hash, name);
+}
+
+struct _BMData {
+       MuBookmarksForeachFunc _func;
+       gpointer               _user_data;
+};
+typedef struct _BMData BMData;
+
+static void
+each_bookmark(const gchar* key, const gchar* val, BMData* bmdata)
+{
+       bmdata->_func(key, val, bmdata->_user_data);
+}
+
+void
+mu_bookmarks_foreach(MuBookmarks* bm, MuBookmarksForeachFunc func, gpointer user_data)
+{
+       BMData bmdata;
+
+       g_return_if_fail(bm);
+       g_return_if_fail(func);
+
+       bmdata._func      = func;
+       bmdata._user_data = user_data;
+
+       g_hash_table_foreach(bm->_hash, (GHFunc)each_bookmark, &bmdata);
+}
diff --git a/lib/mu-bookmarks.hh b/lib/mu-bookmarks.hh
new file mode 100644 (file)
index 0000000..a68d23a
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_BOOKMARKS_HH__
+#define MU_BOOKMARKS_HH__
+
+#include <glib.h>
+/**
+ * @addtogroup MuBookmarks
+ * Functions for dealing with bookmarks
+ * @{
+ */
+
+/*! \struct MuBookmarks
+ * \brief Opaque structure representing a sequence of bookmarks
+ */
+struct MuBookmarks;
+
+/**
+ * create a new bookmarks object. when it's no longer needed, use
+ * mu_bookmarks_destroy
+ *
+ * @param bmpath path to the bookmarks file
+ *
+ * @return a new BookMarks object, or NULL in case of error
+ */
+MuBookmarks* mu_bookmarks_new(const gchar* bmpath) G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * destroy a bookmarks object
+ *
+ * @param bm a bookmarks object, or NULL
+ */
+void mu_bookmarks_destroy(MuBookmarks* bm);
+
+/**
+ * get the value for some bookmark
+ *
+ * @param bm a valid bookmarks object
+ * @param name name of the bookmark to retrieve
+ *
+ * @return the value of the bookmark or NULL in case in error, e.g. if
+ * the bookmark was not found
+ */
+const gchar* mu_bookmarks_lookup(MuBookmarks* bm, const gchar* name);
+
+typedef void (*MuBookmarksForeachFunc)(const gchar* key, const gchar* val, gpointer user_data);
+
+/**
+ * call a function for each bookmark
+ *
+ * @param bm a valid bookmarks object
+ * @param func a callback function to be called for each bookmarks
+ * @param user_data a user pointer passed to the callback
+ */
+void mu_bookmarks_foreach(MuBookmarks* bm, MuBookmarksForeachFunc func, gpointer user_data);
+
+/** @} */
+
+#endif /*__MU_BOOKMARKS_H__*/
diff --git a/lib/mu-contacts-cache.cc b/lib/mu-contacts-cache.cc
new file mode 100644 (file)
index 0000000..c4c8146
--- /dev/null
@@ -0,0 +1,511 @@
+/*
+** Copyright (C) 2019-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-contacts-cache.hh"
+
+#include <mutex>
+#include <unordered_map>
+#include <set>
+#include <sstream>
+#include <functional>
+#include <algorithm>
+#include <regex>
+#include <ctime>
+
+#include <utils/mu-utils.hh>
+#include <glib.h>
+
+using namespace Mu;
+
+struct EmailHash {
+       std::size_t operator()(const std::string& email) const {
+               return lowercase_hash(email);
+       }
+};
+struct EmailEqual {
+       bool operator()(const std::string& email1, const std::string& email2) const {
+               return lowercase_hash(email1) == lowercase_hash(email2);
+       }
+};
+
+using ContactUMap = std::unordered_map<const std::string, Contact, EmailHash, EmailEqual>;
+struct ContactsCache::Private {
+       Private(const std::string& serialized, const StringVec& personal)
+               : contacts_{deserialize(serialized)},
+                 personal_plain_{make_personal_plain(personal)},
+                 personal_rx_{make_personal_rx(personal)},
+                 dirty_{0}
+       {}
+
+       ContactUMap deserialize(const std::string&) const;
+       std::string serialize() const;
+
+       ContactUMap contacts_;
+       std::mutex  mtx_;
+
+       const StringVec               personal_plain_;
+       const std::vector<std::regex> personal_rx_;
+
+       size_t dirty_;
+
+private:
+       /**
+        * Return the non-regex addresses
+        *
+        * @param personal
+        *
+        * @return
+        */
+       StringVec make_personal_plain(const StringVec& personal) const {
+               StringVec svec;
+               std::copy_if(personal.begin(),  personal.end(),
+                            std::back_inserter(svec), [&](auto&& p) {
+                                    return p.size() < 2
+                                            || p.at(0) != '/' || p.at(p.length() - 1) != '/';
+                            });
+               return svec;
+       }
+
+       /**
+        * Return regexps for the regex-addresses
+        *
+        * @param personal
+        *
+        * @return
+        */
+       std::vector<std::regex> make_personal_rx(const StringVec& personal) const {
+               std::vector<std::regex> rxvec;
+               for(auto&& p: personal) {
+                       if (p.size() < 2 || p[0] != '/' || p[p.length()- 1] != '/')
+                               continue;
+                       // a regex pattern.
+                       try {
+                               const auto rxstr{p.substr(1, p.length() - 2)};
+                               rxvec.emplace_back(std::regex(
+                                   rxstr, std::regex::basic | std::regex::optimize |
+                                   std::regex::icase));
+                       } catch (const std::regex_error& rex) {
+                               g_warning("invalid personal address regexp '%s': %s",
+                                         p.c_str(),
+                                         rex.what());
+                       }
+               }
+               return rxvec;
+       }
+};
+
+constexpr auto Separator = "\xff"; // Invalid in UTF-8
+
+
+ContactUMap
+ContactsCache::Private::deserialize(const std::string& serialized) const
+{
+       ContactUMap       contacts;
+       std::stringstream ss{serialized, std::ios_base::in};
+       std::string       line;
+
+       while (getline(ss, line)) {
+               const auto parts = Mu::split(line, Separator);
+               if (G_UNLIKELY(parts.size() != 6)) {
+                       g_warning("error: '%s'", line.c_str());
+                       continue;
+               }
+               Contact ci(parts[1],                                                  // email
+                                 std::move(parts[2]),                                       // name
+                                 (time_t)g_ascii_strtoll(parts[4].c_str(), NULL, 10),       // message_date
+                                 parts[3][0] == '1' ? true : false,                         // personal
+                                 (std::size_t)g_ascii_strtoll(parts[5].c_str(), NULL, 10),  // frequency
+                                 g_get_monotonic_time());                                   // tstamp
+               contacts.emplace(std::move(parts[1]), std::move(ci));
+       }
+
+       return contacts;
+}
+
+ContactsCache::ContactsCache(const std::string& serialized, const StringVec& personal)
+    : priv_{std::make_unique<Private>(serialized, personal)}
+{
+}
+
+ContactsCache::~ContactsCache() = default;
+std::string
+ContactsCache::serialize() const
+{
+       std::lock_guard<std::mutex> l_{priv_->mtx_};
+       std::string                 s;
+
+       for (auto& item : priv_->contacts_) {
+               const auto& ci{item.second};
+               s += Mu::format("%s%s"
+                               "%s%s"
+                               "%s%s"
+                               "%d%s"
+                               "%" G_GINT64_FORMAT "%s"
+                               "%" G_GINT64_FORMAT "\n",
+                               ci.display_name().c_str(),
+                               Separator,
+                               ci.email.c_str(),
+                               Separator,
+                               ci.name.c_str(),
+                               Separator,
+                               ci.personal ? 1 : 0,
+                               Separator,
+                               (gint64)ci.message_date,
+                               Separator,
+                               (gint64)ci.frequency);
+       }
+
+       priv_->dirty_ = 0;
+
+       return s;
+}
+
+bool
+ContactsCache::dirty() const
+{
+       return priv_->dirty_;
+}
+
+//const Contact
+void
+ContactsCache::add(Contact&& contact)
+{
+       std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+       ++priv_->dirty_;
+
+       auto it = priv_->contacts_.find(contact.email);
+
+       if (it == priv_->contacts_.end()) { // completely new contact
+
+               contact.name             = contact.name;
+               if (!contact.personal)
+                       contact.personal = is_personal(contact.email);
+               contact.tstamp           = g_get_monotonic_time();
+
+               auto email{contact.email};
+               // return priv_->contacts_.emplace(ContactUMap::value_type(email, std::move(contact)))
+               //     .first->second;
+
+               priv_->contacts_.emplace(ContactUMap::value_type(email, std::move(contact)));
+
+       } else {        // existing contact.
+               auto& existing{it->second};
+               ++existing.frequency;
+               if (contact.message_date > existing.message_date) {     // update?
+                       existing.email        = std::move(contact.email);
+                       // update name only if new one is not empty.
+                       if (!contact.name.empty())
+                               existing.name = std::move(contact.name);
+                       existing.tstamp       = g_get_monotonic_time();
+                       existing.message_date = contact.message_date;
+               }
+       }
+}
+
+
+void
+ContactsCache::add(Contacts&& contacts, bool& personal)
+{
+       personal = seq_find_if(contacts,[&](auto&& c){
+               return is_personal(c.email); }) != contacts.cend();
+
+       for (auto&& contact: contacts) {
+               contact.personal = personal;
+               add(std::move(contact));
+       }
+}
+
+
+const Contact*
+ContactsCache::_find(const std::string& email) const
+{
+       std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+       const auto it = priv_->contacts_.find(email);
+       if (it == priv_->contacts_.end())
+               return {};
+       else
+               return &it->second;
+}
+
+void
+ContactsCache::clear()
+{
+       std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+       ++priv_->dirty_;
+
+       priv_->contacts_.clear();
+}
+
+std::size_t
+ContactsCache::size() const
+{
+       std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+       return priv_->contacts_.size();
+}
+
+
+/**
+ * This is used for sorting the Contacts in order of relevance. A highly
+ * specific algorithm, but the details don't matter _too_ much.
+ *
+ * This is currently used for the ordering in mu-cfind and auto-completion in
+ * mu4e, if the various completion methods don't override it...
+ */
+constexpr auto RecentOffset{15 * 24 * 3600};
+struct ContactLessThan {
+       ContactLessThan()
+           : recently_{::time({}) - RecentOffset} {}
+
+
+       bool operator()(const Mu::Contact& ci1, const Mu::Contact& ci2) const
+       {
+               // non-personal is less relevant.
+               if (ci1.personal != ci2.personal)
+                       return ci1.personal < ci2.personal;
+
+               // older is less relevant for recent messages
+               if (std::max(ci1.message_date, ci2.message_date) > recently_ &&
+                   ci1.message_date != ci2.message_date)
+                       return ci1.message_date < ci2.message_date;
+
+               // less frequent is less relevant
+               if (ci1.frequency != ci2.frequency)
+                       return ci1.frequency < ci2.frequency;
+
+               // if all else fails, alphabetically
+               return ci1.email < ci2.email;
+       }
+       // only sort recently seen contacts by recency; approx 15 days.
+       // this changes during the lifetime, but that's all fine.
+       const time_t recently_;
+};
+
+using ContactSet = std::set<std::reference_wrapper<const Contact>,
+                           ContactLessThan>;
+
+void
+ContactsCache::for_each(const EachContactFunc& each_contact) const
+{
+       std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+       // first sort them for 'rank'
+       ContactSet sorted;
+       for (const auto& item : priv_->contacts_)
+               sorted.emplace(item.second);
+
+       // return in _reverse_ order, so we get the most relevant ones first.
+       for (auto it = sorted.rbegin(); it != sorted.rend(); ++it) {
+               if (!each_contact(*it))
+                       break;
+       }
+}
+
+bool
+ContactsCache::is_personal(const std::string& addr) const
+{
+       for (auto&& p : priv_->personal_plain_)
+               if (g_ascii_strcasecmp(addr.c_str(), p.c_str()) == 0)
+                       return true;
+
+       for (auto&& rx : priv_->personal_rx_) {
+               std::smatch m; // perhaps cache addr in personal_plain_?
+               if (std::regex_match(addr, m, rx))
+                       return true;
+       }
+
+       return false;
+}
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_mu_contacts_cache_base()
+{
+       Mu::ContactsCache contacts("");
+
+       g_assert_true(contacts.empty());
+       g_assert_cmpuint(contacts.size(), ==, 0);
+
+       contacts.add(Mu::Contact("foo.bar@example.com",
+                                       "Foo", {}, 12345));
+       g_assert_false(contacts.empty());
+       g_assert_cmpuint(contacts.size(), ==, 1);
+
+       contacts.add(Mu::Contact("cuux@example.com", "Cuux", {},
+                                       54321));
+
+       g_assert_cmpuint(contacts.size(), ==, 2);
+
+       contacts.add(
+           Mu::Contact("foo.bar@example.com", "Foo", {}, 77777));
+       g_assert_cmpuint(contacts.size(), ==, 2);
+
+       contacts.add(
+           Mu::Contact("Foo.Bar@Example.Com", "Foo", {}, 88888));
+       g_assert_cmpuint(contacts.size(), ==, 2);
+       // note: replaces first.
+
+       {
+               const auto info = contacts._find("bla@example.com");
+               g_assert_false(info);
+       }
+
+       {
+               const auto info = contacts._find("foo.BAR@example.com");
+               g_assert_true(info);
+
+               g_assert_cmpstr(info->email.c_str(), ==, "Foo.Bar@Example.Com");
+       }
+
+       contacts.clear();
+       g_assert_true(contacts.empty());
+       g_assert_cmpuint(contacts.size(), ==, 0);
+}
+
+static void
+test_mu_contacts_cache_personal()
+{
+       Mu::StringVec personal = {"foo@example.com", "bar@cuux.org", "/bar-.*@fnorb.f./"};
+       Mu::ContactsCache  contacts{"", personal};
+
+       g_assert_true(contacts.is_personal("foo@example.com"));
+       g_assert_true(contacts.is_personal("Bar@CuuX.orG"));
+       g_assert_true(contacts.is_personal("bar-123abc@fnorb.fi"));
+       g_assert_true(contacts.is_personal("bar-zzz@fnorb.fr"));
+
+       g_assert_false(contacts.is_personal("foo@bar.com"));
+       g_assert_false(contacts.is_personal("BÂr@CuuX.orG"));
+       g_assert_false(contacts.is_personal("bar@fnorb.fi"));
+       g_assert_false(contacts.is_personal("bar-zzz@fnorb.xr"));
+}
+
+
+static void
+test_mu_contacts_cache_foreach()
+{
+       Mu::ContactsCache ccache("");
+       ccache.add(Mu::Contact{"a@example.com", "a", 123, true, 1000, 0});
+       ccache.add(Mu::Contact{"b@example.com", "b", 456, true, 1000, 0});
+
+       {
+               size_t n{};
+               g_assert_false(ccache.empty());
+               g_assert_cmpuint(ccache.size(),==,2);
+               ccache.for_each([&](auto&& contact) { ++n; return false; });
+               g_assert_cmpuint(n,==,1);
+       }
+
+       {
+               size_t n{};
+               g_assert_false(ccache.empty());
+               g_assert_cmpuint(ccache.size(),==,2);
+               ccache.for_each([&](auto&& contact) { ++n; return true; });
+               g_assert_cmpuint(n,==,2);
+       }
+
+       {
+               size_t n{};
+               ccache.clear();
+               g_assert_true(ccache.empty());
+               g_assert_cmpuint(ccache.size(),==,0);
+               ccache.for_each([&](auto&& contact) { ++n; return true; });
+               g_assert_cmpuint(n,==,0);
+       }
+}
+
+
+
+static void
+test_mu_contacts_cache_sort()
+{
+       auto result_chars = [](const Mu::ContactsCache& ccache)->std::string {
+               std::string str;
+               if (g_test_verbose())
+                       g_print("contacts-cache:\n");
+
+               ccache.for_each([&](auto&& contact) {
+                       if (g_test_verbose())
+                               g_print("\t- %s\n", contact.display_name().c_str());
+                       str += contact.name;
+                       return true;
+               });
+               return str;
+       };
+
+       const auto now{std::time({})};
+
+       // "first" means more relevant
+
+       { /* recent messages, newer comes first */
+
+               Mu::ContactsCache ccache("");
+               ccache.add(Mu::Contact{"a@example.com", "a", now, true, 1000, 0});
+               ccache.add(Mu::Contact{"b@example.com", "b", now-1, true, 1000, 0});
+               assert_equal(result_chars(ccache), "ab");
+       }
+
+       { /* non-recent messages, more frequent comes first */
+
+               Mu::ContactsCache ccache("");
+               ccache.add(Mu::Contact{"a@example.com", "a", now-2*RecentOffset, true, 1000, 0});
+               ccache.add(Mu::Contact{"b@example.com", "b", now-3*RecentOffset, true, 2000, 0});
+               assert_equal(result_chars(ccache), "ba");
+       }
+
+       { /* personal comes first */
+
+               Mu::ContactsCache ccache("");
+               ccache.add(Mu::Contact{"a@example.com", "a", now-5*RecentOffset, true, 1000, 0});
+               ccache.add(Mu::Contact{"b@example.com", "b", now, false, 8000, 0});
+               assert_equal(result_chars(ccache), "ab");
+       }
+
+       { /* if all else fails, reverse-alphabetically */
+               Mu::ContactsCache ccache("");
+               ccache.add(Mu::Contact{"a@example.com", "a", now, false, 1000, 0});
+               ccache.add(Mu::Contact{"b@example.com", "b", now, false, 1000, 0});
+               g_assert_cmpuint(ccache.size(),==,2);
+               assert_equal(result_chars(ccache), "ba");
+       }
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       mu_test_init(&argc, &argv);
+
+       g_test_add_func("/lib/contacts-cache/base", test_mu_contacts_cache_base);
+       g_test_add_func("/lib/contacts-cache/personal", test_mu_contacts_cache_personal);
+       g_test_add_func("/lib/contacts-cache/for-each", test_mu_contacts_cache_foreach);
+       g_test_add_func("/lib/contacts-cache/sort", test_mu_contacts_cache_sort);
+
+       return g_test_run();
+}
+#endif /*BUILD_TESTS*/
diff --git a/lib/mu-contacts-cache.hh b/lib/mu-contacts-cache.hh
new file mode 100644 (file)
index 0000000..7c871d7
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef __MU_CONTACTS_CACHE_HH__
+#define __MU_CONTACTS_CACHE_HH__
+
+#include <glib.h>
+#include <time.h>
+#include <memory>
+#include <functional>
+#include <chrono>
+#include <string>
+#include <time.h>
+#include <inttypes.h>
+#include <utils/mu-utils.hh>
+
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+class ContactsCache {
+public:
+       /**
+        * Construct a new ContactsCache object
+        *
+        * @param serialized serialized contacts
+        * @param personal personal addresses
+        */
+       ContactsCache(const std::string& serialized = "", const StringVec& personal = {});
+
+       /**
+        * DTOR
+        *
+        */
+       ~ContactsCache();
+
+       /**
+        * Add a contact
+        *
+        * @param contact a Contact object
+        *
+        */
+       void add(Contact&& contact);
+
+
+       /**
+        * Add a contacts sequence; this should be used for the contacts of a
+        * specific message, and determines if it is a "personal" message:
+        * if any of the contacts matches one of the personal addresses,
+        * any of the senders/recipients are considered "personal"
+        *
+        * @param contacts a Contact object sequence
+        * @param is_personal receives true if any of the contacts was personal;
+        * false otherwise
+        */
+       void add(Contacts&& contacts, bool& is_personal);
+       void add(Contacts&& contacts) {
+               bool _ignore;
+               add(std::move(contacts), _ignore);
+       }
+
+
+       /**
+        * Clear all contacts
+        *
+        */
+       void clear();
+
+       /**
+        * Get the number of contacts
+        *
+
+        * @return number of contacts
+        */
+       std::size_t size() const;
+
+       /**
+        * Are there no contacts?
+        *
+        * @return true or false
+        */
+       bool empty() const { return size() == 0; }
+
+       /**
+        * Get the contacts, serialized. This all marks the data as
+        * non-dirty (see dirty())
+        *
+        * @return serialized contacts
+        */
+       std::string serialize() const;
+
+       /**
+        * Has the contacts database change since the last
+        * call to serialize()?
+        *
+        * @return true or false
+        */
+       bool dirty() const;
+
+       /**
+        * Does this look like a 'personal' address?
+        *
+        * @param addr some e-mail address
+        *
+        * @return true or false
+        */
+       bool is_personal(const std::string& addr) const;
+
+       /**
+        * Find a contact based on the email address. This is not safe, since
+        * the returned ptr can be invalidated at any time; only for unit-tests.
+        *
+        * @param email email address
+        *
+        * @return contact info, or {} if not found
+        */
+       const Contact* _find(const std::string& email) const;
+
+       /**
+        * Prototype for a callable that receives a contact
+        *
+        * @param contact some contact
+        *
+        * @return to get more contacts; false otherwise
+        */
+       using EachContactFunc = std::function<bool(const Contact& contact_info)>;
+
+       /**
+        * Invoke some callable for each contact, in _descending_ order of rank (i.e., the
+        * highest ranked contacts come first).
+        *
+        * @param each_contact function invoked for each contact
+        */
+       void for_each(const EachContactFunc& each_contact) const;
+
+private:
+       struct Private;
+       std::unique_ptr<Private> priv_;
+};
+
+} // namespace Mu
+
+#endif /* __MU_CONTACTS_CACHE_HH__ */
diff --git a/lib/mu-maildir.cc b/lib/mu-maildir.cc
new file mode 100644 (file)
index 0000000..8b27aa7
--- /dev/null
@@ -0,0 +1,458 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to 59the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <string>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <stdlib.h>
+
+#include <string.h>
+#include <errno.h>
+#include <glib/gprintf.h>
+#include <gio/gio.h>
+
+#include "glibconfig.h"
+#include "mu-maildir.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-util.h"
+
+using namespace Mu;
+
+#define MU_MAILDIR_NOINDEX_FILE  ".noindex"
+#define MU_MAILDIR_NOUPDATE_FILE ".noupdate"
+
+/* On Linux (and some BSD), we have entry->d_type, but some file
+ * systems (XFS, ReiserFS) do not support it, and set it DT_UNKNOWN.
+ * On other OSs, notably Solaris, entry->d_type is not present at all.
+ * For these cases, we use lstat (in get_dtype) as a slower fallback,
+ * and return it in the d_type parameter
+ */
+static unsigned char
+get_dtype(struct dirent* dentry, const std::string& path, bool use_lstat)
+{
+#ifdef HAVE_STRUCT_DIRENT_D_TYPE
+
+       if (dentry->d_type == DT_UNKNOWN)
+               goto slowpath;
+       if (dentry->d_type == DT_LNK && !use_lstat)
+               goto slowpath;
+
+       return dentry->d_type; /* fastpath */
+
+slowpath:
+#endif /*HAVE_STRUCT_DIRENT_D_TYPE*/
+       return mu_util_get_dtype(path.c_str(), use_lstat);
+}
+
+static Mu::Result<void>
+create_maildir(const std::string& path, mode_t mode)
+{
+       if (path.empty())
+               return Err(Error{Error::Code::File, "path must not be empty"});
+
+       std::array<std::string,3> subdirs = {"new", "cur", "tmp"};
+       for (auto&& subdir: subdirs) {
+
+               const auto fullpath{path + G_DIR_SEPARATOR_S + subdir};
+
+               /* if subdir already exists, don't try to re-create
+                * it */
+               if (mu_util_check_dir(fullpath.c_str(), TRUE, TRUE))
+                       continue;
+
+               int rv{g_mkdir_with_parents(fullpath.c_str(), static_cast<int>(mode))};
+
+               /* note, g_mkdir_with_parents won't detect an error if
+                * there's already such a dir, but with the wrong
+                * permissions; so we need to check */
+               if (rv != 0 || !mu_util_check_dir(fullpath.c_str(), TRUE, TRUE))
+                       return Err(Error{Error::Code::File,
+                                       "creating dir failed for %s: %s",
+                                       fullpath.c_str(), g_strerror(errno)});
+       }
+
+       return Ok();
+}
+
+static Mu::Result<void>                /* create a noindex file if requested */
+create_noindex(const std::string& path)
+{
+       const auto noindexpath{path + G_DIR_SEPARATOR_S MU_MAILDIR_NOINDEX_FILE};
+
+       /* note, if the 'close' failed, creation may still have succeeded...*/
+       int fd = ::creat(noindexpath.c_str(), 0644);
+       if (fd < 0 || ::close(fd) != 0)
+               return Err(Error{Error::Code::File,
+                               "error creating .noindex: %s", g_strerror(errno)});
+       else
+               return Ok();
+}
+
+Mu::Result<void>
+Mu::maildir_mkdir(const std::string& path, mode_t mode, bool noindex)
+{
+       if (auto&& created{create_maildir(path, mode)}; !created)
+               return created; // fail.
+       else if (!noindex)
+               return Ok();
+
+       if (auto&& created{create_noindex(path)}; !created)
+               return created; //fail
+
+       return Ok();
+}
+
+/* determine whether the source message is in 'new' or in 'cur';
+ * we ignore messages in 'tmp' for obvious reasons */
+static Mu::Result<void>
+check_subdir(const std::string& src, bool& in_cur)
+{
+       char *srcpath{g_path_get_dirname(src.c_str())};
+
+       bool invalid{};
+       if (g_str_has_suffix(srcpath, "cur"))
+               in_cur = true;
+       else if (g_str_has_suffix(srcpath, "new"))
+               in_cur = false;
+       else
+               invalid = true;
+
+       g_free(srcpath);
+
+       if (invalid)
+               return Err(Error{Error::Code::File, "invalid source message '%s'",
+                               src.c_str()});
+       else
+               return Ok();
+}
+
+static Mu::Result<std::string>
+get_target_fullpath(const std::string& src, const std::string& targetpath,
+                   bool unique_names)
+{
+       bool in_cur{};
+       if (auto&& res = check_subdir(src, in_cur); !res)
+               return Err(std::move(res.error()));
+
+       char *srcfile{g_path_get_basename(src.c_str())};
+
+       /* create targetpath; note: make the filename *cough* unique by
+        * including a hash of the srcname in the targetname. This helps if
+        * there are copies of a message (which all have the same basename)
+        */
+       std::string fulltargetpath;
+       if (unique_names)
+               fulltargetpath = format("%s%c%s%c%u_%s",
+                                   targetpath.c_str(),
+                                   G_DIR_SEPARATOR, in_cur ? "cur" : "new",
+                                   G_DIR_SEPARATOR,
+                                   g_str_hash(src.c_str()),
+                                   srcfile);
+       else
+               fulltargetpath = format("%s%c%s%c%s",
+                                   targetpath.c_str(),
+                                   G_DIR_SEPARATOR, in_cur ? "cur" : "new",
+                                   G_DIR_SEPARATOR,
+                                   srcfile);
+       g_free(srcfile);
+
+       return fulltargetpath;
+}
+
+Result<void>
+Mu::maildir_link(const std::string& src, const std::string& targetpath,
+                bool unique_names)
+{
+       auto path_res{get_target_fullpath(src, targetpath, unique_names)};
+       if (!path_res)
+               return Err(std::move(path_res.error()));
+
+       auto rv{::symlink(src.c_str(), path_res->c_str())};
+       if (rv != 0)
+               return Err(Error{Error::Code::File,
+                               "error creating link %s => %s: %s",
+                               path_res->c_str(),
+                               src.c_str(),
+                               g_strerror(errno)});
+
+       return Ok();
+}
+
+static bool
+clear_links(const std::string& path, DIR* dir)
+{
+       bool res;
+       struct dirent* dentry;
+
+       res   = true;
+       errno = 0;
+
+       while ((dentry = ::readdir(dir))) {
+
+               if (dentry->d_name[0] == '.')
+                       continue; /* ignore .,.. other dotdirs */
+
+               const auto fullpath{
+                       format("%s" G_DIR_SEPARATOR_S "%s",path.c_str(), dentry->d_name)};
+               const auto d_type   = get_dtype(dentry, fullpath.c_str(), true/*lstat*/);
+               switch(d_type) {
+               case DT_LNK:
+                       if (::unlink(fullpath.c_str()) != 0) {
+                               g_warning("error unlinking %s: %s",
+                                         fullpath.c_str(), g_strerror(errno));
+                               res = false;
+                       }
+                       break;
+               case  DT_DIR: {
+                       DIR* subdir{::opendir(fullpath.c_str())};
+                       if (!subdir) {
+                               g_warning("failed to open dir %s: %s", fullpath.c_str(),
+                                         g_strerror(errno));
+                               res = false;
+                       }
+                       if (!clear_links(fullpath, subdir))
+                               res = false;
+                       ::closedir(subdir);
+               }
+                       break;
+               default:
+                       break;
+               }
+       }
+
+       return res;
+}
+
+Mu::Result<void>
+Mu::maildir_clear_links(const std::string& path)
+{
+       const auto dir{::opendir(path.c_str())};
+       if (!dir)
+               return Err(Error{Error::Code::File, "failed to open %s: %s",
+                               path.c_str(), g_strerror(errno)});
+
+       clear_links(path, dir);
+       ::closedir(dir);
+
+       return Ok();
+}
+
+static Mu::Result<void>
+msg_move_verify(const std::string& src, const std::string& dst)
+{
+       /* double check -- is the target really there? */
+       if (::access(dst.c_str(), F_OK) != 0)
+               return Err(Error{Error::Code::File,
+                               "can't find target (%s->%s)",
+                               src.c_str(), dst.c_str()});
+
+       if (::access(src.c_str(), F_OK) == 0) {
+               if (src ==  dst) {
+                       g_warning("moved %s to itself", src.c_str());
+               }
+               /* this could happen if some other tool (for mail syncing) is
+                * interfering */
+               g_debug("the source is still there (%s->%s)", src.c_str(), dst.c_str());
+       }
+
+       return Ok();
+}
+
+/* use GIO to move files; this is slower than rename() so only use
+ * this when needed: when moving across filesystems */
+static Mu::Result<void>
+msg_move_g_file(const std::string& src, const std::string& dst)
+{
+       GFile *srcfile{g_file_new_for_path(src.c_str())};
+       GFile *dstfile{g_file_new_for_path(dst.c_str())};
+
+       GError* err{};
+       auto res = g_file_move(srcfile, dstfile,
+                              G_FILE_COPY_OVERWRITE,
+                              NULL, NULL, NULL, &err);
+       g_clear_object(&srcfile);
+       g_clear_object(&dstfile);
+
+       if (res)
+               return Ok();
+       else
+               return Err(Error{Error::Code::File, &err/*consumed*/,
+                               "error moving %s -> %s",
+                               src.c_str(), dst.c_str()});
+}
+
+static Mu::Result<void>
+msg_move(const std::string& src, const std::string& dst, bool force_gio)
+{
+       if (::access(src.c_str(), R_OK) != 0)
+               return Err(Error{Error::Code::File, "cannot read %s", src.c_str()});
+
+       if (!force_gio) { /* for testing */
+
+               if (::rename(src.c_str(), dst.c_str()) == 0) /* seems it worked; double-check */
+                       return msg_move_verify(src, dst);
+
+               if (errno != EXDEV) /* some unrecoverable error occurred */
+                       return Err(Error{Error::Code::File, "error moving %s -> %s: %s",
+                                          src.c_str(), dst.c_str(), strerror(errno)});
+       }
+
+       /* the EXDEV / force-gio case -- source and target live on different
+        * filesystems */
+       auto res = msg_move_g_file(src, dst);
+       if (!res)
+               return res;
+       else
+               return msg_move_verify(src, dst);
+}
+
+
+Mu::Result<void>
+Mu::maildir_move_message(const std::string&    oldpath,
+                        const std::string&     newpath,
+                        bool                   force_gio)
+{
+       if (oldpath == newpath)
+               return Ok(); // nothing to do.
+
+       g_debug("moving %s --> %s", oldpath.c_str(), newpath.c_str());
+       return msg_move(oldpath, newpath, force_gio);
+}
+
+static std::string
+reinvent_filename_base()
+{
+       return format("%u.%08x%08x.%s",
+                     static_cast<unsigned>(::time(NULL)),
+                     g_random_int(),
+                     static_cast<uint32_t>(g_get_monotonic_time()),
+                     g_get_host_name());
+}
+
+/**
+ * Determine the destination filename
+ *
+ * @param file a filename
+ * @param flags flags for the destination
+ * @param new_name whether to change the basename
+ *
+ * @return the destion filename.
+ */
+static std::string
+determine_dst_filename(const std::string& file, Flags flags,
+                      bool new_name)
+{
+       /* Recalculate a unique new base file name */
+       auto&& parts{message_file_parts(file)};
+       if (new_name)
+               parts.base = reinvent_filename_base();
+
+       /* for a New message, there are no flags etc.; so we only return the
+        * name sans suffix */
+       if (any_of(flags & Flags::New))
+               return std::move(parts.base);
+
+       const auto flagstr{
+               to_string(
+                       flags_filter(
+                               flags, MessageFlagCategory::Mailfile))};
+
+       return parts.base + parts.separator + "2," + flagstr;
+}
+
+
+/*
+ * sanity checks
+ */
+static Mu::Result<void>
+check_determine_target_params (const std::string& old_path,
+                              const std::string& root_maildir_path,
+                              const std::string& target_maildir,
+                              Flags newflags)
+{
+       if (!g_path_is_absolute(old_path.c_str()))
+               return Err(Error{Error::Code::File,
+                               "old_path is not absolute (%s)", old_path.c_str()});
+
+       if (!g_path_is_absolute(root_maildir_path.c_str()))
+               return Err(Error{Error::Code::File,
+                               "root maildir path is not absolute",
+                               root_maildir_path.c_str()});
+
+       if (!target_maildir.empty() && target_maildir[0] != '/')
+               return Err(Error{Error::Code::File,
+                               "target maildir must be empty or start with / (%s)",
+                               target_maildir.c_str()});
+
+       if (old_path.find(root_maildir_path) != 0)
+               return Err(Error{Error::Code::File,
+                               "old-path must be below root-maildir (%s) (%s)",
+                               old_path.c_str(), root_maildir_path.c_str()});
+
+       if (any_of(newflags & Flags::New) && newflags != Flags::New)
+               return Err(Error{Error::Code::File,
+                                       "if ::New is specified, "
+                                       "it must be the only flag"});
+       return Ok();
+}
+
+
+Mu::Result<std::string>
+Mu::maildir_determine_target(const std::string&        old_path,
+                            const std::string& root_maildir_path,
+                            const std::string& target_maildir,
+                            Flags              newflags,
+                            bool               new_name)
+{
+       /* sanity checks */
+       if (const auto checked{check_determine_target_params(
+               old_path, root_maildir_path, target_maildir, newflags)}; !checked)
+               return Err(Error{std::move(checked.error())});
+
+       /*
+        * this gets us the source maildir filesystem path, the directory
+        * in which new/ & cur/ lives, and the source file
+        */
+       const auto src{base_message_dir_file(old_path)};
+       if (!src)
+               return Err(src.error());
+       const auto& [src_mdir, src_file, is_new] = *src;
+
+       /* if target_mdir is empty, the src_dir does not change (though cur/
+        * maybe become new or vice-versa) */
+       const auto dst_mdir{target_maildir.empty() ? src_mdir :
+               root_maildir_path + target_maildir};
+
+       /* now calculate the message name (incl. its immediate parent dir) */
+       const auto dst_file{determine_dst_filename(src_file, newflags, new_name)};
+
+       /* and the complete path name. */
+       const auto subdir = std::invoke([&]()->std::string {
+               if (none_of(newflags & Flags::New))
+                       return "cur";
+               else
+                       return "new";
+       });
+
+       return dst_mdir + G_DIR_SEPARATOR_S + subdir + G_DIR_SEPARATOR_S + dst_file;
+}
diff --git a/lib/mu-maildir.hh b/lib/mu-maildir.hh
new file mode 100644 (file)
index 0000000..42a49a3
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_MAILDIR_HH__
+#define MU_MAILDIR_HH__
+
+#include <string>
+#include <utils/mu-result.hh>
+
+#include <glib.h>
+#include <time.h>
+#include <sys/types.h> /* for mode_t */
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+/**
+ * Create a new maildir. if parts of the maildir already exists, those will
+ * simply be ignored.
+ *
+ * IOW, if you try to create the same maildir twice, the second will simply be a
+ * no-op (without any errors). Note, if the function fails 'halfway', it will
+ * *not* try to remove the parts the were created. it *will* create any parent
+ * dirs that are not yet existent.
+ *
+ * @param path the path (missing components will be created, as in 'mkdir -p').
+ * must be non-empty
+ * @param mode the file mode (e.g., 0755)
+ * @param noindex add a .noindex file to the maildir, so it will be excluded
+ * from indexing by 'mu index'
+ *
+ * @return a valid result (!!result) or an Error
+ */
+Result<void> maildir_mkdir(const std::string& path, mode_t mode=0700,
+                          bool noindex=false);
+
+/**
+ * Create a symbolic link to a mail message
+ *
+ * @param src the full path to the source message
+ * @param targetpath the path to the target maildir; ie., *not*
+ * MyMaildir/cur, but just MyMaildir/. The function will figure out
+ * the correct subdir then.
+ * @param unique_names whether to create unique names; should be true unless
+ * for tests.
+ *
+ * @return a valid result (!!result) or an Error
+ */
+Result<void> maildir_link(const std::string& src, const std::string& targetpath,
+                         bool unique_names=true);
+
+/**
+ * Recursively delete all the symbolic links in a directory tree
+ *
+ * @param dir top dir
+ *
+ * @return a valid result (!!result) or an Error
+ */
+Result<void> maildir_clear_links(const std::string& dir);
+
+/**
+ * Move a message file to another maildir. If the target exists, it is
+ * overwritten.
+ *
+ * @param oldpath an absolute file system path to an existing message in an
+ * actual maildir
+ * @param newpath the absolete full path to the target file
+ * @param force_gio force the use of GIO for moving; this is done automatically
+ * when needed; forcing is mostly useful for tests
+ *
+ * @return a valid result (!!result) or an Error
+ */
+Result<void> maildir_move_message(const std::string& oldpath,
+                                 const std::string& newpath,
+                                 bool               force_gio = false);
+
+/**
+ * Determine the target path for a to-be-moved message; i.e. this does not
+ * actually move the message, only calculate the path.
+ *
+ * @param old_path an absolute file system path to an existing message in an
+ * actual maildir
+ * @param root_maildir_path the absolete file system path under which
+ * all maidlirs live.
+ * @param target_maildir the target maildir; note that this the base-level
+ * Maildir, ie. /home/user/Maildir/archive, and must _not_ include the
+ * 'cur' or 'new' part. Note that the target maildir must be on the
+ * same filesystem. Can be empty if the message should not be moved to
+ * a different maildir; note that this may still involve a
+ * move to another directory (say, from new/ to cur/)
+ * @param flags to set for the target (influences the filename, path).
+ * @param new_name whether to change the basename of the file
+ * @param err receives error information
+ *
+ * @return Full path name of the target file or std::nullopt in case
+ * of error
+ */
+Result<std::string>
+maildir_determine_target(const std::string&    old_path,
+                           const std::string&  root_maildir_path,
+                           const std::string&  target_maildir,
+                           Flags               newflags,
+                           bool                new_name);
+
+} // namespace Mu
+
+#endif /*MU_MAILDIR_HH__*/
diff --git a/lib/mu-parser.cc b/lib/mu-parser.cc
new file mode 100644 (file)
index 0000000..3cde7c5
--- /dev/null
@@ -0,0 +1,506 @@
+/*
+**  Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+#include "mu-parser.hh"
+
+#include <algorithm>
+#include <regex>
+#include <limits>
+
+#include "mu-tokenizer.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-error.hh"
+#include "message/mu-message.hh"
+
+using namespace Mu;
+
+// 3 precedence levels: units (NOT,()) > factors (OR) > terms (AND)
+
+// query       ->      <term-1> | ε
+// <term-1>    ->      <factor-1> <term-2> | ε
+// <term-2>    ->      OR|XOR <term-1> | ε
+// <factor-1>  ->      <unit> <factor-2> | ε
+// <factor-2>  ->      [AND]|AND NOT <factor-1> | ε
+// <unit>      ->      [NOT] <term-1> | ( <term-1> ) | <data>
+// <data>       ->      <value> | <range> | <regex>
+// <value>      ->      [field:]value
+// <range>      ->      [field:][lower]..[upper]
+// <regex>      ->      [field:]/regex/
+
+#define BUG(...)                                                                                   \
+       Mu::Error(Error::Code::Internal, format("%u: BUG: ", __LINE__) + format(__VA_ARGS__))
+
+/**
+ * Get the "shortcut"/internal fields for the the given fieldstr or empty if there is none
+ *
+ * @param fieldstr a fieldstr, e.g "subject" or "s" for the subject field
+ *
+ * @return a vector with "exploded" values, with a code and a fullname. E.g. "s" might map
+ * to [<"S","subject">], while "recip" could map to [<"to", "T">, <"cc", "C">, <"bcc", "B">]
+ */
+struct FieldInfo {
+       const std::string field;
+       const std::string prefix;
+       bool              supports_phrase;
+       Field::Id         id;
+};
+using FieldInfoVec = std::vector<FieldInfo>;
+struct Parser::Private {
+       Private(const Store& store, Parser::Flags flags) : store_{store}, flags_{flags} {}
+
+       std::vector<std::string> process_regex(const std::string& field,
+                                              const std::regex&  rx) const;
+
+       Mu::Tree term_1(Mu::Tokens& tokens, WarningVec& warnings) const;
+       Mu::Tree term_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const;
+       Mu::Tree factor_1(Mu::Tokens& tokens, WarningVec& warnings) const;
+       Mu::Tree factor_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const;
+       Mu::Tree unit(Mu::Tokens& tokens, WarningVec& warnings) const;
+       Mu::Tree data(Mu::Tokens& tokens, WarningVec& warnings) const;
+       Mu::Tree range(const FieldInfoVec& fields,
+                      const std::string&  lower,
+                      const std::string&  upper,
+                      size_t              pos,
+                      WarningVec&         warnings) const;
+       Mu::Tree regex(const FieldInfoVec& fields,
+                      const std::string&  v,
+                      size_t              pos,
+                      WarningVec&         warnings) const;
+       Mu::Tree value(const FieldInfoVec& fields,
+                      const std::string&  v,
+                      size_t              pos,
+                      WarningVec&         warnings) const;
+
+      private:
+       const Store& store_;
+       const Parser::Flags  flags_;
+};
+
+static std::string
+process_value(const std::string& field, const std::string& value)
+{
+       const auto id_opt{field_from_name(field)};
+       if (id_opt) {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+               switch (id_opt->id) {
+               case Field::Id::Priority: {
+                       if (!value.empty())
+                               return std::string(1, value[0]);
+               } break;
+               case Field::Id::Flags:
+                       if (const auto info{flag_info(value)}; info)
+                               return std::string(1, info->shortcut_lower());
+                       break;
+               default:
+                       break;
+               }
+#pragma GCC diagnostic pop
+       }
+
+       return value; // XXX prio/flags, etc. alias
+}
+
+static void
+add_field(std::vector<FieldInfo>& fields, Field::Id field_id)
+{
+       const auto field{field_from_id(field_id)};
+       if (!field.shortcut)
+               return; // can't be searched
+
+       fields.emplace_back(FieldInfo{std::string{field.name}, field.xapian_term(),
+                           field.is_indexable_term(), field_id});
+}
+
+static std::vector<FieldInfo>
+process_field(const std::string& field_str, Parser::Flags flags)
+{
+       std::vector<FieldInfo> fields;
+       if (any_of(flags & Parser::Flags::UnitTest)) {
+               add_field(fields, Field::Id::MessageId);
+               return fields;
+       }
+
+       if (field_str == "contact" || field_str == "recip") { // multi fields
+               add_field(fields, Field::Id::To);
+               add_field(fields, Field::Id::Cc);
+               add_field(fields, Field::Id::Bcc);
+               if (field_str == "contact")
+                       add_field(fields, Field::Id::From);
+       } else if (field_str.empty()) {
+               add_field(fields, Field::Id::To);
+               add_field(fields, Field::Id::Cc);
+               add_field(fields, Field::Id::Bcc);
+               add_field(fields, Field::Id::From);
+               add_field(fields, Field::Id::Subject);
+               add_field(fields, Field::Id::BodyText);
+       } else if (const auto field_opt{field_from_name(field_str)}; field_opt)
+               add_field(fields, field_opt->id);
+
+       return fields;
+}
+
+static bool
+is_range_field(const std::string& field_str)
+{
+       if (const auto field_opt{field_from_name(field_str)}; !field_opt)
+               return false;
+       else
+               return field_opt->is_range();
+}
+
+struct MyRange {
+       std::string lower;
+       std::string upper;
+};
+
+static MyRange
+process_range(const std::string& field_str,
+             const std::string& lower, const std::string& upper)
+{
+       const auto field_opt{field_from_name(field_str)};
+       if (!field_opt)
+               return {lower, upper};
+
+       std::string l2 = lower;
+       std::string u2 = upper;
+       constexpr auto upper_limit = std::numeric_limits<int64_t>::max();
+
+       if (field_opt->id == Field::Id::Date || field_opt->id == Field::Id::Changed) {
+               l2 = to_lexnum(parse_date_time(lower, true).value_or(0));
+               u2 = to_lexnum(parse_date_time(upper, false).value_or(upper_limit));
+       } else if (field_opt->id == Field::Id::Size) {
+               l2 = to_lexnum(parse_size(lower, true).value_or(0));
+               u2 = to_lexnum(parse_size(upper, false).value_or(upper_limit));
+       }
+
+       return {l2, u2};
+}
+
+std::vector<std::string>
+Parser::Private::process_regex(const std::string& field_str,
+                              const std::regex& rx) const
+{
+       const auto field_opt{field_from_name(field_str)};
+       if (!field_opt)
+               return {};
+
+       const auto prefix{field_opt->xapian_term()};
+       std::vector<std::string> terms;
+       store_.for_each_term(field_opt->id, [&](auto&& str) {
+               auto val{str.c_str() + 1}; // strip off the Xapian prefix.
+               if (std::regex_search(val, rx))
+                       terms.emplace_back(std::move(val));
+               return true;
+       });
+
+       return terms;
+}
+
+static Token
+look_ahead(const Mu::Tokens& tokens)
+{
+       return tokens.front();
+}
+
+static Mu::Tree
+empty()
+{
+       return {{Node::Type::Empty}};
+}
+
+Mu::Tree
+Parser::Private::value(const FieldInfoVec& fields,
+                      const std::string&  v,
+                      size_t              pos,
+                      WarningVec&         warnings) const
+{
+       auto val = utf8_flatten(v);
+
+       if (fields.empty())
+               throw BUG("expected one or more fields");
+
+       if (fields.size() == 1) {
+               const auto item = fields.front();
+               return Tree({Node::Type::Value,
+                               FieldValue{item.id, process_value(item.field, val)}});
+       }
+
+       // a 'multi-field' such as "recip:"
+       Tree tree(Node{Node::Type::OpOr});
+       for (const auto& item : fields)
+               tree.add_child(Tree({Node::Type::Value,
+                                       FieldValue{item.id,
+                                            process_value(item.field, val)}}));
+       return tree;
+}
+
+Mu::Tree
+Parser::Private::regex(const FieldInfoVec& fields,
+                      const std::string&  v,
+                      size_t              pos,
+                      WarningVec&         warnings) const
+{
+       if (v.length() < 2)
+               throw BUG("expected regexp, got '%s'", v.c_str());
+
+       const auto rxstr = utf8_flatten(v.substr(1, v.length() - 2));
+
+       try {
+               Tree tree(Node{Node::Type::OpOr});
+               const auto rx = std::regex(rxstr);
+               for (const auto& field : fields) {
+                       const auto terms = process_regex(field.field, rx);
+                       for (const auto& term : terms) {
+                               tree.add_child(Tree({Node::Type::Value,
+                                                       FieldValue{field.id, term}}));
+                       }
+               }
+
+               if (tree.children.empty())
+                       return empty();
+               else
+                       return tree;
+
+       } catch (...) {
+               // fallback
+               warnings.push_back({pos, "invalid regexp"});
+               return value(fields, v, pos, warnings);
+       }
+}
+
+Mu::Tree
+Parser::Private::range(const FieldInfoVec& fields,
+                      const std::string&  lower,
+                      const std::string&  upper,
+                      size_t              pos,
+                      WarningVec&         warnings) const
+{
+       if (fields.empty())
+               throw BUG("expected field");
+
+       const auto& field = fields.front();
+       if (!is_range_field(field.field))
+               return value(fields, lower + ".." + upper, pos, warnings);
+
+       auto prange = process_range(field.field, lower, upper);
+       if (prange.lower > prange.upper)
+               prange = process_range(field.field, upper, lower);
+
+       return Tree({Node::Type::Range,
+                       FieldValue{field.id, prange.lower, prange.upper}});
+}
+
+Mu::Tree
+Parser::Private::data(Mu::Tokens& tokens, WarningVec& warnings) const
+{
+       const auto token = look_ahead(tokens);
+       if (token.type != Token::Type::Data)
+               warnings.push_back({token.pos, "expected: value"});
+
+       tokens.pop_front();
+
+       std::string field, val;
+       const auto  col = token.str.find(":");
+       if (col != 0 && col != std::string::npos && col != token.str.length() - 1) {
+               field = token.str.substr(0, col);
+               val   = token.str.substr(col + 1);
+       } else
+               val = token.str;
+
+       auto fields = process_field(field, flags_);
+       if (fields.empty()) { // not valid field...
+               warnings.push_back({token.pos, format("invalid field '%s'", field.c_str())});
+               fields = process_field("", flags_);
+               // fallback, treat the whole of foo:bar as a value
+               return value(fields, field + ":" + val, token.pos, warnings);
+       }
+
+       // does it look like a regexp?
+       if (val.length() >= 2)
+               if (val[0] == '/' && val[val.length() - 1] == '/')
+                       return regex(fields, val, token.pos, warnings);
+
+       // does it look like a range?
+       const auto dotdot = val.find("..");
+       if (dotdot != std::string::npos)
+               return range(fields,
+                            val.substr(0, dotdot),
+                            val.substr(dotdot + 2),
+                            token.pos,
+                            warnings);
+       else if (is_range_field(fields.front().field)) {
+               // range field without a range - treat as field:val..val
+               return range(fields, val, val, token.pos, warnings);
+       }
+
+       // if nothing else, it's a value.
+       return value(fields, val, token.pos, warnings);
+}
+
+Mu::Tree
+Parser::Private::unit(Mu::Tokens& tokens, WarningVec& warnings) const
+{
+       if (tokens.empty()) {
+               warnings.push_back({0, "expected: unit"});
+               return empty();
+       }
+
+       const auto token = look_ahead(tokens);
+
+       if (token.type == Token::Type::Not) {
+               tokens.pop_front();
+               Tree tree{{Node::Type::OpNot}};
+               tree.add_child(unit(tokens, warnings));
+               return tree;
+       }
+
+       if (token.type == Token::Type::Open) {
+               tokens.pop_front();
+               auto tree = term_1(tokens, warnings);
+               if (tokens.empty())
+                       warnings.push_back({token.pos, "expected: ')'"});
+               else {
+                       const auto token2 = look_ahead(tokens);
+                       if (token2.type == Token::Type::Close)
+                               tokens.pop_front();
+                       else {
+                               warnings.push_back(
+                                   {token2.pos,
+                                    std::string("expected: ')' but got ") + token2.str});
+                       }
+               }
+               return tree;
+       }
+
+       return data(tokens, warnings);
+}
+
+Mu::Tree
+Parser::Private::factor_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const
+{
+       if (tokens.empty())
+               return empty();
+
+       const auto token = look_ahead(tokens);
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+       switch (token.type) {
+       case Token::Type::And: {
+               tokens.pop_front();
+               op = Node::Type::OpAnd;
+       } break;
+
+       case Token::Type::Open:
+       case Token::Type::Data:
+       case Token::Type::Not:
+               op = Node::Type::OpAnd; // implicit AND
+               break;
+
+       default:
+               return empty();
+       }
+#pragma GCC diagnostic pop
+
+       return factor_1(tokens, warnings);
+}
+
+Mu::Tree
+Parser::Private::factor_1(Mu::Tokens& tokens, WarningVec& warnings) const
+{
+       Node::Type op{Node::Type::Invalid};
+
+       auto t  = unit(tokens, warnings);
+       auto a2 = factor_2(tokens, op, warnings);
+
+       if (a2.empty())
+               return t;
+
+       Tree tree{{op}};
+       tree.add_child(std::move(t));
+       tree.add_child(std::move(a2));
+
+       return tree;
+}
+
+Mu::Tree
+Parser::Private::term_2(Mu::Tokens& tokens, Node::Type& op, WarningVec& warnings) const
+{
+       if (tokens.empty())
+               return empty();
+
+       const auto token = look_ahead(tokens);
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+       switch (token.type) {
+       case Token::Type::Or: op = Node::Type::OpOr; break;
+       case Token::Type::Xor: op = Node::Type::OpXor; break;
+       default:
+               if (token.type != Token::Type::Close)
+                       warnings.push_back({token.pos, "expected OR|XOR"});
+               return empty();
+       }
+#pragma GCC diagnostic pop
+
+       tokens.pop_front();
+
+       return term_1(tokens, warnings);
+}
+
+Mu::Tree
+Parser::Private::term_1(Mu::Tokens& tokens, WarningVec& warnings) const
+{
+       Node::Type op{Node::Type::Invalid};
+
+       auto t  = factor_1(tokens, warnings);
+       auto o2 = term_2(tokens, op, warnings);
+
+       if (o2.empty())
+               return t;
+       else {
+               Tree tree{{op}};
+               tree.add_child(std::move(t));
+               tree.add_child(std::move(o2));
+               return tree;
+       }
+}
+
+Mu::Parser::Parser(const Store& store, Parser::Flags flags) :
+       priv_{std::make_unique<Private>(store, flags)}
+{
+}
+
+Mu::Parser::~Parser() = default;
+
+Mu::Tree
+Mu::Parser::parse(const std::string& expr, WarningVec& warnings) const
+{
+       try {
+               auto tokens = tokenize(expr);
+               if (tokens.empty())
+                       return empty();
+               else
+                       return priv_->term_1(tokens, warnings);
+
+       } catch (const std::runtime_error& ex) {
+               std::cerr << ex.what() << std::endl;
+               return empty();
+       }
+}
diff --git a/lib/mu-parser.hh b/lib/mu-parser.hh
new file mode 100644 (file)
index 0000000..65adc64
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+**  Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#ifndef __PARSER_HH__
+#define __PARSER_HH__
+
+#include "utils/mu-utils.hh"
+#include <string>
+#include <vector>
+#include <memory>
+
+#include <mu-tree.hh>
+#include <mu-store.hh>
+
+// A simple recursive-descent parser for queries. Follows the Xapian syntax,
+// but better handles non-alphanum; also implements regexp
+
+namespace Mu {
+
+/**
+ * A parser warning
+ *
+ */
+struct Warning {
+       size_t            pos{}; /**< pos in string */
+       const std::string msg;   /**< warning message */
+
+       /**
+        * operator==
+        *
+        * @param rhs right-hand side
+        *
+        * @return true if rhs is equal to this; false otherwise
+        */
+       bool operator==(const Warning& rhs) const { return pos == rhs.pos && msg == rhs.msg; }
+};
+using WarningVec = std::vector<Warning>;
+
+/**
+ * operator<<
+ *
+ * @param os an output stream
+ * @param w a warning
+ *
+ * @return the updated output stream
+ */
+inline std::ostream&
+operator<<(std::ostream& os, const Warning& w)
+{
+       os << w.pos << ":" << w.msg;
+       return os;
+}
+
+class Parser {
+      public:
+       enum struct Flags { None = 0, UnitTest = 1 << 0 };
+
+       /**
+        * Construct a query parser object
+        *
+        * @param store a store object ptr, or none
+        */
+       Parser(const Store& store, Flags = Flags::None);
+       /**
+        * DTOR
+        *
+        */
+       ~Parser();
+
+       /**
+        * Parse a query string
+        *
+        * @param query a query string
+        * @param warnings vec to receive warnings
+        *
+        * @return a parse-tree
+        */
+
+       Tree parse(const std::string& query, WarningVec& warnings) const;
+
+      private:
+       struct Private;
+       std::unique_ptr<Private> priv_;
+};
+
+MU_ENABLE_BITOPS(Parser::Flags);
+
+} // namespace Mu
+
+#endif /* __PARSER_HH__ */
diff --git a/lib/mu-query-match-deciders.cc b/lib/mu-query-match-deciders.cc
new file mode 100644 (file)
index 0000000..999d609
--- /dev/null
@@ -0,0 +1,223 @@
+/*
+** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-query-match-deciders.hh"
+
+#include "mu-query-results.hh"
+#include "utils/mu-option.hh"
+
+using namespace Mu;
+
+
+// We use a MatchDecider to gather information about the matches, and decide
+// whether to include them in the results.
+//
+// Note that to include the "related" messages, we need _two_ queries; the first
+// one to get the initial matches (called the Leader-Query) and a Related-Query,
+// to get the Leader matches + all messages that have a thread-id seen in the
+// Leader matches.
+//
+// We use the MatchDecider to gather information and use it for both queries.
+
+struct MatchDecider : public Xapian::MatchDecider {
+       MatchDecider(QueryFlags qflags, DeciderInfo& info) : qflags_{qflags}, decider_info_{info} {}
+       /**
+        * Update the match structure with unreadable/duplicate flags
+        *
+        * @param doc a Xapian document.
+        *
+        * @return a new QueryMatch object
+        */
+       QueryMatch make_query_match(const Xapian::Document& doc) const
+       {
+               QueryMatch qm{};
+
+               auto msgid{opt_string(doc, Field::Id::MessageId)
+                              .value_or(*opt_string(doc, Field::Id::Path))};
+               if (!decider_info_.message_ids.emplace(std::move(msgid)).second)
+                       qm.flags |= QueryMatch::Flags::Duplicate;
+
+               const auto path{opt_string(doc, Field::Id::Path)};
+               if (!path || ::access(path->c_str(), R_OK) != 0)
+                       qm.flags |= QueryMatch::Flags::Unreadable;
+
+               return qm;
+       }
+
+       /**
+        * Should this message be included in the results?
+        *
+        * @param qm a query match
+        *
+        * @return true or false
+        */
+       bool should_include(const QueryMatch& qm) const
+       {
+               if (any_of(qflags_ & QueryFlags::SkipDuplicates) &&
+                   any_of(qm.flags & QueryMatch::Flags::Duplicate))
+                       return false;
+
+               if (any_of(qflags_ & QueryFlags::SkipUnreadable) &&
+                   any_of(qm.flags & QueryMatch::Flags::Unreadable))
+                       return false;
+
+               return true;
+       }
+       /**
+        * Gather thread ids from this match.
+        *
+        * @param doc the document (message)
+        *
+        */
+       void gather_thread_ids(const Xapian::Document& doc) const
+       {
+               auto thread_id{opt_string(doc, Field::Id::ThreadId)};
+               if (thread_id)
+                       decider_info_.thread_ids.emplace(std::move(*thread_id));
+       }
+
+protected:
+       const QueryFlags qflags_;
+       DeciderInfo&     decider_info_;
+
+private:
+       Option<std::string> opt_string(const Xapian::Document& doc, Field::Id id) const noexcept {
+               const auto value_no{field_from_id(id).value_no()};
+               std::string val = xapian_try([&] { return doc.get_value(value_no); }, std::string{""});
+               if (val.empty())
+                       return Nothing;
+               else
+                       return Some(std::move(val));
+       }
+};
+
+struct MatchDeciderLeader final : public MatchDecider {
+       MatchDeciderLeader(QueryFlags qflags, DeciderInfo& info) : MatchDecider(qflags, info) {}
+       /**
+        * operator()
+        *
+        * This receives the documents considered during a Xapian query, and
+        * is to return either true (keep) or false (ignore)
+        *
+        * We use this to potentiallly avoid certain messages (documents):
+        * - with QueryFlags::SkipUnreadable this will return false for message
+        *   that are not readable in the file-system
+        * - with QueryFlags::SkipDuplicates this will return false for messages
+        *   whose message-id was seen before.
+        *
+        * Even if we do not skip these messages entirely, we remember whether
+        * they were unreadable/duplicate (in the QueryMatch::Flags), so we can
+        * quickly find that info when doing the second 'related' query.
+        *
+        * The "leader" query. Matches here get the Leader flag unless they are
+        * duplicates / unreadable. We check the duplicate/readable status
+        * regardless of whether SkipDuplicates/SkipUnreadable was passed
+        * (to gather that information); however those flags
+        * affect our true/false verdict.
+        *
+        * @param doc xapian document
+        *
+        * @return true or false
+        */
+       bool operator()(const Xapian::Document& doc) const override {
+               // by definition, we haven't seen the docid before,
+               // so no need to search
+               auto it = decider_info_.matches.emplace(doc.get_docid(), make_query_match(doc));
+               it.first->second.flags |= QueryMatch::Flags::Leader;
+
+               return should_include(it.first->second);
+       }
+};
+
+std::unique_ptr<Xapian::MatchDecider>
+Mu::make_leader_decider(QueryFlags qflags, DeciderInfo& info)
+{
+       return std::make_unique<MatchDeciderLeader>(qflags, info);
+}
+
+struct MatchDeciderRelated final : public MatchDecider {
+       MatchDeciderRelated(QueryFlags qflags, DeciderInfo& info) : MatchDecider(qflags, info) {}
+       /**
+        * operator()
+        *
+        * This receives the documents considered during a Xapian query, and
+        * is to return either true (keep) or false (ignore)
+        *
+        * We use this to potentially avoid certain messages (documents):
+        * - with QueryFlags::SkipUnreadable this will return false for message
+        *   that are not readable in the file-system
+        * - with QueryFlags::SkipDuplicates this will return false for messages
+        *   whose message-id was seen before.
+        *
+        * Unlike in the "leader" decider (scroll up), we don't need to remember
+        * messages we won't include.
+        *
+        * @param doc xapian document
+        *
+        * @return true or false
+        */
+       bool operator()(const Xapian::Document& doc) const override {
+               // we may have seen this match in the "Leader" query.
+               const auto it = decider_info_.matches.find(doc.get_docid());
+               if (it != decider_info_.matches.end())
+                       return should_include(it->second);
+
+               auto qm{make_query_match(doc)};
+               if (should_include(qm)) {
+                       qm.flags |= QueryMatch::Flags::Related;
+                       decider_info_.matches.emplace(doc.get_docid(), std::move(qm));
+                       return true;
+               } else
+                       return false; // nope.
+       }
+};
+
+std::unique_ptr<Xapian::MatchDecider>
+Mu::make_related_decider(QueryFlags qflags, DeciderInfo& info)
+{
+       return std::make_unique<MatchDeciderRelated>(qflags, info);
+}
+
+struct MatchDeciderThread final : public MatchDecider {
+       MatchDeciderThread(QueryFlags qflags, DeciderInfo& info) : MatchDecider{qflags, info} {}
+       /**
+        * operator()
+        *
+        * This receives the documents considered during a Xapian query, and
+        * is to return either true (keep) or false (ignore)
+        *
+        * Only include documents that earlier checks have decided to include.
+        *
+        * @param doc xapian document
+        *
+        * @return true or false
+        */
+       bool operator()(const Xapian::Document& doc) const override {
+               // we may have seen this match in the "Leader" query,
+               // or in the second (unbuounded) related query;
+               const auto it{decider_info_.matches.find(doc.get_docid())};
+               return it != decider_info_.matches.end() && !it->second.thread_path.empty();
+       }
+};
+
+std::unique_ptr<Xapian::MatchDecider>
+Mu::make_thread_decider(QueryFlags qflags, DeciderInfo& info)
+{
+       return std::make_unique<MatchDeciderThread>(qflags, info);
+}
diff --git a/lib/mu-query-match-deciders.hh b/lib/mu-query-match-deciders.hh
new file mode 100644 (file)
index 0000000..91488a5
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_QUERY_MATCH_DECIDERS_HH__
+#define MU_QUERY_MATCH_DECIDERS_HH__
+
+#include <unordered_set>
+#include <unordered_map>
+#include <memory>
+
+#include <xapian.h>
+
+#include "mu-query-results.hh"
+
+namespace Mu {
+using StringSet = std::unordered_set<std::string>;
+
+struct DeciderInfo {
+       QueryMatches matches;
+       StringSet    thread_ids;
+       StringSet    message_ids;
+};
+
+/**
+ * Make a "leader" decider, that is, a MatchDecider for either a singular or the
+ * first query in the leader/related pair of queries. Gather information for
+ * threading, and the subsequent "related" query.
+ *
+ * @param qflags     query flags
+ * @param match_info receives information about the matches.
+ *
+ * @return a unique_ptr to a match decider.
+ */
+std::unique_ptr<Xapian::MatchDecider> make_leader_decider(QueryFlags qflags, DeciderInfo& info);
+
+/**
+ * Make a "related" decider, that is, a MatchDecider for the second query
+ * in the leader/related pair of queries.
+ *
+ * @param qflags     query flags
+ * @param match_info receives information about the matches.
+ *
+ * @return a unique_ptr to a match decider.
+ */
+std::unique_ptr<Xapian::MatchDecider> make_related_decider(QueryFlags qflags, DeciderInfo& info);
+
+/**
+ * Make a "thread" decider, that is, a MatchDecider that removes all but the
+ * document excepts for the ones found during initial/related searches.
+ *
+ * @param qflags     query flags
+ * @param match_info receives information about the matches.
+ *
+ * @return a unique_ptr to a match decider.
+ */
+std::unique_ptr<Xapian::MatchDecider> make_thread_decider(QueryFlags qflags, DeciderInfo& info);
+
+} // namespace Mu
+
+#endif /* MU_QUERY_MATCH_DECIDERS_HH__ */
diff --git a/lib/mu-query-results.hh b/lib/mu-query-results.hh
new file mode 100644 (file)
index 0000000..7b8a72e
--- /dev/null
@@ -0,0 +1,413 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_QUERY_RESULTS_HH__
+#define MU_QUERY_RESULTS_HH__
+
+#include <algorithm>
+#include <limits>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <limits>
+#include <ostream>
+#include <cmath>
+#include <memory>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <xapian.h>
+#include <glib.h>
+
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+#include <utils/mu-xapian-utils.hh>
+
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+/**
+ * This implements a QueryResults structure, which capture the results of a
+ * Xapian query, and a QueryResultsIterator, which gives C++-compliant iterator
+ * to go over the results. and finally QueryThreader (in query-threader.cc) which
+ * calculates the threads, using the JWZ algorithm.
+ */
+
+/// Flags that influence now matches are presented (or skipped)
+enum struct QueryFlags {
+       None           = 0,      /**< no flags */
+       Descending     = 1 << 0, /**< sort z->a */
+       SkipUnreadable = 1 << 1, /**< skip unreadable msgs */
+       SkipDuplicates = 1 << 2, /**< skip duplicate msgs */
+       IncludeRelated = 1 << 3, /**< include related msgs */
+       Threading      = 1 << 4, /**< calculate threading info */
+       // internal
+       Leader = 1 << 5, /**< This is the leader query (for internal use
+                         * only)*/
+};
+MU_ENABLE_BITOPS(QueryFlags);
+
+/// Stores all the essential information for sorting the results.
+struct QueryMatch {
+       /// Flags for a match (message) found
+       enum struct Flags {
+               None       = 0,      /**< No Flags */
+               Leader     = 1 << 0, /**< Mark direct matches as leader */
+               Related    = 1 << 1, /**< A related message */
+               Unreadable = 1 << 2, /**< No readable file */
+               Duplicate  = 1 << 3, /**< Message-id seen before */
+
+               Root     = 1 << 10, /**< Is this the thread-root? */
+               First    = 1 << 11, /**< Is this the first message in a thread? */
+               Last     = 1 << 12, /**< Is this the last message in a thread? */
+               Orphan   = 1 << 13, /**< Is this message without a parent? */
+               HasChild = 1 << 14, /**< Does this message have a child? */
+
+               ThreadSubject = 1 << 20, /**< Message holds subject for (sub)thread */
+       };
+
+       Flags       flags{Flags::None}; /**< Flags */
+       std::string date_key;           /**< The date-key (for sorting all sub-root levels) */
+       // the thread subject is the subject of the first message in a thread,
+       // and any message that has a different subject compared to its predecessor
+       // (ignoring prefixes such as Re:)
+       //
+       // otherwise, it is empty.
+       std::string subject;        /**< subject for this message */
+       size_t      thread_level{}; /**< The thread level */
+       std::string thread_path;    /**< The hex-numerial path in the thread, ie. '00:01:0a' */
+       std::string thread_date;    /**< date of newest message in thread */
+
+       bool operator<(const QueryMatch& rhs) const { return date_key < rhs.date_key; }
+
+       bool has_flag(Flags flag) const;
+};
+
+MU_ENABLE_BITOPS(QueryMatch::Flags);
+
+inline bool
+QueryMatch::has_flag(QueryMatch::Flags flag) const
+{
+       return any_of(flags & flag);
+}
+
+inline std::ostream&
+operator<<(std::ostream& os, QueryMatch::Flags mflags)
+{
+       if (mflags == QueryMatch::Flags::None) {
+               os << "<none>";
+               return os;
+       }
+
+       if (any_of(mflags & QueryMatch::Flags::Leader))
+               os << "leader ";
+       if (any_of(mflags & QueryMatch::Flags::Unreadable))
+               os << "unreadable ";
+       if (any_of(mflags & QueryMatch::Flags::Duplicate))
+               os << "dup ";
+
+       if (any_of(mflags & QueryMatch::Flags::Root))
+               os << "root ";
+       if (any_of(mflags & QueryMatch::Flags::Related))
+               os << "related ";
+       if (any_of(mflags & QueryMatch::Flags::First))
+               os << "first ";
+       if (any_of(mflags & QueryMatch::Flags::Last))
+               os << "last ";
+       if (any_of(mflags & QueryMatch::Flags::Orphan))
+               os << "orphan ";
+       if (any_of(mflags & QueryMatch::Flags::HasChild))
+               os << "has-child ";
+
+       return os;
+}
+
+using QueryMatches = std::unordered_map<Xapian::docid, QueryMatch>;
+
+inline std::ostream&
+operator<<(std::ostream& os, const QueryMatch& qmatch)
+{
+       os << "qm:[" << qmatch.thread_path << "]: " // " (" << qmatch.thread_level << "): "
+          << "> date:<" << qmatch.date_key << "> "
+          << "flags:{" << qmatch.flags << "}";
+
+       return os;
+}
+
+///
+/// This is a view over the Xapian::MSet, which can optionally filter unreadable
+/// / duplicate messages.
+///
+/// Note, we internally skip unreadable/duplicate messages (when asked too); those
+/// skipped ones do _not_ count towards the max_size
+///
+class QueryResultsIterator {
+public:
+       using iterator_category = std::output_iterator_tag;
+       using value_type        = Message;
+       using difference_type   = void;
+       using pointer           = void;
+       using reference         = void;
+
+       QueryResultsIterator(Xapian::MSetIterator mset_it, QueryMatches& query_matches)
+               : mset_it_{mset_it}, query_matches_{query_matches} {
+       }
+
+       /**
+        * Increment the iterator (we don't support post-increment)
+        *
+        * @return an updated iterator, or end() if we were already at end()
+        */
+       QueryResultsIterator& operator++() {
+               ++mset_it_;
+               mdoc_ = Nothing;
+               return *this;
+       }
+
+       /**
+        * (Non)Equivalence operators
+        *
+        * @param rhs some other iterator
+        *
+        * @return true or false
+        */
+       bool operator==(const QueryResultsIterator& rhs) const { return mset_it_ == rhs.mset_it_; }
+       bool operator!=(const QueryResultsIterator& rhs) const { return mset_it_ != rhs.mset_it_; }
+
+       QueryResultsIterator&       operator*() { return *this; }
+       const QueryResultsIterator& operator*() const { return *this; }
+
+
+       /**
+        * Get the Xapian::Document this iterator is pointing at,
+        * or an empty document when looking at end().
+        *
+        * @return a document
+        */
+       Option<Xapian::Document> document() const {
+               return xapian_try([this]()->Option<Xapian::Document> {
+                       auto doc{mset_it_.get_document()};
+                       if (doc.get_docid() == 0)
+                               return Nothing;
+                       else
+                               return Some(std::move(doc));
+                       }, Nothing);
+       }
+
+
+       /**
+        * get the corresponding Message for this iter, if any
+        *
+        * @return a Message or Nothing
+        */
+       Option<Message> message() const {
+               if (auto&& xdoc{document()}; !xdoc)
+                       return Nothing;
+               else if (auto&& doc{Message::make_from_document(std::move(xdoc.value()))};
+                        !doc)
+                       return Nothing;
+               else
+                       return Some(std::move(doc.value()));
+       }
+
+       /**
+        * Get the doc-id for the document this iterator is pointing at, or 0
+        * when looking at end.
+        *
+        * @return a doc-id.
+        */
+       Xapian::docid doc_id() const { return *mset_it_; }
+
+       /**
+        * Get the message-id for the document (message) this iterator is
+        * pointing at, or not when not available
+        *
+        * @return a message-id
+        */
+       Option<std::string> message_id() const noexcept {
+               return opt_string(Field::Id::MessageId);
+       }
+
+       /**
+        * Get the thread-id for the document (message) this iterator is
+        * pointing at, or Nothing.
+        *
+        * @return a message-id
+        */
+       Option<std::string> thread_id() const noexcept {
+               return opt_string(Field::Id::ThreadId);
+       }
+
+       /**
+        * Get the file-system path for the document (message) this iterator is
+        * pointing at, or Nothing.
+        *
+        * @return a filesystem path
+        */
+       Option<std::string> path() const noexcept {
+               return opt_string(Field::Id::Path);
+       }
+
+       /**
+        * Get the a sortable date str for the document (message) the iterator
+        * is pointing at. pointing at, or Nothing. This (encoded) string
+        * has the same sort-order as the corresponding date.
+        *
+        * @return a filesystem path
+        */
+       Option<std::string> date_str() const noexcept {
+               return opt_string(Field::Id::Date);
+       }
+
+       /**
+        * Get the subject for the document (message) this iterator is pointing
+        * at.
+        *
+        * @return the subject
+        */
+       Option<std::string> subject() const noexcept {
+               return opt_string(Field::Id::Subject);
+       }
+
+       /**
+        * Get the references for the document (messages) this is iterator is
+        * pointing at, or empty if pointing at end of if no references are
+        * available.
+        *
+        * @return references
+        */
+       std::vector<std::string> references() const noexcept {
+               return mu_document().string_vec_value(Field::Id::References);
+       }
+
+       /**
+        * Get some value from the document, or Nothing if empty.
+        *
+        * @param id a message field id
+        *
+        * @return the value
+        */
+       Option<std::string> opt_string(Field::Id id) const noexcept {
+               if (auto&& val{mu_document().string_value(id)}; val.empty())
+                       return Nothing;
+               else
+                       return Some(std::move(val));
+       }
+
+       /**
+        * Get the Query match info for this message.
+        *
+        * @return the match info.
+        */
+       QueryMatch& query_match() {
+               g_assert(query_matches_.find(doc_id()) != query_matches_.end());
+               return query_matches_.find(doc_id())->second;
+       }
+       const QueryMatch& query_match() const {
+               g_assert(query_matches_.find(doc_id()) != query_matches_.end());
+               return query_matches_.find(doc_id())->second;
+       }
+
+private:
+       /**
+        * Get a (cached) reference for the Mu::Document corresponding
+        * to the current iter.
+        *
+        * @return cached mu document,
+        */
+       const Mu::Document& mu_document() const {
+               if (!mdoc_) {
+                       if (auto xdoc = document(); !xdoc)
+                               std::runtime_error("iter without document");
+                       else
+                               mdoc_ = Mu::Document{xdoc.value()};
+               }
+               return mdoc_.value();
+       }
+
+       mutable Option<Mu::Document>    mdoc_; // cache.
+       Xapian::MSetIterator            mset_it_;
+       QueryMatches&                   query_matches_;
+};
+
+constexpr auto MaxQueryResultsSize = std::numeric_limits<size_t>::max();
+
+class QueryResults {
+public:
+       /// Helper types
+       using iterator       = QueryResultsIterator;
+       using const_iterator = const iterator;
+
+       /**
+        * Construct a QueryResults object
+        *
+        * @param mset an Xapian::MSet with matches
+        */
+       QueryResults(const Xapian::MSet& mset, QueryMatches&& query_matches)
+           : mset_{mset}, query_matches_{std::move(query_matches)}
+       {
+       }
+       /**
+        * Is this QueryResults object empty (ie., no matches)?
+        *
+        * @return true are false
+        */
+       bool empty() const { return mset_.empty(); }
+
+       /**
+        * Get the number of matches in this QueryResult
+        *
+        * @return number of matches
+        */
+       size_t size() const { return mset_.size(); }
+
+       /**
+        * Get the begin iterator to the results.
+        *
+        * @return iterator
+        */
+       const iterator begin() const { return QueryResultsIterator(mset_.begin(), query_matches_); }
+
+       /**
+        * Get the end iterator to the results.
+        *
+        * @return iterator
+        */
+       const_iterator end() const { return QueryResultsIterator(mset_.end(), query_matches_); }
+
+       /**
+        * Get the query-matches for these QueryResults. The non-const
+        * version can be use to _steal_ the query results, by moving
+        * them.
+        *
+        * @return query-matches
+        */
+       const QueryMatches& query_matches() const { return query_matches_; }
+       QueryMatches&       query_matches() { return query_matches_; }
+
+private:
+       const Xapian::MSet   mset_;
+       mutable QueryMatches query_matches_;
+};
+
+} // namespace Mu
+
+#endif /* MU_QUERY_RESULTS_HH__ */
diff --git a/lib/mu-query-threads.cc b/lib/mu-query-threads.cc
new file mode 100644 (file)
index 0000000..22a3b7a
--- /dev/null
@@ -0,0 +1,919 @@
+/*
+** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-query-threads.hh"
+#include <message/mu-message.hh>
+
+#include <set>
+#include <unordered_set>
+#include <list>
+#include <cassert>
+#include <cstring>
+#include <iostream>
+#include <iomanip>
+
+#include <utils/mu-option.hh>
+
+using namespace Mu;
+
+struct Container {
+       using Containers = std::vector<Container*>;
+
+       Container() = default;
+       Container(Option<QueryMatch&> msg) : query_match{msg} {}
+       Container(const Container&) = delete;
+       Container(Container&&)      = default;
+
+       void add_child(Container& new_child)
+       {
+               new_child.parent = this;
+               children.emplace_back(&new_child);
+       }
+       void remove_child(Container& child)
+       {
+               children.erase(find_child(child));
+               assert(!has_child(child));
+       }
+
+       Containers::iterator find_child(Container& child)
+       {
+               return std::find_if(children.begin(), children.end(), [&](auto&& c) {
+                       return c == &child;
+               });
+       }
+       Containers::const_iterator find_child(Container& child) const
+       {
+               return std::find_if(children.begin(), children.end(), [&](auto&& c) {
+                       return c == &child;
+               });
+       }
+       bool has_child(Container& child) const { return find_child(child) != children.cend(); }
+
+       bool is_reachable(Container* other) const
+       {
+               auto up{ur_parent()};
+               return up && up == other->ur_parent();
+       }
+       template <typename Func> void for_each_child(Func&& func)
+       {
+               auto it{children.rbegin()};
+               while (it != children.rend()) {
+                       auto next = std::next(it);
+                       func(*it);
+                       it = next;
+               }
+       }
+       // During sorting, this is the cached value for the (recursive) date-key
+       // of this container -- ie.. either the one from the first of its
+       // children, or from its query-match, if it has no children.
+       //
+       // Note that the sub-root-levels of threads are always sorted by date,
+       // in ascending order, regardless of whatever sorting was specified for
+       // the root-level.
+
+       std::string thread_date_key;
+
+       Option<QueryMatch&> query_match;
+       bool                is_nuked{};
+       Container*          parent{};
+       Containers          children;
+
+       using ContainerVec = std::vector<Container*>;
+
+      private:
+       const Container* ur_parent() const
+       {
+               assert(this->parent != this);
+               return parent ? parent->ur_parent() : this;
+       }
+};
+
+using Containers   = Container::Containers;
+using ContainerVec = Container::ContainerVec;
+
+static std::ostream&
+operator<<(std::ostream& os, const Container& container)
+{
+       os << "container: " << std::right << std::setw(10) << &container
+          << ": parent: " << std::right << std::setw(10) << container.parent << " ["
+          << container.thread_date_key << "]"
+          << "\n  children: ";
+
+       for (auto&& c : container.children)
+               os << std::right << std::setw(10) << c << " ";
+
+       os << (container.is_nuked ? " nuked" : "");
+
+       if (container.query_match)
+               os << "\n  " << container.query_match.value();
+
+       return os;
+}
+
+using IdTable  = std::unordered_map<std::string, Container>;
+using DupTable = std::multimap<std::string, Container>;
+
+static void
+handle_duplicates(IdTable& id_table, DupTable& dup_table)
+{
+       size_t n{};
+
+       for (auto&& dup : dup_table) {
+               const auto msgid{dup.first};
+               auto       it = id_table.find(msgid);
+               if (it == id_table.end())
+                       continue;
+
+               // add duplicates as fake children
+               char buf[32];
+               ::snprintf(buf, sizeof(buf), "dup-%zu", ++n);
+               it->second.add_child(id_table.emplace(buf, std::move(dup.second)).first->second);
+       }
+}
+
+template <typename QueryResultsType>
+static IdTable
+determine_id_table(QueryResultsType& qres)
+{
+       // 1. For each query_match
+       IdTable  id_table;
+       DupTable dups;
+       for (auto&& mi : qres) {
+               const auto msgid{mi.message_id().value_or(*mi.path())};
+               // Step 0 (non-JWZ): filter out dups, handle those at the end
+               if (mi.query_match().has_flag(QueryMatch::Flags::Duplicate)) {
+                       dups.emplace(msgid, mi.query_match());
+                       continue;
+               }
+               // 1.A If id_table contains an empty Container for this ID:
+               // Store this query_match (query_match) in the Container's query_match (value) slot.
+               // Else:
+               //   Create a new Container object holding this query_match (query-match);
+               //  Index the Container by Query_Match-ID
+               auto  c_it      = id_table.find(msgid);
+               auto& container = [&]() -> Container& {
+                       if (c_it != id_table.end()) {
+                               if (!c_it->second.query_match) // hmm, dup?
+                                       c_it->second.query_match = mi.query_match();
+                               return c_it->second;
+                       } else {
+                               // Else:
+                               // Create a new Container object holding this query_match
+                               // (query-match); Index the Container by Query_Match-ID
+                               return id_table.emplace(msgid, mi.query_match()).first->second;
+                       }
+               }();
+
+               // We sort by date (ascending), *except* for the root; we don't
+               // know what query_matchs will be at the root level yet, so remember
+               // both. Moreover, even when sorting the top-level in descending
+               // order, still sort the thread levels below that in ascending
+               // order.
+               container.thread_date_key = container.query_match->date_key =
+                   mi.date_str().value_or("");
+               // initial guess for the thread-date; might be updated
+               // later.
+
+               // remember the subject, we use it to determine the (sub)thread subject
+               container.query_match->subject = mi.subject().value_or("");
+
+               // 1.B
+               // For each element in the query_match's References field:
+               Container* parent_ref_container{};
+               for (const auto& ref : mi.references()) {
+                       //   grand_<n>-parent -> grand_<n-1>-parent -> ... -> parent.
+
+                       // Find a Container object for the given Query_Match-ID; If it exists, use
+                       // it; otherwise make one with a null Query_Match.
+                       auto ref_container = [&]() -> Container* {
+                               auto ref_it = id_table.find(ref);
+                               if (ref_it == id_table.end())
+                                       ref_it = id_table.emplace(ref, Nothing).first;
+                               return &ref_it->second;
+                       }();
+
+                       // Link the References field's Containers together in the order implied
+                       // by the References header.
+                       // * If they are already linked, don't change the existing links.
+                       //
+                       // * Do not add a link if adding that link would introduce a loop: that is,
+                       //   before asserting A->B, search down the children of B to see if A is
+                       //   reachable, and also search down the children of A to see if B is
+                       //   reachable. If either is already reachable as a child of the other,
+                       //   don't add the link.
+                       if (parent_ref_container && !ref_container->parent) {
+                               if (!parent_ref_container->is_reachable(ref_container))
+                                       parent_ref_container->add_child(*ref_container);
+                               // else
+                               //         g_message ("%u: reachable %s -> %s", __LINE__,
+                               //         msgid.c_str(), ref.c_str());
+                       }
+
+                       parent_ref_container = ref_container;
+               }
+
+               // Add the query_match to the chain.
+               if (parent_ref_container && !container.parent) {
+                       if (!parent_ref_container->is_reachable(&container))
+                               parent_ref_container->add_child(container);
+                       // else
+                       //         g_message ("%u: reachable %s -> parent", __LINE__,
+                       //         msgid.c_str());
+               }
+       }
+
+       // non-JWZ: add duplicate messages.
+       handle_duplicates(id_table, dups);
+
+       return id_table;
+}
+
+/// Recursively walk all containers under the root set.
+/// For each container:
+///
+///     If it is an empty container with no children, nuke it.
+///
+///     Note: Normally such containers won't occur, but they can show up when two
+///     query_matchs have References lines that disagree. For example, assuming A and
+///     B are query_matchs, and 1, 2, and 3 are references for query_matchs we haven't
+///     seen:
+///
+///         A has references: 1, 2, 3
+///         B has references: 1, 3
+///
+///     There is ambiguity as to whether 3 is a child of 1 or of 2. So,
+///     depending on the processing order, we might end up with either
+///
+///           -- 1
+///              |-- 2
+///                  \-- 3
+///                      |-- A
+///                      \-- B
+///
+///     or
+///
+///           -- 1
+///              |-- 2            <--- non root childless container!
+///              \-- 3
+///                  |-- A
+///                  \-- B
+///
+///     If the Container has no Query_Match, but does have children, remove this
+///     container but promote its children to this level (that is, splice them in
+///     to the current child list.)
+///
+///     Do not promote the children if doing so would promote them to the root
+///     set -- unless there is only one child, in which case, do.
+
+static void
+prune(Container* child)
+{
+       Container* container{child->parent};
+
+       for (auto& grandchild : child->children) {
+               grandchild->parent = container;
+               if (container)
+                       container->children.emplace_back(grandchild);
+       }
+
+       child->children.clear();
+       child->is_nuked = true;
+
+       if (container)
+               container->remove_child(*child);
+}
+
+static bool
+prune_empty_containers(Container& container)
+{
+       Containers to_prune;
+
+       container.for_each_child([&](auto& child) {
+               if (prune_empty_containers(*child))
+                       to_prune.emplace_back(child);
+       });
+
+       for (auto& child : to_prune)
+               prune(child);
+
+       // Never nuke these.
+       if (container.query_match)
+               return false;
+
+       // If it is an empty container with no children, nuke it.
+       //
+       // If the Container is empty, but does have children, remove this
+       // container but promote its children to this level (that is, splice them in
+       // to the current child list.)
+       //
+       // Do not promote the children if doing so would promote them to the root
+       // set -- unless there is only one child, in which case, do.
+       // const auto rootset_child{!container.parent->parent};
+       if (container.parent || container.children.size() <= 1)
+               return true; // splice/nuke it.
+
+       return false;
+}
+
+static void
+prune_empty_containers(IdTable& id_table)
+{
+       for (auto&& item : id_table) {
+               auto& child(item.second);
+               if (child.parent)
+                       continue; // not a root child.
+
+               if (prune_empty_containers(item.second))
+                       prune(&child);
+       }
+}
+
+//
+// Sorting.
+//
+
+/// Register some information about a match (i.e., message) that we can use for
+/// subsequent queries.
+using ThreadPath = std::vector<unsigned>;
+inline std::string
+to_string(const ThreadPath& tpath, size_t digits)
+{
+       std::string str;
+       str.reserve(tpath.size() * digits);
+
+       bool first{true};
+       for (auto&& segm : tpath) {
+               str += format("%s%0*x", first ? "" : ":", (int)digits, segm);
+               first = false;
+       }
+
+       return str;
+}
+
+static bool // compare subjects, ignore anything before the last ':<space>*'
+subject_matches(const std::string& sub1, const std::string& sub2)
+{
+       auto search_str = [](const std::string& s) -> const char* {
+               const auto pos = s.find_last_of(':');
+               if (pos == std::string::npos)
+                       return s.c_str();
+               else {
+                       const auto pos2 = s.find_first_not_of(' ', pos + 1);
+                       return s.c_str() + (pos2 == std::string::npos ? pos : pos2);
+               }
+       };
+
+       // g_debug ("'%s' '%s'", search_str(sub1), search_str(sub2));
+       return g_strcmp0(search_str(sub1), search_str(sub2)) == 0;
+}
+
+static bool
+update_container(Container&         container,
+                bool               descending,
+                ThreadPath&        tpath,
+                size_t             seg_size,
+                const std::string& prev_subject = "")
+{
+       if (!container.children.empty()) {
+               Container* first = container.children.front();
+               if (first->query_match)
+                       first->query_match->flags |= QueryMatch::Flags::First;
+               Container* last = container.children.back();
+               if (last->query_match)
+                       last->query_match->flags |= QueryMatch::Flags::Last;
+       }
+
+       if (!container.query_match)
+               return false; // nothing else to do.
+
+       auto& qmatch(*container.query_match);
+       if (!container.parent)
+               qmatch.flags |= QueryMatch::Flags::Root;
+       else if (!container.parent->query_match)
+               qmatch.flags |= QueryMatch::Flags::Orphan;
+
+       if (!container.children.empty())
+               qmatch.flags |= QueryMatch::Flags::HasChild;
+
+       if (qmatch.has_flag(QueryMatch::Flags::Root) || prev_subject.empty() ||
+           !subject_matches(prev_subject, qmatch.subject))
+               qmatch.flags |= QueryMatch::Flags::ThreadSubject;
+
+       if (descending && container.parent) {
+               // trick xapian by giving it "inverse" sorting key so our
+               // ascending-date sorted threads stay in that order
+               tpath.back() = ((1U << (4 * seg_size)) - 1) - tpath.back();
+       }
+
+       qmatch.thread_path  = to_string(tpath, seg_size);
+       qmatch.thread_level = tpath.size() - 1;
+
+       // ensure thread root comes before its children
+       if (descending)
+               qmatch.thread_path += ":z";
+
+       return true;
+}
+
+static void
+update_containers(Containers&  children,
+                 bool         descending,
+                 ThreadPath&  tpath,
+                 size_t       seg_size,
+                 std::string& prev_subject)
+{
+       size_t idx{0};
+
+       for (auto&& c : children) {
+               tpath.emplace_back(idx++);
+               if (c->query_match) {
+                       update_container(*c, descending, tpath, seg_size, prev_subject);
+                       prev_subject = c->query_match->subject;
+               }
+               update_containers(c->children, descending, tpath, seg_size, prev_subject);
+               tpath.pop_back();
+       }
+}
+
+static void
+update_containers(ContainerVec& root_vec, bool descending, size_t n)
+{
+       ThreadPath tpath;
+       tpath.reserve(n);
+
+       const auto seg_size = static_cast<size_t>(std::ceil(std::log2(n) / 4.0));
+       /*note: 4 == std::log2(16)*/
+
+       size_t idx{0};
+       for (auto&& c : root_vec) {
+               tpath.emplace_back(idx++);
+               std::string prev_subject;
+               if (update_container(*c, descending, tpath, seg_size))
+                       prev_subject = c->query_match->subject;
+               update_containers(c->children, descending, tpath, seg_size, prev_subject);
+               tpath.pop_back();
+       }
+}
+
+static void
+sort_container(Container& container)
+{
+       // 1. childless container.
+       if (container.children.empty())
+               return; // no children;  nothing to sort.
+
+       // 2. container with children.
+       // recurse, depth-first: sort the children
+       for (auto& child : container.children)
+               sort_container(*child);
+
+       // now sort this level.
+       std::sort(container.children.begin(), container.children.end(), [&](auto&& c1, auto&& c2) {
+               return c1->thread_date_key < c2->thread_date_key;
+       });
+
+       // and 'bubble up' the date of the *newest* message with a date. We
+       // reasonably assume that it's later than its parent.
+       const auto& newest_date = container.children.back()->thread_date_key;
+       if (!newest_date.empty())
+               container.thread_date_key = newest_date;
+}
+
+static void
+sort_siblings(IdTable& id_table, bool descending)
+{
+       if (id_table.empty())
+               return;
+
+       // unsorted vec of root containers. We can
+       // only sort these _after_ sorting the children.
+       ContainerVec root_vec;
+       for (auto&& item : id_table) {
+               if (!item.second.parent && !item.second.is_nuked)
+                       root_vec.emplace_back(&item.second);
+       }
+
+       // now sort all threads _under_ the root set (by date/ascending)
+       for (auto&& c : root_vec)
+               sort_container(*c);
+
+       // and then sort the root set.
+       //
+       // The difference with the sub-root containers is that at the top-level,
+       // we can sort either in ascending or descending order, while on the
+       // subroot level it's always in ascending order.
+       //
+       // Note that unless we're testing, _xapian_ will handle
+       // the ascending/descending of the top level.
+       std::sort(root_vec.begin(), root_vec.end(), [&](auto&& c1, auto&& c2) {
+#ifdef BUILD_TESTS
+               if (descending)
+                       return c2->thread_date_key < c1->thread_date_key;
+               else
+#endif /*BUILD_TESTS*/
+                       return c1->thread_date_key < c2->thread_date_key;
+       });
+
+       // now all is sorted... final step is to determine thread paths and
+       // other flags.
+       update_containers(root_vec, descending, id_table.size());
+}
+
+static std::ostream&
+operator<<(std::ostream& os, const IdTable& id_table)
+{
+       os << "------------------------------------------------\n";
+       for (auto&& item : id_table) {
+               os << item.first << " => " << item.second << "\n";
+       }
+       os << "------------------------------------------------\n";
+
+       std::set<std::string> ids;
+       for (auto&& item : id_table) {
+               if (item.second.query_match)
+                       ids.emplace(item.second.query_match->thread_path);
+       }
+
+       for (auto&& id : ids) {
+               auto it = std::find_if(id_table.begin(), id_table.end(), [&](auto&& item) {
+                       return item.second.query_match &&
+                              item.second.query_match->thread_path == id;
+               });
+               assert(it != id_table.end());
+               os << it->first << ": " << it->second << '\n';
+       }
+       return os;
+}
+
+template <typename Results>
+static void
+calculate_threads_real(Results& qres, bool descending)
+{
+       // Step 1: build the id_table
+       auto id_table{determine_id_table(qres)};
+
+       if (g_test_verbose())
+               std::cout << "*** id-table(1):\n" << id_table << "\n";
+
+       // // Step 2: get the root set
+       // // Step 3: discard id_table
+       // Nope: id-table owns the containers.
+       // Step 4: prune empty containers
+       prune_empty_containers(id_table);
+
+       // Step 5: group root-set by subject.
+       // Not implemented.
+
+       // Step 6: we're done threading
+
+       // Step 7: sort siblings. The segment-size is the number of hex-digits
+       // in the thread-path string (so we can lexically compare them.)
+       sort_siblings(id_table, descending);
+
+       // Step 7a:. update querymatches
+       for (auto&& item : id_table) {
+               Container& c{item.second};
+               if (c.query_match)
+                       c.query_match->thread_date = c.thread_date_key;
+       }
+       // if (g_test_verbose())
+       //         std::cout << "*** id-table(2):\n" << id_table << "\n";
+}
+
+void
+Mu::calculate_threads(Mu::QueryResults& qres, bool descending)
+{
+       calculate_threads_real(qres, descending);
+}
+
+#ifdef BUILD_TESTS
+
+struct MockQueryResult {
+       MockQueryResult(const std::string&              message_id_arg,
+                       const std::string&              date_arg,
+                       const std::vector<std::string>& refs_arg = {})
+           : message_id_{message_id_arg}, date_{date_arg}, refs_{refs_arg}
+       {
+       }
+       MockQueryResult(const std::string&              message_id_arg,
+                       const std::vector<std::string>& refs_arg = {})
+           : MockQueryResult(message_id_arg, "", refs_arg)
+       {
+       }
+       Option<std::string>             message_id() const { return message_id_; }
+       Option<std::string>             path() const { return path_; }
+       Option<std::string>             date_str() const { return date_; }
+       Option<std::string>             subject() const { return subject_; }
+       QueryMatch&                     query_match() { return query_match_; }
+       const QueryMatch&               query_match() const { return query_match_; }
+       const std::vector<std::string>& references() const { return refs_; }
+
+       std::string              path_;
+       std::string              message_id_;
+       QueryMatch               query_match_{};
+       std::string              date_;
+       std::string              subject_;
+       std::vector<std::string> refs_;
+};
+
+using MockQueryResults = std::vector<MockQueryResult>;
+
+G_GNUC_UNUSED static std::ostream&
+operator<<(std::ostream& os, const MockQueryResults& qrs)
+{
+       for (auto&& mi : qrs)
+               os << mi.query_match().thread_path << " :: " << mi.message_id().value_or("<none>")
+                  << std::endl;
+
+       return os;
+}
+
+static void
+calculate_threads(MockQueryResults& qres, bool descending)
+{
+       calculate_threads_real(qres, descending);
+}
+
+using Expected = std::vector<std::pair<std::string, std::string>>;
+
+static void
+assert_thread_paths(const MockQueryResults& qrs, const Expected& expected)
+{
+       for (auto&& exp : expected) {
+               auto it = std::find_if(qrs.begin(), qrs.end(), [&](auto&& qr) {
+                       return qr.message_id().value_or("") == exp.first ||
+                              qr.path().value_or("") == exp.first;
+               });
+               g_assert_true(it != qrs.end());
+               g_assert_cmpstr(exp.second.c_str(), ==, it->query_match().thread_path.c_str());
+       }
+}
+
+static void
+test_sort_ascending()
+{
+       auto results = MockQueryResults{MockQueryResult{"m1", "1", {"m2"}},
+                                       MockQueryResult{"m2", "2", {"m3"}},
+                                       MockQueryResult{"m3", "3", {}},
+                                       MockQueryResult{"m4", "4", {}}};
+
+       calculate_threads(results, false);
+
+       assert_thread_paths(results, {{"m1", "0:0:0"}, {"m2", "0:0"}, {"m3", "0"}, {"m4", "1"}});
+}
+
+static void
+test_sort_descending()
+{
+       auto results = MockQueryResults{MockQueryResult{"m1", "1", {"m2"}},
+                                       MockQueryResult{"m2", "2", {"m3"}},
+                                       MockQueryResult{"m3", "3", {}},
+                                       MockQueryResult{"m4", "4", {}}};
+
+       calculate_threads(results, true);
+
+       assert_thread_paths(results,
+                           {{"m1", "1:f:f:z"}, {"m2", "1:f:z"}, {"m3", "1:z"}, {"m4", "0:z"}});
+}
+
+static void
+test_id_table_inconsistent()
+{
+       auto results = MockQueryResults{
+           MockQueryResult{"m1", "1", {"m2"}}, // 1->2
+           MockQueryResult{"m2", "2", {"m1"}}, // 2->1
+           MockQueryResult{"m3", "3", {"m3"}}, // self ref
+           MockQueryResult{"m4", "4", {"m3", "m5"}},
+           MockQueryResult{"m5", "5", {"m4", "m4"}}, // dup parent
+       };
+
+       calculate_threads(results, false);
+       assert_thread_paths(results,
+                           {
+                               {"m2", "0"},
+                               {"m1", "0:0"},
+                               {"m3", "1"},
+                               {"m5", "1:0"},
+                               {"m4", "1:0:0"},
+                           });
+}
+
+static void
+test_dups_dup_last()
+{
+       MockQueryResult r1{"m1", "1", {}};
+       r1.query_match().flags |= QueryMatch::Flags::Leader;
+       r1.path_ = "/path1";
+
+       MockQueryResult r1_dup{"m1", "1", {}};
+       r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate;
+       r1_dup.path_ = "/path2";
+
+       auto results = MockQueryResults{r1, r1_dup};
+
+       calculate_threads(results, false);
+
+       assert_thread_paths(results,
+                           {
+                               {"/path1", "0"},
+                               {"/path2", "0:0"},
+                           });
+}
+
+static void
+test_dups_dup_first()
+{
+       // now dup becomes the leader; this will _demote_
+       // r1.
+
+       MockQueryResult r1_dup{"m1", "1", {}};
+       r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate;
+       r1_dup.path_ = "/path1";
+
+       MockQueryResult r1{"m1", "1", {}};
+       r1.query_match().flags |= QueryMatch::Flags::Leader;
+       r1.path_ = "/path2";
+
+       auto results = MockQueryResults{r1_dup, r1};
+
+       calculate_threads(results, false);
+
+       assert_thread_paths(results,
+                           {
+                               {"/path2", "0"},
+                               {"/path1", "0:0"},
+                           });
+}
+
+static void
+test_do_not_prune_root_empty_with_children()
+{
+       // m7 should not be nuked
+       auto results = MockQueryResults{
+           MockQueryResult{"x1", "1", {"m7"}},
+           MockQueryResult{"x2", "2", {"m7"}},
+       };
+
+       calculate_threads(results, false);
+
+       assert_thread_paths(results,
+                           {
+                               {"x1", "0:0"},
+                               {"x2", "0:1"},
+                           });
+}
+
+static void
+test_prune_root_empty_with_child()
+{
+       // m7 should be nuked
+       auto results = MockQueryResults{
+           MockQueryResult{"m1", "1", {"m7"}},
+       };
+
+       calculate_threads(results, false);
+
+       assert_thread_paths(results,
+                           {
+                               {"m1", "0"},
+                           });
+}
+
+static void
+test_prune_empty_with_children()
+{
+       // m6 should be nuked
+       auto results = MockQueryResults{
+           MockQueryResult{"m1", "1", {"m7", "m6"}},
+           MockQueryResult{"m2", "2", {"m7", "m6"}},
+       };
+
+       calculate_threads(results, false);
+
+       assert_thread_paths(results,
+                           {
+                               {"m1", "0:0"},
+                               {"m2", "0:1"},
+                           });
+}
+
+static void
+test_thread_info_ascending()
+{
+       auto results = MockQueryResults{
+           MockQueryResult{"m1", "5", {}},
+           MockQueryResult{"m2", "1", {}},
+           MockQueryResult{"m3", "3", {"m2"}},
+           MockQueryResult{"m4", "2", {"m2"}},
+           // orphan siblings
+           MockQueryResult{"m10", "6", {"m9"}},
+           MockQueryResult{"m11", "7", {"m9"}},
+       };
+       calculate_threads(results, false);
+
+       assert_thread_paths(results,
+                           {
+                               {"m2", "0"},   // 2
+                               {"m4", "0:0"}, //   2
+                               {"m3", "0:1"}, //   3
+                               {"m1", "1"},   // 5
+
+                               {"m10", "2:0"}, //   6
+                               {"m11", "2:1"}, //   7
+                           });
+
+       g_assert_true(results[0].query_match().has_flag(QueryMatch::Flags::Root));
+       g_assert_true(results[1].query_match().has_flag(QueryMatch::Flags::Root |
+                                                       QueryMatch::Flags::HasChild));
+       g_assert_true(results[2].query_match().has_flag(QueryMatch::Flags::Last));
+       g_assert_true(results[3].query_match().has_flag(QueryMatch::Flags::First));
+       g_assert_true(results[4].query_match().has_flag(QueryMatch::Flags::Orphan |
+                                                       QueryMatch::Flags::First));
+       g_assert_true(
+           results[5].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::Last));
+}
+
+static void
+test_thread_info_descending()
+{
+       auto results = MockQueryResults{
+           MockQueryResult{"m1", "5", {}},
+           MockQueryResult{"m2", "1", {}},
+           MockQueryResult{"m3", "3", {"m2"}},
+           MockQueryResult{"m4", "2", {"m2"}},
+           // orphan siblings
+           MockQueryResult{"m10", "6", {"m9"}},
+           MockQueryResult{"m11", "7", {"m9"}},
+       };
+       calculate_threads(results, true /*descending*/);
+
+       assert_thread_paths(results,
+                           {
+                               {"m1", "1:z"},   // 5
+                               {"m2", "2:z"},   // 2
+                               {"m4", "2:f:z"}, //   2
+                               {"m3", "2:e:z"}, //   3
+
+                               {"m10", "0:f:z"}, //   6
+                               {"m11", "0:e:z"}, //   7
+                           });
+       g_assert_true(results[0].query_match().has_flag(QueryMatch::Flags::Root));
+       g_assert_true(results[1].query_match().has_flag(QueryMatch::Flags::Root |
+                                                       QueryMatch::Flags::HasChild));
+       g_assert_true(results[2].query_match().has_flag(QueryMatch::Flags::Last));
+       g_assert_true(results[3].query_match().has_flag(QueryMatch::Flags::First));
+
+       g_assert_true(
+           results[4].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::Last));
+       g_assert_true(results[5].query_match().has_flag(QueryMatch::Flags::Orphan |
+                                                       QueryMatch::Flags::First));
+}
+
+int
+main(int argc, char* argv[])
+try {
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/threader/sort/ascending", test_sort_ascending);
+       g_test_add_func("/threader/sort/decending", test_sort_descending);
+
+       g_test_add_func("/threader/id-table-inconsistent", test_id_table_inconsistent);
+       g_test_add_func("/threader/dups/dup-last", test_dups_dup_last);
+       g_test_add_func("/threader/dups/dup-first", test_dups_dup_first);
+
+       g_test_add_func("/threader/prune/do-not-prune-root-empty-with-children",
+                       test_do_not_prune_root_empty_with_children);
+       g_test_add_func("/threader/prune/prune-root-empty-with-child",
+                       test_prune_root_empty_with_child);
+       g_test_add_func("/threader/prune/prune-empty-with-children",
+                       test_prune_empty_with_children);
+
+       g_test_add_func("/threader/thread-info/ascending", test_thread_info_ascending);
+       g_test_add_func("/threader/thread-info/descending", test_thread_info_descending);
+
+       return g_test_run();
+} catch (const std::runtime_error& re) {
+       std::cerr << re.what() << "\n";
+       return 1;
+} catch (...) {
+       std::cerr << "caught exception\n";
+       return 1;
+}
+
+#endif /*BUILD_TESTS*/
diff --git a/lib/mu-query-threads.hh b/lib/mu-query-threads.hh
new file mode 100644 (file)
index 0000000..5aab888
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_QUERY_THREADS__
+#define MU_QUERY_THREADS__
+
+#include "mu-query-results.hh"
+
+namespace Mu {
+/**
+ * Calculate the threads for these query results; that is, determine the
+ * thread-paths for each message, so we can let Xapian order them in the correct
+ * order.
+ *
+ * Note - threads are sorted chronologically, and the messages below the top
+ * level are always sorted in ascending orde
+ *
+ * @param qres query results
+ * @param descending whether to sort the top-level in descending order
+ */
+void calculate_threads(QueryResults& qres, bool descending);
+
+} // namespace Mu
+
+#endif /*MU_QUERY_THREADS__*/
diff --git a/lib/mu-query.cc b/lib/mu-query.cc
new file mode 100644 (file)
index 0000000..8be9052
--- /dev/null
@@ -0,0 +1,306 @@
+/*
+** Copyright (C) 2008-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 3 of the License, 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-query.hh>
+
+#include <stdexcept>
+#include <string>
+#include <cctype>
+#include <cstring>
+#include <sstream>
+#include <cmath>
+
+#include <stdlib.h>
+#include <xapian.h>
+#include <glib/gstdio.h>
+
+#include "mu-query-results.hh"
+#include "mu-query-match-deciders.hh"
+#include "mu-query-threads.hh"
+#include <mu-xapian.hh>
+#include "utils/mu-xapian-utils.hh"
+
+using namespace Mu;
+
+struct Query::Private {
+       Private(const Store& store) : store_{store}, parser_{store_} {}
+       // New
+       // bool calculate_threads (Xapian::Enquire& enq, size maxnum);
+
+       Xapian::Enquire make_enquire(const std::string& expr, Field::Id sortfield_id,
+                                    QueryFlags qflags) const;
+       Xapian::Enquire make_related_enquire(const StringSet& thread_ids,
+                                            Field::Id sortfield_id,
+                                            QueryFlags qflags) const;
+
+       Option<QueryResults> run_threaded(QueryResults&& qres, Xapian::Enquire& enq,
+                                         QueryFlags qflags, size_t max_size) const;
+       Option<QueryResults> run_singular(const std::string&       expr,
+                                         Field::Id sortfield_id,
+                                         QueryFlags qflags, size_t maxnum) const;
+       Option<QueryResults> run_related(const std::string&       expr,
+                                        Field::Id sortfield_id,
+                                        QueryFlags qflags, size_t maxnum) const;
+
+       Option<QueryResults> run(const std::string& expr,
+                                Field::Id sortfield_id, QueryFlags qflags,
+                                size_t maxnum) const;
+
+       size_t store_size() const { return store_.database().get_doccount(); }
+
+       const Store& store_;
+       const Parser parser_;
+};
+
+Query::Query(const Store& store) : priv_{std::make_unique<Private>(store)} {}
+
+Query::Query(Query&& other) = default;
+
+Query::~Query() = default;
+
+static Xapian::Enquire&
+sort_enquire(Xapian::Enquire& enq, Field::Id sortfield_id, QueryFlags qflags)
+{
+       const auto value_no{field_from_id(sortfield_id).value_no()};
+       enq.set_sort_by_value(value_no, any_of(qflags & QueryFlags::Descending));
+
+       return enq;
+}
+
+Xapian::Enquire
+Query::Private::make_enquire(const std::string& expr,
+                            Field::Id          sortfield_id,
+                            QueryFlags         qflags) const
+{
+       Xapian::Enquire enq{store_.database()};
+
+       if (expr.empty() || expr == R"("")")
+               enq.set_query(Xapian::Query::MatchAll);
+       else {
+               WarningVec warns;
+               const auto tree{parser_.parse(expr, warns)};
+               for (auto&& w : warns)
+                       g_warning("query warning: %s", to_string(w).c_str());
+               enq.set_query(xapian_query(tree));
+               g_debug("qtree: %s", to_string(tree).c_str());
+       }
+
+       sort_enquire(enq, sortfield_id, qflags);
+
+       return enq;
+}
+
+Xapian::Enquire
+Query::Private::make_related_enquire(const StringSet& thread_ids,
+                                    Field::Id sortfield_id,
+                                    QueryFlags qflags) const
+{
+       Xapian::Enquire            enq{store_.database()};
+       std::vector<Xapian::Query> qvec;
+       for (auto&& t : thread_ids)
+               qvec.emplace_back(field_from_id(Field::Id::ThreadId).xapian_term(t));
+
+       Xapian::Query qr{Xapian::Query::OP_OR, qvec.begin(), qvec.end()};
+       enq.set_query(qr);
+
+       sort_enquire(enq, sortfield_id, qflags);
+
+       return enq;
+}
+
+struct ThreadKeyMaker : public Xapian::KeyMaker {
+       ThreadKeyMaker(const QueryMatches& matches) : match_info_(matches) {}
+       std::string operator()(const Xapian::Document& doc) const override
+       {
+               const auto it{match_info_.find(doc.get_docid())};
+               return (it == match_info_.end()) ? "" : it->second.thread_path;
+       }
+       const QueryMatches& match_info_;
+};
+
+Option<QueryResults>
+Query::Private::run_threaded(QueryResults&& qres, Xapian::Enquire& enq, QueryFlags qflags,
+                            size_t maxnum) const
+{
+       const auto descending{any_of(qflags & QueryFlags::Descending)};
+
+       calculate_threads(qres, descending);
+
+       ThreadKeyMaker key_maker{qres.query_matches()};
+       enq.set_sort_by_key(&key_maker, descending);
+
+       DeciderInfo minfo;
+       minfo.matches = qres.query_matches();
+       auto mset{enq.get_mset(0, maxnum, {}, make_thread_decider(qflags, minfo).get())};
+       mset.fetch();
+
+       return QueryResults{mset, std::move(qres.query_matches())};
+}
+
+Option<QueryResults>
+Query::Private::run_singular(const std::string& expr,
+                            Field::Id sortfield_id,
+                            QueryFlags qflags, size_t maxnum) const
+{
+       // i.e. a query _without_ related messages, but still possibly
+       // with threading.
+       //
+       // In the threading case, the sortfield-id is ignored, we always sort by
+       // date (since threading the threading results are always by date.)
+
+       const auto singular_qflags{qflags | QueryFlags::Leader};
+       const auto threading{any_of(qflags & QueryFlags::Threading)};
+
+       DeciderInfo minfo{};
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wextra"
+       auto enq{make_enquire(expr, threading ? Field::Id::Date : sortfield_id, qflags)};
+#pragma GCC diagnostic ignored "-Wswitch-default"
+#pragma GCC diagnostic pop
+       auto mset{enq.get_mset(0, maxnum, {},
+                              make_leader_decider(singular_qflags, minfo).get())};
+       mset.fetch();
+
+       auto qres{QueryResults{mset, std::move(minfo.matches)}};
+
+       return threading ? run_threaded(std::move(qres), enq, qflags, maxnum) : qres;
+}
+
+static Option<std::string>
+opt_string(const Xapian::Document& doc, Field::Id id) noexcept
+{
+       const auto  value_no{field_from_id(id).value_no()};
+       std::string val =
+           xapian_try([&] { return doc.get_value(value_no); }, std::string{""});
+       if (val.empty())
+               return Nothing;
+       else
+               return Some(std::move(val));
+}
+
+Option<QueryResults>
+Query::Private::run_related(const std::string& expr,
+                           Field::Id sortfield_id,
+                           QueryFlags qflags, size_t maxnum) const
+{
+       // i.e. a query _with_ related messages and possibly with threading.
+       //
+       // In the threading case, the sortfield-id is ignored, we always sort by
+       // date (since threading the threading results are always by date.);
+       // moreover, in either threaded or non-threaded case, we sort the first
+       // ("leader") query by date, i.e, we prefer the newest or oldest
+       // (descending) messages.
+       const auto leader_qflags{QueryFlags::Leader | qflags};
+       const auto threading{any_of(qflags & QueryFlags::Threading)};
+
+       // Run our first, "leader" query
+       DeciderInfo minfo{};
+       auto        enq{make_enquire(expr, Field::Id::Date, leader_qflags)};
+       const auto  mset{
+           enq.get_mset(0, maxnum, {}, make_leader_decider(leader_qflags, minfo).get())};
+
+       // Gather the thread-ids we found
+       mset.fetch();
+       for (auto it = mset.begin(); it != mset.end(); ++it) {
+               auto thread_id{opt_string(it.get_document(), Field::Id::ThreadId)};
+               if (thread_id)
+                       minfo.thread_ids.emplace(std::move(*thread_id));
+       }
+
+       // Now, determine the "related query".
+       //
+       // In the threaded-case, we search among _all_ messages, since complete
+       // threads are preferred; no need to sort in that case since the search
+       // is unlimited and the sorting happens during threading.
+       auto r_enq = std::invoke([&]{
+               if (threading)
+                       return make_related_enquire(minfo.thread_ids, Field::Id::Date,
+                                                   qflags );
+               else
+                       return make_related_enquire(minfo.thread_ids, sortfield_id, qflags);
+       });
+
+       const auto r_mset{r_enq.get_mset(0, threading ? store_size() : maxnum, {},
+                                        make_related_decider(qflags, minfo).get())};
+       auto       qres{QueryResults{r_mset, std::move(minfo.matches)}};
+       return threading ? run_threaded(std::move(qres), r_enq, qflags, maxnum) : qres;
+}
+
+Option<QueryResults>
+Query::Private::run(const std::string&  expr, Field::Id sortfield_id, QueryFlags qflags,
+                   size_t maxnum) const
+{
+       const auto eff_maxnum{maxnum == 0 ? store_size() : maxnum};
+
+       if (any_of(qflags & QueryFlags::IncludeRelated))
+               return run_related(expr, sortfield_id, qflags, eff_maxnum);
+       else
+               return run_singular(expr, sortfield_id, qflags, eff_maxnum);
+}
+
+Result<QueryResults>
+Query::run(const std::string& expr, Field::Id sortfield_id,
+          QueryFlags qflags, size_t maxnum) const
+{
+       // some flags are for internal use only.
+       g_return_val_if_fail(none_of(qflags & QueryFlags::Leader),
+                            Err(Error::Code::InvalidArgument, "cannot pass Leader flag"));
+
+       StopWatch sw{format(
+           "ran query '%s'; related: %s; threads: %s; max-size: %zu", expr.c_str(),
+           any_of(qflags & QueryFlags::IncludeRelated) ? "yes" : "no",
+           any_of(qflags & QueryFlags::Threading) ? "yes" : "no", maxnum)};
+
+       return xapian_try_result([&]{
+               if (auto&& res = priv_->run(expr, sortfield_id, qflags, maxnum); res)
+                       return Result<QueryResults>(Ok(std::move(res.value())));
+               else
+                       return Result<QueryResults>(Err(Error::Code::Query,
+                                                       "failed to run query"));
+       });
+
+}
+
+size_t
+Query::count(const std::string& expr) const
+{
+       return xapian_try(
+           [&] {
+                   const auto enq{priv_->make_enquire(expr, {}, {})};
+                   auto       mset{enq.get_mset(0, priv_->store_size())};
+                   mset.fetch();
+                   return mset.size();
+           },
+           0);
+}
+
+/* LCOV_EXCL_START*/
+std::string
+Query::parse(const std::string& expr, bool xapian) const
+{
+       WarningVec warns;
+       const auto tree{priv_->parser_.parse(expr, warns)};
+       for (auto&& w : warns)
+               g_warning("query warning: %s", to_string(w).c_str());
+
+       if (xapian)
+               return xapian_query(tree).get_description();
+       else
+               return to_string(tree);
+}
+/* LCOV_EXCL_STOP*/
diff --git a/lib/mu-query.hh b/lib/mu-query.hh
new file mode 100644 (file)
index 0000000..ad042e1
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+** Copyright (C) 2008-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 3 of the License, 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_QUERY_HH__
+#define __MU_QUERY_HH__
+
+#include <memory>
+
+#include <glib.h>
+#include <mu-store.hh>
+#include <mu-query-results.hh>
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+#include <utils/mu-result.hh>
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+class Query {
+public:
+       /**
+        * Run a query on the store
+        *
+        * @param expr the search expression
+        * @param sortfield_id the sortfield-id. Default to Date
+        * @param flags query flags
+        * @param maxnum maximum number of results to return. 0 for 'no limit'
+        *
+        * @return the query-results or an error
+        */
+       Result<QueryResults> run(const std::string&     expr,
+                                Field::Id              sortfield_id = Field::Id::Date,
+                                QueryFlags             flags        = QueryFlags::None,
+                                size_t                 maxnum       = 0) const;
+
+       /**
+        * run a Xapian query to count the number of matches; for the syntax, please
+        * refer to the mu-query manpage
+        *
+        * @param expr the search expression; use "" to match all messages
+        *
+        * @return the number of matches
+        */
+       size_t count(const std::string& expr = "") const;
+
+       /**
+        * For debugging, get the internal string representation of the parsed
+        * query
+        *
+        * @param expr a xapian search expression
+        * @param xapian if true, show Xapian's internal representation,
+        * otherwise, mu's.
+
+        * @return the string representation of the query
+        */
+       std::string parse(const std::string& expr, bool xapian) const;
+
+private:
+       friend class Store;
+
+       /**
+        * Construct a new Query instance.
+        *
+        * @param store a MuStore object
+        */
+       Query(const Store& store);
+       /**
+        * DTOR
+        *
+        */
+       ~Query();
+
+       /**
+        * Move CTOR
+        *
+        * @param other
+        */
+       Query(Query&& other);
+
+       struct Private;
+       std::unique_ptr<Private> priv_;
+};
+} // namespace Mu
+
+#endif /*__MU_QUERY_HH__*/
diff --git a/lib/mu-runtime.cc b/lib/mu-runtime.cc
new file mode 100644 (file)
index 0000000..b3b0e60
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+** Copyright (C) 2019-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-runtime.hh"
+#include "utils/mu-util.h"
+#include "utils/mu-logger.hh"
+
+#include <locale.h> /* for setlocale() */
+
+#include <string>
+#include <unordered_map>
+static std::unordered_map<MuRuntimePath, std::string> RuntimePaths;
+
+constexpr auto PartsDir  = "parts";
+constexpr auto LogDir    = "log";
+constexpr auto XapianDir = "xapian";
+constexpr auto MuName    = "mu";
+constexpr auto Bookmarks = "bookmarks";
+
+static const std::string Sepa{G_DIR_SEPARATOR_S};
+
+static void
+init_paths_xdg()
+{
+       RuntimePaths.emplace(MU_RUNTIME_PATH_XAPIANDB,
+                            g_get_user_cache_dir() + Sepa + MuName + Sepa + XapianDir);
+       RuntimePaths.emplace(MU_RUNTIME_PATH_CACHE, g_get_user_cache_dir() + Sepa + MuName);
+       RuntimePaths.emplace(MU_RUNTIME_PATH_MIMECACHE,
+                            g_get_user_cache_dir() + Sepa + MuName + Sepa + PartsDir);
+       RuntimePaths.emplace(MU_RUNTIME_PATH_LOGDIR, g_get_user_cache_dir() + Sepa + MuName);
+       RuntimePaths.emplace(MU_RUNTIME_PATH_BOOKMARKS, g_get_user_config_dir() + Sepa + MuName);
+}
+
+static void
+init_paths_muhome(const char* muhome)
+{
+       RuntimePaths.emplace(MU_RUNTIME_PATH_XAPIANDB, muhome + Sepa + XapianDir);
+       RuntimePaths.emplace(MU_RUNTIME_PATH_CACHE, muhome);
+       RuntimePaths.emplace(MU_RUNTIME_PATH_MIMECACHE, muhome + Sepa + PartsDir);
+       RuntimePaths.emplace(MU_RUNTIME_PATH_LOGDIR, muhome + Sepa + LogDir);
+       RuntimePaths.emplace(MU_RUNTIME_PATH_BOOKMARKS, muhome + Sepa + Bookmarks);
+}
+
+gboolean
+mu_runtime_init(const char* muhome, const char* name, gboolean debug)
+{
+       g_return_val_if_fail(RuntimePaths.empty(), FALSE);
+       g_return_val_if_fail(name, FALSE);
+
+       setlocale(LC_ALL, "");
+
+       if (muhome)
+               init_paths_muhome(muhome);
+       else
+               init_paths_xdg();
+
+       for (const auto& d : RuntimePaths) {
+               char* dir;
+               if (d.first == MU_RUNTIME_PATH_BOOKMARKS) // special case
+                       dir = g_path_get_dirname(d.second.c_str());
+               else
+                       dir = g_strdup(d.second.c_str());
+
+               auto ok = mu_util_create_dir_maybe(dir, 0700, TRUE);
+               if (!ok) {
+                       g_critical("failed to create %s", dir);
+                       g_free(dir);
+                       mu_runtime_uninit();
+                       return FALSE;
+               }
+               g_free(dir);
+       }
+
+       const auto log_path = RuntimePaths[MU_RUNTIME_PATH_LOGDIR] + Sepa + name + ".log";
+
+       using namespace Mu;
+       LogOptions opts{LogOptions::None};
+       if (debug)
+               opts |= (LogOptions::Debug | LogOptions::None);
+
+       Mu::log_init(log_path, opts);
+
+       return TRUE;
+}
+
+void
+mu_runtime_uninit(void)
+{
+       RuntimePaths.clear();
+       Mu::log_uninit();
+}
+
+const char*
+mu_runtime_path(MuRuntimePath path)
+{
+       const auto it = RuntimePaths.find(path);
+       if (it == RuntimePaths.end())
+               return NULL;
+       else
+               return it->second.c_str();
+}
diff --git a/lib/mu-runtime.hh b/lib/mu-runtime.hh
new file mode 100644 (file)
index 0000000..0f0d410
--- /dev/null
@@ -0,0 +1,66 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+**
+** Copyright (C) 2012-2019 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#ifndef __MU_RUNTIME_H__
+#define __MU_RUNTIME_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+/**
+ * initialize the mu runtime system; initializes logging and other
+ * systems. To uninitialize, use mu_runtime_uninit
+ *
+ * @param muhome path where to find the mu home directory (typically, ~/.cache/mu)
+ * @param name of the main program, ie. 'mu', 'mug' or
+ * 'procmule'. this influences the name of the e.g. the logfile
+ * @param debug debug-mode
+ *
+ * @return TRUE if succeeded, FALSE in case of error
+ */
+gboolean mu_runtime_init(const char* muhome, const char* name, gboolean debug);
+
+/**
+ * free all resources
+ *
+ */
+void mu_runtime_uninit(void);
+
+typedef enum {
+       MU_RUNTIME_PATH_XAPIANDB,  /* mu xapian db path */
+       MU_RUNTIME_PATH_BOOKMARKS, /* mu bookmarks file path */
+       MU_RUNTIME_PATH_CACHE,     /* mu cache path */
+       MU_RUNTIME_PATH_MIMECACHE, /* mu cache path for attachments etc. */
+       MU_RUNTIME_PATH_LOGDIR,    /* mu path for log files */
+
+       MU_RUNTIME_PATH_NUM
+} MuRuntimePath;
+
+/**
+ * get a file system path to some 'special' file or directory
+ *
+ * @return ma string which should be not be modified/freed, or NULL in
+ * case of error.
+ */
+const char* mu_runtime_path(MuRuntimePath path);
+
+G_END_DECLS
+
+#endif /*__MU_RUNTIME_H__*/
diff --git a/lib/mu-script.cc b/lib/mu-script.cc
new file mode 100644 (file)
index 0000000..05fe3dc
--- /dev/null
@@ -0,0 +1,375 @@
+/*
+** Copyright (C) 2012-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#ifdef BUILD_GUILE
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wredundant-decls"
+#include <libguile.h>
+#pragma GCC diagnostic pop
+#endif /*BUILD_GUILE*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <errno.h>
+#include <unistd.h>
+
+#include "mu-script.hh"
+#include "utils/mu-util.h"
+
+/**
+ * Structure with information about a certain script.
+ * the values will be *freed* when MuScriptInfo is freed
+ */
+struct MuScriptInfo {
+       char* _name;    /* filename-sans-extension */
+       char* _path;    /* full path to script */
+       char* _oneline; /* one-line description */
+       char* _descr;   /* longer description */
+};
+
+/* create a new MuScriptInfo* object*/
+static MuScriptInfo*
+script_info_new(void)
+{
+       return g_slice_new0(MuScriptInfo);
+}
+
+/* destroy a MuScriptInfo* object */
+static void
+script_info_destroy(MuScriptInfo* msi)
+{
+       if (!msi)
+               return;
+
+       g_free(msi->_name);
+       g_free(msi->_path);
+       g_free(msi->_oneline);
+       g_free(msi->_descr);
+
+       g_slice_free(MuScriptInfo, msi);
+}
+
+/* compare two MuScripInfo* objects (for sorting) */
+static int
+script_info_cmp(MuScriptInfo* msi1, MuScriptInfo* msi2)
+{
+       return strcmp(msi1->_name, msi2->_name);
+}
+
+const char*
+mu_script_info_name(MuScriptInfo* msi)
+{
+       g_return_val_if_fail(msi, NULL);
+       return msi->_name;
+}
+
+const char*
+mu_script_info_path(MuScriptInfo* msi)
+{
+       g_return_val_if_fail(msi, NULL);
+       return msi->_path;
+}
+
+const char*
+mu_script_info_one_line(MuScriptInfo* msi)
+{
+       g_return_val_if_fail(msi, NULL);
+       return msi->_oneline;
+}
+
+const char*
+mu_script_info_description(MuScriptInfo* msi)
+{
+       g_return_val_if_fail(msi, NULL);
+       return msi->_descr;
+}
+
+gboolean
+mu_script_info_matches_regex(MuScriptInfo* msi, const char* rxstr, GError** err)
+{
+       GRegex*  rx;
+       gboolean match;
+
+       g_return_val_if_fail(msi, FALSE);
+       g_return_val_if_fail(rxstr, FALSE);
+
+       rx = g_regex_new(rxstr,
+                        (GRegexCompileFlags)(G_REGEX_CASELESS | G_REGEX_OPTIMIZE),
+                        (GRegexMatchFlags)0,
+                        err);
+       if (!rx)
+               return FALSE;
+
+       match = FALSE;
+       if (msi->_name)
+               match = g_regex_match(rx, msi->_name, (GRegexMatchFlags)0, NULL);
+       if (!match && msi->_oneline)
+               match = g_regex_match(rx, msi->_oneline, (GRegexMatchFlags)0, NULL);
+
+       return match;
+}
+
+void
+mu_script_info_list_destroy(GSList* lst)
+{
+       g_slist_free_full(lst, (GDestroyNotify)script_info_destroy);
+}
+
+static GIOChannel*
+open_channel(const char* path)
+{
+       GError*     err;
+       GIOChannel* io_chan;
+
+       err = NULL;
+
+       io_chan = g_io_channel_new_file(path, "r", &err);
+       if (!io_chan) {
+               g_warning("failed to open '%s': %s",
+                         path,
+                         err ? err->message : "something went wrong");
+               g_clear_error(&err);
+               return NULL;
+       }
+
+       return io_chan;
+}
+
+static void
+end_channel(GIOChannel* io_chan)
+{
+       GIOStatus status;
+       GError*   err;
+
+       err    = NULL;
+       status = g_io_channel_shutdown(io_chan, FALSE, &err);
+       if (status != G_IO_STATUS_NORMAL) {
+               g_warning("failed to shutdown io-channel: %s",
+                         err ? err->message : "something went wrong");
+               g_clear_error(&err);
+       }
+
+       g_io_channel_unref(io_chan);
+}
+
+static gboolean
+get_descriptions(MuScriptInfo* msi, const char* prefix)
+{
+       GIOStatus   io_status;
+       GIOChannel* script_io;
+       GError*     err;
+
+       char *line, *descr, *oneline;
+
+       if (!prefix)
+               return TRUE; /* not an error */
+
+       if (!(script_io = open_channel(msi->_path)))
+               return FALSE;
+
+       err  = NULL;
+       line = descr = oneline = NULL;
+
+       do {
+               g_free(line);
+               io_status = g_io_channel_read_line(script_io, &line, NULL, NULL, &err);
+               if (io_status != G_IO_STATUS_NORMAL)
+                       break;
+
+               if (!g_str_has_prefix(line, prefix))
+                       continue;
+
+               if (!oneline)
+                       oneline = g_strdup(line + strlen(prefix));
+               else {
+                       char* tmp;
+                       tmp   = descr;
+                       descr = g_strdup_printf("%s%s", descr ? descr : "", line + strlen(prefix));
+                       g_free(tmp);
+               }
+
+       } while (TRUE);
+
+       if (io_status != G_IO_STATUS_EOF) {
+               g_warning("error reading %s: %s",
+                         msi->_path,
+                         err ? err->message : "something went wrong");
+               g_clear_error(&err);
+       }
+
+       end_channel(script_io);
+       msi->_oneline = oneline;
+       msi->_descr   = descr;
+
+       return TRUE;
+}
+
+GSList*
+mu_script_get_script_info_list(const char* path,
+                              const char* ext,
+                              const char* descprefix,
+                              GError**    err)
+{
+       DIR*           dir;
+       GSList*        lst;
+       struct dirent* dentry;
+
+       g_return_val_if_fail(path, NULL);
+
+       dir = opendir(path);
+       if (!dir) {
+               mu_util_g_set_error(err,
+                                   MU_ERROR_FILE_CANNOT_OPEN,
+                                   "failed to open '%s': %s",
+                                   path,
+                                   g_strerror(errno));
+               return NULL;
+       }
+
+       /* create a list of names, paths */
+       lst = NULL;
+       while ((dentry = readdir(dir))) {
+               MuScriptInfo* msi;
+               /* only consider files with certain extensions,
+                * if ext != NULL */
+               if (ext && !g_str_has_suffix(dentry->d_name, ext))
+                       continue;
+               msi        = script_info_new();
+               msi->_name = g_strdup(dentry->d_name);
+               if (ext) /* strip the extension */
+                       msi->_name[strlen(msi->_name) - strlen(ext)] = '\0';
+               msi->_path = g_strdup_printf("%s%c%s", path, G_DIR_SEPARATOR, dentry->d_name);
+               /* set the one-line and long description */
+               get_descriptions(msi, descprefix);
+               lst = g_slist_prepend(lst, msi);
+       }
+
+       closedir(dir); /* ignore error checking... */
+
+       return g_slist_sort(lst, (GCompareFunc)script_info_cmp);
+}
+
+MuScriptInfo*
+mu_script_find_script_with_name(GSList* lst, const char* name)
+{
+       GSList* cur;
+
+       g_return_val_if_fail(name, NULL);
+
+       for (cur = lst; cur; cur = g_slist_next(cur)) {
+               MuScriptInfo* msi;
+               msi = (MuScriptInfo*)cur->data;
+
+               if (g_strcmp0(name, mu_script_info_name(msi)) == 0)
+                       return msi;
+       }
+
+       return NULL;
+}
+
+
+
+#ifdef BUILD_GUILE
+static char*
+quoted_from_strv (const gchar **params)
+{
+       GString *str;
+       int i;
+
+       g_return_val_if_fail (params, NULL);
+
+       if (!params[0])
+               return g_strdup ("");
+
+       str = g_string_sized_new (64); /* just a guess */
+
+       for (i = 0; params[i]; ++i) {
+
+               if (i > 0)
+                       g_string_append_c (str, ' ');
+
+               g_string_append_c (str, '"');
+               g_string_append (str, params[i]);
+               g_string_append_c (str, '"');
+       }
+
+       return g_string_free (str, FALSE);
+}
+
+
+static void
+guile_shell(void* closure, int argc, char** argv)
+{
+       scm_shell(argc, argv);
+}
+
+gboolean
+mu_script_guile_run(MuScriptInfo* msi, const char* muhome, const char** args, GError** err)
+{
+       const char* s;
+       char *      mainargs, *expr;
+       char**      argv;
+
+       g_return_val_if_fail(msi, FALSE);
+       g_return_val_if_fail(muhome, FALSE);
+
+       if (access(mu_script_info_path(msi), R_OK) != 0) {
+               mu_util_g_set_error(err,
+                                   MU_ERROR_FILE_CANNOT_READ,
+                                   "failed to read script: %s",
+                                   g_strerror(errno));
+               return FALSE;
+       }
+
+       argv    = g_new0(char*, 6);
+       argv[0] = g_strdup(GUILE_BINARY);
+       argv[1] = g_strdup("-l");
+
+       s       = mu_script_info_path(msi);
+       argv[2] = g_strdup(s ? s : "");
+
+       mainargs = quoted_from_strv(args);
+       expr     = g_strdup_printf("(main '(\"%s\" \"--muhome=%s\" %s))",
+                                  mu_script_info_name(msi),
+                                  muhome,
+                              mainargs ? mainargs : "");
+
+       g_free(mainargs);
+       argv[3] = g_strdup("-c");
+       argv[4] = expr;
+
+       scm_boot_guile(5, argv, guile_shell, NULL);
+
+       /* never reached but let's be correct(TM)*/
+       g_strfreev(argv);
+       return TRUE;
+}
+#else  /*!BUILD_GUILE*/
+gboolean
+mu_script_guile_run(MuScriptInfo* msi, const char* muhome, const char** args, GError** err)
+{
+       mu_util_g_set_error(err, MU_ERROR_INTERNAL, "this mu does not have guile support");
+       return FALSE;
+}
+#endif /*!BUILD_GUILE*/
diff --git a/lib/mu-script.hh b/lib/mu-script.hh
new file mode 100644 (file)
index 0000000..d3f7b1e
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+** Copyright (C) 2012-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_SCRIPT_HH__
+#define MU_SCRIPT_HH__
+
+#include <glib.h>
+
+/* Opaque structure with information about a script */
+struct MuScriptInfo;
+
+/**
+ * get the name of the script (sans-extension, if some extension was
+ * provided to mu_script_get_scripts)
+ *
+ * @param msi a MuScriptInfo structure
+ *
+ * @return the name
+ */
+const char* mu_script_info_name(MuScriptInfo* msi);
+
+/**
+ * get the full filesystem path of the script
+ *
+ * @param msi a MuScriptInfo structure
+ *
+ * @return the path
+ */
+const char* mu_script_info_path(MuScriptInfo* msi);
+
+/**
+ * get a one-line description for the script
+ *
+ * @param msi a MuScriptInfo structure
+ *
+ * @return the description, or NULL if there was none
+ */
+const char* mu_script_info_one_line(MuScriptInfo* msi);
+
+/**
+ * get a full description for the script
+ *
+ * @param msi a MuScriptInfo structure
+ *
+ * @return the description, or NULL if there was none
+ */
+const char* mu_script_info_description(MuScriptInfo* msi);
+
+/**
+ * check whether either the name or one-line description of a
+ * MuScriptInfo matches regular expression rxstr
+ *
+ * @param msi a MuScriptInfo
+ * @param rxstr a regular expression string
+ * @param err receives error information
+ *
+ * @return TRUE if it matches, FALSE if not or in case of error
+ */
+gboolean mu_script_info_matches_regex(MuScriptInfo* msi, const char* rxstr, GError** err);
+
+/**
+ * Get the list of all scripts in path with extension ext
+ *
+ * @param path a file system path
+ * @param ext an extension (e.g., ".scm"), or NULL
+ * @param prefix for the one-line description
+ *        (e.g., ";; DESCRIPTION: "), or NULL
+ * @param err receives error information, if any
+ *
+ * @return a list of Mu
+ */
+GSList* mu_script_get_script_info_list(const char* path,
+                                       const char* ext,
+                                       const char* descprefix,
+                                       GError**    err);
+
+/**
+ * destroy a list of MuScriptInfo* objects
+ *
+ * @param scriptslst a list of MuScriptInfo* objects
+ */
+void mu_script_info_list_destroy(GSList* lst);
+
+/**
+ * find the MuScriptInfo object for the first script with a certain
+ * name, or return NULL if not found.
+ *
+ * @param lst a list of MuScriptInfo* objects
+ * @param name the name to search for
+ *
+ * @return a MuScriptInfo* object, or NULL if not found.
+ */
+MuScriptInfo* mu_script_find_script_with_name(GSList* lst, const char* name);
+
+/**
+ * run the guile script at path
+ *
+ * @param msi MuScriptInfo object for the script
+ * @param muhome path to the mu home dir
+ * @param args NULL-terminated array of strings (argv for the script)
+ * @param err receives error information
+ *
+ * @return FALSE in case of error -- otherwise, this function will
+ * _not return_
+ */
+gboolean
+mu_script_guile_run(MuScriptInfo* msi, const char* muhome, const char** args, GError** err);
+
+#endif /*MU_SCRIPT_HH__*/
diff --git a/lib/mu-server.cc b/lib/mu-server.cc
new file mode 100644 (file)
index 0000000..e1f62d3
--- /dev/null
@@ -0,0 +1,1106 @@
+/*
+** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include "message/mu-message.hh"
+#include "mu-server.hh"
+
+#include <iostream>
+#include <string>
+#include <algorithm>
+#include <atomic>
+#include <thread>
+#include <mutex>
+#include <functional>
+
+#include <cstring>
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "mu-runtime.hh"
+#include "mu-maildir.hh"
+#include "mu-query.hh"
+#include "index/mu-indexer.hh"
+#include "mu-store.hh"
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-option.hh"
+#include "utils/mu-command-parser.hh"
+#include "utils/mu-readline.hh"
+
+using namespace Mu;
+using namespace Command;
+
+/// @brief object to manage the server-context for all commands.
+struct Server::Private {
+       Private(Store& store, Output output)
+           : store_{store}, output_{output}, command_map_{make_command_map()},
+             keep_going_{true}
+       {}
+
+       ~Private() {
+               indexer().stop();
+               if (index_thread_.joinable())
+                       index_thread_.join();
+       }
+       //
+       // construction helpers
+       //
+       CommandMap make_command_map();
+
+       //
+       // acccessors
+       Store&            store() { return store_; }
+       const Store&      store() const { return store_; }
+       Indexer&          indexer() { return store().indexer(); }
+       const CommandMap& command_map() const { return command_map_; }
+
+       //
+       // invoke
+       //
+       bool invoke(const std::string& expr) noexcept;
+
+       //
+       // output
+       //
+       void output_sexp(Sexp&& sexp,Server::OutputFlags flags = {}) const {
+               if (output_)
+                       output_(std::move(sexp), flags);
+       }
+
+       void output_sexp(Sexp::List&& lst, Server::OutputFlags flags = {}) const {
+               output_sexp(Sexp::make_list(std::move(lst)), flags);
+       }
+       size_t output_results(const QueryResults& qres, size_t batch_size) const;
+
+       //
+       // handlers for various commands.
+       //
+       void add_handler(const Parameters& params);
+       void compose_handler(const Parameters& params);
+       void contacts_handler(const Parameters& params);
+       void find_handler(const Parameters& params);
+       void help_handler(const Parameters& params);
+       void index_handler(const Parameters& params);
+       void move_handler(const Parameters& params);
+       void mkdir_handler(const Parameters& params);
+       void ping_handler(const Parameters& params);
+       void quit_handler(const Parameters& params);
+       void remove_handler(const Parameters& params);
+       void sent_handler(const Parameters& params);
+       void view_handler(const Parameters& params);
+
+private:
+       // helpers
+       Sexp build_message_sexp(const Message&            msg,
+                               Store::Id                 docid,
+                               const Option<QueryMatch&> qm) const;
+
+       Sexp::List move_docid(Store::Id docid, Option<std::string> flagstr,
+                             bool new_name, bool no_view);
+
+       Sexp::List perform_move(Store::Id               docid,
+                               const Message&          msg,
+                               const std::string&      maildirarg,
+                               Flags                   flags,
+                               bool                    new_name,
+                               bool                    no_view);
+
+       bool maybe_mark_as_read(Store::Id docid, Flags old_flags, bool rename);
+       bool maybe_mark_msgid_as_read(const std::string& msgid, bool rename);
+
+       Store&            store_;
+       Server::Output    output_;
+       const CommandMap  command_map_;
+       std::atomic<bool> keep_going_{};
+       std::thread       index_thread_;
+};
+
+static Sexp
+build_metadata(const QueryMatch& qmatch)
+{
+       Sexp::List mdata;
+
+       auto symbol_t = [] { return Sexp::make_symbol("t"); };
+
+       mdata.add_prop(":path", Sexp::make_string(qmatch.thread_path));
+       mdata.add_prop(":level", Sexp::make_number(qmatch.thread_level));
+       mdata.add_prop(":date", Sexp::make_string(qmatch.thread_date));
+
+       Sexp::List dlist;
+       const auto td{::atoi(qmatch.thread_date.c_str())};
+       dlist.add(Sexp::make_number((unsigned)(td >> 16)));
+       dlist.add(Sexp::make_number((unsigned)(td & 0xffff)));
+       dlist.add(Sexp::make_number(0));
+       mdata.add_prop(":date-tstamp", Sexp::make_list(std::move(dlist)));
+
+       if (qmatch.has_flag(QueryMatch::Flags::Root))
+               mdata.add_prop(":root", symbol_t());
+       if (qmatch.has_flag(QueryMatch::Flags::Related))
+               mdata.add_prop(":related", symbol_t());
+       if (qmatch.has_flag(QueryMatch::Flags::First))
+               mdata.add_prop(":first-child", symbol_t());
+       if (qmatch.has_flag(QueryMatch::Flags::Last))
+               mdata.add_prop(":last-child", symbol_t());
+       if (qmatch.has_flag(QueryMatch::Flags::Orphan))
+               mdata.add_prop(":orphan", symbol_t());
+       if (qmatch.has_flag(QueryMatch::Flags::Duplicate))
+               mdata.add_prop(":duplicate", symbol_t());
+       if (qmatch.has_flag(QueryMatch::Flags::HasChild))
+               mdata.add_prop(":has-child", symbol_t());
+       if (qmatch.has_flag(QueryMatch::Flags::ThreadSubject))
+               mdata.add_prop(":thread-subject", symbol_t());
+
+       return Sexp::make_list(std::move(mdata));
+}
+
+/*
+ * A message here is a Sexp::List consists of a message s-expression with
+ * optionally a :meta expression added.
+ */
+Sexp
+Server::Private::build_message_sexp(const Message&            msg,
+                                   Store::Id                 docid,
+                                   const Option<QueryMatch&> qm) const
+{
+       auto sexp_list = msg.to_sexp_list();
+       if (docid != 0)
+               sexp_list.add_prop(":docid", Sexp::make_number(docid));
+       if (qm)
+               sexp_list.add_prop(":meta", build_metadata(*qm));
+
+       return Sexp::make_list(std::move(sexp_list));
+}
+
+CommandMap
+Server::Private::make_command_map()
+{
+       CommandMap cmap;
+
+       using Type = Sexp::Type;
+
+       cmap.emplace(
+           "add",
+           CommandInfo{
+               ArgMap{{":path", ArgInfo{Type::String, true, "file system path to the message"}}},
+               "add a message to the store",
+               [&](const auto& params) { add_handler(params); }});
+
+       cmap.emplace(
+           "compose",
+           CommandInfo{
+               ArgMap{
+                   {":type",
+                    ArgInfo{Type::Symbol,
+                            true,
+                            "type of composition: reply/forward/edit/resend/new"}},
+                   {":docid",
+                    ArgInfo{Type::Number, false, "document id of parent-message, if any"}},
+                   {":decrypt",
+                    ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)"}}},
+               "compose a new message",
+               [&](const auto& params) { compose_handler(params); }});
+
+       cmap.emplace(
+           "contacts",
+           CommandInfo{
+               ArgMap{{":personal", ArgInfo{Type::Symbol, false, "only personal contacts"}},
+                      {":after",
+                       ArgInfo{Type::String, false, "only contacts seen after time_t string"}},
+                      {":tstamp", ArgInfo{Type::String, false, "return changes since tstamp"}},
+                      {":maxnum", ArgInfo{Type::Number, false, "max number of contacts to return"}}},
+               "get contact information",
+               [&](const auto& params) { contacts_handler(params); }});
+       cmap.emplace(
+           "find",
+           CommandInfo{
+               ArgMap{{":query", ArgInfo{Type::String, true, "search expression"}},
+                      {":threads",
+                       ArgInfo{Type::Symbol, false, "whether to include threading information"}},
+                      {":sortfield", ArgInfo{Type::Symbol, false, "the field to sort results by"}},
+                      {":descending",
+                       ArgInfo{Type::Symbol, false, "whether to sort in descending order"}},
+                      {":batch-size", ArgInfo{Type::Number, false, "batch size for result"}},
+                      {":maxnum", ArgInfo{Type::Number, false, "maximum number of result (hint)"}},
+                      {":skip-dups",
+                       ArgInfo{Type::Symbol,
+                               false,
+                               "whether to skip messages with duplicate message-ids"}},
+                      {":include-related",
+                       ArgInfo{Type::Symbol,
+                               false,
+                               "whether to include other message related to matching ones"}}},
+               "query the database for messages",
+               [&](const auto& params) { find_handler(params); }});
+
+       cmap.emplace(
+           "help",
+           CommandInfo{
+               ArgMap{{":command", ArgInfo{Type::Symbol, false, "command to get information for"}},
+                      {":full", ArgInfo{Type::Symbol, false, "show full descriptions"}}},
+               "get information about one or all commands",
+               [&](const auto& params) { help_handler(params); }});
+       cmap.emplace(
+           "index",
+           CommandInfo{
+               ArgMap{{":my-addresses", ArgInfo{Type::List, false, "list of 'my' addresses"}},
+                      {":cleanup",
+                       ArgInfo{Type::Symbol,
+                               false,
+                               "whether to remove stale messages from the store"}},
+                      {":lazy-check",
+                       ArgInfo{Type::Symbol,
+                               false,
+                               "whether to avoid indexing up-to-date directories"}}},
+               "scan maildir for new/updated/removed messages",
+               [&](const auto& params) { index_handler(params); }});
+
+       cmap.emplace(
+           "move",
+           CommandInfo{
+               ArgMap{
+                   {":docid", ArgInfo{Type::Number, false, "document-id"}},
+                   {":msgid", ArgInfo{Type::String, false, "message-id"}},
+                   {":flags", ArgInfo{Type::String, false, "new flags for the message"}},
+                   {":maildir", ArgInfo{Type::String, false, "the target maildir"}},
+                   {":rename", ArgInfo{Type::Symbol, false, "change filename when moving"}},
+                   {":no-view",
+                    ArgInfo{Type::Symbol, false, "if set, do not hint at updating the view"}},
+               },
+               "move messages and/or change their flags",
+
+               [&](const auto& params) { move_handler(params); }});
+
+       cmap.emplace(
+           "mkdir",
+           CommandInfo{
+               ArgMap{{":path", ArgInfo{Type::String, true, "location for the new maildir"}}},
+               "create a new maildir",
+               [&](const auto& params) { mkdir_handler(params); }});
+       cmap.emplace(
+           "ping",
+           CommandInfo{
+               ArgMap{
+                   {":queries",
+                    ArgInfo{Type::List, false, "queries for which to get read/unread numbers"}},
+                   {":skip-dups",
+                    ArgInfo{Type::Symbol,
+                            false,
+                            "whether to exclude messages with duplicate message-ids"}},
+               },
+               "ping the mu-server and get information in response",
+               [&](const auto& params) { ping_handler(params); }});
+
+       cmap.emplace("quit", CommandInfo{{}, "quit the mu server", [&](const auto& params) {
+                                                quit_handler(params);
+                                        }});
+
+       cmap.emplace(
+           "remove",
+           CommandInfo{
+               ArgMap{{":docid",
+                       ArgInfo{Type::Number, true, "document-id for the message to remove"}}},
+               "remove a message from filesystem and database",
+               [&](const auto& params) { remove_handler(params); }});
+
+       cmap.emplace(
+           "sent",
+           CommandInfo{ArgMap{{":path", ArgInfo{Type::String, true, "path to the message file"}}},
+                       "tell mu about a message that was sent",
+                       [&](const auto& params) { sent_handler(params); }});
+
+       cmap.emplace(
+           "view",
+           CommandInfo{ArgMap{
+                           {":docid", ArgInfo{Type::Number, false, "document-id"}},
+                           {":msgid", ArgInfo{Type::String, false, "message-id"}},
+                           {":path", ArgInfo{Type::String, false, "message filesystem path"}},
+                           {":mark-as-read",
+                            ArgInfo{Type::Symbol, false, "mark message as read (if not already)"}},
+                           {":rename", ArgInfo{Type::Symbol, false, "change filename when moving"}},
+                       },
+                       "view a message. exactly one of docid/msgid/path must be specified",
+                       [&](const auto& params) { view_handler(params); }});
+       return cmap;
+}
+
+G_GNUC_PRINTF(2, 3)
+static Sexp
+make_error(Error::Code errcode, const char* frm, ...)
+{
+       char*   msg{};
+       va_list ap;
+
+       va_start(ap, frm);
+       g_vasprintf(&msg, frm, ap);
+       va_end(ap);
+
+       Sexp::List err;
+       err.add_prop(":error", Sexp::make_number(static_cast<int>(errcode)));
+       err.add_prop(":message", Sexp::make_string(msg));
+       g_free(msg);
+
+       return Sexp::make_list(std::move(err));
+}
+
+bool
+Server::Private::invoke(const std::string& expr) noexcept
+{
+       if (!keep_going_)
+               return false;
+
+       try {
+               auto call{Sexp::Sexp::make_parse(expr)};
+               Command::invoke(command_map(), call);
+
+       } catch (const Mu::Error& me) {
+               output_sexp(make_error(me.code(), "%s", me.what()));
+               keep_going_ = true;
+       } catch (const Xapian::Error& xerr) {
+               output_sexp(make_error(Error::Code::Internal, "xapian error: %s: %s",
+                                      xerr.get_type(), xerr.get_description().c_str()));
+               keep_going_ = false;
+       } catch (const std::runtime_error& re) {
+               output_sexp(make_error(Error::Code::Internal, "caught exception: %s", re.what()));
+               keep_going_ = false;
+       } catch (...) {
+               output_sexp(make_error(Error::Code::Internal, "something went wrong: quiting"));
+               keep_going_ = false;
+       }
+
+       return keep_going_;
+}
+
+/* 'add' adds a message to the database, and takes two parameters: 'path', which
+ * is the full path to the message, and 'maildir', which is the maildir this
+ * message lives in (e.g. "/inbox"). response with an (:info ...) message with
+ * information about the newly added message (details: see code below)
+ */
+void
+Server::Private::add_handler(const Parameters& params)
+{
+       auto       path{get_string_or(params, ":path")};
+       const auto docid_res{store().add_message(path)};
+
+       if (!docid_res)
+               throw docid_res.error();
+
+       const auto docid{docid_res.value()};
+
+       Sexp::List expr;
+       expr.add_prop(":info", Sexp::make_symbol("add"));
+       expr.add_prop(":path", Sexp::make_string(path));
+       expr.add_prop(":docid", Sexp::make_number(docid));
+
+       output_sexp(Sexp::make_list(std::move(expr)));
+
+       auto msg_res{store().find_message(docid)};
+       if (!msg_res)
+               throw Error(Error::Code::Store,
+                           "failed to get message at %s (docid=%u)",
+                           path.c_str(), docid);
+
+       Sexp::List update;
+       update.add_prop(":update", build_message_sexp(msg_res.value(), docid, {}));
+       output_sexp(Sexp::make_list(std::move(update)));
+}
+
+/* 'compose' produces the un-changed *original* message sexp (ie., the message
+ * to reply to, forward or edit) for a new message to compose). It takes two
+ * parameters: 'type' with the compose type (either reply, forward or
+ * edit/resend), and 'docid' for the message to reply to. Note, type:new does
+ * not have an original message, and therefore does not need a docid
+ *
+ * In returns a (:compose <type> [:original <original-msg>] [:include] )
+ * message (detals: see code below)
+ *
+ * Note ':include' t or nil determines whether to include attachments
+ */
+
+static Option<Sexp>
+maybe_add_attachment(Message& message, const MessagePart& part, size_t index)
+{
+       if (!part.is_attachment())
+               return Nothing;
+
+       const auto cache_path{message.cache_path(index)};
+       if (!cache_path)
+               throw cache_path.error();
+
+       const auto cooked_name{part.cooked_filename()};
+       const auto fname{format("%s/%s", cache_path->c_str(),
+                               cooked_name.value_or("part").c_str())};
+
+       const auto res = part.to_file(fname, true);
+       if (!res)
+               throw res.error();
+
+       Sexp::List pi;
+
+       if (auto cdescr = part.content_description(); cdescr)
+               pi.add_prop(":description", Sexp::make_string(*cdescr));
+       else if (cooked_name)
+               pi.add_prop(":description", Sexp::make_string(cooked_name.value()));
+
+       pi.add_prop(":file-name", Sexp::make_string(fname));
+       pi.add_prop(":mime-type", Sexp::make_string(
+                           part.mime_type().value_or("application/octet-stream")));
+
+       return Some(Sexp::make_list(std::move(pi)));
+}
+
+
+void
+Server::Private::compose_handler(const Parameters& params)
+{
+       const auto ctype{get_symbol_or(params, ":type")};
+
+       Sexp::List comp_lst;
+       comp_lst.add_prop(":compose", Sexp::make_symbol(std::string(ctype)));
+
+
+       if (ctype == "reply" || ctype == "forward" ||
+           ctype == "edit" || ctype == "resend") {
+
+               const unsigned docid{(unsigned)get_int_or(params, ":docid")};
+               auto  msg{store().find_message(docid)};
+               if (!msg)
+                       throw Error{Error::Code::Store, "failed to get message %u", docid};
+
+               comp_lst.add_prop(":original", build_message_sexp(msg.value(), docid, {}));
+
+               if (ctype == "forward") {
+                       // when forwarding, attach any attachment in the orig
+                       size_t index{};
+                       Sexp::List attseq;
+                       for (auto&& part: msg->parts()) {
+                               if (auto attsexp = maybe_add_attachment(
+                                           *msg, part, index); attsexp) {
+                                       attseq.add(std::move(*attsexp));
+                                       ++index;
+                               }
+                       }
+                       if (!attseq.empty()) {
+                               comp_lst.add_prop(":include",
+                                                 Sexp::make_list(std::move(attseq)));
+                               comp_lst.add_prop(":cache-path",
+                                                 Sexp::make_string(*msg->cache_path()));
+                       }
+               }
+
+       } else if (ctype != "new")
+               throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'",
+                           ctype.c_str());
+
+       output_sexp(std::move(comp_lst));
+}
+
+void
+Server::Private::contacts_handler(const Parameters& params)
+{
+       const auto personal  = get_bool_or(params, ":personal");
+       const auto afterstr  = get_string_or(params, ":after");
+       const auto tstampstr = get_string_or(params, ":tstamp");
+       const auto maxnum    = get_int_or(params, ":maxnum", 0 /*unlimited*/);
+
+       const auto after{afterstr.empty() ? 0 :
+               parse_date_time(afterstr, true).value_or(0)};
+       const auto tstamp = g_ascii_strtoll(tstampstr.c_str(), NULL, 10);
+
+       g_debug("find %s contacts last seen >= %s (tstamp: %zu)",
+               personal ? "personal" : "any",
+               time_to_string("%c", after).c_str(),
+               static_cast<size_t>(tstamp));
+
+       auto       n{0};
+       Sexp::List contacts;
+       store().contacts_cache().for_each([&](const Contact& ci) {
+
+               /* since the last time we got some contacts */
+               if (tstamp > ci.tstamp)
+                       return true;
+               /* (maybe) only include 'personal' contacts */
+               if (personal && !ci.personal)
+                       return true;
+               /* only include newer-than-x contacts */
+               if (after > ci.message_date)
+                       return true;
+
+               n++;
+
+               contacts.add(Sexp::make_string(ci.display_name(true/*encode-if-needed*/)));
+               return maxnum == 0 || n < maxnum;
+       });
+
+       Sexp::List seq;
+       seq.add_prop(":contacts", Sexp::make_list(std::move(contacts)));
+       seq.add_prop(":tstamp",
+                    Sexp::make_string(format("%" G_GINT64_FORMAT,
+                                             g_get_monotonic_time())));
+
+       /* dump the contacts cache as a giant sexp */
+       g_debug("sending %d of %zu contact(s)", n, store().contacts_cache().size());
+       output_sexp(std::move(seq), Server::OutputFlags::SplitList);
+}
+
+/* get a *list* of all messages with the given message id */
+static std::vector<Store::Id>
+docids_for_msgid(const Store& store, const std::string& msgid, size_t max = 100)
+{
+       if (msgid.size() > MaxTermLength) {
+               throw Error(Error::Code::InvalidArgument,
+                           "invalid message-id '%s'", msgid.c_str());
+       } else if (msgid.empty())
+               return {};
+
+       const auto xprefix{field_from_id(Field::Id::MessageId).shortcut};
+       /*XXX this is a bit dodgy */
+       auto tmp{g_ascii_strdown(msgid.c_str(), -1)};
+       auto expr{g_strdup_printf("%c:%s", xprefix, tmp)};
+       g_free(tmp);
+
+       GError*    gerr{};
+       std::lock_guard l{store.lock()};
+       const auto res{store.run_query(expr, {}, QueryFlags::None, max)};
+       g_free(expr);
+       if (!res)
+               throw Error(Error::Code::Store, &gerr, "failed to run msgid-query");
+       else if (res->empty())
+               throw Error(Error::Code::NotFound,
+                           "could not find message(s) for msgid %s", msgid.c_str());
+
+       std::vector<Store::Id> docids{};
+       for (auto&& mi : *res)
+               docids.emplace_back(mi.doc_id());
+
+       return docids;
+}
+
+/*
+ * creating a message object just to get a path seems a bit excessive maybe
+ * mu_store_get_path could be added if this turns out to be a problem
+ */
+static std::string
+path_from_docid(const Store& store, Store::Id docid)
+{
+       auto msg{store.find_message(docid)};
+       if (!msg)
+               throw Error(Error::Code::Store, "could not get message from store");
+
+       if (auto path{msg->path()}; path.empty())
+               throw Error(Error::Code::Store, "could not get path for message %u",
+                           docid);
+       else
+               return path;
+}
+
+static std::vector<Store::Id>
+determine_docids(const Store& store, const Parameters& params)
+{
+       auto       docid{get_int_or(params, ":docid", 0)};
+       const auto msgid{get_string_or(params, ":msgid")};
+
+       if ((docid == 0) == msgid.empty())
+               throw Error(Error::Code::InvalidArgument,
+                           "precisely one of docid and msgid must be specified");
+
+       if (docid != 0)
+               return {static_cast<Store::Id>(docid)};
+       else
+               return docids_for_msgid(store, msgid.c_str());
+}
+
+size_t
+Server::Private::output_results(const QueryResults& qres, size_t batch_size) const
+{
+       size_t     n{};
+       Sexp::List headers;
+
+       const auto output_batch = [&](Sexp::List&& hdrs) {
+               Sexp::List batch;
+               batch.add_prop(":headers", Sexp::make_list(std::move(hdrs)));
+               output_sexp(std::move(batch));
+       };
+
+       for (auto&& mi : qres) {
+               auto msg{mi.message()};
+               if (!msg)
+                       continue;
+               ++n;
+
+               // construct sexp for a single header.
+               auto qm{mi.query_match()};
+               auto msgsexp{build_message_sexp(*msg, mi.doc_id(), qm)};
+               msgsexp.formatting_opts |= Sexp::FormattingOptions::SplitList;
+               headers.add(std::move(msgsexp));
+               // we output up-to-batch-size lists of messages. It's much
+               // faster (on the emacs side) to handle such batches than single
+               // headers.
+               if (headers.size() % batch_size == 0) {
+                       output_batch(std::move(headers));
+                       headers.clear();
+               };
+       }
+
+       // remaining.
+       if (!headers.empty())
+               output_batch(std::move(headers));
+
+       return n;
+}
+
+void
+Server::Private::find_handler(const Parameters& params)
+{
+       const auto q{get_string_or(params, ":query")};
+       const auto threads{get_bool_or(params, ":threads", false)};
+       // perhaps let mu4e set this as frame-lines of the appropriate frame.
+       const auto batch_size{get_int_or(params, ":batch-size", 110)};
+       const auto sortfieldstr{get_symbol_or(params, ":sortfield", "")};
+       const auto descending{get_bool_or(params, ":descending", false)};
+       const auto maxnum{get_int_or(params, ":maxnum", -1 /*unlimited*/)};
+       const auto skip_dups{get_bool_or(params, ":skip-dups", false)};
+       const auto include_related{get_bool_or(params, ":include-related", false)};
+
+       auto sort_field = std::invoke([&]()->Option<Field>{
+               if (sortfieldstr.size() < 2)
+                       return Nothing;
+               else
+                       return field_from_name(sortfieldstr.substr(1));
+       });
+       if (!sort_field && !sortfieldstr.empty())
+               throw Error{Error::Code::InvalidArgument, "invalid sort field '%s'",
+                       sortfieldstr.c_str()};
+       if (batch_size < 1)
+               throw Error{Error::Code::InvalidArgument, "invalid batch-size %d", batch_size};
+
+       auto qflags{QueryFlags::SkipUnreadable}; // don't show unreadables.
+       if (descending)
+               qflags |= QueryFlags::Descending;
+       if (skip_dups)
+               qflags |= QueryFlags::SkipDuplicates;
+       if (include_related)
+               qflags |= QueryFlags::IncludeRelated;
+       if (threads)
+               qflags |= QueryFlags::Threading;
+
+       std::lock_guard l{store_.lock()};
+       auto qres{store_.run_query(q, sort_field->id, qflags, maxnum)};
+       if (!qres)
+               throw Error(Error::Code::Query, "failed to run query");
+
+       /* before sending new results, send an 'erase' message, so the frontend
+        * knows it should erase the headers buffer. this will ensure that the
+        * output of two finds will not be mixed. */
+       {
+               Sexp::List lst;
+               lst.add_prop(":erase", Sexp::make_symbol("t"));
+               output_sexp(std::move(lst));
+       }
+
+       const auto foundnum{output_results(*qres, static_cast<size_t>(batch_size))};
+
+       {
+               Sexp::List lst;
+               lst.add_prop(":found", Sexp::make_number(foundnum));
+               output_sexp(std::move(lst));
+       }
+}
+
+void
+Server::Private::help_handler(const Parameters& params)
+{
+       const auto command{get_symbol_or(params, ":command", "")};
+       const auto full{get_bool_or(params, ":full", !command.empty())};
+
+       if (command.empty()) {
+               std::cout << ";; Commands are s-expressions of the form\n"
+                         << ";;   (<command-name> :param1 val1 :param2 val2 ...)\n"
+                         << ";; For instance:\n;;  (help :command quit)\n"
+                         << ";; to get detailed information about the 'quit'\n;;\n";
+               std::cout << ";; The following commands are available:\n\n";
+       }
+
+       std::vector<std::string> names;
+       for (auto&& name_cmd : command_map())
+               names.emplace_back(name_cmd.first);
+       std::sort(names.begin(), names.end());
+
+       for (auto&& name : names) {
+               const auto& info{command_map().find(name)->second};
+
+               if (!command.empty() && name != command)
+                       continue;
+
+               if (!command.empty())
+                       std::cout << ";;   "
+                                 << format("%-10s -- %s\n", name.c_str(), info.docstring.c_str());
+               else
+                       std::cout << ";;  " << name.c_str() << " -- " << info.docstring.c_str()
+                                 << '\n';
+               if (!full)
+                       continue;
+
+               for (auto&& argname : info.sorted_argnames()) {
+                       const auto& arg{info.args.find(argname)};
+                       std::cout << ";;        "
+                                 << format("%-17s  : %-24s ",
+                                           arg->first.c_str(),
+                                           to_string(arg->second).c_str());
+                       std::cout << "  " << arg->second.docstring << "\n";
+               }
+               std::cout << ";;\n";
+       }
+}
+
+static Sexp::List
+get_stats(const Indexer::Progress& stats, const std::string& state)
+{
+       Sexp::List lst;
+
+       lst.add_prop(":info", Sexp::make_symbol("index"));
+       lst.add_prop(":status", Sexp::make_symbol(std::string{state}));
+       lst.add_prop(":checked", Sexp::make_number(stats.checked));
+       lst.add_prop(":updated", Sexp::make_number(stats.updated));
+       lst.add_prop(":cleaned-up", Sexp::make_number(stats.removed));
+
+       return lst;
+}
+
+void
+Server::Private::index_handler(const Parameters& params)
+{
+       Mu::Indexer::Config conf{};
+       conf.cleanup    = get_bool_or(params, ":cleanup");
+       conf.lazy_check = get_bool_or(params, ":lazy-check");
+       // ignore .noupdate with an empty store.
+       conf.ignore_noupdate = store().empty();
+
+       indexer().stop();
+       if (index_thread_.joinable())
+               index_thread_.join();
+
+       // start a background track.
+       index_thread_ = std::thread([this, conf = std::move(conf)] {
+               indexer().start(conf);
+               while (indexer().is_running()) {
+                       std::this_thread::sleep_for(std::chrono::milliseconds(2000));
+                       output_sexp(get_stats(indexer().progress(), "running"),
+                                   Server::OutputFlags::Flush);
+               }
+               output_sexp(get_stats(indexer().progress(), "complete"),
+                           Server::OutputFlags::Flush);
+               store().commit(); /* ensure on-disk database is updated, too */
+       });
+}
+
+void
+Server::Private::mkdir_handler(const Parameters& params)
+{
+       const auto path{get_string_or(params, ":path")};
+       if (auto&& res = maildir_mkdir(path, 0755, FALSE); !res)
+               throw res.error();
+
+       Sexp::List lst;
+       lst.add_prop(":info", Sexp::make_string("mkdir"));
+       lst.add_prop(":message", Sexp::make_string(format("%s has been created", path.c_str())));
+
+       output_sexp(std::move(lst));
+}
+
+Sexp::List
+Server::Private::perform_move(Store::Id                 docid,
+                             const Message&            msg,
+                             const std::string&        maildirarg,
+                             Flags                     flags,
+                             bool                      new_name,
+                             bool                      no_view)
+{
+       bool different_mdir{};
+       auto maildir{maildirarg};
+       if (maildir.empty()) {
+               maildir        = msg.maildir();
+               different_mdir = false;
+       } else /* are we moving to a different mdir, or is it just flags? */
+               different_mdir = maildir != msg.maildir();
+
+       const auto new_msg = store().move_message(docid, maildir, flags, new_name);
+       if (!new_msg)
+               throw new_msg.error();
+
+       Sexp::List seq;
+       seq.add_prop(":update", build_message_sexp(new_msg.value(), docid, {}));
+       /* note, the :move t thing is a hint to the frontend that it
+        * could remove the particular header */
+       if (different_mdir)
+               seq.add_prop(":move", Sexp::make_symbol("t"));
+       if (!no_view)
+               seq.add_prop(":maybe-view", Sexp::make_symbol("t"));
+
+       return seq;
+}
+
+
+static Flags
+calculate_message_flags(const Message& msg, Option<std::string> flagopt)
+{
+       const auto flags = std::invoke([&]()->Option<Flags>{
+                       if (!flagopt)
+                               return msg.flags();
+                       else
+                               return flags_from_expr(*flagopt, msg.flags());
+               });
+
+       if (!flags)
+               throw Error{Error::Code::InvalidArgument,
+                       "invalid flags '%s'", flagopt.value_or("").c_str()};
+       else
+               return flags.value();
+}
+
+Sexp::List
+Server::Private::move_docid(Store::Id          docid,
+                           Option<std::string> flagopt,
+                           bool                new_name,
+                           bool                no_view)
+{
+       if (docid == Store::InvalidId)
+               throw Error{Error::Code::InvalidArgument, "invalid docid"};
+
+       auto msg{store_.find_message(docid)};
+       if (!msg)
+               throw Error{Error::Code::Store, "failed to get message from store"};
+
+       const auto flags = calculate_message_flags(msg.value(), flagopt);
+       auto lst = perform_move(docid, *msg, "", flags, new_name, no_view);
+       return lst;
+}
+
+/*
+ * 'move' moves a message to a different maildir and/or changes its
+ * flags. parameters are *either* a 'docid:' or 'msgid:' pointing to
+ * the message, a 'maildir:' for the target maildir, and a 'flags:'
+ * parameter for the new flags.
+ *
+ * returns an (:update <new-msg-sexp>)
+ *
+ */
+void
+Server::Private::move_handler(const Parameters& params)
+{
+       auto       maildir{get_string_or(params, ":maildir")};
+       const auto flagopt{get_string(params, ":flags")};
+       const auto rename{get_bool_or(params, ":rename")};
+       const auto no_view{get_bool_or(params, ":noupdate")};
+       const auto docids{determine_docids(store_, params)};
+
+       if (docids.size() > 1) {
+               if (!maildir.empty()) // ie. duplicate message-ids.
+                       throw Mu::Error{Error::Code::Store,
+                                       "can't move multiple messages at the same time"};
+               // multi.
+               for (auto&& docid : docids)
+                       output_sexp(move_docid(docid, flagopt,
+                                              rename, no_view));
+               return;
+       }
+       auto docid{docids.at(0)};
+       auto    msg = store().find_message(docid)
+               .or_else([]{throw Error{Error::Code::InvalidArgument,
+                                       "could not create message"};}).value();
+
+       /* if maildir was not specified, take the current one */
+       if (maildir.empty())
+               maildir = msg.maildir();
+
+       /* determine the real target flags, which come from the flags-parameter
+        * we received (ie., flagstr), if any, plus the existing message
+        * flags. */
+       const auto flags = calculate_message_flags(msg, flagopt);
+       output_sexp(perform_move(docid, msg, maildir, flags, rename, no_view));
+}
+
+void
+Server::Private::ping_handler(const Parameters& params)
+{
+       const auto storecount{store().size()};
+       if (storecount == (unsigned)-1)
+               throw Error{Error::Code::Store, "failed to read store"};
+
+       const auto queries{get_string_vec(params, ":queries")};
+       Sexp::List qresults;
+       for (auto&& q : queries) {
+               const auto count{store_.count_query(q)};
+               const auto unreadq{format("flag:unread AND (%s)", q.c_str())};
+               const auto unread{store_.count_query(unreadq)};
+
+               Sexp::List lst;
+               lst.add_prop(":query", Sexp::make_string(q));
+               lst.add_prop(":count", Sexp::make_number(count));
+               lst.add_prop(":unread", Sexp::make_number(unread));
+
+               qresults.add(Sexp::make_list(std::move(lst)));
+       }
+
+       Sexp::List addrs;
+       for (auto&& addr : store().properties().personal_addresses)
+               addrs.add(Sexp::make_string(addr));
+
+       Sexp::List lst;
+       lst.add_prop(":pong", Sexp::make_string("mu"));
+
+       Sexp::List proplst;
+       proplst.add_prop(":version", Sexp::make_string(VERSION));
+       proplst.add_prop(":personal-addresses", Sexp::make_list(std::move(addrs)));
+       proplst.add_prop(":database-path", Sexp::make_string(store().properties().database_path));
+       proplst.add_prop(":root-maildir", Sexp::make_string(store().properties().root_maildir));
+       proplst.add_prop(":doccount", Sexp::make_number(storecount));
+       proplst.add_prop(":queries", Sexp::make_list(std::move(qresults)));
+
+       lst.add_prop(":props", Sexp::make_list(std::move(proplst)));
+
+       output_sexp(std::move(lst));
+}
+
+void
+Server::Private::quit_handler(const Parameters& params)
+{
+       keep_going_ = false;
+}
+
+void
+Server::Private::remove_handler(const Parameters& params)
+{
+       const auto docid{get_int_or(params, ":docid")};
+       const auto path{path_from_docid(store(), docid)};
+
+       if (::unlink(path.c_str()) != 0 && errno != ENOENT)
+               throw Error(Error::Code::File,
+                           "could not delete %s: %s",
+                           path.c_str(),
+                           g_strerror(errno));
+
+       if (!store().remove_message(path))
+               g_warning("failed to remove message @ %s (%d) from store", path.c_str(), docid);
+       // act as if it worked.
+
+       Sexp::List lst;
+       lst.add_prop(":remove", Sexp::make_number(docid));
+
+       output_sexp(std::move(lst));
+}
+
+void
+Server::Private::sent_handler(const Parameters& params)
+{
+       const auto path{get_string_or(params, ":path")};
+       const auto docid = store().add_message(path);
+       if (!docid)
+               throw Error{Error::Code::Store, "failed to add path"};
+
+       Sexp::List lst;
+       lst.add_prop(":sent", Sexp::make_symbol("t"));
+       lst.add_prop(":path", Sexp::make_string(path));
+       lst.add_prop(":docid", Sexp::make_number(docid.value()));
+
+       output_sexp(std::move(lst));
+}
+
+bool
+Server::Private::maybe_mark_as_read(Store::Id docid, Flags oldflags, bool rename)
+{
+       const auto newflags{flags_from_delta_expr("+S-u-N", oldflags)};
+       if (!newflags || oldflags == *newflags)
+               return false; // nothing to do.
+
+       const auto msg = store().move_message(docid, {}, newflags, rename);
+       if (!msg)
+               throw msg.error();
+
+       /* send an update */
+       Sexp::List update;
+       update.add_prop(":update", build_message_sexp(*msg, docid, {}));
+       output_sexp(Sexp::make_list(std::move(update)));
+
+       g_debug("marked message %d as read => %s", docid, msg->path().c_str());
+
+       return true;
+}
+
+bool
+Server::Private::maybe_mark_msgid_as_read(const std::string& msgid, bool rename) try
+{
+       const auto docids = docids_for_msgid(store_, msgid);
+       if (!docids.empty())
+               g_debug("marking %zu messages with message-id '%s' as read",
+                       docids.size(), msgid.c_str());
+
+       for (auto&& docid: docids)
+               if (auto msg{store().find_message(docid)}; msg)
+                       maybe_mark_as_read(docid, msg->flags(), rename);
+
+       return true;
+
+} catch (...) { /* not fatal */
+       g_warning("failed to mark <%s> as read", msgid.c_str());
+       return false;
+}
+
+void
+Server::Private::view_handler(const Parameters& params)
+{
+       const auto mark_as_read{get_bool_or(params, ":mark-as-read")};
+       /* for now, do _not_ rename, as it seems to confuse mbsync */
+       const auto rename{false};
+       //const auto rename{get_bool_or(params, ":rename")};
+
+       const auto docids{determine_docids(store(), params)};
+
+       if (docids.empty())
+               throw Error{Error::Code::Store, "failed to find message for view"};
+
+       const auto docid{docids.at(0)};
+       auto msg = store().find_message(docid)
+               .or_else([]{throw Error{Error::Code::Store,
+                                       "failed to find message for view"};}).value();
+
+       if (mark_as_read) {
+               // maybe mark the main message as read.
+               maybe_mark_as_read(docid, msg.flags(), rename);
+               /* maybe mark _all_ messsage with same message-id as read */
+               maybe_mark_msgid_as_read(msg.message_id(), rename);
+       }
+
+       Sexp::List seq;
+       seq.add_prop(":view", build_message_sexp(msg, docid, {}));
+       output_sexp(std::move(seq));
+}
+
+Server::Server(Store& store, Server::Output output)
+    : priv_{std::make_unique<Private>(store, output)}
+{}
+
+Server::~Server() = default;
+
+bool
+Server::invoke(const std::string& expr) noexcept
+{
+       return priv_->invoke(expr);
+}
diff --git a/lib/mu-server.hh b/lib/mu-server.hh
new file mode 100644 (file)
index 0000000..95c7ffe
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_SERVER_HH__
+#define MU_SERVER_HH__
+
+#include <memory>
+#include <functional>
+
+#include <utils/mu-sexp.hh>
+#include <utils/mu-utils.hh>
+#include <mu-store.hh>
+
+namespace Mu {
+
+/**
+ * @brief Implements the mu    server, as used by mu4e.
+ *
+ */
+class Server {
+public:
+       enum struct OutputFlags {
+               None      = 0,
+               SplitList = 1 << 0,
+               /**< insert newlines between list items */
+               Flush     = 1 << 1,
+               /**< flush output buffer after */
+       };
+
+       /**
+        * Prototype for output function
+        *
+        * @param sexp an s-expression
+        * @param flags flags that influence the behavior
+        */
+       using Output = std::function<void(Sexp&& sexp, OutputFlags flags)>;
+
+       /**
+        * Construct a new server
+        *
+        * @param store a message store object
+        * @param output callable for the server responses.
+        */
+       Server(Store& store, Output output);
+
+       /**
+        * DTOR
+        */
+       ~Server();
+
+       /**
+        * Invoke a call on the server.
+        *
+        * @param expr the s-expression to call
+        *
+        * @return true if we the server is still ready for more
+        * calls, false when it should quit.
+        */
+       bool invoke(const std::string& expr) noexcept;
+
+private:
+       struct Private;
+       std::unique_ptr<Private> priv_;
+};
+MU_ENABLE_BITOPS(Server::OutputFlags);
+
+} // namespace Mu
+
+
+#endif /* MU_SERVER_HH__ */
diff --git a/lib/mu-store.cc b/lib/mu-store.cc
new file mode 100644 (file)
index 0000000..f26a072
--- /dev/null
@@ -0,0 +1,716 @@
+/*
+** Copyright (C) 2021-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <chrono>
+#include <memory>
+#include <mutex>
+#include <array>
+#include <cstdlib>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+#include <atomic>
+#include <type_traits>
+#include <iostream>
+#include <cstring>
+
+#include <vector>
+#include <xapian.h>
+
+#include "mu-maildir.hh"
+#include "mu-store.hh"
+#include "mu-query.hh"
+#include "utils/mu-error.hh"
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-xapian-utils.hh"
+
+using namespace Mu;
+
+static_assert(std::is_same<Store::Id, Xapian::docid>::value, "wrong type for Store::Id");
+
+// Properties
+constexpr auto SchemaVersionKey     = "schema-version";
+constexpr auto RootMaildirKey       = "maildir"; // XXX: make this 'root-maildir'
+constexpr auto ContactsKey          = "contacts";
+constexpr auto PersonalAddressesKey = "personal-addresses";
+constexpr auto CreatedKey           = "created";
+constexpr auto BatchSizeKey         = "batch-size";
+constexpr auto DefaultBatchSize     = 250'000U;
+
+constexpr auto MaxMessageSizeKey     = "max-message-size";
+constexpr auto DefaultMaxMessageSize = 100'000'000U;
+constexpr auto ExpectedSchemaVersion = MU_STORE_SCHEMA_VERSION;
+
+// Stats.
+constexpr auto ChangedKey           = "changed";
+constexpr auto IndexedKey           = "indexed";
+
+
+static std::string
+tstamp_to_string(::time_t t)
+{
+       char buf[17];
+       ::snprintf(buf, sizeof(buf), "%" PRIx64, static_cast<int64_t>(t));
+       return std::string(buf);
+}
+
+static ::time_t
+string_to_tstamp(const std::string& str)
+{
+       return static_cast<::time_t>(::strtoll(str.c_str(), {}, 16));
+}
+
+struct Store::Private {
+       enum struct XapianOpts { ReadOnly, Open, CreateOverwrite };
+
+       Private(const std::string& path, bool readonly)
+               : read_only_{readonly}, db_{make_xapian_db(path,
+                                                          read_only_ ? XapianOpts::ReadOnly
+                                                          : XapianOpts::Open)},
+                 properties_{make_properties(path)},
+                 contacts_cache_{db().get_metadata(ContactsKey),
+               properties_.personal_addresses} {
+       }
+
+       Private(const std::string& path,
+               const std::string& root_maildir,
+               const StringVec& personal_addresses,
+               const Store::Config& conf)
+           : read_only_{false}, db_{make_xapian_db(path, XapianOpts::CreateOverwrite)},
+             properties_{init_metadata(conf, path, root_maildir, personal_addresses)},
+             contacts_cache_{"", properties_.personal_addresses} {
+       }
+
+       ~Private() try {
+
+               g_debug("closing store @ %s", properties_.database_path.c_str());
+               if (!read_only_) {
+                       transaction_maybe_commit(true /*force*/);
+               }
+       } catch (...) {
+               g_critical("caught exception in store dtor");
+       }
+
+       std::unique_ptr<Xapian::Database> make_xapian_db(const std::string db_path, XapianOpts opts)
+       try {
+               /* we do our own flushing, set Xapian's internal one as the
+                * backstop*/
+               g_setenv("XAPIAN_FLUSH_THRESHOLD", "500000", 1);
+
+               if (g_mkdir_with_parents(db_path.c_str(), 0700) != 0)
+                       throw Mu::Error(Error::Code::Internal,
+                               "failed to create database dir %s: %s",
+                                       db_path.c_str(), ::strerror(errno));
+
+               switch (opts) {
+               case XapianOpts::ReadOnly:
+                       return std::make_unique<Xapian::Database>(db_path);
+               case XapianOpts::Open:
+                       return std::make_unique<Xapian::WritableDatabase>(db_path, Xapian::DB_OPEN);
+               case XapianOpts::CreateOverwrite:
+                       return std::make_unique<Xapian::WritableDatabase>(
+                               db_path,
+                           Xapian::DB_CREATE_OR_OVERWRITE);
+               default:
+                       throw std::logic_error("invalid xapian options");
+               }
+
+       } catch (const Xapian::DatabaseLockError& xde) {
+               throw Mu::Error(Error::Code::StoreLock,
+                               "%s", xde.get_msg().c_str());
+       } catch (const Xapian::DatabaseError& xde) {
+               throw Mu::Error(Error::Code::Store,
+                               "%s", xde.get_msg().c_str());
+       } catch (const Mu::Error& me) {
+               throw;
+       } catch (...) {
+               throw Mu::Error(Error::Code::Internal,
+                               "something went wrong when opening store @ %s",
+                               db_path.c_str());
+       }
+
+       const Xapian::Database& db() const { return *db_.get(); }
+
+       Xapian::WritableDatabase& writable_db()
+       {
+               if (read_only_)
+                       throw Mu::Error(Error::Code::AccessDenied, "database is read-only");
+               return dynamic_cast<Xapian::WritableDatabase&>(*db_.get());
+       }
+
+       // If not started yet, start a transaction. Otherwise, just update the transaction size.
+       void transaction_inc() noexcept
+       {
+               if (transaction_size_ == 0) {
+                       g_debug("starting transaction");
+                       xapian_try([this] { writable_db().begin_transaction(); });
+               }
+               ++transaction_size_;
+       }
+
+       // Opportunistically commit a transaction if the transaction size
+       // filled up a batch, or with force.
+       void transaction_maybe_commit(bool force = false) noexcept {
+               if (force || transaction_size_ >= properties_.batch_size) {
+                       if (contacts_cache_.dirty()) {
+                               xapian_try([&] {
+                                       writable_db().set_metadata(ContactsKey,
+                                                                  contacts_cache_.serialize());
+                               });
+                       }
+
+                       if (indexer_) { // save last index time.
+                               if (auto&& t{indexer_->completed()}; t != 0)
+                                       writable_db().set_metadata(
+                                               IndexedKey, tstamp_to_string(t));
+                       }
+
+                       if (transaction_size_ == 0)
+                               return; // nothing more to do here.
+
+                       g_debug("committing transaction (n=%zu,%zu)",
+                               transaction_size_, metadata_cache_.size());
+                       xapian_try([this] {
+                               writable_db().commit_transaction();
+                               for (auto&& mdata : metadata_cache_)
+                                       writable_db().set_metadata(mdata.first, mdata.second);
+                               transaction_size_ = 0;
+                       });
+               }
+       }
+
+       time_t metadata_time_t(const std::string& key) const {
+               const auto ts = db().get_metadata(key);
+               return (time_t)atoll(db().get_metadata(key).c_str());
+       }
+
+       Store::Properties make_properties(const std::string& db_path)
+       {
+               Store::Properties props;
+
+               props.database_path      = db_path;
+               props.schema_version     = db().get_metadata(SchemaVersionKey);
+               props.created            = string_to_tstamp(db().get_metadata(CreatedKey));
+               props.read_only          = read_only_;
+               props.batch_size         = ::atoll(db().get_metadata(BatchSizeKey).c_str());
+               props.max_message_size   = ::atoll(db().get_metadata(MaxMessageSizeKey).c_str());
+               props.root_maildir       = db().get_metadata(RootMaildirKey);
+               props.personal_addresses = Mu::split(db().get_metadata(PersonalAddressesKey), ",");
+
+               return props;
+       }
+
+       Store::Properties init_metadata(const Store::Config& conf,
+                                       const std::string&   path,
+                                       const std::string&   root_maildir,
+                                       const StringVec&     personal_addresses) {
+
+               writable_db().set_metadata(SchemaVersionKey, ExpectedSchemaVersion);
+               writable_db().set_metadata(CreatedKey, tstamp_to_string(::time({})));
+
+               const size_t batch_size = conf.batch_size ? conf.batch_size : DefaultBatchSize;
+               writable_db().set_metadata(BatchSizeKey, Mu::format("%zu", batch_size));
+               const size_t max_msg_size = conf.max_message_size ? conf.max_message_size
+                                                                 : DefaultMaxMessageSize;
+               writable_db().set_metadata(MaxMessageSizeKey, Mu::format("%zu", max_msg_size));
+
+               writable_db().set_metadata(RootMaildirKey, canonicalize_filename(root_maildir, {}));
+
+               std::string addrs;
+               for (const auto& addr : personal_addresses) { // _very_ minimal check.
+                       if (addr.find(",") != std::string::npos)
+                               throw Mu::Error(Error::Code::InvalidArgument,
+                                               "e-mail address '%s' contains comma",
+                                               addr.c_str());
+                       addrs += (addrs.empty() ? "" : ",") + addr;
+               }
+               writable_db().set_metadata(PersonalAddressesKey, addrs);
+
+               return make_properties(path);
+       }
+
+       Option<Message> find_message_unlocked(Store::Id docid) const;
+       Result<Store::Id> update_message_unlocked(Message& msg, Store::Id docid);
+       Result<Store::Id> update_message_unlocked(Message& msg, const std::string& old_path);
+
+       /* metadata to write as part of a transaction commit */
+       std::unordered_map<std::string, std::string> metadata_cache_;
+
+       const bool                        read_only_{};
+       std::unique_ptr<Xapian::Database> db_;
+
+       const Store::Properties properties_;
+       ContactsCache            contacts_cache_;
+       std::unique_ptr<Indexer> indexer_;
+
+       size_t     transaction_size_{};
+       std::mutex lock_;
+};
+
+Result<Store::Id>
+Store::Private::update_message_unlocked(Message& msg, Store::Id docid)
+{
+       msg.update_cached_sexp();
+
+       return xapian_try_result([&]{
+               writable_db().replace_document(docid, msg.document().xapian_document());
+               g_debug("updated message @ %s; docid = %u", msg.path().c_str(), docid);
+               writable_db().set_metadata(ChangedKey, tstamp_to_string(::time({})));
+               return Ok(std::move(docid));
+       });
+}
+
+Result<Store::Id>
+Store::Private::update_message_unlocked(Message& msg, const std::string& path_to_replace)
+{
+       msg.update_cached_sexp();
+
+       return xapian_try_result([&]{
+               auto id = writable_db().replace_document(
+                       field_from_id(Field::Id::Path).xapian_term(path_to_replace),
+                       msg.document().xapian_document());
+
+               writable_db().set_metadata(ChangedKey, tstamp_to_string(::time({})));
+               return Ok(std::move(id));
+       });
+}
+
+Option<Message>
+Store::Private::find_message_unlocked(Store::Id docid) const
+{
+       return xapian_try([&]()->Option<Message> {
+               auto res = Message::make_from_document(db().get_document(docid));
+               if (res)
+                       return Some(std::move(res.value()));
+               else
+                       return Nothing;
+               }, Nothing);
+}
+
+
+Store::Store(const std::string& path, Store::Options opts)
+    : priv_{std::make_unique<Private>(path, none_of(opts & Store::Options::Writable))}
+{
+       if (properties().schema_version == ExpectedSchemaVersion)
+               return; // all is good.
+
+       // Now, it seems the schema versions do not match; shall we automatically
+       // update?
+
+       if (none_of(opts & Store::Options::AutoUpgrade)) {
+               // not allowed to auto-upgrade, so we give up.
+               throw Mu::Error(Error::Code::SchemaMismatch,
+                               "expected schema-version %s, but got %s; "
+                               "cannot auto-upgrade; please use 'mu init'",
+                               ExpectedSchemaVersion,
+                               properties().schema_version.c_str());
+       }
+
+       // Okay, let's attempt an auto-upgrade.
+       g_info("attempt reinit database from schema %s --> %s",
+              properties().schema_version.c_str(), ExpectedSchemaVersion);
+
+       Config  conf;
+       conf.batch_size       = properties().batch_size;
+       conf.max_message_size = properties().max_message_size;
+
+       priv_.reset();
+       priv_ = std::make_unique<Private>(path,
+                                         properties().root_maildir,
+                                         properties().personal_addresses,
+                                         conf);
+       // Now let's try again.
+       priv_.reset();
+       priv_ = std::make_unique<Private>(path, none_of(opts & Store::Options::Writable));
+       if (properties().schema_version != ExpectedSchemaVersion)
+               // Nope, we failed.
+               throw Mu::Error(Error::Code::SchemaMismatch,
+                               "failed to auto-upgrade from %s to %s; "
+                               "please use 'mu init'",
+                               properties().schema_version.c_str(),
+                               ExpectedSchemaVersion);
+}
+
+Store::Store(const std::string&   path,
+            const std::string&   maildir,
+            const StringVec&     personal_addresses,
+            const Store::Config& conf)
+    : priv_{std::make_unique<Private>(path, maildir, personal_addresses, conf)}
+{
+}
+
+Store::Store(Store&& other)
+{
+       priv_ = std::move(other.priv_);
+       priv_->indexer_.reset();
+}
+
+Store::~Store() = default;
+
+const Store::Properties&
+Store::properties() const
+{
+       return priv_->properties_;
+}
+
+Store::Statistics
+Store::statistics() const
+{
+       Statistics stats{};
+
+       stats.size = size();
+       stats.last_change = string_to_tstamp(priv_->db().get_metadata(ChangedKey));
+       stats.last_index = string_to_tstamp(priv_->db().get_metadata(IndexedKey));
+
+       return stats;
+}
+
+
+
+const ContactsCache&
+Store::contacts_cache() const
+{
+       return priv_->contacts_cache_;
+}
+
+const Xapian::Database&
+Store::database() const
+{
+       return priv_->db();
+}
+
+Indexer&
+Store::indexer()
+{
+       std::lock_guard guard{priv_->lock_};
+
+       if (properties().read_only)
+               throw Error{Error::Code::Store, "no indexer for read-only store"};
+       else if (!priv_->indexer_)
+               priv_->indexer_ = std::make_unique<Indexer>(*this);
+
+       return *priv_->indexer_.get();
+}
+
+std::size_t
+Store::size() const
+{
+       std::lock_guard guard{priv_->lock_};
+       return priv_->db().get_doccount();
+}
+
+bool
+Store::empty() const
+{
+       return size() == 0;
+}
+
+Result<Store::Id>
+Store::add_message(const std::string& path, bool use_transaction)
+{
+       if (auto msg{Message::make_from_path(path)}; !msg)
+               return Err(msg.error());
+       else
+               return add_message(msg.value(), use_transaction);
+}
+
+Result<Store::Id>
+Store::add_message(Message& msg, bool use_transaction)
+{
+       std::lock_guard guard{priv_->lock_};
+
+       const auto mdir{maildir_from_path(msg.path(),
+                                            properties().root_maildir)};
+       if (!mdir)
+               return Err(mdir.error());
+
+       if (auto&& res = msg.set_maildir(mdir.value()); !res)
+               return Err(res.error());
+       /* add contacts from this message to cache; this cache
+        * also determines whether those contacts are _personal_, i.e. match
+        * our personal addresses.
+        *
+        * if a message has any personal contacts, mark it as personal; do
+        * this by updating the message flags.
+        */
+       bool is_personal{};
+       priv_->contacts_cache_.add(msg.all_contacts(), is_personal);
+       if (is_personal)
+               msg.set_flags(msg.flags() | Flags::Personal);
+
+       if (use_transaction)
+               priv_->transaction_inc();
+
+       auto res = priv_->update_message_unlocked(msg, msg.path());
+       if (!res)
+               return Err(res.error());
+
+       if (use_transaction) /* commit if batch is full */
+               priv_->transaction_maybe_commit();
+
+       g_debug("added %smessage @ %s; docid = %u",
+               is_personal ? "personal " : "", msg.path().c_str(), *res);
+
+       return res;
+}
+
+
+Result<Store::Id>
+Store::update_message(Message& msg, Store::Id docid)
+{
+       std::lock_guard guard{priv_->lock_};
+
+       return priv_->update_message_unlocked(msg, docid);
+}
+
+bool
+Store::remove_message(const std::string& path)
+{
+       return xapian_try(
+           [&] {
+                   std::lock_guard   guard{priv_->lock_};
+                   const auto term{field_from_id(Field::Id::Path).xapian_term(path)};
+                   priv_->writable_db().delete_document(term);
+                   priv_->writable_db().set_metadata(
+                           ChangedKey, tstamp_to_string(::time({})));
+                   g_debug("deleted message @ %s from store", path.c_str());
+
+                   return true;
+           },
+           false);
+}
+
+void
+Store::remove_messages(const std::vector<Store::Id>& ids)
+{
+       std::lock_guard guard{priv_->lock_};
+
+       priv_->transaction_inc();
+
+       xapian_try([&] {
+               for (auto&& id : ids) {
+                       priv_->writable_db().delete_document(id);
+               }
+               priv_->writable_db().set_metadata(
+                       ChangedKey, tstamp_to_string(::time({})));
+       });
+
+       priv_->transaction_maybe_commit(true /*force*/);
+}
+
+
+Option<Message>
+Store::find_message(Store::Id docid) const
+{
+       std::lock_guard guard{priv_->lock_};
+
+       return priv_->find_message_unlocked(docid);
+}
+
+
+Result<Message>
+Store::move_message(Store::Id id,
+                   Option<const std::string&> target_mdir,
+                   Option<Flags> new_flags, bool change_name)
+{
+       std::lock_guard guard{priv_->lock_};
+
+       auto msg = priv_->find_message_unlocked(id);
+       if (!msg)
+               return Err(Error::Code::Store, "cannot find message <%u>", id);
+
+       const auto      old_path       = msg->path();
+       const auto      target_flags   = new_flags.value_or(msg->flags());
+       const auto      target_maildir = target_mdir.value_or(msg->maildir());
+
+       /* 1. first determine the file system path of the target */
+       const auto target_path =
+               maildir_determine_target(msg->path(), properties().root_maildir,
+                                           target_maildir,target_flags, change_name);
+       if (!target_path)
+               return Err(target_path.error());
+
+       /* 2. let's move it */
+       if (const auto res = maildir_move_message(msg->path(), target_path.value()); !res)
+               return Err(res.error());
+
+       /* 3. file move worked, now update the message with the new info.*/
+       if (auto&& res = msg->update_after_move(
+                   target_path.value(), target_maildir, target_flags); !res)
+               return Err(res.error());
+
+       /* 4. update message worked; re-store it */
+       if (auto&& res = priv_->update_message_unlocked(*msg, old_path); !res)
+               return Err(res.error());
+
+       /* 6. Profit! */
+       return Ok(std::move(msg.value()));
+}
+
+std::string
+Store::metadata(const std::string& key) const
+{
+       // get metadata either from the (uncommitted) cache or from the store.
+
+       std::lock_guard guard{priv_->lock_};
+
+       const auto it = priv_->metadata_cache_.find(key);
+       if (it != priv_->metadata_cache_.end())
+               return it->second;
+       else
+               return xapian_try([&] {
+                       return priv_->db().get_metadata(key);
+               }, "");
+}
+
+void
+Store::set_metadata(const std::string& key, const std::string& val)
+{
+       // get metadata either from the (uncommitted) cache or from the store.
+
+       std::lock_guard guard{priv_->lock_};
+
+       priv_->metadata_cache_.erase(key);
+       priv_->metadata_cache_.emplace(key, val);
+}
+
+
+time_t
+Store::dirstamp(const std::string& path) const
+{
+       constexpr auto epoch = static_cast<time_t>(0);
+       const auto ts{metadata(path)};
+       if (ts.empty())
+               return epoch;
+       else
+               return static_cast<time_t>(strtoll(ts.c_str(), NULL, 16));
+}
+
+void
+Store::set_dirstamp(const std::string& path, time_t tstamp)
+{
+       std::array<char, 2 * sizeof(tstamp) + 1> data{};
+       const auto len = static_cast<size_t>(
+           g_snprintf(data.data(), data.size(), "%zx", tstamp));
+
+       set_metadata(path, std::string{data.data(), len});
+}
+
+bool
+Store::contains_message(const std::string& path) const
+{
+       return xapian_try(
+           [&] {
+                   std::lock_guard   guard{priv_->lock_};
+                   const auto term{field_from_id(Field::Id::Path).xapian_term(path)};
+                   return priv_->db().term_exists(term);
+           },
+           false);
+}
+
+std::size_t
+Store::for_each_message_path(Store::ForEachMessageFunc msg_func) const
+{
+       size_t n{};
+
+       xapian_try([&] {
+               std::lock_guard guard{priv_->lock_};
+               Xapian::Enquire enq{priv_->db()};
+
+               enq.set_query(Xapian::Query::MatchAll);
+               enq.set_cutoff(0, 0);
+
+               Xapian::MSet matches(enq.get_mset(0, priv_->db().get_doccount()));
+               constexpr auto path_no{field_from_id(Field::Id::Path).value_no()};
+               for (auto&& it = matches.begin(); it != matches.end(); ++it, ++n)
+                       if (!msg_func(*it, it.get_document().get_value(path_no)))
+                               break;
+       });
+
+       return n;
+}
+
+void
+Store::commit()
+{
+       std::lock_guard guard{priv_->lock_};
+       priv_->transaction_maybe_commit(true /*force*/);
+}
+
+
+std::size_t
+Store::for_each_term(Field::Id field_id, Store::ForEachTermFunc func) const
+{
+       size_t n{};
+
+       xapian_try([&] {
+               /*
+                * Do _not_ take a lock; this is only called from
+                * the message parser which already has the lock
+                */
+               std::vector<std::string> terms;
+               const auto prefix{field_from_id(field_id).xapian_term()};
+               for (auto it = priv_->db().allterms_begin(prefix);
+                    it != priv_->db().allterms_end(prefix); ++it) {
+                       ++n;
+                       if (!func(*it))
+                               break;
+               }
+       });
+
+       return n;
+}
+
+std::mutex&
+Store::lock() const
+{
+       return priv_->lock_;
+}
+
+Result<QueryResults>
+Store::run_query(const std::string& expr,
+                Field::Id sortfield_id,
+                QueryFlags flags, size_t maxnum) const
+{
+       return Query{*this}.run(expr, sortfield_id, flags, maxnum);
+}
+
+size_t
+Store::count_query(const std::string& expr) const
+{
+       return xapian_try([&] {
+               std::lock_guard guard{priv_->lock_};
+               Query           q{*this};
+               return q.count(expr); }, 0);
+}
+
+std::string
+Store::parse_query(const std::string& expr, bool xapian) const
+{
+       return xapian_try([&] {
+               std::lock_guard guard{priv_->lock_};
+               Query           q{*this};
+
+               return q.parse(expr, xapian);
+       },
+                         std::string{});
+}
diff --git a/lib/mu-store.hh b/lib/mu-store.hh
new file mode 100644 (file)
index 0000000..49b172a
--- /dev/null
@@ -0,0 +1,473 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef __MU_STORE_HH__
+#define __MU_STORE_HH__
+
+#include <string>
+#include <vector>
+#include <mutex>
+#include <ctime>
+
+#include "mu-contacts-cache.hh"
+#include <xapian.h>
+
+#include <utils/mu-utils.hh>
+#include <index/mu-indexer.hh>
+#include <mu-query-results.hh>
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+class Store {
+public:
+       using Id                      = Xapian::docid; /**< Id for a message in the store */
+       static constexpr Id InvalidId = 0;             /**< Invalid  store id */
+
+       /**
+        * Configuration options.
+        *
+        * @param path
+        * @param readonly
+        */
+       enum struct Options {
+               None        = 0,        /**< No specific options */
+               Writable    = 1 << 0,   /**< Open in writable mode */
+               AutoUpgrade = 1 << 1,   /**< automatically re-initialize
+                                        * versions do not match */
+       };
+
+       /**
+        * Make a store for an existing document database
+        *
+        * @param path path to the database
+        * @param options startup options
+        *
+        * A store or an error.
+        */
+       static Result<Store> make(const std::string& path,
+                                 Options opts=Options::None) noexcept try {
+               return Ok(Store{path, opts});
+
+       } catch (const Mu::Error& me) {
+               return Err(me);
+       }
+ /* LCOV_EXCL_START */
+       catch (...) {
+               return Err(Error::Code::Internal, "failed to create store");
+       }
+ /* LCOV_EXCL_STOP */
+
+
+       struct Config {
+               size_t max_message_size{};
+               /**< maximum size (in bytes) for a message, or 0 for default */
+               size_t batch_size{};
+               /**< size of batches before committing, or 0 for default */
+       };
+
+       /**
+        * Construct a store for a not-yet-existing document database
+        *
+        * @param path path to the database
+        * @param maildir maildir to use for this store
+        * @param personal_addresses addresses that should be recognized as
+        * 'personal' for identifying personal messages.
+        * @param config a configuration object
+        */
+       static Result<Store> make_new(const std::string& path,
+                                     const std::string& maildir,
+                                     const StringVec&   personal_addresses,
+                                     const Config&      conf) noexcept try {
+
+               return Ok(Store(path, maildir, personal_addresses, conf));
+
+       } catch (const Mu::Error& me) {
+               return Err(me);
+       }
+ /* LCOV_EXCL_START */
+       catch (...) {
+               return Err(Error::Code::Internal, "failed to create new store");
+       }
+ /* LCOV_EXCL_STOP */
+
+       /**
+        * Move CTOR
+        *
+        */
+       Store(Store&&);
+
+       /**
+        * DTOR
+        */
+       ~Store();
+
+       /**
+        * Store properties
+        */
+       struct Properties {
+               std::string database_path;  /**< Full path to the Xapian database */
+               std::string schema_version; /**< Database schema version */
+               std::time_t created;        /**<  database creation time */
+
+               bool   read_only;  /**< Is the database opened read-only? */
+               size_t batch_size; /**< Maximum database transaction batch size */
+               bool   in_memory;  /**< Is this an in-memory database (for testing)?*/
+
+               std::string root_maildir; /**<  Absolute path to the top-level maildir */
+
+               StringVec personal_addresses; /**< Personal e-mail addresses */
+               size_t    max_message_size;   /**<  Maximus allowed message size */
+       };
+
+       /**
+        * Get properties about this store.
+        *
+        * @return the metadata
+        */
+       const Properties& properties() const;
+
+
+       /**
+        * Store statistics. Unlike the properties, these can change
+        * during the lifetime of a store.
+        *
+        */
+       struct Statistics {
+               size_t   size;  /**< number of messages in store */
+               ::time_t last_change; /**< last time any update happened */
+               ::time_t last_index; /**< last time an indexing op was performed */
+       };
+
+       /**
+        * Get store statistics
+        *
+        * @return statistics
+        */
+       Statistics statistics() const;
+
+
+       /**
+        * Get the ContactsCache object for this store
+        *
+        * @return the Contacts object
+        */
+       const ContactsCache& contacts_cache() const;
+
+       /**
+        * Get the underlying Xapian database for this store.
+        *
+        * @return the database
+        */
+       const Xapian::Database& database() const;
+
+       /**
+        * Get the Indexer associated with this store. It is an error to call
+        * this on a read-only store.
+        *
+        * @return the indexer.
+        */
+       Indexer& indexer();
+
+       /**
+        * Run a query; see the `mu-query` man page for the syntax.
+        *
+        * Multi-threaded callers must acquire the lock and keep it
+        * at least as long as the return value.
+        *
+        * @param expr the search expression
+        * @param sortfieldid the sortfield-id. If the field is NONE, sort by DATE
+        * @param flags query flags
+        * @param maxnum maximum number of results to return. 0 for 'no limit'
+        *
+        * @return the query-results or an error.
+        */
+       std::mutex& lock() const;
+       Result<QueryResults> run_query(const std::string&       expr,
+                                      Field::Id                sortfield_id = Field::Id::Date,
+                                      QueryFlags               flags        = QueryFlags::None,
+                                      size_t                   maxnum       = 0) const;
+
+       /**
+        * run a Xapian query merely to count the number of matches; for the
+        * syntax, please refer to the mu-query manpage
+        *
+        * @param expr the search expression; use "" to match all messages
+        *
+        * @return the number of matches
+        */
+       size_t count_query(const std::string& expr = "") const;
+
+       /**
+        * For debugging, get the internal string representation of the parsed
+        * query
+        *
+        * @param expr a xapian search expression
+        * @param xapian if true, show Xapian's internal representation,
+        * otherwise, mu's.
+        *
+        * @return the string representation of the query
+        */
+       std::string parse_query(const std::string& expr, bool xapian) const;
+
+       /**
+        * Add a message to the store. When planning to write many messages,
+        * it's much faster to do so in a transaction. If so, set
+        * @in_transaction to true. When done with adding messages, call
+        * commit().
+        *
+        * @param path the message path.
+        * @param whether to bundle up to batch_size changes in a transaction
+        *
+        * @return the doc id of the added message or an error.
+        */
+       Result<Id> add_message(const std::string& path, bool use_transaction = false);
+
+       /**
+        * Add a message to the store. When planning to write many messages,
+        * it's much faster to do so in a transaction. If so, set
+        * @in_transaction to true. When done with adding messages, call
+        * commit().
+        *
+        * @param msg a message
+        * @param whether to bundle up to batch_size changes in a transaction
+        *
+        * @return the doc id of the added message or an error.
+        */
+       Result<Id> add_message(Message& msg, bool use_transaction = false);
+
+       /**
+        * Update a message in the store.
+        *
+        * @param msg a message
+        * @param id the id for this message
+        *
+        * @return Ok() or an error.
+        */
+       Result<Store::Id> update_message(Message& msg, Id id);
+
+       /**
+        * Remove a message from the store. It will _not_ remove the message
+        * from the file system.
+        *
+        * @param path the message path.
+        *
+        * @return true if removing happened; false otherwise.
+        */
+       bool remove_message(const std::string& path);
+
+       /**
+        * Remove a number if messages from the store. It will _not_ remove the
+        * message from the file system.
+        *
+        * @param ids vector with store ids for the message
+        */
+       void remove_messages(const std::vector<Id>& ids);
+
+       /**
+        * Remove a message from the store. It will _not_ remove the message
+        * from the file system.
+        *
+        * @param id the store id for the message
+        */
+       void remove_message(Id id) { remove_messages({id}); }
+
+       /**
+        * Find message in the store.
+        *
+        * @param id doc id for the message to find
+        *
+        * @return a message (if found) or Nothing
+        */
+       Option<Message> find_message(Id id) const;
+
+       /**
+        * does a certain message exist in the store already?
+        *
+        * @param path the message path
+        *
+        * @return true if the message exists in the store, false otherwise
+        */
+       bool contains_message(const std::string& path) const;
+
+       /**
+        * Move a message both in the filesystem and in the store.
+        * After a successful move, the message is updated.
+        *
+        * @param id the id for some message
+        * @param target_mdir the target maildir (if any)
+        * @param new_flags new flags (if any)
+        * @param change_name whether to change the name
+        *
+        * @return Result, either the moved message or some error.
+        */
+       Result<Message> move_message(Store::Id id,
+                                    Option<const std::string&> target_mdir = Nothing,
+                                    Option<Flags> new_flags = Nothing,
+                                    bool change_name = false);
+
+       /**
+        * Prototype for the ForEachMessageFunc
+        *
+        * @param id :t store Id for the message
+        * @param path: the absolute path to the message
+        *
+        * @return true if for_each should continue; false to quit
+        */
+       using ForEachMessageFunc = std::function<bool(Id, const std::string&)>;
+
+       /**
+        * Call @param func for each document in the store. This takes a lock on
+        * the store, so the func should _not_ call any other Store:: methods.
+        *
+        * @param func a Callable invoked for each message.
+        *
+        * @return the number of times func was invoked
+        */
+       size_t for_each_message_path(ForEachMessageFunc func) const;
+
+       /**
+        * Prototype for the ForEachTermFunc
+        *
+        * @param term:
+        *
+        * @return true if for_each should continue; false to quit
+        */
+       using ForEachTermFunc = std::function<bool(const std::string&)>;
+
+       /**
+        * Call @param func for each term for the given field in the store. This
+        * takes a lock on the store, so the func should _not_ call any other
+        * Store:: methods.
+        *
+        * @param id the field id
+        * @param func a Callable invoked for each message.
+        *
+        * @return the number of times func was invoked
+        */
+       size_t for_each_term(Field::Id id, ForEachTermFunc func) const;
+
+
+       /**
+        * Get the store metadata for @p key
+        *
+        * @param key the metadata key
+        *
+        * @return the metadata value or empty for none.
+        */
+       std::string metadata(const std::string& key) const;
+
+       /**
+        * Write metadata to the store.
+        *
+        * @param key key
+        * @param val value
+        */
+       void set_metadata(const std::string& key, const std::string& val);
+
+       /**
+        * Get the timestamp for some message, or 0 if not found
+        *
+        * @param path the path
+        *
+        * @return the timestamp, or 0 if not found
+        */
+       time_t message_tstamp(const std::string& path) const;
+
+       /**
+        * Get the timestamp for some directory
+        *
+        * @param path the path
+        *
+        * @return the timestamp, or 0 if not found
+        */
+       time_t dirstamp(const std::string& path) const;
+
+       /**
+        * Set the timestamp for some directory
+        *
+        * @param path a filesystem path
+        * @param tstamp the timestamp for that path
+        */
+       void set_dirstamp(const std::string& path, time_t tstamp);
+
+       /**
+        * Get the number of documents in the document database
+        *
+        * @return the number
+        */
+       std::size_t size() const;
+
+       /**
+        * Is the database empty?
+        *
+        * @return true or false
+        */
+       bool empty() const;
+
+       /**
+        * Commit the current batch of modifications to disk, opportunistically.
+        * If no transaction is underway, do nothing.
+        */
+       void commit();
+
+       /**
+        * Get a reference to the private data. For internal use.
+        *
+        * @return private reference.
+        */
+       struct Private;
+       std::unique_ptr<Private>&       priv() { return priv_; }
+       const std::unique_ptr<Private>& priv() const { return priv_; }
+
+private:
+       /**
+        * Construct a store for an existing document database
+        *
+        * @param path path to the database
+        * @param options startup options
+        */
+       Store(const std::string& path, Options opts=Options::None);
+
+       /**
+        * Construct a store for a not-yet-existing document database
+        *
+        * @param path path to the database
+        * @param maildir maildir to use for this store
+        * @param personal_addresses addresses that should be recognized as
+        * 'personal' for identifying personal messages.
+        * @param config a configuration object
+        */
+       Store(const std::string& path,
+             const std::string& maildir,
+             const StringVec&   personal_addresses,
+             const Config&      conf);
+
+
+       std::unique_ptr<Private> priv_;
+};
+
+MU_ENABLE_BITOPS(Store::Options);
+
+} // namespace Mu
+
+#endif /* __MU_STORE_HH__ */
diff --git a/lib/mu-tokenizer.cc b/lib/mu-tokenizer.cc
new file mode 100644 (file)
index 0000000..14b318b
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+**  Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include "mu-tokenizer.hh"
+#include "utils/mu-utils.hh"
+
+#include <cctype>
+#include <iostream>
+#include <algorithm>
+
+using namespace Mu;
+
+static bool
+is_separator(char c)
+{
+       if (isblank(c))
+               return true;
+
+       const auto seps = std::string("()");
+       return seps.find(c) != std::string::npos;
+}
+
+static Mu::Token
+op_or_value(size_t pos, const std::string& val)
+{
+       auto s = val;
+       std::transform(s.begin(), s.end(), s.begin(), ::tolower);
+
+       if (s == "and")
+               return Token{pos, Token::Type::And, val};
+       else if (s == "or")
+               return Token{pos, Token::Type::Or, val};
+       else if (s == "xor")
+               return Token{pos, Token::Type::Xor, val};
+       else if (s == "not")
+               return Token{pos, Token::Type::Not, val};
+       else
+               return Token{pos, Token::Type::Data, val};
+}
+
+static void
+unread_char(std::string& food, char kar, size_t& pos)
+{
+       food = kar + food;
+       --pos;
+}
+
+static Mu::Token
+eat_token(std::string& food, size_t& pos)
+{
+       bool        quoted{};
+       bool        escaped{};
+       std::string value{};
+
+       while (!food.empty()) {
+               const auto kar = food[0];
+               food.erase(0, 1);
+               ++pos;
+
+               if (kar == '\\') {
+                       escaped = !escaped;
+                       if (escaped)
+                               continue;
+               }
+
+               if (kar == '"') {
+                       if (!escaped && quoted)
+                               return Token{pos, Token::Type::Data, value};
+                       else {
+                               quoted = true;
+                               continue;
+                       }
+               }
+
+               if (!quoted && !escaped && is_separator(kar)) {
+                       if (!value.empty() && kar != ':') {
+                               unread_char(food, kar, pos);
+                               return op_or_value(pos, value);
+                       }
+
+                       if (quoted || isblank(kar))
+                               continue;
+
+                       switch (kar) {
+                       case '(': return {pos, Token::Type::Open, "("};
+                       case ')': return {pos, Token::Type::Close, ")"};
+                       default: break;
+                       }
+               }
+
+               value += kar;
+               escaped = false;
+       }
+
+       return {pos, Token::Type::Data, value};
+}
+
+Mu::Tokens
+Mu::tokenize(const std::string& s)
+{
+       Tokens tokens{};
+
+       std::string food = utf8_clean(s);
+       size_t      pos{0};
+
+       if (s.empty())
+               return {};
+
+       while (!food.empty())
+               tokens.emplace_back(eat_token(food, pos));
+
+       return tokens;
+}
diff --git a/lib/mu-tokenizer.hh b/lib/mu-tokenizer.hh
new file mode 100644 (file)
index 0000000..7016e8b
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+**  Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#ifndef __TOKENIZER_HH__
+#define __TOKENIZER_HH__
+
+#include <string>
+#include <vector>
+#include <deque>
+#include <ostream>
+#include <stdexcept>
+
+// A simple tokenizer, which turns a string into a deque of tokens
+//
+// It recognizes '(', ')', '*' 'and', 'or', 'xor', 'not'
+//
+// Note that even if we recognizes those at the lexical level, they might be demoted to mere strings
+// when we're creating the parse tree.
+//
+// Furthermore, we detect ranges ("a..b") and regexps (/../) at the parser level, since we need a
+// bit more context to resolve ambiguities.
+
+namespace Mu {
+
+// A token
+struct Token {
+       enum class Type {
+               Data, /**< e .g., banana or date:..456 */
+
+               // Brackets
+               Open,  /**< ( */
+               Close, /**< ) */
+
+               // Unops
+               Not, /**< logical not*/
+
+               // Binops
+               And, /**< logical and */
+               Or,  /**< logical not */
+               Xor, /**< logical xor */
+
+               Empty, /**< nothing */
+       };
+
+       size_t            pos{};  /**< position in string */
+       Type              type{}; /**< token type */
+       const std::string str{};  /**< data for this token */
+
+       /**
+        * operator==
+        *
+        * @param rhs right-hand side
+        *
+        * @return true if rhs is equal to this; false otherwise
+        */
+       bool operator==(const Token& rhs) const
+       {
+               return pos == rhs.pos && type == rhs.type && str == rhs.str;
+       }
+};
+
+/**
+ * operator<<
+ *
+ * @param os an output stream
+ * @param t a token type
+ *
+ * @return the updated output stream
+ */
+inline std::ostream&
+operator<<(std::ostream& os, Token::Type t)
+{
+       switch (t) {
+       case Token::Type::Data: os << "<data>"; break;
+
+       case Token::Type::Open: os << "<open>"; break;
+       case Token::Type::Close: os << "<close>"; break;
+
+       case Token::Type::Not: os << "<not>"; break;
+       case Token::Type::And: os << "<and>"; break;
+       case Token::Type::Or: os << "<or>"; break;
+       case Token::Type::Xor: os << "<xor>"; break;
+       case Token::Type::Empty: os << "<empty>"; break;
+       default: // can't happen, but pacify compiler
+               throw std::runtime_error("<<bug>>");
+       }
+
+       return os;
+}
+
+/**
+ * operator<<
+ *
+ * @param os an output stream
+ * @param t a token
+ *
+ * @return the updated output stream
+ */
+inline std::ostream&
+operator<<(std::ostream& os, const Token& t)
+{
+       os << t.pos << ": " << t.type;
+
+       if (!t.str.empty())
+               os << " [" << t.str << "]";
+
+       return os;
+}
+
+/**
+ * Tokenize a string into a vector of tokens. The tokenization always succeeds, ie., ignoring errors
+ * such a missing end-".
+ *
+ * @param s a string
+ *
+ * @return a deque of tokens
+ */
+using Tokens = std::deque<Token>;
+Tokens tokenize(const std::string& s);
+
+} // namespace Mu
+
+#endif /* __TOKENIZER_HH__ */
diff --git a/lib/mu-tree.hh b/lib/mu-tree.hh
new file mode 100644 (file)
index 0000000..ce9093c
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+**  Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#ifndef TREE_HH__
+#define TREE_HH__
+
+#include <vector>
+#include <string>
+#include <string_view>
+#include <iostream>
+#include <message/mu-fields.hh>
+
+#include <utils/mu-option.hh>
+#include <utils/mu-error.hh>
+
+namespace Mu {
+
+struct FieldValue {
+       FieldValue(Field::Id idarg, const std::string valarg):
+               field_id{idarg}, val1{valarg} {}
+       FieldValue(Field::Id idarg, const std::string valarg1, const std::string valarg2):
+               field_id{idarg}, val1{valarg1}, val2{valarg2} {}
+
+       const Field& field() const { return field_from_id(field_id); }
+       const std::string& value() const { return val1; }
+       const std::pair<std::string, std::string> range() const { return { val1, val2 }; }
+
+       const Field::Id         field_id;
+       const std::string       val1;
+       const std::string       val2;
+
+};
+
+
+/**
+ * operator<<
+ *
+ * @param os an output stream
+ * @param fval a field value.
+ *
+ * @return the updated output stream
+ */
+inline std::ostream&
+operator<<(std::ostream& os, const FieldValue& fval)
+{
+       os << ' ' << quote(std::string{fval.field().name});
+
+       if (fval.field().is_range())
+               os << ' ' << quote(fval.range().first)
+                  << ' ' << quote(fval.range().second);
+       else
+               os << ' ' << quote(fval.value());
+
+       return os;
+}
+
+// A node in the parse tree
+struct Node {
+       enum class Type {
+               Empty, // only for empty trees
+               OpAnd,
+               OpOr,
+               OpXor,
+               OpAndNot,
+               OpNot,
+               Value,
+               Range,
+               Invalid
+       };
+
+       Node(Type _type, FieldValue&& fval) : type{_type}, field_val{std::move(fval)} {}
+       Node(Type _type) : type{_type} {}
+       Node(Node&& rhs) = default;
+
+       Type                  type;
+       Option<FieldValue>    field_val;
+
+       static constexpr std::string_view type_name(Type t) {
+               switch (t) {
+               case Type::Empty:
+                       return "";
+               case Type::OpAnd:
+                       return "and";
+               case Type::OpOr:
+                       return "or";
+               case Type::OpXor:
+                       return "xor";
+               case Type::OpAndNot:
+                       return "andnot";
+               case Type::OpNot:
+                       return "not";
+               case Type::Value:
+                       return "value";
+               case Type::Range:
+                       return "range";
+               case Type::Invalid:
+                       return "<invalid>";
+               default:
+                       return "<error>";
+               }
+       }
+
+       static constexpr bool is_binop(Type t) {
+               return t == Type::OpAnd || t == Type::OpAndNot || t == Type::OpOr ||
+                      t == Type::OpXor;
+       }
+};
+
+inline std::ostream&
+operator<<(std::ostream& os, const Node& t)
+{
+       os << Node::type_name(t.type);
+       if (t.field_val)
+               os << t.field_val.value();
+
+       return os;
+}
+
+struct Tree {
+       Tree(Node&& _node) : node(std::move(_node)) {}
+       Tree(Tree&& rhs) = default;
+
+       void add_child(Tree&& child) { children.emplace_back(std::move(child)); }
+       bool empty() const { return node.type == Node::Type::Empty; }
+
+       Node              node;
+       std::vector<Tree> children;
+};
+
+inline std::ostream&
+operator<<(std::ostream& os, const Tree& tree)
+{
+       os << '(' << tree.node;
+       for (const auto& subtree : tree.children)
+               os << subtree;
+       os << ')';
+
+       return os;
+}
+
+} // namespace Mu
+
+#endif /* TREE_HH__ */
diff --git a/lib/mu-xapian.cc b/lib/mu-xapian.cc
new file mode 100644 (file)
index 0000000..829667c
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+** Copyright (C) 2017-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include <config.h>
+
+#include <xapian.h>
+#include "mu-xapian.hh"
+#include <utils/mu-error.hh>
+
+using namespace Mu;
+
+static Xapian::Query
+xapian_query_op(const Mu::Tree& tree)
+{
+       if (tree.node.type ==  Node::Type::OpNot) { // OpNot x ::= <all> AND NOT x
+               if (tree.children.size() != 1)
+                       throw std::runtime_error("invalid # of children");
+               return Xapian::Query(Xapian::Query::OP_AND_NOT,
+                                    Xapian::Query::MatchAll,
+                                    xapian_query(tree.children.front()));
+       }
+
+       const auto op = std::invoke([](Node::Type ntype) {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+               switch (ntype) {
+               case Node::Type::OpAnd:
+                       return Xapian::Query::OP_AND;
+               case Node::Type::OpOr:
+                       return Xapian::Query::OP_OR;
+               case Node::Type::OpXor:
+                       return Xapian::Query::OP_XOR;
+               case Node::Type::OpAndNot:
+                       return Xapian::Query::OP_AND_NOT;
+               case Node::Type::OpNot:
+               default:
+                       throw Mu::Error(Error::Code::Internal, "invalid op"); // bug
+               }
+#pragma GCC diagnostic pop
+       }, tree.node.type);
+
+       std::vector<Xapian::Query> childvec;
+       for (const auto& subtree : tree.children)
+               childvec.emplace_back(xapian_query(subtree));
+
+       return Xapian::Query(op, childvec.begin(), childvec.end());
+}
+
+static Xapian::Query
+make_query(const FieldValue& fval, bool maybe_wildcard)
+{
+       const auto vlen{fval.value().length()};
+       if (!maybe_wildcard || vlen <= 1 || fval.value()[vlen - 1] != '*')
+               return Xapian::Query(fval.field().xapian_term(fval.value()));
+       else
+               return Xapian::Query(Xapian::Query::OP_WILDCARD,
+                                    fval.field().xapian_term(fval.value().substr(0, vlen - 1)));
+}
+
+static Xapian::Query
+xapian_query_value(const Mu::Tree& tree)
+{
+       // indexable field implies it can be use with a phrase search.
+       const auto& field_val{tree.node.field_val.value()};
+       if (!field_val.field().is_indexable_term()) { //
+               /* not an indexable field; no extra magic needed*/
+               return make_query(field_val, true /*maybe-wildcard*/);
+       }
+
+       const auto parts{split(field_val.value(), " ")};
+       if (parts.empty())
+               return Xapian::Query::MatchNothing; // shouldn't happen
+       else if (parts.size() == 1)
+               return make_query(field_val, true /*maybe-wildcard*/);
+
+       std::vector<Xapian::Query> phvec;
+       for (const auto& p : parts) {
+               FieldValue fv{field_val.field_id, p};
+               phvec.emplace_back(make_query(fv, false /*no wildcards*/));
+       }
+
+       return Xapian::Query(Xapian::Query::OP_PHRASE, phvec.begin(), phvec.end());
+}
+
+static Xapian::Query
+xapian_query_range(const Mu::Tree& tree)
+{
+       const auto& field_val{tree.node.field_val.value()};
+
+       return Xapian::Query(Xapian::Query::OP_VALUE_RANGE,
+                            field_val.field().value_no(),
+                            field_val.range().first,
+                            field_val.range().second);
+}
+
+Xapian::Query
+Mu::xapian_query(const Mu::Tree& tree)
+{
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+       switch (tree.node.type) {
+       case Node::Type::Empty:
+               return Xapian::Query();
+       case Node::Type::OpNot:
+       case Node::Type::OpAnd:
+       case Node::Type::OpOr:
+       case Node::Type::OpXor:
+       case Node::Type::OpAndNot:
+               return xapian_query_op(tree);
+       case Node::Type::Value:
+               return xapian_query_value(tree);
+       case Node::Type::Range:
+               return xapian_query_range(tree);
+       default:
+               throw Mu::Error(Error::Code::Internal, "invalid query"); // bug
+       }
+#pragma GCC diagnostic pop
+}
diff --git a/lib/mu-xapian.hh b/lib/mu-xapian.hh
new file mode 100644 (file)
index 0000000..54ee006
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#ifndef MU_XAPIAN_HH__
+#define MU_XAPIAN_HH__
+
+#include <xapian.h>
+#include <mu-parser.hh>
+
+namespace Mu {
+
+/**
+ * Transform a parse-tree into a Xapian query object
+ *
+ * @param tree a parse tree
+ *
+ * @return a Xapian query object
+ */
+Xapian::Query xapian_query(const Mu::Tree& tree);
+
+} // namespace Mu
+
+#endif /* MU_XAPIAN_H__ */
diff --git a/lib/tests/bench-indexer.cc b/lib/tests/bench-indexer.cc
new file mode 100644 (file)
index 0000000..b83bd90
--- /dev/null
@@ -0,0 +1,550 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <glib.h>
+#include <string>
+#include <thread>
+#include <vector>
+#include <iostream>
+#include <regex>
+#include <fstream>
+
+#include <utils/mu-utils.hh>
+#include <mu-store.hh>
+#include "mu-maildir.hh"
+
+#include "utils/mu-test-utils.hh"
+
+using namespace Mu;
+
+constexpr auto test_msg =
+R"(Return-Path: <htcondor-users-bounces@cs.wisc.edu>
+Received: from pop3.web.de [212.227.17.177]
+       by localhost with POP3 (fetchmail-6.4.6)
+       for <arne@localhost> (single-drop); Fri, 26 Jun 2020 12:56:08 +0200 (CEST)
+Received: from jeeves.cs.wisc.edu ([128.105.6.16]) by mx-ha.web.de (mxweb112
+ [212.227.17.8]) with ESMTPS (Nemesis) id 1MdMYE-1jFXaM2gnA-00ZKvt for
+ <@ID@@web.de>; Fri, 26 Jun 2020 01:28:11 +0200
+Received: from jeeves.cs.wisc.edu (localhost [127.0.0.1])
+       by jeeves.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLgek013419;
+       Thu, 25 Jun 2020 18:22:23 -0500
+Received: from shale.cs.wisc.edu (shale.cs.wisc.edu [128.105.6.25])
+       by jeeves.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLaf0013414
+       (version=TLSv1/SSLv3 cipher=AES256-GCM-SHA384 bits=256 verify=OK)
+       for <htcondor-users@jeeves.cs.wisc.edu>; Thu, 25 Jun 2020 18:21:36 -0500
+Received: from smtp7.wiscmail.wisc.edu (wmmta4.doit.wisc.edu [144.92.197.245])
+       by shale.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLaMK013694
+       (version=TLSv1/SSLv3 cipher=DHE-RSA-AES128-GCM-SHA256 bits=128
+       verify=NO)
+       for <htcondor-users@cs.wisc.edu>; Thu, 25 Jun 2020 18:21:36 -0500
+Received: from USG02-CY1-obe.outbound.protection.office365.us
+       ([23.103.209.108]) by smtp7.wiscmail.wisc.edu
+       (Oracle Communications Messaging Server 8.0.2.4.20190812 64bit (built
+       Aug 12
+       2019)) with ESMTPS id <0QCI042LC8VUXFC0@smtp7.wiscmail.wisc.edu> for
+       htcondor-users@cs.wisc.edu (ORCPT htcondor-users@cs.wisc.edu); Thu,
+       25 Jun 2020 18:21:31 -0500 (CDT)
+X-Spam-Report: IsSpam=no, Probability=11%, Hits= RETURN_RECEIPT 0.5,
+       FROM_US_TLD 0.1, HTML_00_01 0.05, HTML_00_10 0.05, SUPERLONG_LINE 0.05,
+       BODYTEXTP_SIZE_3000_LESS 0, BODY_SIZE_10000_PLUS 0, DKIM_SIGNATURE 0,
+       KNOWN_MTA_TFX 0, NO_URI_HTTPS 0, SPF_PASS 0, SXL_IP_TFX_WM 0,
+       WEBMAIL_SOURCE 0, WEBMAIL_XOIP 0, WEBMAIL_X_IP_HDR 0, __ANY_URI 0,
+       __ARCAUTH_DKIM_PASSED 0, __ARCAUTH_DMARC_PASSED 0, __ARCAUTH_PASSED 0,
+       __ATTACHMENT_SIZE_0_10K 0, __ATTACHMENT_SIZE_10_25K 0,
+       __BODY_NO_MAILTO 0,
+       __CT 0, __CTYPE_HAS_BOUNDARY 0, __CTYPE_MULTIPART 0, __HAS_ATTACHMENT 0,
+       __HAS_ATTACHMENT1 0, __HAS_ATTACHMENT2 0, __HAS_FROM 0, __HAS_MSGID 0,
+       __HAS_XOIP 0, __HIGHBITS 0, __MIME_TEXT_P 0, __MIME_TEXT_P1 0,
+       __MIME_TEXT_P2 0, __MIME_VERSION 0, __MULTIPLE_RCPTS_TO_X2 0,
+       __NO_HTML_TAG_RAW 0, __RETURN_RECEIPT_TO 0, __SANE_MSGID 0,
+       __TO_MALFORMED_2 0, __TO_NAME 0, __TO_NAME_DIFF_FROM_ACC 0,
+       __TO_NO_NAME 0,
+       __TO_REAL_NAMES 0, __URI_IN_BODY 0, __URI_MAILTO 0, __URI_NOT_IMG 0,
+       __URI_NO_PATH 0, __URI_NS , __URI_WITHOUT_PATH 0
+X-Wisc-Doma: @ID@X@numerica.us,numerica.us
+X-Wisc-Env-From-B64: d2VzbGV5LnRheWxvckBudW1lcmljYS51cw==
+X-Spam-PmxInfo: Server=avs-13, Version=6.4.7.2805085,
+       Antispam-Engine: 2.7.2.2107409, Antispam-Data: 2020.6.25.231519,
+       AntiVirus-Engine: 5.74.0, AntiVirus-Data: 2020.6.25.5740002,
+       SenderIP=[23.103.209.108]
+X-Wisc-DKIM-Verify: @ID@XXXXXXX@numerica.us,numericaus.onmicrosoft.com!pass
+X-Spam-Score: *
+ARC-Seal: i=1; a=rsa-sha256; s=arcselector5401; d=microsoft.com; cv=none;
+       b=KyXoddJsnsHsBwhdlO5rcljgMRaylJAUAxWTjG4jQL1C8XJAMgeERtH2sRffdjibYUFfSuDUNJmrTrvrbjKGUt2I8J2M2MgUB/upMoroVPNBrP1Fy9wMeZJQuSS4r4KjZZktsl2i8eq667pzOZO6+wX2IA5M7YtxDqglcWOE6btWzbABVjx+9eCXMt0eMd1+UI6ABK8Frd33EFQLKT0h/cxidWR9l+0gCMAcRxsLrQ82+ckU606AIV/DA1E4Tq7ADe/+CRv4QszDN93pWL/1N2/OOh9vFTs9g9ZG6uXjN+Km/IAdylPbfHgKW60ev3/Bvv6N3pA7DjpuiKj6BnW7mQ==
+ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
+       s=arcselector5401;
+       h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
+       bh=OZrj1we1ZUH0xBMhJ5/F6EQnB0cmitFs2xZW1fLMRNs=;
+       b=Pq07a3u26s2UdpucJuVQ0h68272wx46Wp61x/30TelPPFLCRxVjmlH1U3IBmIsZ1jOEtGXFJRv65L3HmwGxRUdLlMOdPRB64BBfHQ9NGWUBykKQmOrJNGJs635nEdpugpzngzIdcg1PS5vHxPJAnOeqoo71OVPI3JqPrPEn2TJJgb9J6PApexkqIbVl35prGPsyS/t2IlYw3/ihWzORG6wvqJeqedgpJTBXeGaDoMa+MQ1BeUsdvybh8+hau4ASpM5lwyeXlGmJ5mUTZi39jp+dFdDrmCj/VM4ezeuXeH9+HFtDjKLZJaTDWUID0IBcr91BaoQE/4r6y+lpkah6LLQ==
+ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass
+       smtp.mailfrom=numerica.us;
+       dmarc=pass action=none header.from=numerica.us;
+       dkim=pass header.d=numerica.us; arc=none
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+       d=numericaus.onmicrosoft.com; s=selector1-numericaus-onmicrosoft-com;
+       h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
+       bh=OZrj1we1ZUH0xBMhJ5/F6EQnB0cmitFs2xZW1fLMRNs=;
+       b=cFn0eL5k2IKry9U8qa8mbVaxRiyicUAWzRc3NUtj+VEbgShfrz8SO6FPX20WTQQJg/Fu/3isqsSEUt+9NSEEbgd5eQ1EVz5E/JVeNjPe9GXR0JEF/g3f6yM7CO+kKTvXSRvQjce683U0j7Aj1pSDEktoVNP4xvOS2Gx9VjdWTmc=
+Received: from DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM (2001:489a:200:413::10)
+       by DM3P110MB0490.NAMP110.PROD.OUTLOOK.COM (2001:489a:200:413::14)
+       with Microsoft SMTP Server
+       (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)
+       id 15.20.3109.25; Thu, 25 Jun 2020 23:21:07 +0000
+Received: from DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM
+       ([fe80::f548:f084:9867:9375]) by DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM
+       ([fe80::f548:f084:9867:9375%11]) with mapi id 15.20.3131.024; Thu,
+       25 Jun 2020 23:21:07 +0000
+From: Raul Endymion <XXXXXXXXXXXXXXXXXXXX@numerica.us>
+To: "'htcondor-users@cs.wisc.edu'" <htcondor-users@cs.wisc.edu>
+Thread-topic: OPINIONS WANTED: Are there any blatent downsides I am missing to
+       the following Condor configuration
+Thread-index: AdZLRbEvYoEDBZChS62aOHgPzKD8kw==
+Date: Thu, 25 Jun 2020 23:21:06 +0000
+Message-id: <DM3P110MB04746CDBA55B3E597EFD1877FA920@DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM>
+Accept-Language: en-US
+Content-language: en-US
+X-MS-Has-Attach: yes
+X-MS-TNEF-Correlator:
+X-Originating-IP: [50.233.29.54]
+x-ms-publictraffictype: Email
+x-ms-office365-filtering-correlation-id: f4edecdf-5582-4b2d-226a-08d8195e7007
+x-ms-traffictypediagnostic: DM3P110MB0490:
+x-microsoft-antispam-prvs: <DM3P110MB04907C313C4243B4FE42A20CFA920@DM3P110MB0490.NAMP110.PROD.OUTLOOK.COM>
+x-ms-oob-tlc-oobclassifiers: OLM:10000;
+x-forefront-prvs: 0445A82F82
+x-ms-exchange-senderadcheck: 1
+x-microsoft-antispam: BCL:0;
+X-Forefront-Antispam-Report: CIP:255.255.255.255; CTRY:; LANG:en; SCL:1; SRV:;
+       IPV:NLI; SFV:NSPM; H:DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM;
+       PTR:; CAT:NONE; SFTY:;
+       SFS:(346002)(366004)(6916009)(83380400001)(71200400001)(8936002)(55016002)(5660300002)(8676002)(9686003)(66616009)(64756008)(33656002)(52536014)(66446008)(66476007)(66556008)(66946007)(99936003)(76116006)(186003)(86362001)(26005)(7696005)(6506007)(44832011)(508600001)(2906002)(80162005)(80862006)(491001)(554374003);
+       DIR:OUT; SFP:1102;
+x-ms-exchange-transport-forked: True
+MIME-version: 1.0
+X-OriginatorOrg: numerica.us
+X-MS-Exchange-CrossTenant-Network-Message-Id: f4edecdf-5582-4b2d-226a-08d8195e7007
+X-MS-Exchange-CrossTenant-OriginalArrivalTime: 25 Jun 2020 23:21:06.8341 (UTC)
+X-MS-Exchange-CrossTenant-fromentityheader: Hosted
+X-MS-Exchange-CrossTenant-id: fae7a2ae-df1d-444e-91be-babb0900b9c2
+X-MS-Exchange-CrossTenant-mailboxtype: HOSTED
+X-MS-Exchange-Transport-CrossTenantHeadersStamped: DM3P110MB0490
+Subject: [HTCondor-users] OPINIONS WANTED: Are there any blatent downsides I
+ am missing to the following Condor configuration
+X-BeenThere: htcondor-users@cs.wisc.edu
+X-Mailman-Version: 2.1.19
+Precedence: list
+List-Id: HTCondor-Users Mail List <htcondor-users.cs.wisc.edu>
+List-Unsubscribe: <https://lists.cs.wisc.edu/mailman/options/htcondor-users>,
+       <mailto:htcondor-users-request@cs.wisc.edu?subject=unsubscribe>
+List-Archive: <https://www-auth.cs.wisc.edu/lists/htcondor-users/>
+List-Post: <mailto:htcondor-users@cs.wisc.edu>
+List-Help: <mailto:htcondor-users-request@cs.wisc.edu?subject=help>
+List-Subscribe: <https://lists.cs.wisc.edu/mailman/listinfo/htcondor-users>,
+       <mailto:htcondor-users-request@cs.wisc.edu?subject=subscribe>
+Reply-To: HTCondor-Users Mail List <htcondor-users@cs.wisc.edu>
+Content-Type: multipart/mixed; boundary="===============0678627779074767862=="
+Errors-To: htcondor-users-bounces@cs.wisc.edu
+Sender: "HTCondor-users" <htcondor-users-bounces@cs.wisc.edu>
+Envelope-To: <@ID@XXXXX@web.de>
+X-UI-Filterresults: unknown:2;V03:K0:cdojl5YHfkg=:jhTbQXp38SL2za/LB4M7aUwpyw
+ 5rDHoN1+/ScH/O9/G1fKWbGryQ203thF+1ZrHUOOwq8MVOc5SsoqzSTsaNbEAdthFcDDz3Oui
+ SHxX1hdpV3UOjZEHzWlpjEjRe7t74g2RI/ESELmkPuLg/LZC7SjAsg70cTJBIfDPYxJkJAcUl
+ 9W6OEBsmtTDO0va/EQRYjfkpoF9tjfmfMNw9KSKHuDdqZu2Xfak8mQKnWsoxWeUkD31r60iPC
+ yikbj7KP5AlHaWMzyTTdlvtjYRLfSuUSe1uqjI5NWCnZDDjz7zODoaWPp7p2U/MQenXEjN6+M
+ WnZL6ZC8AGtze/hYgOCXcLf4ydQ7m9YueJiY5nDn7g+cwnhxypVNFTL5NjSpKKXbkzbyu9Tdl
+ ez+92g/9pGW17iOo5NrFtfctLlmCEH0RxjouKI7FBmv3bIvFC4FvfghiNf7OZmRg2/nT5i+1o
+ AICYNAx2y5CezKsKM2f1tm60dkydQIR8pK45dDKZPz3i7NeJm9dknZ2OYFTnucUvdPaT8nR43
+ cK3kk2QUE48Ngo/0NwepSGrV9TkOt+hY3PUYkXWp/mwP2QPSjy4cALyvLyKwG24qZ9CiiRLMV
+ KqPFlCRnoDG5MHJ4d0krFlqmg8rNsWzV3oWfMNKFZmD24lVUmWGb+ExxbCFc0xzIt12o/EqBw
+ nVkXLu+E0apM+cmG6ubYfOymRoUpiKsZI9ivc+mAaEE+v2RBzcAURzlhzQHIn81onvzbQwCge
+ tMtBkSEfqyoa1HjalX4B9WQ90M42K+7xW039ydakQ7JOeYVpkPXYoBF7mbrXRckhMXjQatLQ8
+ MWA8+U031Xfa1ueOIfCCkzJ49wyx1LoLPyqdjCvnzaRd72yNEMJ5zM/itMIPE9reIHBtpom0i
+ RhIYdJFDrL+SKqE68lcJakCcF3R+VLApwLKOr0HChGQjdEk7c/rm5E0dF5f3oYlHf591QoXIJ
+ h16yfcJYe6fMo1YYunkvbEDFPpzttIq7aIk0FzxrOdRvj3yQajDbwOpYI/5T/DaabPn3M8lK7
+ 8pn7LrbmyCaLHhkYMS4h3SDkYWsifza6vkldizrK7IPf6KhS7AhTkbnEonWS6454GLUg1nYGX
+ W5Qp/G9LzvjtEGQMcwnCN5jb5zq7o3f+9FrROKjpwFxL+mL85CEXY/KMOVpf6hDJJfSyu6/X6
+ FpbwlJVLFdGeA0/+xcKcmutpkJACgK2kHqvZ8MZxt+5jBJWVlIDLZKa8/IoGWC+ikLX2/hPNB
+ 4TU89QYG5ygPmwwDXruFG7N7jVURZceHqWNKtqegS6YQ5nirsPJWJR7jzgr+HbntUaQETXNpn
+ QrxpsVHXfqRu2GlP5h28RaIpvBVUcwqrs+eLJELStvBzyAmaVPVoKFjEWFfwrmE89W6Bmz2W3
+ kHExOq3hI3gDsGXKjTjT/kjHkaHmtnVUXr4vqovf8Ht4Vwmtf0S4xsgpYjnYjUIzG9eiwIFAZ
+ hL2gvjwW51qtMvybf01C50xTiS9GSfO0SR7meBPA67skcA+wFo11wmwXsUk1irpKnC+Y9hVZX
+ 2vPkfZ1T2VXNo997cQC59lBpi/TU5gnuM7H/Vcl7tF3Lqtmqut7s6HkPWCegDZ3O2W7shH7aZ
+ 1bOXbO+W/SNC+WcMnj+fhuP+dHcrt0Vw4RD9knJOOzdZTH3OCli/vpjqgTbCKEaWMhCIeM2g0
+ RiLFxTeTEEBCa49bwa8n2r4T/vA3duZd8F/DNKvWTfhRr1Mxtz3n15EOar13fFijtnieEiv4/
+ vO/5uRF+H86Fcoua7B8AswThbiG1vou6M48g0Zo6iGEcrueKEaHMI4XM7wQF77KazMdn5f1BP
+ +KyQX83aHJN/qGniXgF8yu+h0M7Nf0YrTteYQd2C/HZrIA8IaLqqvLoGRl7dRBnbZiP7jRdQm
+ 1YEYtjX4XBoShrXPfIxPnJBUBnnOaePYxOJkS2FaBv19jPkMnyc9xuJYD7JOTFnXKzAnoaBqT
+ OR+dGrLLGZ1MM/0gqclKTv7Hcce+6CJyTWkx5mq42w49HFI/kdHBRxU8xIRv4B9l0ePf9EbWr
+ cDcrssee//6KXiRmF4fm7jq828/uhj8MIJet9sIU5ncKwHEse3I4YmVT5+dB+ZGZh0gbJPFj6
+ xcICpshhYct+euMCdNfy3lkxiRr76RwfBzLAOP5+1U3GAx/hcsL2AgyBHMwWo+Kkeq8pPy4YI
+ pQMxJyylI6JMa/DbBggnDk+xNZpRKo/XA4lAJY57DCOPL2ZcL8kU2aCd5LjtYHK0ZWSFtOjxs
+ oIEr/f2vvg+zibxzaANBzylZn3yPe9pI/IBefu9fL4MVaYY3aboxuncX4fyi0VH0WbFkSYXRi
+ a7LIu3LI2LTU13C/LE7j9hmxP6TApyiXi14f0GSa2sbF6HWp2v2rhYM7h67AAn3SQgvcJLpgb
+ Hz5ABb/OAk6ABVEl+a483zexJ6iT2P0gYc08zmewy8Jf8AD9r846k9pGZuhBaOHREx3bA16Bj
+ uWYh3QzSI6MQoJM3XbBGLVkX36Lfj54T9kk97lLaxfbGPuNoyOV9iTBKxts3m2KD+52iH3EEi
+ glbH6HNIUHyCHdEXsXyGVFwfM9V7OQcVO/g266KIQ74wU16x/Zdsq4p/1PcRXHRnoMxP/pUrj
+ EOLWzFU71qzC/OSkYWRil9HXUyucTFGQ0N08jZNXctI9lElWtgq3iI+Cz2F20rz+LJGhSHSkZ
+ 0G5JgXrtspeJN5yoH6TOE0hblr5sZcAM0wiSP7x/hPBeYHswzTA5/laWMn++9aTPVgpPaJ9/x
+ wyLm55OZr4Jl+StWd3MqLCgiRB3cNGrDX7f8Eqnj4wfCHiGIUHewD4qrfXraZQhIk17W+9JyD
+ osmUiVD9ZRdNCY2eNnu8ZkJ4uzKl44lwLL43sInKBjdAHlnoxrR2FOrYXbnU31ujwxdeUr6Hs
+ xPFy0Git0CpWCWYmaz37KA8GW7PE4ffWzcfCmz6AKBrbHcCreeUnyqnSEDy9ubnz7mcLRnu3W
+ RAWi6diI8gcS9g0+r4z5PtZX9rveXRekHJ4k08VuYVmdiz3gjXmHPlm9IKPEAbygP2EYgjwGE
+ RbReLc8xHJlfLbwdXyGw0HU=
+
+--===============0678627779074767862==
+Content-language: en-US
+Content-type: multipart/signed; protocol="application/x-pkcs7-signature";
+ micalg=2.16.840.1.101.3.4.2.3;
+ boundary="----=_NextPart_000_0018_01D64B14.F58791A0"
+
+------=_NextPart_000_0018_01D64B14.F58791A0
+Content-Type: text/plain;
+       charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+Hey!
+
+I am architecting our final HTCondor configuration over here and I have =
+an idea I am unsure about and I would like to ask some experienced users =
+for their opinion.
+
+Background, we have a small, relatively homogenous cluster (with no =
+special universes) and less than 10 users. Since each user has their own =
+workstation separate from our cluster I thought the following =
+configuration would suit our needs, but I want to make sure there isn't =
+a huge disadvantage I am missing:
+
+1. Set the Central Manager to be highly available to the point of =
+tolerating N cluster machine failures
+2. Put a Submit on each of the users' workstations (I am a little =
+worried about the resource usage of the condor_shadow and condor_schedd, =
+my users are already running into RAM consumption issues over time as it =
+is)
+3. Place an Execute on each of the cluster machines, which would lead to =
+the central manager being on a machine that is also executing jobs
+
+Fortunately both my users' and cluster machines all have access to the =
+same network storage, and we have centralized authentication so we can =
+just use our users' credentials to authenticate everywhere.=20
+
+Before I set this in dry mud, does anyone have any retrospective =
+recommendations I could benefit hearing from, since I am still pretty =
+new to the project?
+
+Thank you!
+-Raul
+
+Raul Endymion =E2=80=93 Cluster Manager
+Numerica Corporation (www.numerica.us)
+5042 Technology Parkway #100
+Fort Collins, Colorado 80528
+=E2=98=8E=EF=B8=8F (970) 207 2233
+=F0=9F=93=A7 @ID@XXXXXXXXXXXXXXX@numerica.us
+
+
+
+------=_NextPart_000_0018_01D64B14.F58791A0
+Content-Type: application/pkcs7-signature;
+       name="smime.p7s"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+       filename="smime.p7s"
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0BBwEAAKCCEv4w
+ggWpMIIDkaADAgECAhAV2Tfkh0+gtEu0gskeSMTdMA0GCSqGSIb3DQEBCwUAMFsxEjAQBgoJkiaJ
+k/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQx
+FzAVBgNVBAMTDmFkLUdJTEdBTEFELUNBMB4XDTE2MDcyNDE5NTcxM1oXDTM2MDcyNDIwMDcxMlow
+WzESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYKCZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQBgoJkiaJ
+k/IsZAEZFgJhZDEXMBUGA1UEAxMOYWQtR0lMR0FMQUQtQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQCq+/935KPrc8clxrq76k7GrrUHRbsM4FCfyrWicGPZsOKbJfcoloF2EAfj6AYR
+QyU/l9um/8NqW+cu6/TY6YcY622L+UtT1QWC/Kt0kVL7cTtZN+VK/BkjcDVbUOqdeFY1q0tMzdco
+WFxqjayGRYnX6oEZ7krDsGtJBBET/504Z3vDq/0ZD3lNG2dCWp1y+3VzUcb+OKkOPwMGHpw3gZM5
+lZN/znB7d7qwxFSRoLzZZB3nZKKJHcp2ZuyJR+pCT5VdHGGV4gpVQKuL49/UoJBA0o8Kv0DGPByD
++LVwhlyFMi2jlnCd5lqiWRw9JAE3fqS/Di/cGbMjXMI2CplBj+GmZH8fgy4BQRwmsOUELTaYkJyJ
+otcHGENO1+xYrR/lFEQLhh+8V2IJvBM2G1dgJ3EuEslL4q0xGeYLZJd7Z9xvXkAJaX/eWjHWICFI
+zbsH/6fBqXYow/V8hfZhb20dGGnPESXPqMv/1mLgUIqr++Fjl6zKM5mYZuHlmrtd+eLgg7VsjDvh
+cMxdQnju+jzJflxlmY2KSwt5lsu7viqmQyqVUnHFaEsV116B0uCROc5o1pBdRMdeeLrRoj6xPVlc
+IzmIZz3wZERxCAWeJqBx5d1kXe+cDL4pMNQ/hmah4mshjtyOGv+oEgcdxzUQ72W7JNLhSv8C6gpU
+eQwPq8usFAvUOwIDAQABo2kwZzATBgkrBgEEAYI3FAIEBh4EAEMAQTAOBgNVHQ8BAf8EBAMCAYYw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUF+CLMX/eZk96ElRSeiEHqnsujqEwEAYJKwYBBAGC
+NxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIBACcwALtn+SFUx+YTrLCFY+Ghh4yubQt3YdEI6hOQ
+JnmNPKsUEzCvoRE5L2ZLkG2VhJNX3KAJmXgkZMCGBPbiA/65r/cbYqZATQEG/g9aVicz/IBHXvg4
+7+YDDN9VpRy8c93AZNNTRf83Pw+CDsdIGG7mg8rc0tiCgt0V3gN0wF8oRSsb/trqd+ujk41bvaPw
+Rl+8JUeRN0Pq9lH4VGGk9GEIQv8JXhr2VKFmJcGKLB+qvMRvWQZ5oPTGDE3pUYI5q8f7/fMiJKU6
+hb9l+tXP7uDLWIawg/MoUc2BwAThyXFk9LZhkYWYpzbaf2Ez2JYieD4ey8RjEKvis9mF6Z/p6+69
+GbYvuf2bRikYenrmboXCUO820totjP2UyHczexZsMP/XznmyDJuN+BDLzLjm7ks8lXDwpF/Kqnjm
+1EyiQI0OB4cn889yM039U7raJeHpuiwju2/YO6krE+plLQhkM7pl6v6Ly/ZKICwDfbcU8k8LE4+K
+3VaXmVYRYbSXx8l2Ke0CWKNfehBGQ024gKjNt8t7gCgInG5s+roumqeKyfCWlhYll1FAxEQmwP/6
+966y7uJrGLra0VUjdppbZpAENSF0pdX08VfsasSZ20hnCaLWO1b3i0ZOBLBAoNzeCm+BdS6DAOhy
+JnHHZ+OBoiaYwCSjSvTDmHyQkNK3wmu+/wyNMIIGnDCCBISgAwIBAgITbwAAAEFhCq43is5OqAAA
+AAAAQTANBgkqhkiG9w0BAQsFADBbMRIwEAYKCZImiZPyLGQBGRYCdXMxGDAWBgoJkiaJk/IsZAEZ
+FghudW1lcmljYTESMBAGCgmSJomT8ixkARkWAmFkMRcwFQYDVQQDEw5hZC1HSUxHQUxBRC1DQTAe
+Fw0xOTA3MjIxNDE4MDFaFw0yMTA3MjIxNDI4MDFaMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYG
+CgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNF
+TEVCUklBTi1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKRLgjg0yC0P2jLwTCIA
+V/zEGk/PEc3pZxNAo7m0I/SXdNulUEkjxai5Wq53i0EhWVLpUU8XY3joXax46yCMqh0PUn90QmMD
+BybLyFDX6av8tVS5cQs0HbTZdIuj7A/dsKzKKIrSHd3SQ9MLNPRkSRdhagmf5LCF1Y4xEEiuAA/H
+XdYAxGIcl8n6b2CcLlZzq4W13Ipv8FIZoDsG1u0b9NGfeSOOHidi5kdD6r8lM5PaSPmZsl5PdKK6
++E1Y6rBCvITu0MBo5Tjuwt5cok3Ve0BK5Fg89aIL2/rMicm20qG6nbqxLhHeR0mhPO98KIIzDoeL
+rLpAlWS7GoPvJqbRzxsCAwEAAaOCAlYwggJSMBAGCSsGAQQBgjcVAQQDAgEBMCMGCSsGAQQBgjcV
+AgQWBBSv5TU1Bjnw5n3u1iO2y+BHQXk7MTAdBgNVHQ4EFgQUoeMyqBhiyBcgwJN8zbr7pRbgs+sw
+GQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB
+Af8wHwYDVR0jBBgwFoAUF+CLMX/eZk96ElRSeiEHqnsujqEwgdMGA1UdHwSByzCByDCBxaCBwqCB
+v4aBvGxkYXA6Ly8vQ049YWQtR0lMR0FMQUQtQ0EsQ049R2lsZ2FsYWQsQ049Q0RQLENOPVB1Ymxp
+YyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9YWQsREM9
+bnVtZXJpY2EsREM9dXM/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNz
+PWNSTERpc3RyaWJ1dGlvblBvaW50MIHGBggrBgEFBQcBAQSBuTCBtjCBswYIKwYBBQUHMAKGgaZs
+ZGFwOi8vL0NOPWFkLUdJTEdBTEFELUNBLENOPUFJQSxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNl
+cyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWFkLERDPW51bWVyaWNhLERDPXVzP2NB
+Q2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MA0GCSqG
+SIb3DQEBCwUAA4ICAQBmRoSlPe++k7tsAJOvq0+0dNI6yk6gOBmY4g5jL9NTEjSxPWkeYegIwLr2
+UqpiIIZmAh9e9v3z0T2egVyRqNezLPXLkg/2gUfV6D0kRyKtG5mL0yAn/0hkkVyf6jWJpCKmH77x
+0w3UpnfKs79jv5YpQDhC2eRFivN50HhIkigLWScPq4zd81ghmN8VFTHVQmsGua/mm1Oj5/pBFuQF
+B4ljon1N//wX5ZJZaUlJR9eR9tM9m+Gyds2flr5+mZT6Zgm26fKiC5zs91aGnzqGx6s30jfXELP2
+FjFrrR46ooV7ehhnyBlCACxIWqXe5sSZsSh9oEYZ7Ux5Vq0thkfArBWsF7HA+LovKCUyHLcXbVBB
+6/VAwZ3GLYi/bqbVIEFlVRu4nv/JyKWwoGbAhGyzZNWoeHszFrEIQbQMoMsEumVkMZreE6AxP+zb
+6JPPOjlhpymtMo54z1MDYJPyo4HmcpL4xUjHZgqgOxMrbHC4oIVLvKZ/scbVBhPnd0tHHSZqj3ZS
+gfTvG/ut/tLNTXXe48PkLBw4KguhbLm61Elu3wJALT0UL+ENgUWwb7csUGQBqOyPAHXGYnf/ACOc
+UBqQckcrK8Jq3u8rnCloW3uDw86hw7MFM+YjmhVRdYRxpJmhKVPT6Amufp2WsSVId8q3CSqTH33L
+fcxbV1n7hLWHA67MhTCCBq0wggWVoAMCAQICEycAAAsJMaw2RjtHZFUAAQAACwkwDQYJKoZIhvcN
+AQELBQAwXDESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYKCZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQ
+BgoJkiaJk/IsZAEZFgJhZDEYMBYGA1UEAxMPYWQtQ0VMRUJSSUFOLUNBMB4XDTIwMDUxMjE1MDk0
+MloXDTIxMDcyMjE0MjgwMVowgcExEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkW
+CG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxETAPBgNVBAsTCE51bWVyaWNhMQ4wDAYDVQQL
+EwVVc2VyczEYMBYGA1UECxMPUHJlc2VudCBJbnRlcm5zMRYwFAYDVQQDEw1XZXNsZXkgVGF5bG9y
+MSgwJgYJKoZIhvcNAQkBFhl3ZXNsZXkudGF5bG9yQG51bWVyaWNhLnVzMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEA5clDLapXkiLVXhAFP9GJv+JJkt+cacyvWaX9xEvqMQXOXb7MqO5E
+DJE8XPMfxaX84WhuMMePOc9SNUKpDtTa2SHz+AOom+JH38ce2gfrdOPwez/e6RrUb3o8ZvMr3hJl
+Yy+6vEFEADIICfHSlIjkLJbGNFTRDccvkOPjD2W+fmzFAtWyNb/eqM+mwdTuXjOxTvP6V34zJsvc
+YKJUzhhD8jI7GdqOoNoirTlaMVTH5udK0P2KvzD6F0LfwcOlc3bTvY9uI585xhdniK4yAIka8OMq
+5zmyEQLYOadcVSscjAlkC1sQ0gbwL3AdwS+bntryq+2Ds380OJ+Z1Uy7TRkeBQIDAQABo4IDADCC
+AvwwPAYJKwYBBAGCNxUHBC8wLQYlKwYBBAGCNxUI9/Bss4wDhbmBGISeqheH4YBfgSWC6qJEgcjE
+IgIBZQIBKDATBgNVHSUEDDAKBggrBgEFBQcDBDAOBgNVHQ8BAf8EBAMCBaAwGwYJKwYBBAGCNxUK
+BA4wDDAKBggrBgEFBQcDBDBEBgkqhkiG9w0BCQ8ENzA1MA4GCCqGSIb3DQMCAgIAgDAOBggqhkiG
+9w0DBAICAIAwBwYFKw4DAgcwCgYIKoZIhvcNAwcwHQYDVR0OBBYEFDZHoDwoOKD5uzpF/2CcZSeg
+XWLmMB8GA1UdIwQYMBaAFKHjMqgYYsgXIMCTfM26+6UW4LPrMIHVBgNVHR8Egc0wgcowgceggcSg
+gcGGgb5sZGFwOi8vL0NOPWFkLUNFTEVCUklBTi1DQSxDTj1DZWxlYnJpYW4sQ049Q0RQLENOPVB1
+YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9YWQs
+REM9bnVtZXJpY2EsREM9dXM/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENs
+YXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIHHBggrBgEFBQcBAQSBujCBtzCBtAYIKwYBBQUHMAKG
+gadsZGFwOi8vL0NOPWFkLUNFTEVCUklBTi1DQSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2Vy
+dmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1hZCxEQz1udW1lcmljYSxEQz11
+cz9jQUNlcnRpZmljYXRlP2Jhc2U/b2JqZWN0Q2xhc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTBS
+BgNVHREESzBJoCwGCisGAQQBgjcUAgOgHgwcd2VzbGV5LnRheWxvckBhZC5udW1lcmljYS51c4EZ
+d2VzbGV5LnRheWxvckBudW1lcmljYS51czANBgkqhkiG9w0BAQsFAAOCAQEAX3zFhiDYU+vQap2J
+hiysyC9L7nkL7VI2OQWg4Z/JnNJTFiA6BwtoDYAT4qq1Jix4hZc+g78Gj99OnkhlBQDe9Hq12yI9
+muboQSDAYO6iDK76wQv3Rt8Fl4SUD4Ygwy52QrkTDrj/HZxTNask5p/2ilGBJnG9KT2VbEgGJkP9
+kXn1vAgOl3BCxgjdWekWCvxpmffr+Z3UtmQIiZAB3OsKcgdsSy9pveTMjxtKJemaH3kpXQiTgCev
+CMuWZb3YnqXI8Fd+uUw6HwA4c+ZH62G9Q8KGkwXyhOPizmm3UeSlMo27yUCE+cF5EIHBxpGJ6z83
+7MbxMVKnS1Wz1n8MtW2ezDGCBCEwggQdAgEBMHMwXDESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYK
+CZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQBgoJkiaJk/IsZAEZFgJhZDEYMBYGA1UEAxMPYWQtQ0VM
+RUJSSUFOLUNBAhMnAAALCTGsNkY7R2RVAAEAAAsJMA0GCWCGSAFlAwQCAwUAoIICfzAYBgkqhkiG
+9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDA2MjUyMzIwNDRaME8GCSqGSIb3
+DQEJBDFCBEBaj66vdgjAhEO0p7lO6X44h+LpUlAcROa5Hi4Jp5aWS4hU8CuqOrH12y2GRNmNhKLa
+0YieL4fCL3YqDRfop79NMFIGCyqGSIb3DQEJEAIBMUMwQQQdAAAAABAAAACgLzslsB99TKIYKeHy
+Wh5cAQAAAACAAQAwHTAbgRl3ZXNsZXkudGF5bG9yQG51bWVyaWNhLnVzMIGCBgkrBgEEAYI3EAQx
+dTBzMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYK
+CZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNFTEVCUklBTi1DQQITJwAACwkxrDZGO0dkVQAB
+AAALCTCBhAYLKoZIhvcNAQkQAgsxdaBzMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT
+8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNFTEVCUklB
+Ti1DQQITJwAACwkxrDZGO0dkVQABAAALCTCBkwYJKoZIhvcNAQkPMYGFMIGCMAsGCWCGSAFlAwQB
+KjALBglghkgBZQMEARYwCgYIKoZIhvcNAwcwCwYJYIZIAWUDBAECMA4GCCqGSIb3DQMCAgIAgDAN
+BggqhkiG9w0DAgIBQDALBglghkgBZQMEAgMwCwYJYIZIAWUDBAICMAsGCWCGSAFlAwQCATAHBgUr
+DgMCGjANBgkqhkiG9w0BAQEFAASCAQBNFxhcbK6Rmw0Xyu+79cH5kUsXENcdUaJPKlegcY/gl2BZ
+0CPpGcRnwz6z8OPYjvw3jrkiAE8nBbuCKu1CPtuk1h4Cybk7exyMybYvK5xge+N+dz2mFipRfGSY
+rl/ztX1jyvcDruxaSJwb8WMhAGs505yfaCJfwgFOI3QGi+wUunbOIKy3QQZTXDv89yslZqi0wmeI
+8sVRqSAYZRIPEylwS9CU2ReK9BJlfVLZnNP1At4gHE6S2hk8T0eVeLT8uhQiUXXJe4644UoPhoA4
+Fxgm7Q62KT6yP9O7c4eZzmQ4A9hdlWM6CtZ5pgMAzLOrVFdypzSc+S1j8DqcFkALCw83AAAAAAAA
+
+------=_NextPart_000_0018_01D64B14.F58791A0--
+
+--===============0678627779074767862==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+HTCondor-users mailing list
+To unsubscribe, send a message to htcondor-users-request@cs.wisc.edu with a
+subject: Unsubscribe
+You can also unsubscribe by visiting
+https://lists.cs.wisc.edu/mailman/listinfo/htcondor-users
+
+The archives can be found at:
+https://lists.cs.wisc.edu/archive/htcondor-users/
+--===============0678627779074767862==--)";
+
+
+static std::string
+message(const std::regex& rx, size_t id)
+{
+       char buf[16];
+       ::snprintf(buf, sizeof(buf), "%zu", id);
+       return std::regex_replace(test_msg, rx, buf);
+}
+
+struct TestData {
+       size_t num_maildirs;
+       size_t num_messages;
+       size_t num_threads;
+};
+
+
+static void
+setup(const TestData& tdata)
+{
+       /* create toplevel */
+       auto top_maildir = std::string{BENCH_MAILDIRS};
+       int res = g_mkdir_with_parents(top_maildir.c_str(), 0700);
+       g_assert_cmpuint(res,==, 0);
+
+       /* create maildirs */
+       for (size_t i = 0; i != tdata.num_maildirs; ++i) {
+               const auto mdir = format("%s/maildir-%zu", top_maildir.c_str(), i);
+               auto res = maildir_mkdir(mdir);
+               g_assert(!!res);
+       }
+       const auto rx = std::regex("@ID@");
+       /* create messages */
+       for (size_t n = 0; n != tdata.num_messages; ++n) {
+               auto mpath = format("%s/maildir-%zu/cur/msg-%zu:2,S",
+                                   top_maildir.c_str(),
+                                   n % tdata.num_maildirs,
+                                   n);
+               std::ofstream stream(mpath);
+               auto msg = message(rx, n);
+               stream.write(msg.c_str(), msg.size());
+               g_assert_true(stream.good());
+       }
+}
+
+static void
+tear_down()
+{
+       /* ugly */
+       GError *err{};
+       const auto cmd{format("/bin/rm -rf '%s' '%s'", BENCH_MAILDIRS, BENCH_STORE)};
+       if (!g_spawn_command_line_sync(cmd.c_str(), NULL, NULL, NULL, &err)) {
+               g_warning("error: %s\n", err ? err->message : "?");
+               g_clear_error(&err);
+       }
+}
+
+void
+black_hole(void)
+{
+       return; /* do nothing */
+}
+
+static void
+benchmark_indexer(gconstpointer testdata)
+{
+       using namespace std::chrono_literals;
+       using Clock = std::chrono::steady_clock;
+       const auto tdata = reinterpret_cast<const TestData*>(testdata);
+
+       setup(*tdata);
+       auto start = Clock::now();
+
+       {
+               auto store{Store::make_new(BENCH_STORE, BENCH_MAILDIRS, {}, {})};
+               g_assert_true(!!store);
+               Indexer::Config conf{};
+               conf.max_threads = tdata->num_threads;
+
+               auto res = store->indexer().start(conf);
+               g_assert_true(res);
+               while(store->indexer().is_running()) {
+                       std::this_thread::sleep_for(100ms);
+               }
+               g_assert_cmpuint(store->size(),==, tdata->num_messages);
+       }
+
+       const auto elapsed = Clock::now() - start;
+       std::cout << "indexed " << tdata->num_messages << " messages in "
+                 << tdata->num_maildirs << " maildirs in "
+                 << to_ms(elapsed) << "ms; "
+                 << to_us(elapsed) / tdata->num_messages << " μs/message; "
+                 << static_cast<size_t>(1000*tdata->num_messages / to_ms(elapsed))
+                 << " messages/s"
+                 << " (" << tdata->num_threads << " thread(s))\n";
+
+       tear_down();
+}
+
+int
+main(int argc, char *argv[])
+{
+       size_t num_maildirs{}, num_messages{};
+       g_test_init(&argc, &argv, nullptr);
+       if (g_test_perf()) {
+               num_maildirs = 20;
+               num_messages = 5000;
+       } else {
+               num_maildirs = 10;
+               num_messages = 1000;
+       }
+
+       g_log_set_handler(
+           NULL,
+           (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+           (GLogFunc)black_hole,
+           NULL);
+
+
+       size_t thread_num{};
+       const auto tnum = g_getenv("THREAD_NUM");
+       if (tnum)
+               thread_num = ::strtol(tnum, NULL, 10);
+
+       if (thread_num != 0) {
+               /* THREAD_NUM specified */
+               static TestData tdata{num_maildirs, num_messages, thread_num};
+               char *name = g_strdup_printf("/bench/indexer/%zu-cores", thread_num);
+               g_test_add_data_func(name, &tdata, benchmark_indexer);
+               g_free(name);
+       } else {
+               /* no THREAD_NUM specified */
+
+               const size_t hw_threads = std::thread::hardware_concurrency();
+
+               {
+                       static TestData tdata{num_maildirs, num_messages, 1};
+                       g_test_add_data_func("/bench/indexer/1-core", &tdata, benchmark_indexer);
+               }
+
+               if (hw_threads > 2) {
+                       static TestData tdata{num_maildirs, num_messages, hw_threads/2};
+                       char *name = g_strdup_printf("/bench/indexer/%zu-cores", hw_threads/2);
+                       g_test_add_data_func(name, &tdata, benchmark_indexer);
+                       g_free(name);
+               }
+
+               if (hw_threads > 1) {
+                       static TestData tdata{num_maildirs, num_messages, hw_threads};
+                       char *name = g_strdup_printf("/bench/indexer/%zu-cores", hw_threads);
+                       g_test_add_data_func(name, &tdata, benchmark_indexer);
+                       g_free(name);
+               }
+       }
+
+       tear_down();
+
+       return g_test_run();
+}
diff --git a/lib/tests/cjk/cur/test1 b/lib/tests/cjk/cur/test1
new file mode 100644 (file)
index 0000000..1538790
--- /dev/null
@@ -0,0 +1,10 @@
+From: "Bob" <bob@builder.com>
+Subject: CJK 1
+To: "Chase" <chase@ppatrol.org>
+Date: Thu, 18 Nov 2021 08:35:34 +0200
+Message-Id: 112342343e9dfo.fsf@builder.com
+User-Agent: mu4e 1.7.5; emacs 29.0.50
+
+    サーバがダウンしました
+
+https://github.com/djcb/mu/issues/1428
diff --git a/lib/tests/cjk/cur/test2 b/lib/tests/cjk/cur/test2
new file mode 100644 (file)
index 0000000..875bff5
--- /dev/null
@@ -0,0 +1,10 @@
+From: "Bob" <bob@builder.com>
+Subject: CJK 2
+To: "Chase" <chase@ppatrol.org>
+Date: Thu, 18 Nov 2021 08:35:34 +0200
+Message-Id: 271r2342343e9dfo.fsf@builder.com
+User-Agent: mu4e 1.7.5; emacs 29.0.50
+
+    スポンサーシップ募集
+
+https://github.com/djcb/mu/issues/1428
diff --git a/lib/tests/cjk/cur/test3 b/lib/tests/cjk/cur/test3
new file mode 100644 (file)
index 0000000..f0efe71
--- /dev/null
@@ -0,0 +1,10 @@
+From: "Bob" <bob@builder.com>
+Subject: CJK 3
+To: "Chase" <chase@ppatrol.org>
+Date: Thu, 18 Nov 2021 08:35:34 +0200
+Message-Id: 3871r2342343e9dfo.fsf@builder.com
+User-Agent: mu4e 1.7.5; emacs 29.0.50
+
+    サービス開始について
+
+https://github.com/djcb/mu/issues/1428
diff --git a/lib/tests/cjk/cur/test4 b/lib/tests/cjk/cur/test4
new file mode 100644 (file)
index 0000000..2bad399
--- /dev/null
@@ -0,0 +1,10 @@
+From: "Bob" <bob@builder.com>
+Subject: CJK 4
+To: "Chase" <chase@ppatrol.org>
+Date: Thu, 18 Nov 2021 08:35:34 +0200
+Message-Id: 4871r2342343e9dfo.fsf@builder.com
+User-Agent: mu4e 1.7.5; emacs 29.0.50
+
+    ショルダーバック
+
+https://github.com/djcb/mu/issues/1428
diff --git a/lib/tests/meson.build b/lib/tests/meson.build
new file mode 100644 (file)
index 0000000..17a9b72
--- /dev/null
@@ -0,0 +1,81 @@
+## Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+#
+# tests
+#
+test('test-maildir',
+     executable('test-maildir',
+               'test-mu-maildir.cc',
+               install: false,
+               dependencies: [glib_dep, lib_mu_dep]))
+test('test-msg',
+     executable('test-msg',
+               'test-mu-msg.cc',
+               install: false,
+               dependencies: [glib_dep, lib_mu_dep]))
+test('test-store',
+     executable('test-store',
+               'test-mu-store.cc',
+               install: false,
+               dependencies: [glib_dep, lib_mu_dep]))
+test('test-query',
+     executable('test-query',
+               'test-query.cc',
+               install: false,
+               dependencies: [glib_dep, gmime_dep, lib_mu_dep]))
+
+test('test-tokenizer',
+     executable('test-tokenizer',
+               'test-tokenizer.cc',
+               install: false,
+               dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-parser',
+     executable('test-parser',
+               'test-parser.cc',
+               install: false,
+               dependencies: [glib_dep, gmime_dep, lib_mu_dep]))
+
+test('test-store-query',
+     executable('test-store-query',
+               'test-mu-store-query.cc',
+               install: false,
+               dependencies: [glib_dep, gmime_dep, lib_mu_dep]))
+#
+# benchmarks
+#
+bench_maildirs=join_paths(meson.current_build_dir(), 'maildirs')
+bench_store=join_paths(meson.current_build_dir(), 'store')
+bench_indexer_exe = executable(
+  'bench-indexer',
+  'bench-indexer.cc',
+  install:false,
+  cpp_args:['-DBENCH_MAILDIRS="' + bench_maildirs + '"',
+            '-DBENCH_STORE="' + bench_store + '"',
+           ],
+  dependencies: [lib_mu_dep, glib_dep])
+
+benchmark('bench-indexer', bench_indexer_exe, args: ['-m', 'perf'])
+
+#
+# below does _not_ pass; it is believed that it's a false alarm.
+#    https://gitlab.gnome.org/GNOME/glib/-/issues/2662
+
+# also register benchmark as a normal test so it gets included for
+# valgrind/helgrind etc.
+# test('test-bench-indexer', bench_indexer_exe,
+#      args : ['-m', 'quick'], env: ['THREADNUM=16'])
diff --git a/lib/tests/test-indexer.cc b/lib/tests/test-indexer.cc
new file mode 100644 (file)
index 0000000..32e9ea3
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+#include <unistd.h>
+
+#include "mu-indexer.hh"
+#include "utils/mu-utils.hh"
+#include "test-mu-common.h"
+
+using namespace Mu;
+
+static void
+test_index_maildir()
+{
+       allow_warnings();
+
+       Store   store{test_mu_common_get_random_tmpdir(), std::string{MU_TESTMAILDIR}};
+       Indexer idx{Indexer::Config{}, store};
+
+       g_assert_true(idx.start());
+       while (idx.is_running()) {
+               sleep(1);
+       }
+
+       g_print("again!\n");
+
+       g_assert_true(idx.start());
+       while (idx.is_running()) {
+               sleep(1);
+       }
+}
+
+int
+main(int argc, char* argv[])
+try {
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/indexer/index-maildir", test_index_maildir);
+
+       return g_test_run();
+
+} catch (const std::runtime_error& re) {
+       std::cerr << re.what() << "\n";
+       return 1;
+} catch (...) {
+       std::cerr << "caught exception\n";
+       return 1;
+}
diff --git a/lib/tests/test-mu-container.cc b/lib/tests/test-mu-container.cc
new file mode 100644 (file)
index 0000000..4fb1939
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+** Copyright (C) 2014  Jakub Sitnicki <jsitnicki@gmail.com>
+**
+** 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 "config.h"
+#include <glib.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-container.hh"
+
+static gboolean
+container_has_children(const MuContainer* c)
+{
+       return c && c->child;
+}
+
+static gboolean
+container_is_sibling_of(const MuContainer* c, const MuContainer* sibling)
+{
+       const MuContainer* cur;
+
+       for (cur = c; cur; cur = cur->next) {
+               if (cur == sibling)
+                       return TRUE;
+       }
+
+       return container_is_sibling_of(sibling, c);
+}
+
+static void
+test_mu_container_splice_children_when_parent_has_no_siblings(void)
+{
+       MuContainer *child, *parent, *root_set;
+
+       child  = mu_container_new(NULL, 0, "child");
+       parent = mu_container_new(NULL, 0, "parent");
+       parent = mu_container_append_children(parent, child);
+
+       root_set = parent;
+       root_set = mu_container_splice_children(root_set, parent);
+
+       g_assert(root_set != NULL);
+       g_assert(!container_has_children(parent));
+       g_assert(container_is_sibling_of(root_set, child));
+
+       mu_container_destroy(parent);
+       mu_container_destroy(child);
+}
+
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/mu-container/mu-container-splice-children-when-parent-has-no-siblings",
+                       test_mu_container_splice_children_when_parent_has_no_siblings);
+
+       g_log_set_handler(
+           NULL,
+           (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+           (GLogFunc)black_hole,
+           NULL);
+
+       return g_test_run();
+}
diff --git a/lib/tests/test-mu-maildir.cc b/lib/tests/test-mu-maildir.cc
new file mode 100644 (file)
index 0000000..d3713f6
--- /dev/null
@@ -0,0 +1,577 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <vector>
+#include <fstream>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-maildir.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-util.h"
+
+using namespace Mu;
+
+static void
+test_maildir_mkdir_01(void)
+{
+       int          i;
+       gchar *      tmpdir, *mdir, *tmp;
+       const gchar* subs[] = {"tmp", "cur", "new"};
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       mdir   = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "cuux");
+
+       g_assert_true(!!maildir_mkdir(mdir, 0755, FALSE));
+
+       for (i = 0; i != G_N_ELEMENTS(subs); ++i) {
+               gchar* dir;
+
+               dir = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, subs[i]);
+               g_assert_cmpuint(g_access(dir, R_OK), ==, 0);
+               g_assert_cmpuint(g_access(dir, W_OK), ==, 0);
+               g_free(dir);
+       }
+
+       tmp = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, ".noindex");
+       g_assert_cmpuint(g_access(tmp, F_OK), !=, 0);
+
+       g_free(tmp);
+       g_free(tmpdir);
+       g_free(mdir);
+}
+
+static void
+test_maildir_mkdir_02(void)
+{
+       int          i;
+       gchar *      tmpdir, *mdir, *tmp;
+       const gchar* subs[] = {"tmp", "cur", "new"};
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       mdir   = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "cuux");
+
+       g_assert_true(!!maildir_mkdir(mdir, 0755, TRUE));
+
+       for (i = 0; i != G_N_ELEMENTS(subs); ++i) {
+               gchar* dir;
+
+               dir = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, subs[i]);
+               g_assert_cmpuint(g_access(dir, R_OK), ==, 0);
+
+               g_assert_cmpuint(g_access(dir, W_OK), ==, 0);
+               g_free(dir);
+       }
+
+       tmp = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, ".noindex");
+       g_assert_cmpuint(g_access(tmp, F_OK), ==, 0);
+
+       g_free(tmp);
+       g_free(tmpdir);
+       g_free(mdir);
+}
+
+static void
+test_maildir_mkdir_03(void)
+{
+       int          i;
+       gchar *      tmpdir, *mdir, *tmp;
+       const gchar* subs[] = {"tmp", "cur", "new"};
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       mdir   = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "cuux");
+
+       /* create part of the structure already... */
+       {
+               gchar* dir;
+               dir = g_strdup_printf("%s%ccur", mdir, G_DIR_SEPARATOR);
+               g_assert_cmpuint(g_mkdir_with_parents(dir, 0755), ==, 0);
+               g_free(dir);
+       }
+
+       /* this should still work */
+       g_assert_true(!!maildir_mkdir(mdir, 0755, FALSE));
+
+       for (i = 0; i != G_N_ELEMENTS(subs); ++i) {
+               gchar* dir;
+
+               dir = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, subs[i]);
+               g_assert_cmpuint(g_access(dir, R_OK), ==, 0);
+               g_assert_cmpuint(g_access(dir, W_OK), ==, 0);
+               g_free(dir);
+       }
+
+       tmp = g_strdup_printf("%s%c%s", mdir, G_DIR_SEPARATOR, ".noindex");
+       g_assert_cmpuint(g_access(tmp, F_OK), !=, 0);
+
+       g_free(tmp);
+       g_free(tmpdir);
+       g_free(mdir);
+}
+
+static void
+test_maildir_mkdir_04(void)
+{
+       gchar *tmpdir, *mdir;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       mdir   = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "cuux");
+
+       /* create part of the structure already... */
+       {
+               gchar* dir;
+               g_assert_cmpuint(g_mkdir_with_parents(mdir, 0755), ==, 0);
+               dir = g_strdup_printf("%s%ccur", mdir, G_DIR_SEPARATOR);
+               g_assert_cmpuint(g_mkdir_with_parents(dir, 0000), ==, 0);
+               g_free(dir);
+       }
+
+       /* this should fail now, because cur is not read/writable  */
+       if (geteuid() != 0)
+               g_assert_false(!!maildir_mkdir(mdir, 0755, false));
+
+       g_free(tmpdir);
+       g_free(mdir);
+}
+
+static gboolean
+ignore_error(const char* log_domain, GLogLevelFlags log_level, const gchar* msg, gpointer user_data)
+{
+       return FALSE; /* don't abort */
+}
+
+static void
+test_maildir_mkdir_05(void)
+{
+       /* this must fail */
+       g_test_log_set_fatal_handler((GTestLogFatalFunc)ignore_error, NULL);
+
+       g_assert_false(!!maildir_mkdir({}, 0755, true));
+}
+
+[[maybe_unused]] static void
+assert_matches_regexp(const char* str, const char* rx)
+{
+       if (!g_regex_match_simple(rx, str, (GRegexCompileFlags)0, (GRegexMatchFlags)0)) {
+               if (g_test_verbose())
+                       g_print("%s does not match %s", str, rx);
+               g_assert(0);
+       }
+}
+
+
+static void
+test_determine_target_ok(void)
+{
+       struct TestCase {
+               std::string old_path;
+               std::string root_maildir;
+               std::string target_maildir;
+               Flags new_flags;
+               bool new_name;
+               std::string expected;
+       };
+       const std::vector<TestCase> testcases = {
+                TestCase{ /* change some flags */
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       "/home/foo/Maildir",
+                       {},
+                       Flags::Seen | Flags::Passed,
+                       false,
+                       "/home/foo/Maildir/test/cur/123456:2,PS"
+               },
+
+                TestCase{ /* from cur -> new */
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       "/home/foo/Maildir",
+                       {},
+                       Flags::New,
+                       false,
+                       "/home/foo/Maildir/test/new/123456"
+               },
+
+                TestCase{ /* from new->cur */
+                       "/home/foo/Maildir/test/cur/123456",
+                       "/home/foo/Maildir",
+                       {},
+                       Flags::Seen | Flags::Flagged,
+                       false,
+                       "/home/foo/Maildir/test/cur/123456:2,FS"
+                },
+
+                TestCase{ /* change maildir */
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       "/home/foo/Maildir",
+                       "/test2",
+                       Flags::Flagged | Flags::Replied,
+                       false,
+                       "/home/foo/Maildir/test2/cur/123456:2,FR"
+               },
+                TestCase{ /* remove all flags */
+                       "/home/foo/Maildir/test/new/123456",
+                       "/home/foo/Maildir",
+                       {},
+                       Flags::None,
+                       false,
+                       "/home/foo/Maildir/test/cur/123456:2,"
+               },
+       };
+
+       for (auto&& testcase: testcases) {
+               const auto res = maildir_determine_target(
+                       testcase.old_path,
+                       testcase.root_maildir,
+                       testcase.target_maildir,
+                       testcase.new_flags,
+                       testcase.new_name);
+               g_assert_true(!!res);
+               g_assert_cmpstr(testcase.expected.c_str(), ==,
+                               res.value().c_str());
+       }
+}
+
+
+
+static void
+test_determine_target_fail(void)
+{
+       struct TestCase {
+               std::string old_path;
+               std::string root_maildir;
+               std::string target_maildir;
+               Flags new_flags;
+               bool new_name;
+               std::string expected;
+       };
+       const std::vector<TestCase> testcases = {
+                TestCase{ /* fail: no absolute path */
+                       "../foo/Maildir/test/cur/123456:2,FR-not-absolute",
+                       "/home/foo/Maildir",
+                       {},
+                       Flags::Seen | Flags::Passed,
+                       false,
+                       "/home/foo/Maildir/test/cur/123456:2,PS"
+               },
+
+                TestCase{ /* fail: no absolute root */
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       "../foo/Maildir-not-absolute",
+                       {},
+                       Flags::New,
+                       false,
+                       "/home/foo/Maildir/test/new/123456"
+               },
+
+                TestCase{ /* fail: maildir must start with '/' */
+                       "/home/foo/Maildir/test/cur/123456",
+                       "/home/foo/Maildir",
+                       "mymaildirwithoutslash",
+                       Flags::Seen | Flags::Flagged,
+                       false,
+                       "/home/foo/Maildir/test/cur/123456:2,FS"
+                },
+
+                TestCase{ /* fail: path must be below maildir */
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       "/home/bar/Maildir",
+                       "/test2",
+                       Flags::Flagged | Flags::Replied,
+                       false,
+                       "/home/foo/Maildir/test2/cur/123456:2,FR"
+               },
+                TestCase{ /* fail: New cannot be combined */
+                       "/home/foo/Maildir/test/new/123456",
+                       "/home/foo/Maildir",
+                       {},
+                       Flags::New | Flags::Replied,
+                       false,
+                       "/home/foo/Maildir/test/cur/123456:2,"
+               },
+       };
+
+       for (auto&& testcase: testcases) {
+               const auto res = maildir_determine_target(
+                       testcase.old_path,
+                       testcase.root_maildir,
+                       testcase.target_maildir,
+                       testcase.new_flags,
+                       testcase.new_name);
+               g_assert_false(!!res);
+       }
+}
+
+
+
+static void
+test_maildir_get_new_path_01(void)
+{
+       struct {
+               std::string     oldpath;
+               Flags           flags;
+               std::string     newpath;
+       } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR",
+                     Flags::Replied,
+                     "/home/foo/Maildir/test/cur/123456:2,R"},
+                    {"/home/foo/Maildir/test/cur/123456:2,FR",
+                     Flags::New,
+                     "/home/foo/Maildir/test/new/123456"},
+                    {"/home/foo/Maildir/test/new/123456:2,FR",
+                     (Flags::Seen | Flags::Replied),
+                     "/home/foo/Maildir/test/cur/123456:2,RS"},
+                    {"/home/foo/Maildir/test/new/1313038887_0.697",
+                       (Flags::Seen | Flags::Flagged | Flags::Passed),
+                       "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"},
+                    {"/home/foo/Maildir/test/new/1313038887_0.697:2,",
+                       (Flags::Seen | Flags::Flagged | Flags::Passed),
+                       "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"},
+                    /* note the ':2,' suffix on the new message is
+                     * removed */
+
+                    {"/home/foo/Maildir/trash/new/1312920597.2206_16.cthulhu",
+                     Flags::Seen,
+                     "/home/foo/Maildir/trash/cur/1312920597.2206_16.cthulhu:2,S"}};
+
+       for (int i = 0; i != G_N_ELEMENTS(paths); ++i) {
+               const auto newpath{maildir_determine_target(paths[i].oldpath,
+                                                           "/home/foo/Maildir",
+                                                           {}, paths[i].flags, false)};
+               assert_valid_result(newpath);
+               assert_equal(*newpath, paths[i].newpath);
+       }
+}
+
+static void
+test_maildir_get_new_path_02(void)
+{
+       struct {
+               std::string     oldpath;
+               Flags           flags;
+               std::string     targetdir;
+               std::string     newpath;
+               std::string     root_maildir;
+       } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR",
+                     Flags::Replied,
+                     "/blabla",
+                     "/home/foo/Maildir/blabla/cur/123456:2,R",
+                     "/home/foo/Maildir"},
+                    {"/home/bar/Maildir/test/cur/123456:2,FR",
+                     Flags::New,
+                     "/coffee",
+                     "/home/bar/Maildir/coffee/new/123456",
+                     "/home/bar/Maildir"
+                    },
+                    {"/home/cuux/Maildir/test/new/123456",
+                     (Flags::Seen | Flags::Replied),
+                     "/tea",
+                     "/home/cuux/Maildir/tea/cur/123456:2,RS",
+                     "/home/cuux/Maildir"},
+                    {"/home/boy/Maildir/test/new/1313038887_0.697:2,",
+                     (Flags::Seen | Flags::Flagged | Flags::Passed),
+                     "/stuff",
+                     "/home/boy/Maildir/stuff/cur/1313038887_0.697:2,FPS",
+                     "/home/boy/Maildir"}};
+
+       for (int i = 0; i != G_N_ELEMENTS(paths); ++i) {
+               auto newpath{maildir_determine_target(paths[i].oldpath,
+                                                     paths[i].root_maildir,
+                                                     paths[i].targetdir,
+                                                     paths[i].flags,
+                                                     false)};
+               assert_valid_result(newpath);
+               assert_equal(*newpath, paths[i].newpath);
+       }
+}
+
+static void
+test_maildir_get_new_path_custom(void)
+{
+       struct {
+               std::string     oldpath;
+               Flags           flags;
+               std::string     targetdir;
+               std::string     newpath;
+               std::string     root_maildir;
+       } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR",
+                     Flags::Replied,
+                     "/blabla",
+                     "/home/foo/Maildir/blabla/cur/123456:2,R",
+                     "/home/foo/Maildir"},
+                    {"/home/foo/Maildir/test/cur/123456:2,hFeRllo123",
+                     Flags::Flagged,
+                     "/blabla",
+                     "/home/foo/Maildir/blabla/cur/123456:2,F",
+                     "/home/foo/Maildir"},
+                    {"/home/foo/Maildir/test/cur/123456:2,abc",
+                     Flags::Passed,
+                     "/blabla",
+                     "/home/foo/Maildir/blabla/cur/123456:2,P",
+                     "/home/foo/Maildir"}};
+
+       for (int i = 0; i != G_N_ELEMENTS(paths); ++i) {
+               auto newpath{maildir_determine_target(paths[i].oldpath,
+                                                     paths[1].root_maildir,
+                                                     paths[i].targetdir,
+                                                     paths[i].flags,
+                                                     false)};
+               assert_valid_result(newpath);
+               assert_equal(*newpath, paths[i].newpath);
+       }
+}
+
+static void
+test_maildir_from_path(void)
+{
+       unsigned u;
+
+       struct {
+               std::string path, exp;
+       } cases[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", "/test"},
+                    {"/home/foo/Maildir/lala/new/1313038887_0.697:2,", "/lala"}};
+
+       for (u = 0; u != G_N_ELEMENTS(cases); ++u) {
+               auto mdir{maildir_from_path(cases[u].path, "/home/foo/Maildir")};
+               assert_valid_result(mdir);
+               assert_equal(*mdir, cases[u].exp);
+       }
+}
+
+static void
+test_maildir_link()
+{
+       TempDir tmpdir;
+
+       assert_valid_result(maildir_mkdir(tmpdir.path() + "/foo"));
+       assert_valid_result(maildir_mkdir(tmpdir.path() + "/bar"));
+
+       const auto srcpath1 = tmpdir.path() + "/foo/cur/msg1";
+       const auto srcpath2 = tmpdir.path() + "/foo/new/msg2";
+
+       {
+               std::ofstream stream(srcpath1);
+               stream.write("cur", 3);
+               g_assert_true(stream.good());
+               stream.close();
+       }
+
+       {
+               std::ofstream stream(srcpath2);
+               stream.write("new", 3);
+               g_assert_true(stream.good());
+               stream.close();
+       }
+
+       assert_valid_result(maildir_link(srcpath1, tmpdir.path() + "/bar", false));
+       assert_valid_result(maildir_link(srcpath2, tmpdir.path() + "/bar", false));
+
+       const auto dstpath1 = tmpdir.path() + "/bar/cur/msg1";
+       const auto dstpath2 = tmpdir.path() + "/bar/new/msg2";
+
+       g_assert_true(g_access(dstpath1.c_str(), F_OK) == 0);
+       g_assert_true(g_access(dstpath2.c_str(), F_OK) == 0);
+
+       assert_valid_result(maildir_clear_links(tmpdir.path() + "/bar"));
+       g_assert_false(g_access(dstpath1.c_str(), F_OK) == 0);
+       g_assert_false(g_access(dstpath2.c_str(), F_OK) == 0);
+}
+
+
+static void
+test_maildir_move(bool use_gio)
+{
+       TempDir tmpdir;
+
+       assert_valid_result(maildir_mkdir(tmpdir.path() + "/foo"));
+       assert_valid_result(maildir_mkdir(tmpdir.path() + "/bar"));
+
+       const auto srcpath1 = tmpdir.path() + "/foo/cur/msg1";
+       const auto srcpath2 = tmpdir.path() + "/foo/new/msg2";
+
+       {
+               std::ofstream stream(srcpath1);
+               stream.write("cur", 3);
+               g_assert_true(stream.good());
+               stream.close();
+       }
+
+       {
+               std::ofstream stream(srcpath2);
+               stream.write("new", 3);
+               g_assert_true(stream.good());
+               stream.close();
+       }
+
+       const auto dstpath = tmpdir.path() + "/test1";
+
+       assert_valid_result(maildir_move_message(srcpath1, dstpath, use_gio));
+       assert_valid_result(maildir_move_message(srcpath2, dstpath, use_gio));
+
+
+       //g_assert_true(g_access(dstpath.c_str(), F_OK) == 0);
+}
+
+static void
+test_maildir_move_vanilla()
+{
+       test_maildir_move(false/*!gio*/);
+}
+
+static void
+test_maildir_move_gio()
+{
+       test_maildir_move(true/*gio*/);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       /* mu_util_maildir_mkmdir */
+       g_test_add_func("/mu-maildir/mu-maildir-mkdir-01", test_maildir_mkdir_01);
+       g_test_add_func("/mu-maildir/mu-maildir-mkdir-02", test_maildir_mkdir_02);
+       g_test_add_func("/mu-maildir/mu-maildir-mkdir-03", test_maildir_mkdir_03);
+       g_test_add_func("/mu-maildir/mu-maildir-mkdir-04", test_maildir_mkdir_04);
+       g_test_add_func("/mu-maildir/mu-maildir-mkdir-05", test_maildir_mkdir_05);
+
+       g_test_add_func("/mu-maildir/mu-maildir-determine-target-ok",
+                       test_determine_target_ok);
+       g_test_add_func("/mu-maildir/mu-maildir-determine-target-fail",
+                       test_determine_target_fail);
+
+       // /* get/set flags */
+       g_test_add_func("/mu-maildir/mu-maildir-get-new-path-01", test_maildir_get_new_path_01);
+       g_test_add_func("/mu-maildir/mu-maildir-get-new-path-02", test_maildir_get_new_path_02);
+       g_test_add_func("/mu-maildir/mu-maildir-get-new-path-custom",
+                       test_maildir_get_new_path_custom);
+       g_test_add_func("/mu-maildir/mu-maildir-from-path",
+                       test_maildir_from_path);
+
+       g_test_add_func("/mu-maildir/mu-maildir-link", test_maildir_link);
+
+       g_test_add_func("/mu-maildir/mu-maildir-move-vanilla", test_maildir_move_vanilla);
+       g_test_add_func("/mu-maildir/mu-maildir-move-gio", test_maildir_move_gio);
+
+       return g_test_run();
+}
diff --git a/lib/tests/test-mu-msg-fields.cc b/lib/tests/test-mu-msg-fields.cc
new file mode 100644 (file)
index 0000000..5f5df16
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-message-fields.hh"
+
+static void
+test_mu_msg_field_body(void)
+{
+       Field::Id field;
+
+       field = Field::Id::BodyText;
+
+       g_assert_cmpstr(mu_msg_field_name(field), ==, "body");
+       g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 'b');
+       g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'B');
+
+       g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, FALSE);
+}
+
+static void
+test_mu_msg_field_subject(void)
+{
+       Field::Id field;
+
+       field = Field::Id::Subject;
+
+       g_assert_cmpstr(mu_msg_field_name(field), ==, "subject");
+       g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 's');
+       g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'S');
+
+       g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, FALSE);
+}
+
+static void
+test_mu_msg_field_to(void)
+{
+       Field::Id field;
+
+       field = Field::Id::To;
+
+       g_assert_cmpstr(mu_msg_field_name(field), ==, "to");
+       g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 't');
+       g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'T');
+
+       g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, FALSE);
+}
+
+static void
+test_mu_msg_field_prio(void)
+{
+       Field::Id field;
+
+       field = Field::Id::Priority;
+
+       g_assert_cmpstr(mu_msg_field_name(field), ==, "prio");
+       g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 'p');
+       g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'P');
+
+       g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, TRUE);
+}
+
+static void
+test_mu_msg_field_flags(void)
+{
+       Field::Id field;
+
+       field = Field::Id::Flags;
+
+       g_assert_cmpstr(mu_msg_field_name(field), ==, "flag");
+       g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 'g');
+       g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'G');
+
+       g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, TRUE);
+}
+
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       /* mu_msg_str_date */
+       g_test_add_func("/mu-msg-fields/mu-msg-field-body", test_mu_msg_field_body);
+       g_test_add_func("/mu-msg-fields/mu-msg-field-subject", test_mu_msg_field_subject);
+       g_test_add_func("/mu-msg-fields/mu-msg-field-to", test_mu_msg_field_to);
+       g_test_add_func("/mu-msg-fields/mu-msg-field-prio", test_mu_msg_field_prio);
+       g_test_add_func("/mu-msg-fields/mu-msg-field-flags", test_mu_msg_field_flags);
+
+       /* FIXME: add tests for mu_msg_str_flags; but note the
+        * function simply calls mu_msg_field_str */
+
+       g_log_set_handler(
+           NULL,
+           (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+           (GLogFunc)black_hole,
+           NULL);
+
+       return g_test_run();
+}
diff --git a/lib/tests/test-mu-msg.cc b/lib/tests/test-mu-msg.cc
new file mode 100644 (file)
index 0000000..12a64ce
--- /dev/null
@@ -0,0 +1,354 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+#include <array>
+#include <string>
+
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+
+#include <message/mu-message.hh>
+
+using namespace Mu;
+
+using ExpectedContacts = const std::vector<std::pair<std::string, std::string>>;
+
+static void
+assert_contacts_equal(const Contacts& contacts,
+                     const ExpectedContacts& expected)
+{
+       g_assert_cmpuint(contacts.size(), ==, expected.size());
+
+       size_t n{};
+       for (auto&& contact: contacts) {
+               if (g_test_verbose())
+                       g_message("{ \"%s\", \"%s\"},\n", contact.name.c_str(), contact.email.c_str());
+               assert_equal(contact.name, expected.at(n).first);
+               assert_equal(contact.email, expected.at(n).second);
+               ++n;
+       }
+       g_print("\n");
+}
+
+
+static void
+test_mu_msg_01(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1220863042.12663_1.mindcrime!2,S")
+               .value()};
+
+       assert_contacts_equal(msg.to(), {{ "Donald Duck", "gcc-help@gcc.gnu.org" }});
+       assert_contacts_equal(msg.from(), {{ "Mickey Mouse", "anon@example.com" }});
+
+       assert_equal(msg.subject(), "gcc include search order");
+       assert_equal(msg.message_id(),
+                    "3BE9E6535E3029448670913581E7A1A20D852173@"
+                    "emss35m06.us.lmco.com");
+       assert_equal(msg.header("Mailing-List").value_or(""),
+                    "contact gcc-help-help@gcc.gnu.org; run by ezmlm");
+       g_assert_true(msg.priority() == Priority::Normal);
+       g_assert_cmpuint(msg.date(), ==, 1217530645);
+
+       assert_contacts_equal(msg.all_contacts(), {
+                       { "", "gcc-help-owner@gcc.gnu.org"},
+                       { "Mickey Mouse", "anon@example.com" },
+                       { "Donald Duck", "gcc-help@gcc.gnu.org" }
+               });
+
+}
+
+static void
+test_mu_msg_02(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1220863087.12663_19.mindcrime!2,S")
+               .value()};
+
+       assert_equal(msg.to().at(0).email, "help-gnu-emacs@gnu.org");
+       assert_equal(msg.subject(), "Re: Learning LISP; Scheme vs elisp.");
+       assert_equal(msg.from().at(0).email, "anon@example.com");
+       assert_equal(msg.message_id(), "r6bpm5-6n6.ln1@news.ducksburg.com");
+       assert_equal(msg.header("Errors-To").value_or(""),
+                    "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org");
+       g_assert_true(msg.priority() /* 'low' */
+                     == Priority::Low);
+       g_assert_cmpuint(msg.date(), ==, 1218051515);
+               g_print("flags: %s\n", Mu::to_string(msg.flags()).c_str());
+       g_assert_true(msg.flags() == (Flags::Seen|Flags::MailingList));
+
+       assert_contacts_equal(msg.all_contacts(), {
+                       { "", "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org"},
+                       { "", "anon@example.com"},
+                       { "", "help-gnu-emacs@gnu.org"},
+               });
+
+}
+
+static void
+test_mu_msg_03(void)
+{
+       //const GSList* params;
+
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1283599333.1840_11.cthulhu!2,")
+               .value()};
+
+       assert_equal(msg.to().at(0).display_name(), "Bilbo Baggins <bilbo@anotherexample.com>");
+       assert_equal(msg.subject(), "Greetings from Lothlórien");
+       assert_equal(msg.from().at(0).display_name(), "Frodo Baggins <frodo@example.com>");
+       g_assert_true(msg.priority() == Priority::Normal);
+       g_assert_cmpuint(msg.date(), ==, 0);
+       assert_equal(msg.body_text().value_or(""),
+                    "\nLet's write some fünkÿ text\nusing umlauts.\n\nFoo.\n");
+
+       // params = mu_msg_get_body_text_content_type_parameters(msg, MU_MSG_OPTION_NONE);
+       // g_assert_cmpuint(g_slist_length((GSList*)params), ==, 2);
+
+       // assert_equal((char*)params->data, "charset");
+       // params = g_slist_next(params);
+       // assert_equal((char*)params->data, "UTF-8");
+       g_assert_true(msg.flags() == (Flags::Unread));
+}
+
+static void
+test_mu_msg_04(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/mail5").value()};
+
+       assert_equal(msg.to().at(0).display_name(), "George Custer <gac@example.com>");
+       assert_equal(msg.subject(), "pics for you");
+       assert_equal(msg.from().at(0).display_name(), "Sitting Bull <sb@example.com>");
+       g_assert_true(msg.priority() /* 'low' */
+                     == Priority::Normal);
+       g_assert_cmpuint(msg.date(), ==, 0);
+       g_assert_true(msg.flags() ==
+                     (Flags::HasAttachment|Flags::Unread));
+       g_assert_true(msg.flags() ==
+                     (Flags::HasAttachment|Flags::Unread));
+}
+
+static void
+test_mu_msg_multimime(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/multimime!2,FS").value()};
+
+       /* ie., are text parts properly concatenated? */
+       assert_equal(msg.subject(), "multimime");
+       assert_equal(msg.body_text().value_or(""), "abcdef");
+       g_assert_true(msg.flags() == (Flags::HasAttachment|Flags::Flagged|Flags::Seen));
+}
+
+static void
+test_mu_msg_flags(void)
+{
+       std::array<std::pair<std::string, Flags>, 2> tests= {{
+                       {MU_TESTMAILDIR4 "/multimime!2,FS",
+                        (Flags::Flagged | Flags::Seen |
+                         Flags::HasAttachment)},
+                       {MU_TESTMAILDIR4 "/special!2,Sabc",
+                        (Flags::Seen)}
+               }};
+
+       for (auto&& test: tests) {
+                auto msg = Message::make_from_path(test.first);
+                assert_valid_result(msg);
+                g_assert_true(msg->flags() == test.second);
+       }
+}
+
+static void
+test_mu_msg_umlaut(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,")
+               .value()};
+
+       assert_contacts_equal(msg.to(), { { "Helmut Kröger", "hk@testmu.xxx"}});
+       assert_contacts_equal(msg.from(), { { "Mü", "testmu@testmu.xx"}});
+
+       assert_equal(msg.subject(), "Motörhead");
+       assert_equal(msg.from().at(0).display_name(), "Mü <testmu@testmu.xx>");
+       g_assert_true(msg.priority() == Priority::Normal);
+       g_assert_cmpuint(msg.date(), ==, 0);
+}
+
+static void
+test_mu_msg_references(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,")
+               .value()};
+
+       std::array<std::string, 4> expected_refs = {
+               "non-exist-01@msg.id",
+               "non-exist-02@msg.id",
+               "non-exist-03@msg.id",
+               "non-exist-04@msg.id"
+       };
+
+       assert_equal_seq_str(msg.references(), expected_refs);
+       assert_equal(msg.thread_id(), expected_refs[0]);
+}
+
+static void
+test_mu_msg_references_dups(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1252168370_3.14675.cthulhu!2,S")
+               .value()};
+
+       std::array<std::string, 6> expected_refs = {
+               "439C1136.90504@euler.org",
+               "4399DD94.5070309@euler.org",
+               "20051209233303.GA13812@gauss.org",
+               "439B41ED.2080402@euler.org",
+               "439A1E03.3090604@euler.org",
+               "20051211184308.GB13513@gauss.org"
+       };
+
+       assert_equal_seq_str(msg.references(), expected_refs);
+       assert_equal(msg.thread_id(), expected_refs[0]);
+}
+
+static void
+test_mu_msg_references_many(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR2 "/bar/cur/181736.eml")
+               .value()};
+
+       std::array<std::string, 11> expected_refs = {
+               "e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt.googlegroups.com",
+               "87hbblwelr.fsf@sapphire.mobileactivedefense.com",
+               "pql248-4va.ln1@wilbur.25thandClement.com",
+               "ikns6r$li3$1@Iltempo.Update.UU.SE",
+               "8762s0jreh.fsf@sapphire.mobileactivedefense.com",
+               "ikqqp1$jv0$1@Iltempo.Update.UU.SE",
+               "87hbbjc5jt.fsf@sapphire.mobileactivedefense.com",
+               "ikr0na$lru$1@Iltempo.Update.UU.SE",
+               "tO8cp.1228$GE6.370@news.usenetserver.com",
+               "ikr6ks$nlf$1@Iltempo.Update.UU.SE",
+               "8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk"
+       };
+
+       assert_equal_seq_str(msg.references(), expected_refs);
+       assert_equal(msg.thread_id(), expected_refs[0]);
+}
+
+static void
+test_mu_msg_tags(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/mail1").value()};
+
+       assert_contacts_equal(msg.to(), {{ "Julius Caesar", "jc@example.com" }});
+       assert_contacts_equal(msg.from(), {{ "John Milton", "jm@example.com" }});
+
+       assert_equal(msg.subject(),"Fere libenter homines id quod volunt credunt");
+
+       g_assert_true(msg.priority() == Priority::High);
+       g_assert_cmpuint(msg.date(), ==, 1217530645);
+
+       std::array<std::string, 4> expected_tags = {
+               "Paradise",
+               "losT",
+               "john",
+               "milton"
+       };
+       assert_equal_seq_str(msg.tags(), expected_tags);
+}
+
+static void
+test_mu_msg_comp_unix_programmer(void)
+{
+       auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/181736.eml").value()};
+
+       g_assert_true(msg.to().empty());
+       assert_equal(msg.subject(),
+                       "Re: Are writes \"atomic\" to readers of the file?");
+       assert_equal(msg.from().at(0).display_name(), "Jimbo Foobarcuux <jimbo@slp53.sl.home>");
+       assert_equal(msg.message_id(), "oktdp.42997$Te.22361@news.usenetserver.com");
+
+       auto refs = join(msg.references(), ',');
+       assert_equal(refs,
+                    "e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt"
+                       ".googlegroups.com,"
+                       "87hbblwelr.fsf@sapphire.mobileactivedefense.com,"
+                       "pql248-4va.ln1@wilbur.25thandClement.com,"
+                       "ikns6r$li3$1@Iltempo.Update.UU.SE,"
+                       "8762s0jreh.fsf@sapphire.mobileactivedefense.com,"
+                       "ikqqp1$jv0$1@Iltempo.Update.UU.SE,"
+                       "87hbbjc5jt.fsf@sapphire.mobileactivedefense.com,"
+                       "ikr0na$lru$1@Iltempo.Update.UU.SE,"
+                       "tO8cp.1228$GE6.370@news.usenetserver.com,"
+                       "ikr6ks$nlf$1@Iltempo.Update.UU.SE,"
+                       "8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk");
+
+       //"jimbo@slp53.sl.home (Jimbo Foobarcuux)";
+       g_assert_true(msg.priority() == Priority::Normal);
+       g_assert_cmpuint(msg.date(), ==, 1299603860);
+}
+
+static void
+test_mu_str_prio_01(void)
+{
+       g_assert_true(priority_name(Priority::Low) == "low");
+       g_assert_true(priority_name(Priority::Normal) == "normal");
+       g_assert_true(priority_name(Priority::High) == "high");
+}
+
+G_GNUC_UNUSED static gboolean
+ignore_error(const char* log_domain, GLogLevelFlags log_level, const gchar* msg, gpointer user_data)
+{
+       return FALSE; /* don't abort */
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       int rv;
+
+       g_test_init(&argc, &argv, NULL);
+
+       /* mu_msg_str_date */
+       g_test_add_func("/mu-msg/mu-msg-01", test_mu_msg_01);
+       g_test_add_func("/mu-msg/mu-msg-02", test_mu_msg_02);
+       g_test_add_func("/mu-msg/mu-msg-03", test_mu_msg_03);
+       g_test_add_func("/mu-msg/mu-msg-04", test_mu_msg_04);
+       g_test_add_func("/mu-msg/mu-msg-multimime", test_mu_msg_multimime);
+
+       g_test_add_func("/mu-msg/mu-msg-flags", test_mu_msg_flags);
+
+       g_test_add_func("/mu-msg/mu-msg-tags", test_mu_msg_tags);
+       g_test_add_func("/mu-msg/mu-msg-references", test_mu_msg_references);
+       g_test_add_func("/mu-msg/mu-msg-references_dups", test_mu_msg_references_dups);
+       g_test_add_func("/mu-msg/mu-msg-references_many", test_mu_msg_references_many);
+
+       g_test_add_func("/mu-msg/mu-msg-umlaut", test_mu_msg_umlaut);
+       g_test_add_func("/mu-msg/mu-msg-comp-unix-programmer", test_mu_msg_comp_unix_programmer);
+
+       g_test_add_func("/mu-str/mu-str-prio-01", test_mu_str_prio_01);
+
+       rv = g_test_run();
+
+       return rv;
+}
diff --git a/lib/tests/test-mu-store-query.cc b/lib/tests/test-mu-store-query.cc
new file mode 100644 (file)
index 0000000..c22cc59
--- /dev/null
@@ -0,0 +1,648 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#include "utils/mu-result.hh"
+#include <array>
+#include <thread>
+#include <string>
+#include <string_view>
+#include <fstream>
+#include <unordered_map>
+
+#include <mu-store.hh>
+#include <mu-maildir.hh>
+#include <utils/mu-utils.hh>
+#include <utils/mu-test-utils.hh>
+#include <message/mu-message.hh>
+
+using namespace Mu;
+
+
+/// map of some (unique) path-tail to the message-text
+using TestMap = std::unordered_map<std::string, std::string>;
+
+static Store
+make_test_store(const std::string& test_path, const TestMap& test_map,
+               const StringVec &personal_addresses)
+{
+       std::string maildir = test_path + "/Maildir";
+
+       /* write messages to disk */
+       for (auto&& item: test_map) {
+
+               const auto msgpath = maildir + "/" + item.first;
+
+               /* create the directory for the message */
+               auto dir = to_string_gchar(g_path_get_dirname(msgpath.c_str()));
+               if (g_test_verbose())
+                       g_message("create message dir %s", dir.c_str());
+
+               g_assert_cmpuint(g_mkdir_with_parents(dir.c_str(), 0700), ==, 0);
+
+               /* write the file */
+               std::ofstream stream(msgpath);
+               stream.write(item.second.data(), item.second.size());
+               g_assert_true(stream.good());
+               stream.close();
+       }
+
+       /* make the store */
+       auto store = Store::make_new(test_path, maildir, personal_addresses, {});
+       assert_valid_result(store);
+
+       /* index the messages */
+       auto res = store->indexer().start({});
+       g_assert_true(res);
+       while(store->indexer().is_running()) {
+               using namespace std::chrono_literals;
+               std::this_thread::sleep_for(100ms);
+       }
+
+       if (test_map.size() > 0)
+               g_assert_false(store->empty());
+
+       g_assert_cmpuint(store->size(),==,test_map.size());
+
+       /* and we have a fully-ready store */
+       return std::move(store.value());
+}
+
+
+static void
+test_simple()
+{
+       const TestMap test_msgs = {{
+
+// "sqlite-msg" "Simple mailing list message.
+{
+"basic/cur/sqlite-msg:2,S",
+R"(Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+  by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F
+  for <xxxx@localhost>; Mon,  4 Aug 2008 21:49:34 +0300 (EEST)
+Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: "Foo Example" <foo@example.com>
+To: sqlite-dev@sqlite.org
+Cc: "Bank of America" <bank@example.com>
+Bcc: Aku Ankka <donald.duck@duckstad.nl>
+Mime-Version: 1.0 (Apple Message framework v926)
+Date: Mon, 4 Aug 2008 11:40:49 +0200
+X-Mailer: Apple Mail (2.926)
+Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: sqlite-dev-bounces@sqlite.org
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch
+statement you can use something like:
+goto * instructions[pOp->opcode];
+
+I said: "Aujourd'hui!"
+)"},
+}};
+       TempDir tdir;
+       auto store{make_test_store(tdir.path(), test_msgs, {})};
+
+       // matches
+       for (auto&& expr: {
+                       "Inside",
+                       "from:foo@example.com",
+                       "from:Foo",
+                       "from:\"Foo Example\"",
+                       "from:/Foo.*Example/",
+                       "recip:\"Bank Of America\"",
+                       "cc:bank@example.com",
+                       "cc:bank",
+                       "cc:america",
+                       "bcc:donald.duck@duckstad.nl",
+                       "bcc:donald.duck",
+                       "bcc:duckstad.nl",
+                       "bcc:aku",
+                       "bcc:ankka",
+                       "bcc:\"aku ankka\"",
+                       "date:2008-08-01..2008-09-01",
+                       "prio:low",
+                       "to:sqlite-dev@sqlite.org",
+                       "list:sqlite-dev.sqlite.org",
+                       "aujourd'hui",
+               }) {
+
+               if (g_test_verbose())
+                       g_message("query: '%s'", expr);
+               auto qr = store.run_query(expr);
+               assert_valid_result(qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 1);
+       }
+
+       auto qr = store.run_query("statement");
+       assert_valid_result(qr);
+       g_assert_false(qr->empty());
+       g_assert_cmpuint(qr->size(), ==, 1);
+
+       assert_equal(qr->begin().subject().value_or(""),
+                    "[sqlite-dev] VM optimization inside sqlite3VdbeExec");
+       g_assert_true(qr->begin().references().empty());
+       //g_assert_cmpuint(qr->begin().date().value_or(0), ==, 123454);
+}
+
+static void
+test_spam_address_components()
+{
+       const TestMap test_msgs = {{
+
+// "sqlite-msg" "Simple mailing list message.
+{
+"spam/cur/spam-msg:2,S",
+R"(Message-Id: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+To: example@example.com
+Subject: ***SPAM*** this is a test
+
+Boo!
+)"},
+}};
+       TempDir tdir;
+       auto store{make_test_store(tdir.path(), test_msgs, {})};
+
+       g_test_bug("2278");
+       g_test_bug("2281");
+
+       // matches both
+       for (auto&& expr: {
+                       "SPAM",
+                       "spam",
+                       "/.*SPAM.*/",
+                       "subject:SPAM",
+                       "from:bar@example.com",
+                       "subject:\\*\\*\\*SPAM\\*\\*\\*",
+                       "bar",
+                       "example.com"
+               }) {
+
+               if (g_test_verbose())
+                       g_message("query: '%s'", expr);
+               auto qr = store.run_query(expr);
+               assert_valid_result(qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 1);
+       }
+}
+
+
+static void
+test_dups_related()
+{
+       const TestMap test_msgs = {{
+/* parent */
+{
+"inbox/cur/msg1:2,S",
+R"(Message-Id: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Sat, 06 Aug 2022 11:01:54 -0700
+To: example@example.com
+Subject: test1
+
+Parent
+)"},
+/* child (dup vv) */
+{
+"boo/cur/msg2:1,S",
+R"(Message-Id: <edcba@foo.bar>
+In-Reply-To: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Sat, 06 Aug 2022 13:01:54 -0700
+To: example@example.com
+Subject: Re: test1
+
+Child
+)"},
+/* child (dup ^^) */
+{
+"inbox/cur/msg2:1,S",
+R"(Message-Id: <edcba@foo.bar>
+In-Reply-To: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Sat, 06 Aug 2022 14:01:54 -0700
+To: example@example.com
+Subject: Re: test1
+
+Child
+)"},
+}};
+       TempDir tdir;
+       auto store{make_test_store(tdir.path(), test_msgs, {})};
+       {
+               // direct matches
+               auto qr = store.run_query("test1", Field::Id::Date,
+                                         QueryFlags::None);
+               g_assert_true(!!qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 3);
+       }
+
+       {
+               // skip duplicate messages; which one is skipped is arbitrary.
+               auto qr = store.run_query("test1", Field::Id::Date,
+                                         QueryFlags::SkipDuplicates);
+               g_assert_true(!!qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 2);
+       }
+
+       {
+               // no related
+               auto qr = store.run_query("Parent", Field::Id::Date);
+               g_assert_true(!!qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 1);
+       }
+
+       {
+               // find related messages
+               auto qr = store.run_query("Parent", Field::Id::Date,
+                                         QueryFlags::IncludeRelated);
+               g_assert_true(!!qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 3);
+       }
+
+       {
+               // find related messages, skip dups. the leader message
+               // should _not_ be skipped.
+               auto qr = store.run_query("test1 AND maildir:/inbox",
+                                         Field::Id::Date,
+                                         QueryFlags::IncludeRelated|
+                                         QueryFlags::SkipDuplicates);
+               g_assert_true(!!qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 2);
+
+               // ie the /boo is to be skipped, since it's not in the leader
+               // set.
+               for (auto&& m: *qr)
+                       assert_equal(m.message()->maildir(), "/inbox");
+       }
+
+       {
+               // find related messages, find parent from child.
+               auto qr = store.run_query("Child and maildir:/inbox",
+                                         Field::Id::Date,
+                                         QueryFlags::IncludeRelated);
+               g_assert_true(!!qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 3);
+
+       }
+
+       {
+               // find related messages, find parent from child.
+               // leader message wins
+               auto qr = store.run_query("Child and maildir:/inbox",
+                                         Field::Id::Date,
+                                         QueryFlags::IncludeRelated|
+                                         QueryFlags::SkipDuplicates|
+                                         QueryFlags::Descending);
+               g_assert_true(!!qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 2);
+
+               // ie the /boo is to be skipped, since it's not in the leader
+               // set.
+               for (auto&& m: *qr)
+                       assert_equal(m.message()->maildir(), "/inbox");
+       }
+}
+
+
+static void
+test_related_missing_root()
+{
+       const TestMap test_msgs = {{
+{
+"inbox/cur/msg1:2,S",
+R"(Content-Type: text/plain; charset=utf-8
+References:  <EZrZOnVCsYfFcX3Ls0VFoRnJdCGV4GM5YtO739l-iOB2ADNH7cIJWb0DaO5Of3BWDUEKq18Rz3a7rNoI96bNwQ==@protonmail.internalid>
+To: "Joerg Roedel" <joro@8bytes.org>, "Suman Anna" <s-anna@ti.com>
+Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com>
+From: "Dan Carpenter" <dan.carpenter@oracle.com>
+Subject: [PATCH] iommu/omap: fix buffer overflow in debugfs
+Date: Thu, 4 Aug 2022 17:32:39 +0300
+Message-Id: <YuvYh1JbE3v+abd5@kili>
+List-Id: <kernel-janitors.vger.kernel.org>
+Precedence: bulk
+
+There are two issues here:
+)"},
+{
+"inbox/cur/msg2:2,S",
+R"(Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=utf-8
+References: <YuvYh1JbE3v+abd5@kili>
+ <9pEUi_xoxa7NskF7EK_qfrlgjXzGsyw9K7cMfYbo-KI6fnyVMKTpc8E2Fu94V8xedd7cMpn0LlBrr9klBMflpw==@protonmail.internalid>
+Reply-To: "Laurent Pinchart" <laurent.pinchart@ideasonboard.com>
+From: "Laurent Pinchart" <laurent.pinchart@ideasonboard.com>
+Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs
+List-Id: <kernel-janitors.vger.kernel.org>
+Message-Id: <YuvzKJM66k+ZPD9c@pendragon.ideasonboard.com>
+Precedence: bulk
+In-Reply-To: <YuvYh1JbE3v+abd5@kili>
+
+Hi Dan,
+
+Thank you for the patch.
+)"},
+{
+"inbox/cur/msg3:2,S",
+R"(Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=utf-8
+References: <YuvYh1JbE3v+abd5@kili>
+ <G6TStg8J52Q-uSMTR7wRQdPeloxpZMiEQT_F8_JIDYM25eEPeHGgrNKO0fuO78MiQgD9Mz4BDtsZlZgmPKFe4Q==@protonmail.internalid>
+To: "Dan Carpenter" <dan.carpenter@oracle.com>, "Joerg Roedel"
+ <joro@8bytes.org>, "Suman Anna" <s-anna@ti.com>
+Reply-To: "Robin Murphy" <robin.murphy@arm.com>
+From: "Robin Murphy" <robin.murphy@arm.com>
+Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs
+List-Id: <kernel-janitors.vger.kernel.org>
+Message-Id: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+Precedence: bulk
+In-Reply-To: <YuvYh1JbE3v+abd5@kili>
+Date: Thu, 4 Aug 2022 17:31:39 +0100
+
+On 04/08/2022 3:32 pm, Dan Carpenter wrote:
+> There are two issues here:
+)"},
+{
+"inbox/new/msg4",
+R"(Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=utf-8
+References: <YuvYh1JbE3v+abd5@kili>
+ <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+ <T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid>
+To: "Robin Murphy" <robin.murphy@arm.com>
+Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com>
+From: "Dan Carpenter" <dan.carpenter@oracle.com>
+Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs
+List-Id: <kernel-janitors.vger.kernel.org>
+Date: Fri, 5 Aug 2022 09:37:02 +0300
+In-Reply-To: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+Precedence: bulk
+Message-Id: <20220805063702.GH3438@kadam>
+
+On Thu, Aug 04, 2022 at 05:31:39PM +0100, Robin Murphy wrote:
+> On 04/08/2022 3:32 pm, Dan Carpenter wrote:
+> > There are two issues here:
+)"},
+}};
+       TempDir tdir;
+       auto store{make_test_store(tdir.path(), test_msgs, {})};
+       {
+               auto qr = store.run_query("fix buffer overflow in debugfs",
+                                         Field::Id::Date, QueryFlags::IncludeRelated);
+               g_assert_true(!!qr);
+               g_assert_cmpuint(qr->size(), ==, 4);
+       }
+
+       {
+               auto qr = store.run_query("fix buffer overflow in debugfs and flag:unread",
+                                         Field::Id::Date, QueryFlags::None);
+               g_assert_true(!!qr);
+               g_assert_cmpuint(qr->size(), ==, 1);
+               assert_equal(qr->begin().message_id().value_or(""), "20220805063702.GH3438@kadam");
+               assert_equal(qr->begin().thread_id().value_or(""), "YuvYh1JbE3v+abd5@kili");
+       }
+
+       {
+               /* this one failed earlier, because the 'protonmail' id is the
+                * first reference, which means it does _not_ have the same
+                * thread-id as the rest; however, we filter these
+                * fake-message-ids now.*/
+               g_test_bug("2312");
+
+               auto qr = store.run_query("fix buffer overflow in debugfs and flag:unread",
+                                         Field::Id::Date, QueryFlags::IncludeRelated);
+               g_assert_true(!!qr);
+               g_assert_cmpuint(qr->size(), ==, 4);
+       }
+}
+
+
+static void
+test_body_matricula()
+{
+       const TestMap test_msgs = {{
+{
+"basic/cur/matricula-msg:2,S",
+R"(From: XXX <XX@XX.com>
+Subject:
+ =?iso-8859-1?Q?EF_-_Pago_matr=EDcula_de_la_matr=EDcula_de_inscripci=F3n_a?=
+Date: Thu, 4 Aug 2022 14:29:41 +0000
+Message-ID:
+ <VE1PR03MB5471882920DE08CFE44D97A0FE9F9@VE1PR03MB5471.eurprd03.prod.outlook.com>
+Accept-Language: es-AR, es-ES, en-US
+Content-Language: es-AR
+X-MS-Has-Attach: yes
+Content-Type: multipart/mixed;
+       boundary="_004_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_"
+MIME-Version: 1.0
+X-OriginatorOrg: ef.com
+X-MS-Exchange-CrossTenant-AuthAs: Internal
+X-MS-Exchange-CrossTenant-AuthSource: VE1PR03MB5471.eurprd03.prod.outlook.com
+
+--_004_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_
+Content-Type: multipart/alternative;
+       boundary="_000_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_"
+
+--_000_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+Buenas tardes Familia,
+
+
+Espero que est=E9n muy bien.
+
+
+
+Ya cargamos en sistema su pre inscripci=F3n para el curso
+
+
+Quedamos atentos ante cualquier consulta que surja.
+
+Saludos,
+)"},
+}};
+       TempDir tdir;
+       auto store{make_test_store(tdir.path(), test_msgs, {})};
+
+       /* i.e., non-utf8 text parts were not converted */
+       g_test_bug("2333");
+
+       // matches
+       for (auto&& expr: {
+                       "subject:matrícula",
+                       "subject:matricula",
+                       "body:atentos",
+                       "body:inscripción"
+               }) {
+
+               if (g_test_verbose())
+                       g_message("query: '%s'", expr);
+               auto qr = store.run_query(expr);
+               assert_valid_result(qr);
+               g_assert_false(qr->empty());
+               g_assert_cmpuint(qr->size(), ==, 1);
+       }
+}
+
+
+
+static void
+test_duplicate_refresh_real(bool rename)
+{
+       g_test_bug("2327");
+
+       const TestMap test_msgs = {{
+               "inbox/new/msg",
+               { R"(Message-Id: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Wed, 26 Oct 2022 11:01:54 -0700
+To: example@example.com
+Subject: Rainy night in Helsinki
+
+Boo!
+)"},
+               }};
+
+       /* create maildir with message */
+       TempDir tdir;
+       auto store{make_test_store(tdir.path(), test_msgs, {})};
+       g_debug("%s", store.properties().root_maildir.c_str());
+       /* ensure we have a proper maildir, with new/, cur/ */
+       auto mres = maildir_mkdir(store.properties().root_maildir + "/inbox");
+       assert_valid_result(mres);
+       g_assert_cmpuint(store.size(), ==, 1U);
+
+       /*
+        * find the one msg with a query
+        */
+       auto qr = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None);
+       g_assert_true(!!qr);
+       g_assert_cmpuint(qr->size(), ==, 1);
+       const auto old_path = qr->begin().path().value();
+       const auto old_docid = qr->begin().doc_id();
+       assert_equal(qr->begin().message()->path(), old_path);
+       g_assert_true(::access(old_path.c_str(), F_OK) == 0);
+
+       /*
+        * mark as read, i.e. move to cur/; ensure it really moved.
+        */
+       auto moved_msg = store.move_message(old_docid, Nothing, Flags::Seen, rename);
+       assert_valid_result(moved_msg);
+       const auto new_path = moved_msg->path();
+       if (!rename)
+               assert_equal(new_path, store.properties().root_maildir + "/inbox/cur/msg:2,S");
+       g_assert_cmpuint(store.size(), ==, 1);
+       g_assert_false(::access(old_path.c_str(), F_OK) == 0);
+       g_assert_true(::access(new_path.c_str(), F_OK) == 0);
+
+       /* also ensure thath the cached sexp for the message has been updated;
+        * that's what mu4e uses */
+       const auto moved_sexp{moved_msg->to_sexp().to_sexp_string()};
+       /* clumsy */
+       g_assert_true(moved_sexp.find(new_path) != std::string::npos);
+
+       /*
+        * find new message with query, ensure it's really that new one.
+        */
+       auto qr2 = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None);
+       g_assert_true(!!qr2);
+       g_assert_cmpuint(qr2->size(), ==, 1);
+       assert_equal(qr2->begin().path().value(), new_path);
+
+       /* index the messages */
+       auto res = store.indexer().start({});
+       g_assert_true(res);
+       while(store.indexer().is_running()) {
+               using namespace std::chrono_literals;
+               std::this_thread::sleep_for(100ms);
+       }
+       g_assert_cmpuint(store.size(), ==, 1);
+
+       /*
+        * ensure query still has the right results
+        */
+       auto qr3 = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None);
+       g_assert_true(!!qr3);
+       g_assert_cmpuint(qr3->size(), ==, 1);
+       const auto path3{qr3->begin().path().value()};
+       assert_equal(path3, new_path);
+       assert_equal(qr3->begin().message()->path(), new_path);
+       g_assert_true(::access(path3.c_str(), F_OK) == 0);
+}
+
+
+static void
+test_duplicate_refresh()
+{
+       test_duplicate_refresh_real(false/*no rename*/);
+}
+
+
+static void
+test_duplicate_refresh_rename()
+{
+       test_duplicate_refresh_real(true/*rename*/);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       mu_test_init(&argc, &argv);
+
+       g_test_bug_base("https://github.com/djcb/mu/issues/");
+
+       g_test_add_func("/store/query/simple", test_simple);
+       g_test_add_func("/store/query/spam-address-components",
+                       test_spam_address_components);
+       g_test_add_func("/store/query/dups-related",
+                       test_dups_related);
+       g_test_add_func("/store/query/related-missing-root",
+                       test_related_missing_root);
+       g_test_add_func("/store/query/body-matricula",
+                       test_body_matricula);
+       g_test_add_func("/store/query/duplicate-refresh",
+                       test_duplicate_refresh);
+       g_test_add_func("/store/query/duplicate-refresh-rename",
+                       test_duplicate_refresh_rename);
+
+       return g_test_run();
+}
diff --git a/lib/tests/test-mu-store.cc b/lib/tests/test-mu-store.cc
new file mode 100644 (file)
index 0000000..1a140e3
--- /dev/null
@@ -0,0 +1,365 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <stdlib.h>
+#include <thread>
+#include <unistd.h>
+#include <time.h>
+#include <fstream>
+
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-store.hh"
+#include "utils/mu-result.hh"
+#include <utils/mu-utils.hh>
+#include "mu-maildir.hh"
+
+using namespace Mu;
+
+static std::string MuTestMaildir  = Mu::canonicalize_filename(MU_TESTMAILDIR, "/");
+static std::string MuTestMaildir2 = Mu::canonicalize_filename(MU_TESTMAILDIR2, "/");
+
+static void
+test_store_ctor_dtor()
+{
+       TempDir tempdir;
+       auto store{Store::make_new(tempdir.path(), "/tmp", {}, {})};
+       assert_valid_result(store);
+
+       g_assert_true(store->empty());
+       g_assert_cmpuint(0, ==, store->size());
+
+       g_assert_cmpstr(MU_STORE_SCHEMA_VERSION, ==,
+                       store->properties().schema_version.c_str());
+}
+
+static void
+test_store_add_count_remove()
+{
+       TempDir tempdir{false};
+
+       auto store{Store::make_new(tempdir.path() + "/xapian", MuTestMaildir, {}, {})};
+       assert_valid_result(store);
+
+       const auto msgpath{MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"};
+       const auto id1 = store->add_message(msgpath);
+       assert_valid_result(id1);
+       store->commit();
+
+       g_assert_cmpuint(store->size(), ==, 1);
+       g_assert_true(store->contains_message(msgpath));
+
+       g_assert_true(store->contains_message(msgpath));
+
+       const auto id2 = store->add_message(MuTestMaildir2 + "/bar/cur/mail3");
+       g_assert_false(!!id2); // wrong maildir.
+       store->commit();
+
+       const auto msg3path{MuTestMaildir + "/cur/1252168370_3.14675.cthulhu!2,S"};
+       const auto id3 = store->add_message(msg3path);
+       assert_valid_result(id3);
+
+       g_assert_cmpuint(store->size(), ==, 2);
+       g_assert_true(store->contains_message(msg3path));
+
+       store->remove_message(id1.value());
+       g_assert_cmpuint(store->size(), ==, 1);
+       g_assert_false(
+           store->contains_message(MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"));
+
+       store->remove_message(msg3path);
+       g_assert_true(store->empty());
+       g_assert_false(store->contains_message(msg3path));
+}
+
+
+static void
+test_message_mailing_list()
+{
+       constexpr const char *test_message_1 =
+R"(Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+  by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F
+  for <xxxx@localhost>; Mon,  4 Aug 2008 21:49:34 +0300 (EEST)
+Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: anon@example.com
+To: sqlite-dev@sqlite.org
+Mime-Version: 1.0 (Apple Message framework v926)
+Date: Mon, 4 Aug 2008 11:40:49 +0200
+X-Mailer: Apple Mail (2.926)
+Subject: Capybaras United
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: sqlite-dev-bounces@sqlite.org
+Content-Length: 639
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch
+statement you can use something like:
+goto * instructions[pOp->opcode];
+)";
+       TempDir tempdir;
+       auto store{Store::make_new(tempdir.path(), "/home/test/Maildir", {}, {})};
+       assert_valid_result(store);
+
+       const auto msgpath{"/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S"};
+       auto message{Message::make_from_text(test_message_1, msgpath)};
+       assert_valid_result(message);
+
+       const auto docid = store->add_message(*message);
+       assert_valid_result(docid);
+       g_assert_cmpuint(store->size(),==, 1);
+
+       /* ensure 'update' dtrt, i.e., nothing. */
+       const auto docid2 = store->update_message(*message, *docid);
+       assert_valid_result(docid2);
+       g_assert_cmpuint(store->size(),==, 1);
+       g_assert_cmpuint(*docid,==,*docid2);
+
+       auto msg2{store->find_message(*docid)};
+       g_assert_true(!!msg2);
+       assert_equal(message->path(), msg2->path());
+
+       g_assert_true(store->contains_message(message->path()));
+
+       const auto qr = store->run_query("to:sqlite-dev@sqlite.org");
+       g_assert_true(!!qr);
+       g_assert_cmpuint(qr->size(), ==, 1);
+}
+
+
+static void
+test_message_attachments(void)
+{
+       constexpr const char* msg_text =
+R"(Return-Path: <foo@example.com>
+Received: from pop.gmail.com [256.85.129.309]
+       by evergrey with POP3 (fetchmail-6.4.29)
+       for <djcb@localhost> (single-drop); Thu, 24 Mar 2022 20:12:40 +0200 (EET)
+Sender: "Foo, Example" <foo@example.com>
+User-agent: mu4e 1.7.11; emacs 29.0.50
+From: "Foo Example" <foo@example.com>
+To: bar@example.com
+Subject: =?utf-8?B?w6R0dMOkY2htZcOxdHM=?=
+Date: Thu, 24 Mar 2022 20:04:39 +0200
+Organization: ACME Inc.
+Message-Id: <3144HPOJ0VC77.3H1XTAG2AMTLH@"@WILSONB.COM>
+MIME-Version: 1.0
+X-label: @NextActions operation:mindcrime Queensrÿche
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+
+Hello,
+--=-=-=
+Content-Type: image/jpeg
+Content-Disposition: attachment; filename=file-01.bin
+Content-Transfer-Encoding: base64
+
+AAECAw==
+--=-=-=
+Content-Type: audio/ogg
+Content-Disposition: inline; filename=/tmp/file-02.bin
+Content-Transfer-Encoding: base64
+
+BAUGBw==
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: attachment;
+ filename="message.eml"
+
+From: "Fnorb" <fnorb@example.com>
+To: Bob <bob@example.com>
+Subject: news for you
+Date: Mon, 28 Mar 2022 22:53:26 +0300
+
+Attached message!
+
+--=-=-=
+Content-Type: text/plain
+
+World!
+--=-=-=--
+)";
+
+       TempDir tempdir;
+       auto store{Store::make_new(tempdir.path(), "/home/test/Maildir", {}, {})};
+       assert_valid_result(store);
+
+       auto message{Message::make_from_text(
+                       msg_text,
+                       "/home/test/Maildir/inbox/cur/1649279256.abcde_1.evergrey:2,S")};
+       assert_valid_result(message);
+
+       const auto docid = store->add_message(*message);
+       assert_valid_result(docid);
+       store->commit();
+
+       auto msg2{store->find_message(*docid)};
+       g_assert_true(!!msg2);
+       assert_equal(message->path(), msg2->path());
+
+       g_assert_true(store->contains_message(message->path()));
+
+       // for (auto&& term = msg2->document().xapian_document().termlist_begin();
+       //      term != msg2->document().xapian_document().termlist_end(); ++term)
+       //      g_message(">>> %s", (*term).c_str());
+
+       const auto stats{store->statistics()};
+       g_assert_cmpuint(stats.size,==,store->size());
+       g_assert_cmpuint(stats.last_index,==,0);
+       g_assert_cmpuint(stats.last_change,>=,::time({}));
+}
+
+
+static void
+test_index_move()
+{
+       using namespace std::chrono_literals;
+
+        const std::string msg_text =
+R"(From: Valentine Michael Smith <mike@example.com>
+To: Raul Endymion <raul@example.com>
+Cc: emacs-devel@gnu.org
+Subject: Re: multi-eq hash tables
+Date: Tue, 03 May 2022 20:58:02 +0200
+Message-ID: <87h766tzzz.fsf@gnus.org>
+MIME-Version: 1.0
+Content-Type: text/plain
+Precedence: list
+List-Id: "Emacs development discussions." <emacs-devel.gnu.org>
+List-Post: <mailto:emacs-devel@gnu.org>
+
+Raul Endymion <raul@example.com> writes:
+
+> Maybe we should introduce something like:
+>
+>     (define-hash-table-test shallow-equal
+>       (lambda (x1 x2) (while (and (consp x1) (consp x2) (eql (car x1) (car x2)))
+>                         (setq x1 (cdr x1)) (setq x2 (cdr x2)))
+>                       (equal x1 x2)))
+>       ...)
+
+Yes, that would be excellent.
+)";
+
+        TempDir tempdir2;
+
+        { // create a message file.
+                const auto res1 = maildir_mkdir(tempdir2.path() + "/Maildir/a");
+                assert_valid_result(res1);
+
+                std::ofstream output{tempdir2.path() + "/Maildir/a/new/msg"};
+                output.write(msg_text.c_str(), msg_text.size());
+                output.close();
+                g_assert_true(output.good());
+        }
+
+        // Index it into a store.
+        TempDir tempdir;
+        auto store{Store::make_new(tempdir.path(), tempdir2.path() + "/Maildir", {}, {})};
+        assert_valid_result(store);
+
+        store->indexer().start({});
+        size_t n{};
+        while (store->indexer().is_running()) {
+                std::this_thread::sleep_for(100ms);
+                g_assert_cmpuint(n++,<=,25);
+        }
+        g_assert_true(!store->indexer().is_running());
+        const auto& prog{store->indexer().progress()};
+        g_assert_cmpuint(prog.updated,==,1);
+        g_assert_cmpuint(store->size(), ==, 1);
+        g_assert_false(store->empty());
+
+        // Find the message
+        auto qr = store->run_query("path:" + tempdir2.path() + "/Maildir/a/new/msg");
+        assert_valid_result(qr);
+        g_assert_cmpuint(qr->size(),==,1);
+
+        const auto msg = qr->begin().message();
+        g_assert_true(!!msg);
+
+        // Check the message
+        const auto oldpath{msg->path()};
+        assert_equal(msg->subject(), "Re: multi-eq hash tables");
+        g_assert_true(msg->docid() != 0);
+        g_debug("%s", msg->to_sexp().to_sexp_string().c_str());
+
+        // Move the message from new->cur
+        std::this_thread::sleep_for(1s); /* ctime should change */
+        const auto msg3 = store->move_message(msg->docid(), {}, Flags::Seen);
+        assert_valid_result(msg3);
+        assert_equal(msg3->maildir(), "/a");
+        assert_equal(msg3->path(), tempdir2.path() + "/Maildir/a/cur/msg:2,S");
+        g_assert_true(::access(msg3->path().c_str(), R_OK)==0);
+        g_assert_false(::access(oldpath.c_str(), R_OK)==0);
+
+        g_debug("%s", msg3->to_sexp().to_sexp_string().c_str());
+        g_assert_cmpuint(store->size(), ==, 1);
+}
+
+
+static void
+test_store_fail()
+{
+       {
+               const auto store = Store::make("/root/non-existent-path/12345");
+               g_assert_false(!!store);
+       }
+
+       {
+               const auto store = Store::make_new("/../../root/non-existent-path/12345",
+                                                  "/../../root/non-existent-path/54321",
+                                                  {}, {});
+               g_assert_false(!!store);
+       }
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       mu_test_init(&argc, &argv);
+
+       g_test_add_func("/store/ctor-dtor", test_store_ctor_dtor);
+       g_test_add_func("/store/add-count-remove", test_store_add_count_remove);
+       g_test_add_func("/store/message/mailing-list",
+                       test_message_mailing_list);
+       g_test_add_func("/store/message/attachments",
+                       test_message_attachments);
+       g_test_add_func("/store/index/move", test_index_move);
+       g_test_add_func("/store/index/fail", test_store_fail);
+
+       return g_test_run();
+}
diff --git a/lib/tests/test-parser.cc b/lib/tests/test-parser.cc
new file mode 100644 (file)
index 0000000..74b5522
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+** Copyright (C) 2017-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+
+#include "utils/mu-test-utils.hh"
+
+#include "mu-parser.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+using namespace Mu;
+
+struct Case {
+       const std::string expr;
+       const std::string expected;
+       WarningVec        warnings{};
+};
+
+using CaseVec = std::vector<Case>;
+
+static void
+test_cases(const CaseVec& cases)
+{
+       char* tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert(tmpdir);
+       auto dummy_store{Store::make_new(tmpdir, "/tmp", {}, {})};
+       assert_valid_result(dummy_store);
+
+       g_free(tmpdir);
+
+       Parser parser{*dummy_store, Parser::Flags::UnitTest};
+
+       for (const auto& casus : cases) {
+               WarningVec warnings;
+               const auto tree = parser.parse(casus.expr, warnings);
+
+               std::stringstream ss;
+               ss << tree;
+
+               if (g_test_verbose()) {
+                       std::cout << "\n";
+                       std::cout << casus.expr << std::endl;
+                       std::cout << "exp:" << casus.expected << std::endl;
+                       std::cout << "got:" << ss.str() << std::endl;
+               }
+
+               assert_equal(casus.expected, ss.str());
+       }
+}
+
+static void
+test_basic()
+{
+       CaseVec cases = {
+           //{ "", R"#((atom :value ""))#"},
+           {
+               "foo",
+               R"#((value "message-id" "foo"))#",
+           },
+           {"foo       or         bar", R"#((or(value "message-id" "foo")(value "message-id" "bar")))#"},
+           {"foo and bar", R"#((and(value "message-id" "foo")(value "message-id" "bar")))#"},
+       };
+
+       test_cases(cases);
+}
+
+static void
+test_complex()
+{
+       CaseVec cases = {
+           {"foo and bar or cuux",
+            R"#((or(and(value "message-id" "foo")(value "message-id" "bar")))#" +
+                std::string(R"#((value "message-id" "cuux")))#")},
+           {"a and not b", R"#((and(value "message-id" "a")(not(value "message-id" "b"))))#"},
+           {"a and b and c",
+            R"#((and(value "message-id" "a")(and(value "message-id" "b")(value "message-id" "c"))))#"},
+           {"(a or b) and c",
+            R"#((and(or(value "message-id" "a")(value "message-id" "b"))(value "message-id" "c")))#"},
+           {"a b", // implicit and
+            R"#((and(value "message-id" "a")(value "message-id" "b")))#"},
+           {"a not b", // implicit and not
+            R"#((and(value "message-id" "a")(not(value "message-id" "b"))))#"},
+           {"not b", // implicit and not
+            R"#((not(value "message-id" "b")))#"}};
+
+       test_cases(cases);
+}
+
+G_GNUC_UNUSED static void
+test_range()
+{
+       CaseVec cases = {
+           {"range:a..b", // implicit and
+            R"#((range "range" "a" "b"))#"},
+       };
+
+       test_cases(cases);
+}
+
+static void
+test_flatten()
+{
+       CaseVec cases = {{" Mötørhęåđ", R"#((value "message-id" "motorhead"))#"}};
+
+       test_cases(cases);
+}
+
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/parser/basic", test_basic);
+       g_test_add_func("/parser/complex", test_complex);
+       // g_test_add_func ("/parser/range",    test_range);
+       g_test_add_func("/parser/flatten", test_flatten);
+
+       return g_test_run();
+}
diff --git a/lib/tests/test-query.cc b/lib/tests/test-query.cc
new file mode 100644 (file)
index 0000000..d1ca0bb
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <config.h>
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+#include <unistd.h>
+
+#include "mu-store.hh"
+#include "mu-query.hh"
+#include "index/mu-indexer.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-test-utils.hh"
+
+using namespace Mu;
+
+static void
+test_query()
+{
+       allow_warnings();
+       char* tdir;
+
+       tdir = test_mu_common_get_random_tmpdir();
+       auto store = Store::make_new(tdir, std::string{MU_TESTMAILDIR}, {}, {});
+       assert_valid_result(store);
+       g_free(tdir);
+
+       auto&& idx{store->indexer()};
+
+       g_assert_true(idx.start(Indexer::Config{}));
+       while (idx.is_running()) {
+               sleep(1);
+       }
+
+       auto dump_matches = [](const QueryResults& res) {
+               size_t n{};
+               for (auto&& item : res) {
+                       std::cout << item.query_match() << '\n';
+                       if (g_test_verbose())
+                               g_debug("%02zu %s %s",
+                                       ++n,
+                                       item.path().value_or("<none>").c_str(),
+                                       item.message_id().value_or("<none>").c_str());
+               }
+       };
+
+       g_assert_cmpuint(store->size(), ==, 19);
+
+       {
+               const auto res = store->run_query("", {}, QueryFlags::None);
+               g_assert_true(!!res);
+               g_assert_cmpuint(res->size(), ==, 19);
+               dump_matches(*res);
+
+               g_assert_cmpuint(store->count_query(""), ==, 19);
+
+       }
+
+       {
+               const auto res = store->run_query("", Field::Id::Path, QueryFlags::None, 11);
+               g_assert_true(!!res);
+               g_assert_cmpuint(res->size(), ==, 11);
+               dump_matches(*res);
+       }
+}
+
+int
+main(int argc, char* argv[])
+try {
+       mu_test_init(&argc, &argv);
+
+       g_test_add_func("/query", test_query);
+
+       return g_test_run();
+
+} catch (const std::runtime_error& re) {
+       std::cerr << re.what() << "\n";
+       return 1;
+} catch (...) {
+       std::cerr << "caught exception\n";
+       return 1;
+}
diff --git a/lib/tests/test-tokenizer.cc b/lib/tests/test-tokenizer.cc
new file mode 100644 (file)
index 0000000..6e287f0
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include <vector>
+#include <glib.h>
+#include <iostream>
+#include <sstream>
+
+#include "mu-tokenizer.hh"
+
+struct Case {
+       const char*      str;
+       const Mu::Tokens tokens;
+};
+
+using CaseVec = std::vector<Case>;
+
+using namespace Mu;
+using TT = Token::Type;
+
+static void
+test_cases(const CaseVec& cases)
+{
+       for (const auto& casus : cases) {
+               const auto tokens = tokenize(casus.str);
+
+               g_assert_cmpuint((guint)tokens.size(), ==, (guint)casus.tokens.size());
+               for (size_t u = 0; u != tokens.size(); ++u) {
+                       if (g_test_verbose()) {
+                               std::cerr << "case " << u << " " << casus.str << std::endl;
+                               std::cerr << "exp: '" << casus.tokens[u] << "'" << std::endl;
+                               std::cerr << "got: '" << tokens[u] << "'" << std::endl;
+                       }
+                       g_assert_true(tokens[u] == casus.tokens[u]);
+               }
+       }
+}
+
+static void
+test_basic()
+{
+       CaseVec cases = {
+           {"", {}},
+
+           {"foo", Tokens{Token{3, TT::Data, "foo"}}},
+
+           {"foo bar cuux",
+            Tokens{Token{3, TT::Data, "foo"},
+                   Token{7, TT::Data, "bar"},
+                   Token{12, TT::Data, "cuux"}}},
+
+           {"\"foo bar\"", Tokens{Token{9, TT::Data, "foo bar"}}},
+
+           // ie. ignore missing closing '"'
+           {"\"foo bar", Tokens{Token{8, TT::Data, "foo bar"}}},
+
+       };
+
+       test_cases(cases);
+}
+
+static void
+test_specials()
+{
+       CaseVec cases = {
+           {")*(",
+            Tokens{Token{1, TT::Close, ")"}, Token{2, TT::Data, "*"}, Token{3, TT::Open, "("}}},
+           {"\")*(\"", Tokens{Token{5, TT::Data, ")*("}}},
+       };
+
+       test_cases(cases);
+}
+
+static void
+test_ops()
+{
+       CaseVec cases = {{"foo and bar oR cuux XoR fnorb",
+                         Tokens{Token{3, TT::Data, "foo"},
+                                Token{7, TT::And, "and"},
+                                Token{11, TT::Data, "bar"},
+                                Token{14, TT::Or, "oR"},
+                                Token{19, TT::Data, "cuux"},
+                                Token{23, TT::Xor, "XoR"},
+                                Token{29, TT::Data, "fnorb"}}},
+                        {"NOT (aap or mies)",
+                         Tokens{Token{3, TT::Not, "NOT"},
+                                Token{5, TT::Open, "("},
+                                Token{8, TT::Data, "aap"},
+                                Token{11, TT::Or, "or"},
+                                Token{16, TT::Data, "mies"},
+                                Token{17, TT::Close, ")"}}}};
+
+       test_cases(cases);
+}
+
+static void
+test_escape()
+{
+       CaseVec cases = {{"foo\"bar\"", Tokens{Token{8, TT::Data, "foobar"}}},
+                        {"\"fnorb\"", Tokens{Token{7, TT::Data, "fnorb"}}},
+                        {"\\\"fnorb\\\"", Tokens{Token{9, TT::Data, "fnorb"}}},
+                        {"foo\\\"bar\\\"", Tokens{Token{10, TT::Data, "foobar"}}}};
+
+       test_cases(cases);
+}
+
+static void
+test_to_string()
+{
+       std::stringstream ss;
+       for (auto&& t : tokenize("foo and bar xor not cuux or fnorb"))
+               ss << t << ' ';
+
+       g_assert_true(ss.str() == "3: <data> [foo] 7: <and> [and] 11: <data> [bar] "
+                                 "15: <xor> [xor] 19: <not> [not] 24: <data> [cuux] "
+                                 "27: <or> [or] 33: <data> [fnorb] ");
+}
+
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/tokens/basic", test_basic);
+       g_test_add_func("/tokens/specials", test_specials);
+       g_test_add_func("/tokens/ops", test_ops);
+       g_test_add_func("/tokens/escape", test_escape);
+       g_test_add_func("/tokens/to-string", test_to_string);
+
+       return g_test_run();
+}
diff --git a/lib/tests/testdir/cur/1220863042.12663_1.mindcrime!2,S b/lib/tests/testdir/cur/1220863042.12663_1.mindcrime!2,S
new file mode 100644 (file)
index 0000000..ab1500f
--- /dev/null
@@ -0,0 +1,146 @@
+Return-Path: <gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-4.9 required=3.0 tests=BAYES_00,DATE_IN_PAST_96_XX,
+       RCVD_IN_DNSWL_MED autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id 5123469CB3
+       for <xxxx@localhost>; Thu,  7 Aug 2008 08:10:19 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:19 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs39272wfh; Wed, 6 Aug 2008
+ 20:15:17 -0700 (PDT)
+Received: by 10.65.133.8 with SMTP id k8mr2071878qbn.7.1218078916289; Wed, 06
+ Aug 2008 20:15:16 -0700 (PDT)
+Received: from sourceware.org (sourceware.org [209.132.176.174]) by
+ mx.google.com with SMTP id 28si7904461qbw.0.2008.08.06.20.15.15; Wed, 06 Aug
+ 2008 20:15:16 -0700 (PDT)
+Received-SPF: neutral (google.com: 209.132.176.174 is neither permitted nor
+ denied by domain of gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org)
+ client-ip=209.132.176.174;
+Authentication-Results: mx.google.com; spf=neutral (google.com:
+ 209.132.176.174 is neither permitted nor denied by domain of
+ gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org)
+ smtp.mail=gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org
+Received: (qmail 13493 invoked by alias); 7 Aug 2008 03:15:13 -0000
+Received: (qmail 13485 invoked by uid 22791); 7 Aug 2008 03:15:12 -0000
+Received: from mailgw1a.lmco.com (HELO mailgw1a.lmco.com) (192.31.106.7)    
+ by sourceware.org (qpsmtpd/0.31) with ESMTP; Thu, 07 Aug 2008 03:14:27 +0000
+Received: from emss07g01.ems.lmco.com (relay5.ems.lmco.com [166.29.2.16])by
+ mailgw1a.lmco.com  (LM-6) with ESMTP id m773EPZH014730for
+ <gcc-help@gcc.gnu.org>; Wed, 6 Aug 2008 21:14:25 -0600 (MDT)
+Received: from CONVERSION2-DAEMON.lmco.com by lmco.com (PMDF V6.3-x14 #31428) 
+ id <0K5700601NO18J@lmco.com> for gcc-help@gcc.gnu.org; Wed, 06 Aug 2008
+ 21:14:25 -0600 (MDT)
+Received: from EMSS04I00.us.lmco.com ([166.17.13.135]) by lmco.com (PMDF
+ V6.3-x14 #31428)  with ESMTP id <0K5700H5MNNWGX@lmco.com> for
+ gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 21:14:20 -0600 (MDT)
+Received: from EMSS35M06.us.lmco.com ([158.187.107.143]) by
+ EMSS04I00.us.lmco.com with Microsoft SMTPSVC(5.0.2195.6713); Wed,  06 Aug
+ 2008 23:14:20 -0400
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Mickey Mouse" <anon@example.com>
+Subject: gcc include search order
+To: "Donald Duck" <gcc-help@gcc.gnu.org>
+Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Content-class: urn:content-classes:message
+Mailing-List: contact gcc-help-help@gcc.gnu.org; run by ezmlm
+Precedence: klub
+List-Id: <gcc-help.gcc.gnu.org>
+List-Unsubscribe: <mailto:gcc-help-unsubscribe-xxxx.klub=gmail.com@gcc.gnu.org>
+List-Archive: <http://gcc.gnu.org/ml/gcc-help/>
+List-Post: <mailto:gcc-help@gcc.gnu.org>
+List-Help: <mailto:gcc-help-help@gcc.gnu.org>
+Sender: gcc-help-owner@gcc.gnu.org
+Delivered-To: mailing list gcc-help@gcc.gnu.org
+Content-Length: 3024
+
+
+Hi.
+In my unit testing I need to change some header files (target is
+vxWorks, which supports some things that the sun does not). 
+So, what I do is fetch the development tree, and then in a new unit test
+directory I attempt to compile the unit under test. Since this is NOT
+vxworks, I use sed to change some of the .h files and put them in a
+./changed directory.
+
+When I try to compile the file, it is still using the .h file from the
+original location, even though I have listed the include path for
+./changed before the include path for the development tree.
+
+Here is a partial output from gcc using the -v option
+
+GNU CPP version 3.1 (cpplib) (sparc ELF)
+GNU C++ version 3.1 (sparc-sun-solaris2.8)
+        compiled by GNU C version 3.1.
+ignoring nonexistent directory "NONE/include"
+#include "..." search starts here:
+#include <...> search starts here:
+ .
+ changed
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/mp/interface
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/interface
+ /usr/local/include/g++-v3
+ /usr/local/include/g++-v3/sparc-sun-solaris2.8
+ /usr/local/include/g++-v3/backward
+ /usr/local/include
+ /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/3.1/include
+ /usr/local/sparc-sun-solaris2.8/include
+ /usr/include
+End of search list.
+
+I know the changed file is correct and that the include is not working
+as expected, because when I copy the file from ./changed, back into the
+development tree, the compilation works as expected.
+
+One more bit of information. The source that I cam compiling is in
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app
+And it is including files from
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+These include files should be including the files from ./changed (when
+they exist) but they are ignoring the .h files in the ./changed
+directory and are instead using other, unchanged files in the
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+directory.
+
+The gcc command line is something like
+
+  TEST_DIR="."
+  
+  CHANGED_DIR_NAME=changed
+  CHANGED_FILES_DIR=${TEST_DIR}/${CHANGED_DIR_NAME}
+
+  CICU_HEADER_FILES="-I ${AP_INTERFACE_FILES} -I ${AP_APP_FILES} -I
+${SHARED_COMMON_FILES} -I ${SHARED_INTERFACE_FILES}"
+  
+  HEADERS="-I ./ -I ${CHANGED_FILES_DIR} ${CICU_HEADER_FILES}"
+  DEFINES="-DSUNRUN -DA10_DEBUG -DJOETEST"
+  
+  CFLAGS="-v -c -g -O1 -pipe -Wformat -Wunused -Wuninitialized -Wshadow
+-Wmissing-prototypes -Wmissing-declarations"
+  
+  printf "Compiling the UUT File\n"
+  gcc -fprofile-arcs -ftest-coverage ${CFLAGS} ${HEADERS} ${DEFINES}
+${AP_APP_FILES}/unitUnderTest.cpp 
+
+
+I hope this explanation is clear. If anyone knows how to fix the command
+line so that it gets the .h files in the "changed" directory are used
+instead of files in the other include directories.
+
+Thanks
+Joe
+
+----------------------------------------------------
+Time Flies like an Arrow. Fruit Flies like a Banana
+
diff --git a/lib/tests/testdir/cur/1220863060.12663_3.mindcrime!2,S b/lib/tests/testdir/cur/1220863060.12663_3.mindcrime!2,S
new file mode 100644 (file)
index 0000000..d0ff0d7
--- /dev/null
@@ -0,0 +1,230 @@
+Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00,HTML_MESSAGE
+       autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id D724F6963B
+       for <xxxx@localhost>; Mon,  4 Aug 2008 21:49:27 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:27 +0300 (EEST)
+Received: by 10.142.51.12 with SMTP id y12cs86537wfy; Mon, 4 Aug 2008 00:38:51
+ -0700 (PDT)
+Received: by 10.151.113.5 with SMTP id q5mr272266ybm.37.1217835529913; Mon, 04
+ Aug 2008 00:38:49 -0700 (PDT)
+Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with
+ ESMTP id 5si5754915ywd.8.2008.08.04.00.38.30; Mon, 04 Aug 2008 00:38:50 -0700
+ (PDT)
+Received-SPF: pass (google.com: best guess record for domain of
+ sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender)
+ client-ip=67.18.92.124;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record
+ for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as
+ permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org
+Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with
+ ESMTP id 765A511C46; Mon,  4 Aug 2008 03:38:27 -0400 (EDT)
+X-Original-To: sqlite-dev@sqlite.org
+Delivered-To: sqlite-dev@sqlite.org
+Received: from ik-out-1112.google.com (ik-out-1112.google.com [66.249.90.176])
+ by sqlite.org (Postfix) with ESMTP id 4C59511C41 for <sqlite-dev@sqlite.org>;
+ Mon,  4 Aug 2008 03:38:23 -0400 (EDT)
+Received: by ik-out-1112.google.com with SMTP id b32so2163423ika.0 for
+ <sqlite-dev@sqlite.org>; Mon, 04 Aug 2008 00:38:23 -0700 (PDT)
+Received: by 10.210.54.19 with SMTP id c19mr14589042eba.107.1217835502549;
+ Mon, 04 Aug 2008 00:38:22 -0700 (PDT)
+Received: by 10.210.115.10 with HTTP; Mon, 4 Aug 2008 00:38:22 -0700 (PDT)
+Message-ID: <477821040808040038s381bf382p7411451e3c1a2e4e@mail.gmail.com>
+Date: Mon, 4 Aug 2008 10:38:22 +0300
+From: anon@example.com
+To: sqlite-dev@sqlite.org
+In-Reply-To: <73d4fc50808030747g303a170ieac567723c2d4f24@mail.gmail.com>
+MIME-Version: 1.0
+References: <477821040808030533y41f1501dq32447b568b6e6ca5@mail.gmail.com>
+ <73d4fc50808030747g303a170ieac567723c2d4f24@mail.gmail.com>
+Subject: Re: [sqlite-dev] SQLite exception A&B
+X-BeenThere: sqlite-dev@sqlite.org
+X-Mailman-Version: 2.1.9
+Priority: normal 
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+  <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe>
+List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev>
+List-Post: <mailto:sqlite-dev@sqlite.org>
+List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help>
+List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, 
+ <mailto:sqlite-dev-request@sqlite.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============2123623832=="
+Mime-version: 1.0
+Sender: sqlite-dev-bounces@sqlite.org
+Errors-To: sqlite-dev-bounces@sqlite.org
+Content-Length: 8475
+
+--===============2123623832==
+Content-Type: multipart/alternative; 
+       boundary="----=_Part_29556_25702991.1217835502493"
+
+------=_Part_29556_25702991.1217835502493
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+Hi Grant,
+
+Thanks for your reply.
+I am using a different session for each thread, whenever a thread wishes to
+access the database it gets a session from the session pool and works with
+that session until its work is done.
+
+Most of the actions the threads are doing on the database are quite
+complicated and are required to be fully committed or completely ignored, so
+yes, I am (most of the time) explicitly beginning and committing my
+transactions.
+
+Regarding the SQLiteStatementImpl, I believe the Poco manual explains that
+sessions and statements for that matter cannot be shared between threads,
+therefore if you are using a session via one thread only it should work
+fine.
+
+My first impression was that the problem was in the Poco infrastructure (I
+have found several Poco related bugs in the past), but the problem ALWAYS
+occurs when I perform the "BEGIN IMMEDIATE" action, if it were a Poco
+related bug, I would expect to see it here and there without any relation to
+this specific statement, but that is not the case.
+
+None the less, I will also post my question on the Poco forums.
+
+Nadav.
+
+On Sun, Aug 3, 2008 at 5:47 PM, Grant Gatchel <grant.gatchel@gmail.com>wrote:
+
+> Are you using the same Poco::Session for every thread or does each call
+> create a new session/handle to the database?
+>
+> Are you explicitly BEGINning and COMMITting your transactions?
+>
+> In looking at the 1.3.2 branch of Poco::Data::SQLite, there appears to be a
+> race condition in the SQLiteStatementImpl::next() method in which the member
+> _nextResponse is being accessed before the SQLiteStatementImpl::hasNext()
+> method has a chance to interpret that value and throw an exception.
+>
+> This question might be more suitable in the Poco forums or mailinglist.
+>
+> - Grant
+>
+> On Sun, Aug 3, 2008 at 8:33 AM, nadav g <nadav.gr@gmail.com> wrote:
+>
+>> Hi All,
+>>
+>> I have been using SQLite with Poco (www.appinf.com) as my infrastructure.
+>> The program is running several threads that access this database very
+>> often and are synchronized by SQLite itself.
+>> Everything seems to work just fine most of time (usually days - weeks) but
+>> I do get an occasional exception:
+>>
+>> Exception: SQL error or missing database: Iterator Error: trying to check
+>> if there is a next value
+>>
+>> The backtrace leads to this statement:
+>> *"BEGIN IMMEDIATE"*
+>>
+>> This specific code runs numerous times before an exception occurs (if
+>> occurs at all) and I cannot think of any reason for it to fail later rather
+>> than sooner.
+>> It is pretty obvious that this situation occurs due to some rare thread
+>> state, but I could not find any information that gives me any hint as to
+>> what this state might be.
+>>
+>> So what I am asking is:
+>> 1) Does anyone know why this sort of exception occurs?
+>> 2) Can anyone think of a reason for such an exception to occur in the
+>> situation I have described?
+>>
+>> Thanks in advance,
+>> Nadav.
+>>
+>>
+>> _______________________________________________
+>> sqlite-dev mailing list
+>> sqlite-dev@sqlite.org
+>> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+>>
+>>
+>
+> _______________________________________________
+> sqlite-dev mailing list
+> sqlite-dev@sqlite.org
+> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+>
+>
+
+------=_Part_29556_25702991.1217835502493
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+<div dir="ltr">Hi Grant,<br><br>Thanks for your reply.<br>I am using a different session for each thread, whenever a thread wishes to access the database it gets a session from the session pool and works with that session until its work is done.<br>
+<br>Most of the actions the threads are doing on the database are quite complicated and are required to be fully committed or completely ignored, so yes, I am (most of the time) explicitly beginning and committing my transactions.<br>
+<br>Regarding the SQLiteStatementImpl, I believe the Poco manual explains that sessions and statements for that matter cannot be shared between threads, therefore if you are using a session via one thread only it should work fine.<br>
+<br>My first impression was that the problem was in the Poco infrastructure (I have found several Poco related bugs in the past), but the problem ALWAYS occurs when I perform the &quot;BEGIN IMMEDIATE&quot; action, if it were a Poco related bug, I would expect to see it here and there without any relation to this specific statement, but that is not the case.<br>
+<br>None the less, I will also post my question on the Poco forums.<br><br>Nadav.<br><br><div class="gmail_quote">On Sun, Aug 3, 2008 at 5:47 PM, Grant Gatchel <span dir="ltr">&lt;<a href="mailto:grant.gatchel@gmail.com">grant.gatchel@gmail.com</a>&gt;</span> wrote:<br>
+<blockquote class="gmail_quote" style="border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt 0.8ex; padding-left: 1ex;"><div dir="ltr">Are you using the same Poco::Session for every thread or does each call create a new session/handle to the database?<br>
+<br>Are you explicitly BEGINning and COMMITting your transactions?<br><br>In looking at the 1.3.2 branch of Poco::Data::SQLite, there appears to be a race condition in the SQLiteStatementImpl::next() method in which the member _nextResponse is being accessed before the SQLiteStatementImpl::hasNext() method has a chance to interpret that value and throw an exception.<br>
+
+<br>This question might be more suitable in the Poco forums or mailinglist.<br><br>- Grant<br>
+<br><div class="gmail_quote"><div><div></div><div class="Wj3C7c">
+On Sun, Aug 3, 2008 at 8:33 AM, nadav g <span dir="ltr">&lt;<a href="http://nadav.gr" target="_blank">nadav.gr</a>@<a href="http://gmail.com" target="_blank">gmail.com</a>&gt;</span> wrote:<br></div></div><blockquote class="gmail_quote" style="border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt 0.8ex; padding-left: 1ex;">
+<div><div></div><div class="Wj3C7c">
+
+
+<div dir="ltr">Hi All,<br><br>I have been using SQLite with Poco (<a href="http://www.appinf.com" target="_blank">www.appinf.com</a>) as my infrastructure.<br>The program is running several threads that access this database very often and are synchronized by SQLite itself.<br>
+
+
+
+
+Everything seems to work just fine most of time (usually days - weeks) but I do get an occasional exception:<br><br>Exception: SQL error or missing database: Iterator Error: trying to check if there is a next value<br><br>
+
+
+
+
+The backtrace leads to this statement:<br><b>&quot;BEGIN IMMEDIATE&quot;</b><br><br>This specific code runs numerous times before an exception occurs (if occurs at all) and I cannot think of any reason for it to fail later rather than sooner.<br>
+
+
+
+
+It is pretty obvious that this situation occurs due to some rare thread state, but I could not find any information that gives me any hint as to what this state might be.<br><br>So what I am asking is:<br>1) Does anyone know why this sort of exception occurs?<br>
+
+
+
+
+2) Can anyone think of a reason for such an exception to occur in the situation I have described?<br><br>Thanks in advance,<br>Nadav.<br><br></div>
+<br></div></div>_______________________________________________<br>
+sqlite-dev mailing list<br>
+<a href="mailto:sqlite-dev@sqlite.org" target="_blank">sqlite-dev@sqlite.org</a><br>
+<a href="http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev" target="_blank">http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</a><br>
+<br></blockquote></div><br></div>
+<br>_______________________________________________<br>
+sqlite-dev mailing list<br>
+<a href="mailto:sqlite-dev@sqlite.org">sqlite-dev@sqlite.org</a><br>
+<a href="http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev" target="_blank">http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</a><br>
+<br></blockquote></div><br></div>
+
+------=_Part_29556_25702991.1217835502493--
+
+--===============2123623832==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+
+--===============2123623832==--
+
diff --git a/lib/tests/testdir/cur/1220863087.12663_15.mindcrime!2,PS b/lib/tests/testdir/cur/1220863087.12663_15.mindcrime!2,PS
new file mode 100644 (file)
index 0000000..d6487c0
--- /dev/null
@@ -0,0 +1,136 @@
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW,
+       SPF_PASS,WHOIS_NETSOLPR autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id 1A6CD69CB6
+       for <xxxx@localhost>; Tue, 12 Aug 2008 21:42:38 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.109]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Tue, 12 Aug 2008 21:42:38 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs123119wfh; Sun, 10 Aug 2008
+ 22:06:31 -0700 (PDT)
+Received: by 10.100.166.10 with SMTP id o10mr9327844ane.0.1218431190107; Sun,
+ 10 Aug 2008 22:06:30 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id c29si10110392anc.13.2008.08.10.22.06.29; Sun, 10 Aug 2008
+ 22:06:30 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:45637 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KSPbx-0006dj-96 for
+ xxxx.klub@gmail.com; Mon, 11 Aug 2008 01:06:29 -0400
+Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id
+ 1KSPbE-0006cQ-Nd for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:44 -0400
+Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id
+ 1KSPbD-0006bs-Px for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:44 -0400
+Received: from [199.232.76.173] (port=37426 helo=monty-python.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KSPbD-0006bk-HT for
+ help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:43 -0400
+Received: from main.gmane.org ([80.91.229.2]:46446 helo=ciao.gmane.org) by
+ monty-python.gnu.org with esmtps (TLS-1.0:RSA_AES_256_CBC_SHA1:32) (Exim
+ 4.60) (envelope-from <geh-help-gnu-emacs@m.gmane.org>) id 1KSPbD-0003Kl-CA
+ for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:43 -0400
+Received: from list by ciao.gmane.org with local (Exim 4.43) id
+ 1KSPb9-00080r-CX for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 05:05:39 +0000
+Received: from bas2-toronto63-1088792724.dsl.bell.ca ([64.229.168.148]) by
+ main.gmane.org with esmtp (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for
+ <help-gnu-emacs@gnu.org>; Mon, 11 Aug 2008 05:05:39 +0000
+Received: from cpchan by bas2-toronto63-1088792724.dsl.bell.ca with local
+ (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for <help-gnu-emacs@gnu.org>; Mon,
+ 11 Aug 2008 05:05:39 +0000
+X-Injected-Via-Gmane: http://gmane.org/
+To: help-gnu-emacs@gnu.org
+From: anon@example.com
+Date: Mon, 11 Aug 2008 01:03:22 -0400
+Organization: Linux Private Site
+Message-ID: <87bq00nnxh.fsf@MagnumOpus.Mercurius>
+References: <877iav5s49.fsf@163.com> <86hc9yc5sj.fsf@timbral.net>
+ <877iat7udd.fsf@163.com> <87fxphcsxi.fsf@lion.rapttech.com.au>
+ <8504ddd4-5e3b-4ed5-bf77-aa9cce81b59a@1g2000pre.googlegroups.com>
+ <87k5es59we.fsf@lion.rapttech.com.au>
+ <63c824e3-62b1-4a93-8fa8-2813e1f9397f@v13g2000pro.googlegroups.com>
+ <874p5vsgg8.fsf@nonospaz.fatphil.org>
+ <8250972e-1886-4021-80bc-376e34881c80@v39g2000pro.googlegroups.com>
+ <87zlnnqvvs.fsf@nonospaz.fatphil.org>
+ <57add0e0-b39d-4c71-8d2c-d3b9ddfaa1a9@1g2000pre.googlegroups.com>
+ <87sktfnz5p.fsf@atthis.clsnet.nl>
+ <562e1111-d9e7-4b6a-b661-3f9af13fea17@b30g2000prf.googlegroups.com>
+ <87d4khoq97.fsf@atthis.clsnet.nl>
+ <0fe404c5-cab8-4692-8a27-532e737a7813@i24g2000prf.googlegroups.com>
+Mime-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1;
+ protocol="application/pgp-signature"
+X-Complaints-To: usenet@ger.gmane.org
+X-Gmane-NNTP-Posting-Host: bas2-toronto63-1088792724.dsl.bell.ca
+X-Face: G;
+ Z,`sm>)4t4LB/GUrgH$W`!AmfHMj,LG)Z}X0ax@s9:0>0)B&@vcm{v-le)wng)?|o]D<V6&ay<F=H{M5?$T%p!dPdJeF,au\E@TA"v22K!Zl\\mzpU4]6$ZnAI3_L)h;
+ fpd}mn2py/7gv^|*85-D_f:07cT>\Z}0:6X
+User-Agent: Gnus/5.110011 (No Gnus v0.11) Emacs/23.0.60 (gnu/linux)
+Cancel-Lock: sha1:IKyfrl5drOw6HllHFSmWHAKEeC8=
+X-detected-kernel: by monty-python.gnu.org: Linux 2.6, seldom 2.4 (older, 4)
+Subject: Re: Can anybody tell me how to send HTML-format mail in gnus
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 1229
+Lines: 36
+
+--=-=-=
+Content-Type: text/plain
+
+Xah <xahlee@gmail.com> writes:
+
+> So, i was reading about it in Wikipedia. Although i don't have a TV,
+> and haven't had since 2000, but i still enjoyed the festive spirits
+> anyhow. After all, i'm Chinese by blood. So, in my wandering, i ran
+> into this welcome song on youtube:
+>
+> http://www.youtube.com/watch?v=1HEndNYVhZo
+
+What is your point? Your email is in plain text and I can click on the
+link just fine- it is not exactly rocket science to implement parsing of
+URL's to workable links in an Email program (a lot of programs does
+that, including Gnus). Images can be included inline if you want. Also
+mail markups such as *this*, **this** and _this_ have been around since
+the Usenet days and displayed appropriately by a number of mailers. Like
+others have said, most html messages that I have seen either contains
+useless information, or are plain spam and can introduce a host of
+security problems in some mailers.
+
+Charles
+
+
+--=-=-=
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v2.0.4-svn0 (GNU/Linux)
+
+iD8DBQFIn8gm3epPyyKbwPYRApbvAKDRirXwzMzI+NHV77+QcP3EgTPaCgCfb/6m
+GtNVKdYAeftaYm1nwRVoCDA=
+=ULo3
+-----END PGP SIGNATURE-----
+--=-=-=--
+
+
+
diff --git a/lib/tests/testdir/cur/1220863087.12663_19.mindcrime!2,S b/lib/tests/testdir/cur/1220863087.12663_19.mindcrime!2,S
new file mode 100644 (file)
index 0000000..78efa2a
--- /dev/null
@@ -0,0 +1,77 @@
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+       version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id C4D6569CB3
+       for <xxxx@localhost>; Thu,  7 Aug 2008 08:10:08 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:08 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs34794wfh; Wed, 6 Aug 2008
+ 13:40:29 -0700 (PDT)
+Received: by 10.100.33.13 with SMTP id g13mr1093301ang.79.1218055228418; Wed,
+ 06 Aug 2008 13:40:28 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d19si15908789and.17.2008.08.06.13.40.27; Wed, 06 Aug 2008
+ 13:40:28 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:56316 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQpo3-0007Pc-Qk for
+ xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:40:27 -0400
+From: anon@example.com
+Newsgroups: gnu.emacs.help
+Date: Wed, 6 Aug 2008 20:38:35 +0100
+Message-ID: <r6bpm5-6n6.ln1@news.ducksburg.com>
+References: <55dbm5-qcl.ln1@news.ducksburg.com>
+ <mailman.15710.1217599959.18990.help-gnu-emacs@gnu.org>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+X-Trace: individual.net bABVU1hcJwWAuRwe/097AAoOXnGGeYR8G1In635iFGIyfDLPUv
+X-Orig-Path: news.ducksburg.com!news
+Cancel-Lock: sha1:wK7dsPRpNiVxpL/SfvmNzlvUR94=
+ sha1:oepBoM0tJBLN52DotWmBBvW5wbg=
+User-Agent: slrn/pre0.9.9-120/mm/ao (Ubuntu Hardy)
+Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!feeder.erje.net!proxad.net!feeder1-2.proxad.net!feed.ac-versailles.fr!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail
+Xref: news.stanford.edu gnu.emacs.help:160868
+To: help-gnu-emacs@gnu.org
+Subject: Re: Learning LISP; Scheme vs elisp.
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 417
+Lines: 11
+
+On 2008-08-01, Thien-Thi Nguyen wrote:
+
+> warriors attack, felling foe after foe,
+> few growing old til they realize: to know
+> what deceit is worth deflection;
+> such receipt reversed rejection!
+> then their heavy arms, e'er transformed to shields:
+> balanced hooked charms, ploughed deep, rich yields.
+
+Aha: the exercise for the reader is to place the parens correctly.
+Might take me a while to solve this puzzle.
+
diff --git a/lib/tests/testdir/cur/1220863087.12663_5.mindcrime!2,S b/lib/tests/testdir/cur/1220863087.12663_5.mindcrime!2,S
new file mode 100644 (file)
index 0000000..de46cc8
--- /dev/null
@@ -0,0 +1,84 @@
+Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+       version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F
+       for <xxxx@localhost>; Mon,  4 Aug 2008 21:49:34 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:34 +0300 (EEST)
+Received: by 10.142.51.12 with SMTP id y12cs89397wfy; Mon, 4 Aug 2008 02:41:16
+ -0700 (PDT)
+Received: by 10.150.156.20 with SMTP id d20mr963580ybe.104.1217842875596; Mon,
+ 04 Aug 2008 02:41:15 -0700 (PDT)
+Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with
+ ESMTP id 6si3605185ywi.1.2008.08.04.02.40.57; Mon, 04 Aug 2008 02:41:15 -0700
+ (PDT)
+Received-SPF: pass (google.com: best guess record for domain of
+ sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender)
+ client-ip=67.18.92.124;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record
+ for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as
+ permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org
+Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with
+ ESMTP id 7147F11C45; Mon,  4 Aug 2008 05:40:55 -0400 (EDT)
+X-Original-To: sqlite-dev@sqlite.org
+Delivered-To: sqlite-dev@sqlite.org
+Received: from relay00.pair.com (relay00.pair.com [209.68.5.9]) by sqlite.org
+ (Postfix) with SMTP id B5F901192C for <sqlite-dev@sqlite.org>; Mon,  4 Aug
+ 2008 05:40:52 -0400 (EDT)
+Received: (qmail 59961 invoked from network); 4 Aug 2008 09:40:50 -0000
+Received: from unknown (HELO ?192.168.0.17?) (unknown) by unknown with SMTP; 4
+ Aug 2008 09:40:50 -0000
+X-pair-Authenticated: 87.13.75.164
+Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: anon@example.com
+To: sqlite-dev@sqlite.org
+Mime-Version: 1.0 (Apple Message framework v926)
+Date: Mon, 4 Aug 2008 11:40:49 +0200
+X-Mailer: Apple Mail (2.926)
+Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+X-BeenThere: sqlite-dev@sqlite.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+  <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe>
+List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev>
+List-Post: <mailto:sqlite-dev@sqlite.org>
+List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help>
+List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, 
+ <mailto:sqlite-dev-request@sqlite.org?subject=subscribe>
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: sqlite-dev-bounces@sqlite.org
+Errors-To: sqlite-dev-bounces@sqlite.org
+Content-Length: 639
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the  
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch  
+statement you can use something like:
+goto * instructions[pOp->opcode];
+---
+Marco Bambini
+http://www.sqlabs.net
+http://www.sqlabs.net/blog/
+http://www.sqlabs.net/realsqlserver/
+
+
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+
diff --git a/lib/tests/testdir/cur/1220863087.12663_7.mindcrime!2,RS b/lib/tests/testdir/cur/1220863087.12663_7.mindcrime!2,RS
new file mode 100644 (file)
index 0000000..b5c0651
--- /dev/null
@@ -0,0 +1,138 @@
+Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+       version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id 3EBAB6963B
+       for <xxxx@localhost>; Mon,  4 Aug 2008 21:49:35 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:35 +0300 (EEST)
+Received: by 10.142.51.12 with SMTP id y12cs89536wfy; Mon, 4 Aug 2008 02:48:56
+ -0700 (PDT)
+Received: by 10.150.134.21 with SMTP id h21mr7950048ybd.181.1217843335665;
+ Mon, 04 Aug 2008 02:48:55 -0700 (PDT)
+Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with
+ ESMTP id 6si5897081ywi.1.2008.08.04.02.48.35; Mon, 04 Aug 2008 02:48:55 -0700
+ (PDT)
+Received-SPF: pass (google.com: best guess record for domain of
+ sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender)
+ client-ip=67.18.92.124;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record
+ for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as
+ permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org
+Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with
+ ESMTP id ED01611C4E; Mon,  4 Aug 2008 05:48:31 -0400 (EDT)
+X-Original-To: sqlite-dev@sqlite.org
+Delivered-To: sqlite-dev@sqlite.org
+Received: from mx0.security.ro (mx0.security.ro [80.96.72.194]) by sqlite.org
+ (Postfix) with ESMTP id EB3F51192C for <sqlite-dev@sqlite.org>; Mon,  4 Aug
+ 2008 05:48:28 -0400 (EDT)
+Received: (qmail 348 invoked from network); 4 Aug 2008 12:48:03 +0300
+Received: from dev.security.ro (HELO ?192.168.1.70?) (192.168.1.70) by
+ mx0.security.ro with SMTP; 4 Aug 2008 12:48:03 +0300
+Message-ID: <4896D06A.8000901@security.ro>
+Date: Mon, 04 Aug 2008 12:48:26 +0300
+From: anon@example.com
+User-Agent: Thunderbird 2.0.0.16 (Windows/20080708)
+MIME-Version: 1.0
+To: sqlite-dev@sqlite.org
+References: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+In-Reply-To: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+Content-Type: multipart/mixed; boundary="------------000207070200050102060301"
+X-BitDefender-Scanner: Clean, Agent: BitDefender qmail 2.0.0 on
+ mx0.security.ro
+X-BitDefender-Spam: No (0)
+X-BitDefender-SpamStamp: v1, whitelisted, total: 0
+Subject: Re: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+X-BeenThere: sqlite-dev@sqlite.org
+X-Mailman-Version: 2.1.9
+Precedence: high 
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+  <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe>
+List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev>
+List-Post: <mailto:sqlite-dev@sqlite.org>
+List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help>
+List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, 
+ <mailto:sqlite-dev-request@sqlite.org?subject=subscribe>
+Sender: sqlite-dev-bounces@sqlite.org
+Errors-To: sqlite-dev-bounces@sqlite.org
+Content-Length: 2212
+
+This is a multi-part message in MIME format.
+--------------000207070200050102060301
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+
+Marco Bambini wrote:
+> Inside sqlite3VdbeExec there is a very big switch statement.
+> In order to increase performance with few modifications to the  
+> original code, why not use this technique ?
+> http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+>
+> With a properly defined "instructions" array, instead of the switch  
+> statement you can use something like:
+> goto * instructions[pOp->opcode];
+> ---
+> Marco Bambini
+> http://www.sqlabs.net
+> http://www.sqlabs.net/blog/
+> http://www.sqlabs.net/realsqlserver/
+>
+>
+>
+> _______________________________________________
+> sqlite-dev mailing list
+> sqlite-dev@sqlite.org
+> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+>   
+All the world's not a VAX.  This technique is GCC-specific. The SQLite 
+source must be as portable as possible thus tying it to a specific 
+compiler is out of the question. While one could conceivably use some 
+preprocessor magic to provide alternate implementations, that would be 
+impractical considering the sheer size of the code affected.
+On the other hand - perhaps you could benchmark the change and provide 
+some data on whether this actually improves performance?
+
+
+--------------000207070200050102060301
+Content-Type: text/x-vcard; charset=utf-8;
+ name="mihailim.vcf"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment;
+ filename="mihailim.vcf"
+
+begin:vcard
+fn:Mihai Limbasan
+n:Limbasan;Mihai
+org:SC SECPRAL COM SRL
+adr:;;str. Actorului nr. 9;Cluj-Napoca;Cluj;400441;Romania
+email;internet:mihailim@security.ro
+title:SoftwareDeveloper
+tel;work:+40 264 449579
+tel;fax:+40 264 418594
+tel;cell:+40 729 038302
+url:http://secpral.ro/
+version:2.1
+end:vcard
+
+
+--------------000207070200050102060301
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+
+--------------000207070200050102060301--
+
diff --git a/lib/tests/testdir/cur/1252168370_3.14675.cthulhu!2,S b/lib/tests/testdir/cur/1252168370_3.14675.cthulhu!2,S
new file mode 100644 (file)
index 0000000..4fad706
--- /dev/null
@@ -0,0 +1,21 @@
+Return-Path: <dfgh@floppydisk.nl>
+X-Spam-Checker-Version: SpamAssassin 3.1.0 (2005-09-13) on mindcrime
+X-Spam-Level: 
+Delivered-To: dfgh@floppydisk.nl
+Message-ID: <43A09C49.9040902@euler.org>
+Date: Wed, 14 Dec 2005 23:27:21 +0100
+From: Fred Flintstone <fred@euler.org>
+User-Agent: Mozilla Thunderbird 1.0.7 (X11/20051010)
+X-Accept-Language: nl-NL, nl, en
+MIME-Version: 1.0
+To: dfgh@floppydisk.nl
+Subject: Re: xyz
+References: <439C1136.90504@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439B41ED.2080402@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439A1E03.3090604@euler.org> <20051211184308.GB13513@gauss.org>
+In-Reply-To: <20051211184308.GB13513@gauss.org>
+X-Enigmail-Version: 0.92.0.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+X-UIDL: T<?"!%LG"!cAK"!_j(#!
+Content-Length: 1879
+
+Test 123.
diff --git a/lib/tests/testdir/cur/1283599333.1840_11.cthulhu!2, b/lib/tests/testdir/cur/1283599333.1840_11.cthulhu!2,
new file mode 100644 (file)
index 0000000..25c7180
--- /dev/null
@@ -0,0 +1,16 @@
+From: Frodo Baggins <frodo@example.com>
+To: Bilbo Baggins <bilbo@anotherexample.com>
+Subject: Greetings from =?UTF-8?B?TG90aGzDs3JpZW4=?=
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+Fcc: .sent
+Organization: The Fellowship of the Ring
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Message-Id: <abcd$efgh@example.com>
+
+
+Let's write some fünkÿ text
+using umlauts.
+
+Foo.
diff --git a/lib/tests/testdir/cur/1305664394.2171_402.cthulhu!2, b/lib/tests/testdir/cur/1305664394.2171_402.cthulhu!2,
new file mode 100644 (file)
index 0000000..863f714
--- /dev/null
@@ -0,0 +1,17 @@
+From: =?UTF-8?B?TcO8?= <testmu@testmu.xx>
+To: Helmut =?UTF-8?B?S3LDtmdlcg==?= <hk@testmu.xxx>
+Subject: =?UTF-8?B?TW90w7ZyaGVhZA==?=
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+References: <non-exist-01@msg.id> <non-exist-02@msg.id> <non-exist-03@msg.id> <non-exist-04@msg.id>
+1n-Reply-To: <non-exist-04@msg.id>
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+
+Test for issue #38, where apparently searching for accented words in subject,
+to etc. fails.
+
+What about here? Queensrÿche. Mötley Crüe.
+
+
diff --git a/lib/tests/testdir/cur/encrypted!2,S b/lib/tests/testdir/cur/encrypted!2,S
new file mode 100644 (file)
index 0000000..f75fd40
--- /dev/null
@@ -0,0 +1,56 @@
+Return-path: <>
+Envelope-to: peter@example.com
+Delivery-date: Fri, 11 May 2012 16:22:03 +0300
+Received: from localhost.example.com ([127.0.0.1] helo=borealis)
+       by borealis with esmtp (Exim 4.77)
+       id 1SSpnB-00038a-Ux
+       for djcb@localhost; Fri, 11 May 2012 16:21:58 +0300
+Delivered-To: peter@example.com
+From: Brian <brian@example.com>
+To: Peter <peter@example.com>
+Subject: encrypted
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:21:42 +0300
+Message-ID: <!&!AAAAAAAAAYAAAAAAAAAOH1+8mkk+lLn7Gg5fke7FbCgAAAEAAAAJ7eBDgcactKhXL6r8cEnJ8BAAAAAA==@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+hQQOA1T38TPQrHD6EA//YXkUB4Dy09ngCRyHWbXmV3XBjuKTr8xrak5ML1kwurav
+gyagOHKLMU+5CKvObChiKtXhtgU0od7IC8o+ALlHevQ0XXcqNYA2KUfX8R7akq7d
+Xx9mA6D8P7Y/P8juUCLBpfrCi2GC42DtvPZSUu3bL/ctUJ3InPHIfHibKF2HMm7/
+gUHAKY8VPJF39dLP8GLcfki6qFdeWbxgtzmuyzHfCBCLnDL0J9vpEQBpGDFMcc4v
+cCbmMJaiPOmRb6U4WOuRVnuXuTztLiIn0jMslzOSFDcLTVBAsrC01r71O+XZKfN4
+mIfcpcWJYKM2NQW8Jwf+8Hr84uznBqs8uTTlrmppjkAHZGqGMjiQDxLhDVaCQzMy
+O8PSV4xT6HPlKXOwV1OLc+vm0A0RAdSBctgZg40oFn4XdB1ur8edwAkLvc0hJKaz
+gyTQiPaXm2Uh2cDeEx4xNgXmwCKasqc9jAlnDC2QwA33+pw3OqgZT5h1obn0fAeR
+mgB+iW1503DIi/96p8HLZcr2EswLEH9ViHIEaFj/vlR5BaOncsLB0SsNV/MHRvym
+Xg5GUjzPIiyBZ3KaR9OIBiZ5eXw+bSrPAo/CAs0Zwxag7W3CH//oK39Qo1GnkYpc
+4IQxhx4IwkzqtCnripltV/kfpGu0yA/OdK8lOjkUqCwvL97o73utXIxm21Zd3mEP
+/iLNrduZjMCq+goz1pDAQa9Dez6VjwRuRPTqeAac8Fx/nzrVzIoIEAt36hpuaH1l
+KpbmHpKgsUWcrE5iYT0RRlRRtRF4PfJg8PUmP1hvw8TaEmNfT+0HgzcJB/gRsVdy
+gTzkzUDzGZLhRcpmM5eW4BkuUmIO7625pM6Jd3HOGyfCGSXyEZGYYeVKzv8xbzYf
+QM6YYKooRN9Ya2jdcWguW0sCSJO/RZ9eaORpTeOba2+Fp6w5L7lga+XM9GLfgref
+Cf39XX1RsmRBsrJTw0z5COf4bT8G3/IfQP0QyKWIFITiFjGmpZhLsKQ3KT4vSe/d
+gTY1xViVhkjvMFn3cgSOSrvktQpAhsXx0IRazN0T7pTU33a5K0SrZajY9ynFDIw9
+we7XYyVwZzYEXjGih5mTH1PhWYK5fZZEKKqaz5TyYv9SeWJ+8FrHeXUKD38SQEHM
+qkpl9Iv17RF4Qy9uASWwRoobhKO+GykTaBSTyw8R8ctG/hfAlnaZxQ3TwNyHWyvU
+9SVJsp27ulv/W9MLZtGpEMK0ckAR164Vyou1KOn200BqxbC2tJpegNeD2TP5ZtdY
+HIcxkgKr0haYcDnVEf1ulSxv23pZWIexbgvVCG7dRL0eB+6O28f9CWehle10MDyM
+0AYyw8Da2cu7PONMovqt4nayScyGTacFBp7c2KXR9DGZ0mcBwOjL/mGRKcVWN3MG
+2auCrwn2KVWmKZI3Jp0T8KhfGBnFs9lUElpDTOiED1/2bKz6Yoc385QtWx99DFMZ
+IWiH5wMxkWFpzjE+GHiJ09vSbTTL4JY9eu2n5nxQmtjYMBVxQm7S7qwH
+=0Paa
+-----END PGP MESSAGE-----
+--=-=-=--
diff --git a/lib/tests/testdir/cur/multimime!2,FS b/lib/tests/testdir/cur/multimime!2,FS
new file mode 100644 (file)
index 0000000..84f85aa
--- /dev/null
@@ -0,0 +1,27 @@
+Return-path: <>
+Envelope-to: djcb@localhost
+Delivery-date: Sun, 20 May 2012 09:59:51 +0300
+From: Steve Jobs <jobs@example.com>
+To: Bill Gates <bg@example.com>
+Subject: multimime
+User-agent: mu4e 0.9.8.4; emacs 23.3.1
+Date: Sat, 19 May 2012 20:57:56 +0100
+Message-ID: <m2fwaw2baz.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+
+abc
+--=-=-=
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="test1.C"
+Content-Transfer-Encoding: base64
+
+aGVyZSBpcyBhIHNpbXBsZSB0ZXN0IGZpbGUuCg==
+--=-=-=
+Content-Type: text/plain
+
+def
+--=-=-=--
diff --git a/lib/tests/testdir/cur/multirecip!2,S b/lib/tests/testdir/cur/multirecip!2,S
new file mode 100644 (file)
index 0000000..c997503
--- /dev/null
@@ -0,0 +1,11 @@
+Date: Thu, 15 May 2016 14:57:25 -0200
+From: 
+To: a@example.com,b@example.com,c@example.com
+Cc: d@example.com,e@example.com
+Subject: test with multi to and cc
+Message-id: <3BE9E652343245@emss35m06.us.lmco.com>
+
+Message with multi cc and to.
+
+
+
diff --git a/lib/tests/testdir/cur/signed!2,S b/lib/tests/testdir/cur/signed!2,S
new file mode 100644 (file)
index 0000000..a2e7e21
--- /dev/null
@@ -0,0 +1,36 @@
+Return-path: <>
+Envelope-to: skipio@localhost
+Delivery-date: Fri, 11 May 2012 16:21:57 +0300
+Received: from localhost.roma.net([127.0.0.1] helo=borealis)
+       by borealis with esmtp (Exim 4.77)
+       id 1SSpnB-00038a-55
+       for djcb@localhost; Fri, 11 May 2012 16:21:57 +0300
+Delivered-To: diggler@gmail.com
+From: Skipio <skipio@roma.net>
+To: Hannibal <hanni@carthago.net> 
+Subject: signed
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:20:45 +0300
+Message-ID: <878vgy97ma.fsf@roma.net>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1;
+       protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+
+
+I am signed!
+
+--=-=-=
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+iEYEARECAAYFAk+tEi0ACgkQ6WrHoQF92jxTzACeKd/XxY+P7bpymWL3JBRHaW9p
+DpwAoKw7PDW4z/lNTkWjndVTjoO9jGhs
+=blXz
+-----END PGP SIGNATURE-----
+--=-=-=--
+
diff --git a/lib/tests/testdir/cur/signed-encrypted!2,S b/lib/tests/testdir/cur/signed-encrypted!2,S
new file mode 100644 (file)
index 0000000..a3910e6
--- /dev/null
@@ -0,0 +1,54 @@
+Return-path: <>
+Envelope-to: karjala@localhost
+Delivery-date: Fri, 11 May 2012 16:37:57 +0300
+From: karjala@example.com
+To: lapinkulta@example.com
+Subject: signed + encrypted
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:36:08 +0300
+Message-ID: <874nrm96wn.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+hQQOA1T38TPQrHD6EA/+K4kSpMa7zk+qihUkQnHSq28xYxisNQx6X5DVNjA/Qx16
+uZj/40ae+PoSMTVfklP+B2S/IomuTW6dwVqS7aQ3u4MTzi+YOi11k1lEMD7hR0Wb
+L0i48o3/iCPuCTpnOsaLZvRL06g+oTi0BF2pgz/YdsgsBTGrTb3pkDGSlLIhvh/J
+P8eE3OuzkXS6d8ymJKx2S2wQJrc1AFf1BgJfgc5T0iAvcV+zIMG+PIYcVd04zVpj
+cORFEfvGgfxWkeX+Ks3tu/l5PA1EesnoqFdNFZm+RKBg3RFsOm8tBlJ46xJjfeHg
+zLgifeSLy3tOX7CvWYs9torrx7s7UOI2gV8kzBqz+a7diyCMezceeQ9l0nIRybwW
+C9Egp8Bpfb02iXTOGdE/vRiNItQH14GKmXf4nCSwdtQUm3yzaqY9yL3xBxAlW53e
+YOFfPMESt+E7IlPn0c7llWGrcdrhJbUEoGOIPezES7kdeNPzi8G1lLtvT04/SSZJ
+QxPH5FNzSFaYFAQSdI7TR69P7L7vtLL8ndkjY49HfLFXochQQzsqrzVxzRCruHxA
+zbZSRptNf9SuXEaX9buO1vlFHheGvrCKzEWa6O7JD/DiyrE/zqy4jdlh9abMCouQ
+GWGSbn8jk6SMTQQ2Yv/VOyFqifHZp0UJD59tyIdenpxoYu5M0lwHLNVDlRjLEwUQ
+AIDz1tbLoM7lxs2FOKGr8QqbKIeMfL+NUmbvVIDc4mJrOlRnHh+cZYm4Z49iTl1v
+bYNMYgR5nY7W6rqh0ae7ZOW0h2NzpkAwTzuf1YrSjNavd9KBwOCFtAoZhRwfwFVx
+ju+ByHFNnf7g/R6DekHS0pSiatM0cPDJT05atEZb+13CRHHznonmLHi+VahXjrpg
+cIUA8Lhjdfm6Fsabo7gNZnTTRxNBqUXKK2vJF/XLbNrH5K2BH2dCCmUNtm3yFWiM
+DOzaw3665Y3S6MvZdyKpatbNrVoJdBpRgPxJ1YCSEituFUqHJBStay+aRb5fVkQR
+w3+9hWw+Ob0+2EumKbgfQ7iMwTZBCZP4VOxkoqdHvs9aWm4N7wHtXsyCew3icbJx
+lyUWsDx/FI+HlQRfOqeAMxmp8kKybmHNw8oGiw+uPPUHSD1NFYVm2DtwhYll3Fvs
+YY7r5s3yP1ZnwxMqWI3OsExVUXs8MS4UTAgO+cggO7YidPcANbBDihBFP8mTXtni
+Oo5n5v+/eRoLfHmnsGcaK8EkKsfFHpbqn4gxXGcBuHaTTJ/ZhbW6bi1WWZA9ExaJ
+IeTDtp5Bks1pJvTjCDacvgwl3rEBM6yaeIvB7575Y/GPMTOZhawhfOxV1smMmTKI
+JOWYb3+PuN2cvWetkjFgH8re4sRXq22DKBZHJEWYU8sH0sACAePnIr+pkrOtGeJB
+t1zBqZUnrupH6ptk9n/AjbQ+XSMTEKu55gSjYLAYx1EHApx52QLkdh+ej5xCIVeY
+6wS1Iipkoc6/r6F7CKctupXurNY2AlD4uQIOfD6kQgkqK4PY3hsRHQA+Zqj6oRfr
+kxysFJZvhgt26IeBVapFs10WuYt9iHfpbPUBQUIZCLyPAh08UdVW64Uc2DvUPy+I
+C+3RrmTHQPP/YNKgDQaZ3ySVEDkqjaDPmXr5K0Ibaib2dtPCLcA=
+=pv03
+-----END PGP MESSAGE-----
+--=-=-=--
+
diff --git a/lib/tests/testdir/cur/special!2,Sabc b/lib/tests/testdir/cur/special!2,Sabc
new file mode 100644 (file)
index 0000000..7f1de8e
--- /dev/null
@@ -0,0 +1,10 @@
+Date: Thu, 1 Jun 2012 14:57:25 -0200
+From: "Rocky Balboa" <rocky@example.com>
+To: "Ivan Drago" <ivan@example.com>
+Subject: currying and tail optimization
+Message-id: <3BE9E653ef345@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+
+Test 123. I'm a special message with special flags.
diff --git a/lib/tests/testdir/new/1220863087.12663_21.mindcrime b/lib/tests/testdir/new/1220863087.12663_21.mindcrime
new file mode 100644 (file)
index 0000000..4101716
--- /dev/null
@@ -0,0 +1,111 @@
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+       version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id 6389969CB2
+       for <xxxx@localhost>; Thu,  7 Aug 2008 08:10:07 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:07 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs34769wfh; Wed, 6 Aug 2008
+ 13:38:53 -0700 (PDT)
+Received: by 10.100.6.13 with SMTP id 13mr4103508anf.83.1218055131215; Wed, 06
+ Aug 2008 13:38:51 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id b32si10199298ana.34.2008.08.06.13.38.49; Wed, 06 Aug 2008
+ 13:38:51 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+DomainKey-Status: good (test mode)
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org; domainkeys=pass
+ (test mode) header.From=juanma_bellon@yahoo.es
+Received: from localhost ([127.0.0.1]:55648 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQpmT-0005W9-AQ for
+ xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:38:49 -0400
+Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id
+ 1KQplz-0005U5-Pk for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:19 -0400
+Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id
+ 1KQplw-0005Nw-OG for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:19 -0400
+Received: from [199.232.76.173] (port=45465 helo=monty-python.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQplw-0005NX-I6 for
+ help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:16 -0400
+Received: from n74a.bullet.mail.sp1.yahoo.com ([98.136.45.21]:29868) by
+ monty-python.gnu.org with smtp (Exim 4.60) (envelope-from
+ <juanma_bellon@yahoo.es>) id 1KQplw-0007EF-7Z for help-gnu-emacs@gnu.org;
+ Wed, 06 Aug 2008 16:38:16 -0400
+Received: from [216.252.122.216] by n74.bullet.mail.sp1.yahoo.com with NNFMP;
+ 06 Aug 2008 20:38:14 -0000
+Received: from [68.142.237.89] by t1.bullet.sp1.yahoo.com with NNFMP; 06 Aug
+ 2008 20:38:14 -0000
+Received: from [69.147.75.180] by t5.bullet.re3.yahoo.com with NNFMP; 06 Aug
+ 2008 20:38:14 -0000
+Received: from [127.0.0.1] by omp101.mail.re1.yahoo.com with NNFMP; 06 Aug
+ 2008 20:38:14 -0000
+X-Yahoo-Newman-Id: 778995.62909.bm@omp101.mail.re1.yahoo.com
+Received: (qmail 43643 invoked from network); 6 Aug 2008 20:38:14 -0000
+DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; s=s1024; d=yahoo.es;
+ h=Received:X-YMail-OSG:X-Yahoo-Newman-Property:From:To:Subject:Date:User-Agent:References:In-Reply-To:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-Disposition:Message-Id;
+ b=ThdHlND5CNUsLPGuk+XhCWkdUA9w7lg4hiAgx8F8egsmQteMpwUlV/Y5tfe6K3O2jzHjtsklkzWqm7WY3VAcxxD/QgxLnianK5ZQHoelDAiGaFRqu8Y42XMZso2ccCBFWUQaKo9C+KIfa3e3ci73qehVxTtmr7bxLjurcSYEBPo=
+ ;
+Received: from unknown (HELO 212251170160.customer.cdi.no)
+ (juanma_bellon@212.251.170.160 with plain) by smtp109.plus.mail.re1.yahoo.com
+ with SMTP; 6 Aug 2008 20:38:14 -0000
+X-YMail-OSG: k86L54kVM1kiZbUlYx7gayoBrCLYMFIRDL.KJLBKetNucAbwU4RjeeE1vhjw33hREaUig0CCjG7BTwIfbeZZpRmUcHbxl6gR0z6Sd3lYqA--
+X-Yahoo-Newman-Property: ymail-3
+From: anon@example.com
+To: help-gnu-emacs@gnu.org
+Date: Wed, 6 Aug 2008 22:38:15 +0200
+User-Agent: KMail/1.9.6 (enterprise 0.20070907.709405)
+References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org>
+ <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org>
+ <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+In-Reply-To: <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Message-Id: <200808062238.15634.juanma_bellon@yahoo.es>
+X-detected-kernel: by monty-python.gnu.org: FreeBSD 6.x (1)
+Subject: Re: basic question: going back to dired
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 361
+
+On Thursday 31 July 2008, Xah wrote:
+> what's the logic of =E2=80=9COK=E2=80=9D?
+
+=46or all I know, it comes from "0 Knock-outs" (from USA civil war times,
+IIRC), i.e., all went really well.
+
+But this is really off-topic.
+=2D-=20
+Juanma
+
+"Having a smoking section in a restaurant is like
+ having a peeing section in a swimming pool."
+       -- Edward Burr
+
+
+
+
+
diff --git a/lib/tests/testdir/new/1220863087.12663_23.mindcrime b/lib/tests/testdir/new/1220863087.12663_23.mindcrime
new file mode 100644 (file)
index 0000000..ca46f2b
--- /dev/null
@@ -0,0 +1,105 @@
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+       version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id C3EF069CB3
+       for <xxxx@localhost>; Thu,  7 Aug 2008 08:10:10 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:10 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs35153wfh; Wed, 6 Aug 2008
+ 13:58:17 -0700 (PDT)
+Received: by 10.100.166.10 with SMTP id o10mr4182182ane.0.1218056296101; Wed,
+ 06 Aug 2008 13:58:16 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d34si13875743and.3.2008.08.06.13.58.14; Wed, 06 Aug 2008
+ 13:58:16 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org; dkim=pass (test
+ mode) header.i=@gmail.com
+Received: from localhost ([127.0.0.1]:33418 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQq5G-0001aY-Cr for
+ xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:58:14 -0400
+Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id
+ 1KQq4n-0001Z9-06 for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:45 -0400
+Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id
+ 1KQq4l-0001V8-6c for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:44 -0400
+Received: from [199.232.76.173] (port=46438 helo=monty-python.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQq4k-0001Un-V2 for
+ help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:42 -0400
+Received: from ik-out-1112.google.com ([66.249.90.180]:17562) by
+ monty-python.gnu.org with esmtp (Exim 4.60) (envelope-from
+ <lekktu@gmail.com>) id 1KQq4k-0001fk-OW for help-gnu-emacs@gnu.org; Wed, 06
+ Aug 2008 16:57:42 -0400
+Received: by ik-out-1112.google.com with SMTP id c21so94956ika.2 for
+ <help-gnu-emacs@gnu.org>; Wed, 06 Aug 2008 13:57:41 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma;
+ h=domainkey-signature:received:received:message-id:date:from:to
+ :subject:cc:in-reply-to:mime-version:content-type
+ :content-transfer-encoding:content-disposition:references;
+ bh=TTNY9749hpg1+TXOwdaCr+zbQGhBUt3IvsjLWp+pxp0=;
+ b=BOfudUT/SiW9V4e9+k3dXDzwm+ogdrq4m5OlO+f1H+oE6OAYGIm8dbdqDAOwUewBoS
+ jRpfZo07YamP9rkko79SeFdQnf7UAPFAw9x7DFCm3x6muSlCcJBR7vYs1rgHOSINAn2B
+ vQx2//lKR4fXfKNURNu+B30KrvoEmw6m2C8dI=
+DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma;
+ h=message-id:date:from:to:subject:cc:in-reply-to:mime-version
+ :content-type:content-transfer-encoding:content-disposition :references;
+ b=UMDBulH/LwxDywEH0pfK3DbJ4u2kIZCVDLIM++PqrdcR82HjcS/O3Jhf5OFrf7Fnyj
+ GH76xmc7zkTG/3aQy2WY6DeWCJaFarEItmhxy3h/xS+kUKeDARzNox0OzK6lIv/u9bdy
+ f2LnFlYRJ7Q5vy3lxpxAWB4v0qCwtF9LjWFg4=
+Received: by 10.210.47.7 with SMTP id u7mr3100239ebu.30.1218056261587; Wed, 06
+ Aug 2008 13:57:41 -0700 (PDT)
+Received: by 10.210.71.14 with HTTP; Wed, 6 Aug 2008 13:57:41 -0700 (PDT)
+Message-ID: <f7ccd24b0808061357t453f5962w8b61f9a453b684d0@mail.gmail.com>
+Date: Wed, 6 Aug 2008 22:57:41 +0200
+From: anon@example.com
+To: Juanma <juanma_bellon@yahoo.es>
+In-Reply-To: <200808062238.15634.juanma_bellon@yahoo.es>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org>
+ <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org>
+ <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+ <200808062238.15634.juanma_bellon@yahoo.es>
+X-detected-kernel: by monty-python.gnu.org: Linux 2.6 (newer, 2)
+Cc: help-gnu-emacs@gnu.org
+Subject: Re: basic question: going back to dired
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 309
+
+On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote:
+
+> For all I know, it comes from "0 Knock-outs" (from USA civil war times,
+> IIRC), i.e., all went really well.
+
+See http://en.wikipedia.org/wiki/Okay#Etymology
+
+"0 knock-outs" is among the "Improbable or refuted etymologies".
+
+   Juanma
+
+
diff --git a/lib/tests/testdir/new/1220863087.12663_25.mindcrime b/lib/tests/testdir/new/1220863087.12663_25.mindcrime
new file mode 100644 (file)
index 0000000..588ace1
--- /dev/null
@@ -0,0 +1,98 @@
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW,
+       SPF_PASS autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id D68E769CB5
+       for <xxxx@localhost>; Fri,  8 Aug 2008 20:56:25 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Fri, 08 Aug 2008 20:56:25 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs71287wfh; Fri, 8 Aug 2008
+ 07:40:46 -0700 (PDT)
+Received: by 10.100.122.8 with SMTP id u8mr3824321anc.77.1218206446062; Fri,
+ 08 Aug 2008 07:40:46 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d35si2718351and.38.2008.08.08.07.40.45; Fri, 08 Aug 2008
+ 07:40:46 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:47349 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KRT93-0006Po-A3 for
+ xxxx.klub@gmail.com; Fri, 08 Aug 2008 10:40:45 -0400
+Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!news-out.readnews.com!news-xxxfer.readnews.com!panix!not-for-mail
+From: anon@example.com
+Newsgroups: gnu.emacs.help
+Date: Fri, 08 Aug 2008 10:07:30 -0400
+Organization: PANIX Public Access Internet and UNIX, NYC
+Message-ID: <uwsireh25.fsf@one.dot.net>
+References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org>
+ <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org>
+ <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+ <200808062238.15634.juanma_bellon@yahoo.es>
+ <mailman.15958.1218056266.18990.help-gnu-emacs@gnu.org>
+NNTP-Posting-Host: panix5.panix.com
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+X-Trace: reader1.panix.com 1218204439 22850 166.84.1.5 (8 Aug 2008 14:07:19
+ GMT)
+X-Complaints-To: abuse@panix.com
+NNTP-Posting-Date: Fri, 8 Aug 2008 14:07:19 +0000 (UTC)
+User-Agent: Gnus/5.11 (Gnus v5.11) Emacs/22.2 (windows-nt)
+Cancel-Lock: sha1:Ckkp5oJPIMuAVgEHGnS/9MkZsEs=
+Xref: news.stanford.edu gnu.emacs.help:160963
+To: help-gnu-emacs@gnu.org
+Subject: Re: basic question: going back to dired
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 710
+Lines: 27
+
+I seem to remember from my early school days it was a campaign slogan
+for someone nick-named Kinderhook that went something like
+
+Old Kinderhook is OK
+
+- Chris
+
+"Juanma Barranquero" <lekktu@gmail.com> writes:
+
+> On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote:
+>
+>> For all I know, it comes from "0 Knock-outs" (from USA civil war times,
+>> IIRC), i.e., all went really well.
+>
+> See http://en.wikipedia.org/wiki/Okay#Etymology
+>
+> "0 knock-outs" is among the "Improbable or refuted etymologies".
+>
+>    Juanma
+>
+>
+
+-- 
+     (.   .)
+  =ooO=(_)=Ooo=====================================
+  Chris McMahan | first_initiallastname@one.dot.net
+  =================================================
+
diff --git a/lib/tests/testdir/new/1220863087.12663_9.mindcrime b/lib/tests/testdir/new/1220863087.12663_9.mindcrime
new file mode 100644 (file)
index 0000000..734ee35
--- /dev/null
@@ -0,0 +1,209 @@
+Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-1.2 required=3.0 tests=BAYES_00,HTML_MESSAGE,
+       MIME_QP_LONG_LINE autolearn=no version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id 4E3CF6963B
+       for <xxxx@localhost>; Mon,  4 Aug 2008 21:49:37 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:37 +0300 (EEST)
+Received: by 10.142.51.12 with SMTP id y12cs94317wfy; Mon, 4 Aug 2008 05:48:28
+ -0700 (PDT)
+Received: by 10.150.152.17 with SMTP id z17mr1245909ybd.194.1217854107583;
+ Mon, 04 Aug 2008 05:48:27 -0700 (PDT)
+Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with
+ ESMTP id 9si6334793yws.5.2008.08.04.05.47.57; Mon, 04 Aug 2008 05:48:27 -0700
+ (PDT)
+Received-SPF: pass (google.com: best guess record for domain of
+ sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender)
+ client-ip=67.18.92.124;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record
+ for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as
+ permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org
+Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with
+ ESMTP id 4FBC111C6F; Mon,  4 Aug 2008 08:47:54 -0400 (EDT)
+X-Original-To: sqlite-dev@sqlite.org
+Delivered-To: sqlite-dev@sqlite.org
+Received: from cpsmtpo-eml02.kpnxchange.com (cpsmtpo-eml02.kpnxchange.com
+ [213.75.38.151]) by sqlite.org (Postfix) with ESMTP id AA4F111C10 for
+ <sqlite-dev@sqlite.org>; Mon,  4 Aug 2008 08:47:51 -0400 (EDT)
+Received: from hpsmtp-eml21.kpnxchange.com ([213.75.38.121]) by
+ cpsmtpo-eml02.kpnxchange.com with Microsoft SMTPSVC(6.0.3790.1830);  Mon, 4
+ Aug 2008 14:47:50 +0200
+Received: from cpbrm-eml13.kpnsp.local ([195.121.247.250]) by
+ hpsmtp-eml21.kpnxchange.com with Microsoft SMTPSVC(6.0.3790.1830);  Mon, 4
+ Aug 2008 14:47:50 +0200
+Received: from hpsmtp-eml30.kpnxchange.com ([10.94.53.250]) by
+ cpbrm-eml13.kpnsp.local with Microsoft SMTPSVC(6.0.3790.1830);  Mon, 4 Aug
+ 2008 14:47:50 +0200
+Received: from localhost ([10.94.53.250]) by hpsmtp-eml30.kpnxchange.com with
+ Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 Aug 2008 14:47:49 +0200
+Content-class: urn:content-classes:message
+MIME-Version: 1.0
+X-MimeOLE: Produced By Microsoft Exchange V6.5
+Date: Mon, 4 Aug 2008 14:46:06 +0200
+Message-ID: <F687EC042917A94E8BB4B0902946453AE17D6C@CPEXBE-EML18.kpnsp.local>
+X-MS-Has-Attach: 
+X-MS-TNEF-Correlator: 
+Thread-Topic: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+Thread-Index: Acj2FjkWvteFtLHTTYeVz4ES7E2ggAAGRxeI
+References: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: anon@example.com
+To: <sqlite-dev@sqlite.org>
+X-OriginalArrivalTime: 04 Aug 2008 12:47:49.0650 (UTC)
+ FILETIME=[4D577720:01C8F630]
+Subject: Re: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+X-BeenThere: sqlite-dev@sqlite.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+  <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe>
+List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev>
+List-Post: <mailto:sqlite-dev@sqlite.org>
+List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help>
+List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, 
+ <mailto:sqlite-dev-request@sqlite.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============1911358387=="
+Mime-version: 1.0
+Sender: sqlite-dev-bounces@sqlite.org
+Errors-To: sqlite-dev-bounces@sqlite.org
+Content-Length: 5318
+
+This is a multi-part message in MIME format.
+
+--===============1911358387==
+Content-class: urn:content-classes:message
+Content-Type: multipart/alternative;
+       boundary="----_=_NextPart_001_01C8F630.0FC2EC1E"
+
+This is a multi-part message in MIME format.
+
+------_=_NextPart_001_01C8F630.0FC2EC1E
+Content-Type: text/plain;
+       charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+Actually, almost every C compiler will already do what you suggest: if =
+the range of case labels is compact, the switch will be compiled using a =
+jump table. Only if the range is limited and/or sparse other techniques =
+will be used, such as linear search and binary search.
+=20
+I'm pretty sure, if you perform the tests suggested by Mihai, that you =
+will find zero performance difference, neither better, nor worse.
+=20
+Paul
+=20
+________________________________
+
+From: anon@example.com
+Sent: Mon 8/4/2008 11:40 AM
+To: sqlite-dev@sqlite.org
+Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+
+
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the=20
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html =
+<http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html>=20
+
+With a properly defined "instructions" array, instead of the switch=20
+statement you can use something like:
+goto * instructions[pOp->opcode];
+---
+Marco Bambini
+http://www.sqlabs.net <http://www.sqlabs.net/>=20
+http://www.sqlabs.net/blog/ <http://www.sqlabs.net/blog/>=20
+http://www.sqlabs.net/realsqlserver/ =
+<http://www.sqlabs.net/realsqlserver/>=20
+
+
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev =
+<http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>=20
+
+
+
+------_=_NextPart_001_01C8F630.0FC2EC1E
+Content-Type: text/html;
+       charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<HTML dir=3Dltr><HEAD><TITLE>[sqlite-dev] VM optimization inside =
+sqlite3VdbeExec</TITLE>=0A=
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dunicode">=0A=
+<META content=3D"MSHTML 6.00.2715.400" name=3DGENERATOR></HEAD>=0A=
+<BODY>=0A=
+<DIV id=3DidOWAReplyText54900 dir=3Dltr>=0A=
+<DIV dir=3Dltr><FONT face=3DArial color=3D#000000 size=3D2>Actually, =
+almost every C compiler will already do what you suggest: if the range =
+of case labels is compact, the switch will be compiled using a jump =
+table. Only if the range is limited and/or sparse other techniques will =
+be used, such as linear search and binary search.</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2>I'm pretty sure, if you =
+perform the tests suggested by Mihai, that you will find zero =
+performance difference, neither better, nor worse.</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2>Paul</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2>=0A=
+<HR tabIndex=3D-1>=0A=
+</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial><FONT size=3D2><B>From:</B> =
+sqlite-dev-bounces@sqlite.org on behalf of Marco Bambini<BR><B>Sent:</B> =
+Mon 8/4/2008 11:40 AM<BR><B>To:</B> =
+sqlite-dev@sqlite.org<BR><B>Subject:</B> [sqlite-dev] VM optimization =
+inside sqlite3VdbeExec<BR><BR></FONT></FONT></DIV></DIV>=0A=
+<DIV>=0A=
+<P><FONT face=3DArial size=3D2>Inside sqlite3VdbeExec there is a very =
+big switch statement.<BR>In order to increase performance with few =
+modifications to the&nbsp;<BR>original code, why not use this technique =
+?<BR></FONT><A =
+href=3D"http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html">=
+<FONT face=3DArial =
+size=3D2>http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html<=
+/FONT></A><BR><BR><FONT face=3DArial size=3D2>With a properly defined =
+"instructions" array, instead of the switch&nbsp;<BR>statement you can =
+use something like:<BR>goto * =
+instructions[pOp-&gt;opcode];<BR>---<BR>Marco Bambini<BR></FONT><A =
+href=3D"http://www.sqlabs.net/"><FONT face=3DArial =
+size=3D2>http://www.sqlabs.net</FONT></A><BR><A =
+href=3D"http://www.sqlabs.net/blog/"><FONT face=3DArial =
+size=3D2>http://www.sqlabs.net/blog/</FONT></A><BR><A =
+href=3D"http://www.sqlabs.net/realsqlserver/"><FONT face=3DArial =
+size=3D2>http://www.sqlabs.net/realsqlserver/</FONT></A><BR><BR><BR><BR><=
+FONT face=3DArial =
+size=3D2>_______________________________________________<BR>sqlite-dev =
+mailing list<BR>sqlite-dev@sqlite.org<BR></FONT><A =
+href=3D"http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev"><FONT=
+ face=3DArial =
+size=3D2>http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</FONT=
+></A><BR></P></DIV></BODY></HTML>
+------_=_NextPart_001_01C8F630.0FC2EC1E--
+
+
+--===============1911358387==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+
+--===============1911358387==--
+
diff --git a/lib/tests/testdir/tmp/1220863087.12663.ignore b/lib/tests/testdir/tmp/1220863087.12663.ignore
new file mode 100644 (file)
index 0000000..588ace1
--- /dev/null
@@ -0,0 +1,98 @@
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW,
+       SPF_PASS autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id D68E769CB5
+       for <xxxx@localhost>; Fri,  8 Aug 2008 20:56:25 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Fri, 08 Aug 2008 20:56:25 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs71287wfh; Fri, 8 Aug 2008
+ 07:40:46 -0700 (PDT)
+Received: by 10.100.122.8 with SMTP id u8mr3824321anc.77.1218206446062; Fri,
+ 08 Aug 2008 07:40:46 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d35si2718351and.38.2008.08.08.07.40.45; Fri, 08 Aug 2008
+ 07:40:46 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:47349 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KRT93-0006Po-A3 for
+ xxxx.klub@gmail.com; Fri, 08 Aug 2008 10:40:45 -0400
+Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!news-out.readnews.com!news-xxxfer.readnews.com!panix!not-for-mail
+From: anon@example.com
+Newsgroups: gnu.emacs.help
+Date: Fri, 08 Aug 2008 10:07:30 -0400
+Organization: PANIX Public Access Internet and UNIX, NYC
+Message-ID: <uwsireh25.fsf@one.dot.net>
+References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org>
+ <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org>
+ <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+ <200808062238.15634.juanma_bellon@yahoo.es>
+ <mailman.15958.1218056266.18990.help-gnu-emacs@gnu.org>
+NNTP-Posting-Host: panix5.panix.com
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+X-Trace: reader1.panix.com 1218204439 22850 166.84.1.5 (8 Aug 2008 14:07:19
+ GMT)
+X-Complaints-To: abuse@panix.com
+NNTP-Posting-Date: Fri, 8 Aug 2008 14:07:19 +0000 (UTC)
+User-Agent: Gnus/5.11 (Gnus v5.11) Emacs/22.2 (windows-nt)
+Cancel-Lock: sha1:Ckkp5oJPIMuAVgEHGnS/9MkZsEs=
+Xref: news.stanford.edu gnu.emacs.help:160963
+To: help-gnu-emacs@gnu.org
+Subject: Re: basic question: going back to dired
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 710
+Lines: 27
+
+I seem to remember from my early school days it was a campaign slogan
+for someone nick-named Kinderhook that went something like
+
+Old Kinderhook is OK
+
+- Chris
+
+"Juanma Barranquero" <lekktu@gmail.com> writes:
+
+> On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote:
+>
+>> For all I know, it comes from "0 Knock-outs" (from USA civil war times,
+>> IIRC), i.e., all went really well.
+>
+> See http://en.wikipedia.org/wiki/Okay#Etymology
+>
+> "0 knock-outs" is among the "Improbable or refuted etymologies".
+>
+>    Juanma
+>
+>
+
+-- 
+     (.   .)
+  =ooO=(_)=Ooo=====================================
+  Chris McMahan | first_initiallastname@one.dot.net
+  =================================================
+
diff --git a/lib/tests/testdir2/Foo/cur/arto.eml b/lib/tests/testdir2/Foo/cur/arto.eml
new file mode 100644 (file)
index 0000000..ffa0526
--- /dev/null
@@ -0,0 +1,448 @@
+Return-Path: <>
+X-Original-To: f00f@localhost
+Delivered-To: f00f@localhost
+Received: from puppet (puppet [127.0.0.1])
+       by f00fmachines.nl (Postfix) with ESMTP id A534D39C7F1
+       for <f00f@localhost>; Mon, 23 May 2011 20:30:05 +0300 (EEST)
+Delivered-To: diggler@gmail.com
+Received: from ew-in-f109.1e100.net [174.15.27.101]
+       by puppet with POP3 (fetchmail-6.3.18)
+       for <f00f@localhost> (single-drop); Mon, 23 May 2011 20:30:05 +0300 (EEST)
+Received: by 10.142.147.13 with SMTP id u13cs87252wfd;
+        Mon, 23 May 2011 01:54:10 -0700 (PDT)
+Received: by 10.204.7.74 with SMTP id c10mr1984197bkc.104.1306140849326;
+        Mon, 23 May 2011 01:54:09 -0700 (PDT)
+Received: from MTX4.mbn1.net (mtx4.mbn1.net [213.188.129.252])
+        by mx.google.com with ESMTP id e6si18117551bkw.39.2011.05.23.01.54.07;
+        Mon, 23 May 2011 01:54:08 -0700 (PDT)
+Received-SPF: pass (google.com: best guess record for domain of MTX4.mbn1.net designates 213.188.129.252 as permitted sender) client-ip=213.188.129.252;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of MTX4.mbn1.net designates 213.188.129.252 as permitted sender) smtp.mail=
+Resent-From: <fwdgrp_11163824_f00f@f00fmachines.nl>
+X-Default-Received-SPF: pass (skip=forwardok (res=PASS)) x-ip-name=192.168.10.123;
+From: ArtOlive <artolive@mailinglijst.nl>
+To: "f00f@f00fmachines.nl" <f00f@f00fmachines.nl>
+Reply-To: <artolive@mailinglijst.nl>
+Date: Mon, 23 May 2011 10:53:45 +0200
+Subject: NIEUWSBRIEF ART OLIVE | juni exposite in galerie ArtOlive
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+       boundary="_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d"
+X-Mailer: aspNetEmail ver 3.5.2.10
+X-Sender: 87.93.13.24
+X-RemoteIP: 87.93.13.24
+Originating-IP: 87.93.13.24
+X-MAILINGLIJST-ID: <10374608.109906.11909.2011523105345.MSGID@mailinglijst.nl>
+Message-ID: <10374608.109906.11909.2011523105345.MSGID@mailinglijst.nl>
+X-Authenticated-User: guest@mailinglijst.eu 
+X-STA-Metric: 0 (engine=030)
+X-STA-NotSpam: geinformeerd vormen spec:usig:3.8.2 twee samen
+X-STA-Spam: 2011 &bull &bull; e-mailing subject:juni
+X-BTI-AntiSpam: score:0,sta:0/030,dnsbl:passed,sw:passed,bsn:10/passed,spf:off,bsctr:passed/1,dk:off,pbmf:none,ipr:0/3,trusted:no,ts:no,bs:no,ubl:passed
+X-Auto-Response-Suppress: DR, RN, NRN, OOF, AutoReply
+Resent-Message-Id: <19740414233016.EB6835A132F5FCF4@MTX4.mbn1.net>
+Resent-Date: Mon, 23 May 2011 10:54:07 +0200 (CEST)
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d
+Content-Type: text/plain; charset="iso-8859-15"
+Content-Transfer-Encoding: quoted-printable
+
+ART-O-NEWS; juni 2011 Westergasfabriekterrein   Polonceaukade 17 10=
+14 DA Amsterdam   tel: 020-6758504  info@artolive.nl  www.artolive.nlJuni=
+ expositie bij ArtOlive: Peter van den Akker en Marinel Vieleers
+
+
+Zondag 5 juni
+ Elke maand vindt er in de galerie van ArtOlive een expositie plaats. We =
+lichten enkele kunstenaars uit (die je misschien al kent van onze website=
+), waarbij we een spannende mix van materiaal en techniek presenteren. Ti=
+jdens de expositie staan we klaar om elke vraag over ons kunstaanbod te b=
+eantwoorden.=20
+
+De exposities zijn te bezoeken van maandag t/m vrijdag, tussen 10:00 en 1=
+7:00 uur. De opening is altijd op de eerste zondag van de maand. Dit valt=
+ samen met de Sunday Market die elke maand op het Cultuurpark Westergasfa=
+briek georganiseerd wordt. De Sunday Market is gratis te bezoeken en staa=
+t vol met kunst, design, mode en heerlijke hapjes, en er hangt altijd een=
+ vrolijke sfeer. Een ideaal moment dus om in te haken en deze maand twee =
+kunstenaars te presenteren: Peter van den Akker en Marinel Vieleers.
+
+We verwelkomen je graag op zondag 5 juni 2011, van 12:00 t/m 17:00 uur op=
+ de Polonceaukade 17 van het Cultuurpark Westergasfabriek in Amsterdam!=20=
+
+
+
+  bekijk meer werk op www.artolive.nl...    Peter van den Akker
+
+
+"In mijn beelden en schilderijen staat het mensbeeld centraal; niet als i=
+ndividu maar als universele gestalte, waarbij ik op transparante wijze ti=
+jdsbeelden en gelaagdheid in het menselijke handelen naar voren breng. Ve=
+rhoudingen tussen mensen, verschuivingen in wereldculturen en verandering=
+en in techniek, architectuur, natuur en mensbeeld vormen mijn inspiratieb=
+ronnen. Het zijn allemaal beelden en sferen die naast en met elkaar besta=
+an. Mijn werkwijze omvat vele technieken in verschillende materialen: sch=
+ilderijen, gemengde technieken op papier/collages, zeefdrukken, beelden i=
+n cortenstaal, keramische objecten."
+
+Peter van den Akker exposeert regelmatig in binnen- en buitenland bij gal=
+erie=EBn en musea en is in verschillende kunstinstellingen en bedrijfscol=
+lecties opgenomen.
+
+
+  lees meer over Peter...    Marinel Vieleers
+
+
+Marinel Vieleers probeert het menselijke in de bouwwerken - en ook vaak i=
+ets van het karakter van de bouwer of bewoner - te laten zien. Het zijn m=
+aar subtiele details die dat alles weergeven.
+
+De 'tand des tijds' of invloed van mensen op de gebouwen spelen vaak mee =
+in het werk. Koper, cement, lood en andere materialen worden in haar nieu=
+we werk gebruikt. Op deze manier kan ze gemakkelijker improviseren en nog=
+ directer op haar gevoel afgaan.
+
+Marinel is gefascineerd door de schoonheid van het imperfecte. De gelaagd=
+heid van ouderdom, het verval. De imperfectie die ontstaat door toevallig=
+e omstandigheden maakt een huis, een muur, een schutting, hout of steen b=
+oeiend. Het is doorleefd en het toont een stukje van zijn geschiedenis.
+
+
+  lees meer over Marinel...   =20
+ZONDAG 5 MEI - Juni expositie in de galerie van ArtOlive met Marinel Viel=
+eers en Peter van den Akker
+
+Opening op zondag 5 mei, tijdens de Sunday Market op het Cultuurpark West=
+ergasfabriek in Amsterdam. Je bent van harte uitgenodigd om tussen 12:00 =
+en 17:00 uur langs te komen!
+
+Daarna is de expositie te zien op werkdagen (ma - vrij) tussen 10:00 en 1=
+7:00. De expositie duurt tot 24 juni 2011.
+ wil je niet langer door artolive ge=EFnformeerd worden? Klik dan hier om=
+ je af te melden.=20
+ kreeg je dit mailtje doorgestuurd en wil je voortaan zelf ook graag de n=
+ieuwsbrief ontvangen?=20
+ klik dan hier om je aan te melden.=20
+
+Deze e-mailing is verzorgd met MailingLijst
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d
+Content-Type: text/html; charset="iso-8859-15"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://ww=
+w.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns=3D"http://www.w3.org/1999/xhtml">
+    <head>
+        <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3D=
+ISO-8859-15">
+        <title>Artolive</title>
+    </head>
+    <body style=3D"line-height: 13px; font-family: Verdana; color: rgb(11=
+9, 119, 119); font-size: 10px;" vlink=3D"#666666" alink=3D"#666666" link=3D=
+"#666666">
+        <style type=3D"text/css">
+            A {
+            COLOR: #666666; TEXT-DECORATION: none
+            }
+            TD {
+            FONT-FAMILY: Verdana; COLOR: #777777; FONT-SIZE: 10px; VERTIC=
+AL-ALIGN: top
+            }
+        </style>
+        <table style=3D"width: 631px;" width=3D"631" align=3D"center" cel=
+lpadding=3D"0" cellspacing=3D"10">
+            <tbody>
+                <tr>
+                    <td><a href=3D"http://www.mailinglijst.eu/redirect.as=
+px?l=3D154041&a=3D10374608&t=3DH" target=3D"_blank"><img style=3D"height:=
+ 42px; width: 188px;" alt=3D"artolive" src=3D"http://mailinglijst.eu/klan=
+ten/11909/Sjabloon/logo.jpg" width=3D"188" border=3D"0" height=3D"42"></a=
+> </td>
+                </tr>
+                <tr>
+                    <td style=3D"text-align: right; padding-right: 5px; c=
+olor: rgb(0, 0, 0); font-size: 10px;">ART-O-NEWS&nbsp;&bull; juni 2011 </=
+td>
+                </tr>
+                <tr>
+                    <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172); padding: 5px 10px 5px 15px; color: rgb(=
+167, 169, 172); font-size: 9px;">Westergasfabriekterrein &nbsp; Polonceau=
+kade 17 1014 DA Amsterdam &nbsp; tel: 020-6758504&nbsp; <a style=3D"color=
+: rgb(167, 169, 172);" href=3D"mailto:info@artolive.nl">info@artolive.nl<=
+/a>&nbsp; <a style=3D"color: rgb(167, 169, 172);" href=3D"http://www.mail=
+inglijst.eu/redirect.aspx?l=3D154041&a=3D10374608&t=3DH" target=3D"_blank=
+">www.artolive.nl</a> </td>
+                </tr>
+                <tr>
+                    <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(81, 81, 81);">
+                    <table width=3D"100%" cellpadding=3D"10" cellspacing=3D=
+"0">
+                        <tbody style=3D"color: rgb(119, 119, 119); font-s=
+ize: 10px; vertical-align: top;">
+                            <tr>
+                                <td style=3D"vertical-align: top;"><img s=
+tyle=3D"height: 338px; width: 252px;" src=3D"http://mailinglijst.eu/klant=
+en/11909/juni2011/IMG_1698_%28Medium%29.JPG" width=3D"252" height=3D"338"=
+> </td>
+                                <td style=3D"vertical-align: top;"><span =
+style=3D"line-height: normal; font-size: 24px; color: rgb(0, 0, 0);">Juni=
+ expositie bij ArtOlive: Peter van den Akker en Marinel Vieleers</span><b=
+r>
+                                <p><strong>Zondag 5 juni</strong><br>
+                                Elke maand vindt er in de galerie van Art=
+Olive een expositie plaats. We lichten enkele kunstenaars uit (die je mis=
+schien al kent van onze website), waarbij we een spannende mix van materi=
+aal en techniek presenteren. Tijdens de expositie staan we klaar om elke =
+vraag over ons kunstaanbod te beantwoorden. </p>
+                                <p>De exposities zijn te bezoeken van maa=
+ndag t/m vrijdag, tussen 10:00 en 17:00 uur. De opening is altijd op de e=
+erste zondag van de maand. Dit valt samen met de Sunday Market die elke m=
+aand op het Cultuurpark Westergasfabriek georganiseerd wordt. De Sunday M=
+arket is gratis te bezoeken en staat vol met kunst, design, mode en heerl=
+ijke hapjes, en er hangt altijd een vrolijke sfeer. Een ideaal moment dus=
+ om in te haken en deze maand twee kunstenaars te presenteren: Peter van =
+den Akker en Marinel Vieleers.</p>
+                                <p>We verwelkomen je graag op zondag 5 ju=
+ni 2011, van 12:00 t/m 17:00 uur op de Polonceaukade 17 van het Cultuurpa=
+rk Westergasfabriek in Amsterdam! </p>
+                                <br>
+                                <p align=3D"right"><img style=3D"height: =
+4px; width: 4px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/11909/Sj=
+abloon/green_point.jpg" width=3D"4" height=3D"4">&nbsp; <strong></strong>=
+<a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154041&a=3D10374608&t=3DH"></a><a target=3D"_blank" href=3D"http://www.ma=
+ilinglijst.eu/redirect.aspx?l=3D154041&a=3D10374608&t=3DH"><strong>bekijk=
+ meer werk op www.artolive.nl...</strong></a> &nbsp;&nbsp; </p>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+                    </td>
+                </tr>
+                <tr>
+                    <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172);">
+                    <table width=3D"100%" cellpadding=3D"0" cellspacing=3D=
+"0">
+                        <tbody>
+                            <tr>
+                                <td><img style=3D"display: block; height:=
+ 24px; width: 629px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/1190=
+9/Sjabloon/header_uitgelicht_fade.jpg" width=3D"629" height=3D"24"> </td>=
+
+                            </tr>
+                            <tr>
+                                <td>
+                                <table width=3D"100%" cellpadding=3D"10" =
+cellspacing=3D"0">
+                                    <tbody style=3D"color: rgb(119, 119, =
+119); font-size: 10px; vertical-align: top;">
+                                        <tr>
+                                            <td style=3D"width: 150px;"><=
+a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154043&a=3D10374608&t=3DH"><img style=3D"height: 214px; width: 156px; bor=
+der-width: 0px; border-style: solid;" src=3D"http://mailinglijst.eu/klant=
+en/11909/juni2011/akker-adam-eva.jpg" width=3D"156" height=3D"214"></a> <=
+/td>
+                                            <td><span style=3D"color: rgb=
+(0, 0, 0);">Peter van den Akker</span><br>
+                                            <p>"In mijn beelden en schild=
+erijen staat het mensbeeld centraal; niet als individu maar als universel=
+e gestalte, waarbij ik op transparante wijze tijdsbeelden en gelaagdheid =
+in het menselijke handelen naar voren breng. Verhoudingen tussen mensen, =
+verschuivingen in wereldculturen en veranderingen in techniek, architectu=
+ur, natuur en mensbeeld vormen mijn inspiratiebronnen. Het zijn allemaal =
+beelden en sferen die naast en met elkaar bestaan. Mijn werkwijze omvat v=
+ele technieken in verschillende materialen: schilderijen, gemengde techni=
+eken op papier/collages, zeefdrukken, beelden in cortenstaal, keramische =
+objecten.&rdquo;</p>
+                                            <p>Peter van den Akker expose=
+ert regelmatig in binnen- en buitenland bij galerie&euml;n en musea en is=
+ in verschillende kunstinstellingen en bedrijfscollecties opgenomen.</p>
+                                            <br>
+                                            <p align=3D"right"><img style=
+=3D"height: 4px; width: 4px;" alt=3D"" src=3D"http://mailinglijst.eu/klan=
+ten/11909/Sjabloon/green_point.jpg" width=3D"4" height=3D"4">&nbsp;&nbsp;=
+<a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154044&a=3D10374608&t=3DH"></a><a target=3D"_blank" href=3D"http://www.ma=
+ilinglijst.eu/redirect.aspx?l=3D154043&a=3D10374608&t=3DH"></a><a target=3D=
+"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154044&a=3D=
+10374608&t=3DH"><strong>lees meer over Peter...</strong></a><strong></str=
+ong>&nbsp;&nbsp;&nbsp; </p>
+                                            </td>
+                                        </tr>
+                                        <tr>
+                                            <td align=3D"center"><a targe=
+t=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154045&=
+a=3D10374608&t=3DH"><img style=3D"border-width: 0px; border-style: solid;=
+ height: 279px; width: 44px;" src=3D"http://mailinglijst.eu/klanten/11909=
+/juni2011/vieleer-thesky032_bijgesneden.jpg" width=3D"44" height=3D"279">=
+</a> </td>
+                                            <td><span style=3D"color: rgb=
+(0, 0, 0);">Marinel Vieleers</span><br>
+                                            <p>Marinel Vieleers probeert =
+het menselijke in de bouwwerken - en ook vaak iets van het karakter van d=
+e bouwer of bewoner - te laten zien. Het zijn maar subtiele details die d=
+at alles weergeven.</p>
+                                            <p>De &lsquo;tand des tijds&r=
+squo; of invloed van mensen op de gebouwen spelen vaak mee in het werk. K=
+oper, cement, lood en andere materialen worden in haar nieuwe werk gebrui=
+kt. Op deze manier kan ze gemakkelijker improviseren en nog directer op h=
+aar gevoel afgaan.</p>
+                                            <p>Marinel is gefascineerd do=
+or de schoonheid van het imperfecte. De gelaagdheid van ouderdom, het ver=
+val. De imperfectie die ontstaat door toevallige omstandigheden maakt een=
+ huis, een muur, een schutting, hout of steen boeiend. Het is doorleefd e=
+n het toont een stukje van zijn geschiedenis.</p>
+                                            <br>
+                                            <p align=3D"right"><img style=
+=3D"height: 4px; width: 4px;" alt=3D"" src=3D"http://mailinglijst.eu/klan=
+ten/11909/Sjabloon/green_point.jpg" width=3D"4" height=3D"4">&nbsp;&nbsp;=
+<a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154045&a=3D10374608&t=3DH"></a><a target=3D"_blank" href=3D"http://www.ma=
+ilinglijst.eu/redirect.aspx?l=3D154046&a=3D10374608&t=3DH"></a><a target=3D=
+"_blank" href=3D"http://www.artolive.nl/work/165738"><strong>lees meer ov=
+er Marinel...</strong></a>&nbsp;&nbsp;&nbsp; </p>
+                                            </td>
+                                        </tr>
+                                    </tbody>
+                                </table>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+                    </td>
+                </tr>
+                <tr>
+                    <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172);">
+                    <table width=3D"100%" cellpadding=3D"0" cellspacing=3D=
+"0">
+                        <tbody>
+                            <tr>
+                                <td><img style=3D"display: block; height:=
+ 24px; width: 629px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/1190=
+9/Sjabloon/header_selection_fade.jpg" width=3D"629" height=3D"24"> </td>
+                            </tr>
+                            <tr>
+                                <td style=3D"padding: 5px 5px 2px;">
+                                <table width=3D"100%" cellpadding=3D"5" c=
+ellspacing=3D"0">
+                                    <tbody>
+                                        <tr>
+                                            <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154048&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17372=
+0.jpg" width=3D"92" height=3D"92"></a> </td>
+                                            <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154049&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17386=
+9.jpg" width=3D"92" height=3D"92"></a> </td>
+                                            <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154050&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17398=
+0.jpg" width=3D"92" height=3D"92"></a> </td>
+                                            <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154051&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17390=
+5.jpg" width=3D"92" height=3D"92"></a> </td>
+                                            <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154052&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17390=
+4.jpg" width=3D"92" height=3D"92"></a> </td>
+                                            <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154053&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17398=
+4.jpg" width=3D"92" height=3D"92"></a> </td>
+                                        </tr>
+                                    </tbody>
+                                </table>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+                    </td>
+                </tr>
+                <tr>
+                    <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172);">
+                    <table width=3D"100%" cellpadding=3D"0" cellspacing=3D=
+"0">
+                        <tbody>
+                            <tr>
+                                <td><img style=3D"display: block; height:=
+ 24px; width: 629px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/1190=
+9/Sjabloon/header_agenda_fade.jpg" width=3D"629" height=3D"24"> </td>
+                            </tr>
+                            <tr>
+                                <td>
+                                <table width=3D"100%" cellpadding=3D"10" =
+cellspacing=3D"0">
+                                    <tbody style=3D"color: rgb(119, 119, =
+119); font-size: 10px; vertical-align: top;" valign=3D"top">
+                                        <tr>
+                                            <td valign=3D"top"><br>
+                                            </td>
+                                            <td><span style=3D"color: rgb=
+(0, 0, 0);">ZONDAG 5 MEI - Juni expositie in de galerie van ArtOlive met =
+Marinel Vieleers en Peter van den Akker</span><br>
+                                            </td>
+                                        </tr>
+                                        <tr>
+                                            <td valign=3D"top"><br>
+                                            </td>
+                                            <td>Opening op zondag 5 mei, =
+tijdens de Sunday Market op het Cultuurpark Westergasfabriek in Amsterdam=
+. Je bent van harte uitgenodigd om tussen 12:00 en 17:00 uur langs te kom=
+en!<br>
+                                            </td>
+                                        </tr>
+                                        <tr>
+                                            <td valign=3D"top"><br>
+                                            </td>
+                                            <td><span style=3D"color: rgb=
+(0, 0, 0);"></span>Daarna is de expositie te zien op werkdagen (ma - vrij=
+) tussen 10:00 en 17:00. De expositie duurt tot 24 juni 2011.<br>
+                                            </td>
+                                        </tr>
+                                    </tbody>
+                                </table>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+                    </td>
+                </tr>
+                <tr>
+                    <td style=3D"padding: 30px 15px 15px; text-transform:=
+ uppercase; color: rgb(119, 119, 119); font-size: 8px;"><img style=3D"hei=
+ght: 58px; width: 59px;" alt=3D"Kunst Koop" src=3D"http://mailinglijst.eu=
+/klanten/11909/Sjabloon/kunstkoop.jpg" width=3D"59" align=3D"right" heigh=
+t=3D"58"> wil je niet langer door artolive ge&iuml;nformeerd worden? Klik=
+ dan <a href=3D'http://www.mailinglijst.eu/nieuwsbrief/edit/?e=3Df00f@djc=
+bmachines.nl&c=3D9856&l=3D100549'>hier</a>&nbsp;om je af te melden. <br>
+                    kreeg je dit mailtje doorgestuurd en wil je voortaan =
+zelf ook graag de nieuwsbrief ontvangen? <br>
+                    klik dan&nbsp;<a href=3D"http://www.mailinglijst.eu/r=
+edirect.aspx?l=3D154054&a=3D10374608&t=3DH" target=3D"_blank">hier</a> om=
+ je aan te melden. </td>
+                </tr>
+            </tbody>
+        </table>
+    <!-- MailingLijst_code --><img src=3D"http://www.mailinglijst.eu/imag=
+es/10374608.109906.aspx" border=3D0><!-- einde MailingLijst_code --><p><C=
+ENTER><SPAN STYLE=3D"COLOR:#d3d3d3;FONT-FAMILY:verdana;FONT-SIZE: 10px"><=
+HR SIZE=3D1 STYLE=3D"COLOR:#d3d3d3" SIZE=3D1>Deze e-mailing is verzorgd m=
+et <a href=3D"http://www.mailinglijst.com" target=3D_blank class=3D"ml_li=
+nk">MailingLijst</a></SPAN></CENTER></p></BODY>
+</html>
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d--
diff --git a/lib/tests/testdir2/Foo/cur/fraiche.eml b/lib/tests/testdir2/Foo/cur/fraiche.eml
new file mode 100644 (file)
index 0000000..c0bf442
--- /dev/null
@@ -0,0 +1,10 @@
+From: Sender <test@example.com>
+To: Recip <recip@example.com>
+Subject: search accents
+Date: 2012-12-08 00:48
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+line 1: Глокая куздра штеко будланула бокра и курдячит бокрёнка
+line 2: crème fraîche
diff --git a/lib/tests/testdir2/Foo/cur/mail5 b/lib/tests/testdir2/Foo/cur/mail5
new file mode 100644 (file)
index 0000000..b72195d
--- /dev/null
@@ -0,0 +1,625 @@
+From: Sitting Bull <sb@example.com>
+To: George Custer <gac@example.com>
+Subject: pics for you
+Mail-Reply-To: djcb@djcbsoftware.nl
+User-Agent: Hunkpapa/2.15.9 (Almost Unreal)
+Message-Id: CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz=bfogtmbGGs5A@mail.gmail.com
+Fcc: .sent
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: multipart/mixed;
+ boundary="Multipart_Sun_Oct_17_10:37:40_2010-1"
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: text/plain; charset=US-ASCII
+
+Dude! Here are some pics!
+
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: image/jpeg
+Content-Disposition: inline; filename="sittingbull.jpg"
+Content-Transfer-Encoding: base64
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4QvoRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB
+AAAASAAAABsBCQABAAAASAAAACgBCQABAAAAAgAAADEBAgAOAAAAbgAAADIBAgAUAAAAfAAAABMC
+CQABAAAAAQAAAGmHBAABAAAAkAAAAN4AAABndGh1bWIgMi4xMS4zADIwMTA6MTA6MTcgMTA6MzM6
+MzcABgAAkAcABAAAADAyMjEBkQcABAAAAAECAwAAoAcABAAAADAxMDABoAkAAQAAAAEAAAACoAkA
+AQAAAMgAAAADoAkAAQAAAGsBAAAAAAAABgADAQMAAQAAAAYAAAAaAQkAAQAAAEgAAAAbAQkAAQAA
+AEgAAAAoAQkAAQAAAAIAAAABAgQAAQAAACwBAAACAgQAAQAAALMKAAAAAAAA/9j/4AAQSkZJRgAB
+AQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwc
+KDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCACAAEcDASIAAhEBAxEB/8QAHwAA
+AQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIh
+MUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpT
+VFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5
+usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAA
+AAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEI
+FEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVm
+Z2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK
+0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDq77xdrX/CQ6laRXjRxQTF
+ECovA/EUg8Sa6W/5CUuP9xP8K5yWQnxjrw9Lwj9BWjkgZHFAG6mu6yV51OXP+4n/AMTUq61rBB/4
+mU2f9xP/AImsJJTuAJFW0YDnfmgCTUPFGqWFq882p3G1eyqmT/47VfRfGGpawkgGp3CyIeg2cj1+
+7XK+O7zybCGMNjzHyR6gD/69ZvgG8zqU67vvRZH4EUAesJe6m/XVLv8ANf8A4mpf7Qvl/wCX+6b6
+uP8ACs+ObKdaeh3Hg9aANTw/4gurjxTLpU7tIv2cTKzHpgkH+n5UVheHGI+KWzJwdNP/AKFRQBzD
+7f8AhMfEDEHH24j/AMdWrs0oCkDrVKJs+NfEsZ+79u/9kWrd5GqKTmgCstwwkyT0p5uzu61mOzbj
+zSFn3DmgDB8ePLPe2MEQZyykhRzk9/5Va8D6Vd2Mz3d3CYxJHiPd16+la0hhMybkUvxhj1HWr5uM
+uB0wMYoA3YJARjvV+DBPasC2lYsOuK3LVunWgCLQRj4sIPXTGP8A4/RSaF/yV2P30tv/AEOigDmY
+QD408UE9ftw/9AFXpv3iFT9Kgs4t3jXxV6C+H/oAq5cxkMcCgDFltXVyMVVv7iGwtzNcNsQfiT+F
+a8jbAxdgFAzmuTZZfEV81vG+xTyX/uIPT3P9aAIBr9vdNHcQI/lxk5DDBrfsLuK+jE0MqupPOOx9
+KzNY8L6fbaaYrdGXb3BOcnuT+FcpodzN4c8RRRyylrW4IDE9MdM/UUAes2wbOAK27PhRms6CJlwc
+VrWowRkUAV9CP/F3YffSm/8AQ6Kfo+P+FuWp9dLf/wBDooAxrH/kd/Ff/X8P/Ra1evUOcgVW01Qf
+G/izIz/py/8Aota2LqPK4xQBxniWc2mi3MxBGFA/Mgf1rmtEF/Z6HNqMNuzvPnY+7G1V6Hoe+T0r
+qfGmnT3Xhm8WNWJVQ+B/skH+lUPBt3d3PhuzXyBM6xBY0YfKDnALewxmgDE1BfEDaPaXNzMRJPIQ
++TjCgDHb69u9ZGt2Us2lrdNDtMLAgq27Kng84Fd74qnaMwWB8qWRTnzUcfePGSOx4ziuf1kzT6S9
+tuRHlVUG5sDJOMA+lAHofh5/tvh3T7k4ZnhXcfcDB/UVuRQEdqzvDelPo/hywsJGDSRRjeR0yeTj
+2ya3I8/3aAMXSU2/FmzJ/wCgbJ/6FRUunf8AJV7H/sGy/wDoQooAyNJXf448XYPS+X/0Wtb8ynyj
+0rm/DIll8W+KDKQ0pvF3FehPlr0rvINMzbfN8rsc7upH0oA5ie3mktZSI1ICn5W43e1ec6ZrDwax
+facIj9liUNtUcgE8j0IzXrHiqS20rQJbiadoyBsWQjc2T2HvXnvhbREuzeXTbvMlfILcsF6D6jFA
+GJr+pWE1ymFkwFzhlwo+i1xevazLd3Fva2+UiQhh7kdPyr0jVfA8t0BeXNybe35UK2EJAJwST/QG
+uS1Pw7HYalbKHUIxYxyDd8wHUnNAHsnhXVBrGhWkrBlmEYVww6sAATXQInA5rn/AOZtIa3mQHZI+
+xwfvAnJ6d8n9a6yazEKhlzgUAc1YAr8WbH302X/0IUU6xBPxYsSe2my/+hUUAV/Bdj5fi3xWJJDJ
+JHeopY8bj5a5OK9AUArwARXEeFjjxh4xbub5f/RYrsIZgJhGTjcuQMGgDnfHiwnw1KJoVkUuB8yg
+hfeuZ+HemTLpjx3OCZNzKUbPy54/Sut8Z263OlJE1wYgzkkjvgH86yfBb+XYWuIGiEithWzn9aAN
+loTcO0ctuGjV9oMg5JGCSOOnp9K8/wDH1qH1iERrukRAqqB3Jzj9BXpsk6F+oyCuRjJ54rhNcg+3
+Ge5XiUSL5ZGc87sdPagDQ+HlvJHoAdo9h85mUY7dK7WSRCoB6HiuV8IiW10JYs7yszDJ7fN/k1tG
+Rpb4xj7qpnj3Iwfx5oAwLMgfF+1UHI/suTH/AH3RTLJNnxltx2Olvj/vuigB3hgf8Vp4vH/T8v8A
+6AK6aRWFk2CA2CPSua8M4/4T3xcp/wCftD/45XR6q32e1JjUySCRdqA4J3HH9aAKHiJTceH4mliK
+r5e5lDfMpx2Iqp4eQR6Zp75Y4jX7xyfTn8q29djjbS/LMqxYGFdugNZWlskOh2pKgYj2AqO4OB/M
+0AW7+NLQ3Fwi/O6hsk5yRwOO3WuS1qGJtNuvN3iNJkX5e+EIxn8f1re1e4ubq8jSOMiBArZJ/wBY
+xOcfQcZ+tVNTsYh4dnjmG9PMJIP8XYUAQ20z2Hg6OeJGTYQzd+N3Le+RzXQ6TGwtjLLkuxAy3XAH
+f8Saw9Mlt7vwsI4yZI9m07xtyM/y5rqodqxIFAIx1oA5iDj4w2ZHfS3/APQjRSw8/GGzx20x/wD0
+I0UAee+I/GV/4S+IXiAWlvFKJ7gM28njC+1Ubn4v6xclC1hbAq6vwzdjn+lXviB4X1O88b6lPBYX
+EkUkgZXWJiDwPQVzH/CH61zjSbwj1EDf4UAbV78YdZvYPJbT7UA+7HP61HbfFXXLW3SFdOtSqZ67
+v8fesg+Ddbzn+yL3P/XBv8Kcvg3Xc5Oj3x/7YP8A4UAaY+KuvIQP7PtM5JXKt/jUF78Udcu7F7WS
+ytEVv4grZHPB61VPg/Ws/wDIGvs9v3T/AOFMPg7XcHOk32P+uD/4UAWLb4l6vb2zQJZ2m1gP4WGC
+FAz19q17f4va0sSobS04GB8rf41z3/CIayOuk3g/7d2/wqRfCWr8f8S27/78P/hQB33w78Q3fib4
+jR3l3HHG6WTxgR5xjOe/1oq78JvCmo6dq8+qXUBhhETQqJAVYsSDkA8496KAP//ZAP/bAEMABQME
+BAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4k
+HB4fHv/bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e
+Hh4eHh4eHh4eHh4eHh4eHv/AABEIAWsAyAMBIgACEQEDEQH/xAAdAAABBAMBAQAAAAAAAAAAAAAH
+AwQFBgACCAEJ/8QAVhAAAQMCBAIHAwYHCwoGAgMAAQIDEQAEBQYSITFBBwgTIlFhcRSBkRUjMqGx
+0RYXQoKSssElJjNDRFJicpOi8CQ0NmNzo7PC0uEnNVNUVYMJRWR08f/EABQBAQAAAAAAAAAAAAAA
+AAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDqjNudMEyw6lvE3HEqUnUS
+kDSmeEkkCTB241Aq6X8mgSL5J/PT99DPrX3S232GwohPbp5/6v8A70PMsrSuwYKgkgtjiONB0Ovp
+lykFQLhJ8+0Fefjoyh/6/wDfFBctsJWkpt0wRzSK9ft7cwVMNmf6IoDOOmjJ5/lB/SFejpmyeT/n
+I/TFBdFqwACGGR6IFOWbRhZ3Ya349wUBgT0y5QP8qT+n/wBq3T0x5OPG8A/OFCy3tbZCRpt2j+YK
+cotLMjvWjM/1BQEs9MWTR/LUn0VWfjiyb/72hki1sw5q9kaHL6ApZDFspQHsze3DuCgI6umLJqUg
++2Eg+AJ/ZSR6aMmj+UOfon7qozjTACUhlsCP5opo402AQWkeumgIK+mrKAA0uuK8e6R+ykj03ZU5
+JePx+6h6gaTGlPHmkUwzDjlrhDKVvtIWXVaUIjj4+6gKKOm7KZUQrtU+YCj/AMteHpuyvvpbeV7l
+f9NAHEc3Ls8ed9iYZLTZIAI4+vpRAy1i7WKYS3dhtIDghQHIjiKC9npwy3O1rcH3K/6a1V034Ef4
+OyfPqlf/AE1WG1KKxAAHhFO0LgTCfhQTf47cGPCwuJ/qr/6a9R004as7YXcn0bX/ANNRLbqhsAOF
+OWnVgEnh4UEm30wWbhhvBL5e3Jpz/ppwOlMLSC3lzElT4suD/kqHN642YT9tIu3T7h2UYHnQWW36
+S9YleXcQA8Qhf7U05b6SMPXiFpYGwfZfu19myl9XZ6leEkR9dVBy7XEFXLcUN+mPEn7VnCrplZQ6
+zclaFeCgAQfqoOqbO4L6VBxvsnURrRqConcbisqo9FOYE5mw1zGENdkLllpZQSCUkApO/wCbWUAY
+63i9N0x3v5SkHy+aqj5QBXhFqRzbFWrrjrWm5a1ER7Unh/saqOSXP3Bst/4pNBbWhLY1nvCt1NyZ
+mRWtuoKESCOdKjSOB99BiEjYHYU4ZQpKpTwpArTxpVt8g7RQPmlQkpP2UoFE8VkU0buEkK4A+NbN
+rCzHDagfQCmN69TtsPjTRLoBA1b0oHFaZPwoH6IPODFJPwOG9asklJJ228a8egp9aBspcHhvQf6T
+b55eZHWVu6m2kgISDsnaT76Kt46hlpx1WyUJKj7q59xzEFXV9dXC1krccK9/M0CybtYUTrMnj50R
+uiPGLl69OHBTaWG2isgjdRkffQgFyoqO441eOiW4WM1W8D6aVBXpFAfGCVGQmnDZO07jmaYW74G0
+yKfNuJUBB50D0ISAIAiNyK3UUkQIpDtgUhAAIApNToAKQACedAotaZgCfOvQrUCAIpsVAma31kEE
+bbUG60Dwn1oW9O6S3h2HqnYvK291E1SlEkzvQr6f1qRg1gVHc3B4+lAaeq2rXkFhcz3NPwcXWU06
+pzqnchtQ4CkBQjT/AKxVZQDXrpOEXLJO0XYj+xFVDI6lHAbEeLSfsq1dd2UPWyxMG7HHhPZCqtkX
+/wAhw9Svo9gj7KC4Wx2MCKdt7ncTTaxbGmBz3p4hISNzHhQJrQSYnjXiUkQOYpZzupJJpFK5g7n3
+0ChQSkbxSluSFDfam5WACokj1NetPAqA29KCXb0HdQ9TSwQlSduVR6XBpJnetkXWgCTQSTQCRpMk
+Uk8djzpsm9ExNaOP6pOqBQRWcblNtly/eI3SwoTPiIrnbEXAl7j4UaulrEHLbJ9z2ZEuKS2fQneg
+Fevy62ASQYoHCHkqXsqrl0U3aWs52mpelLgU3v5jah20/C9zO/hVmyc8Bj+HjfvXCB9dB09bq0mD
+zqQZUkAVDtriOdO7ZzSYO8UEoHBw3r0KB3mm6XUxERWdpO0UDhJBVvSulKh9kU2bUdzvvSzKu9tI
+mgXShOjfaTQk6xZ/cOw4GLk7/m0XlqlGxEcCDzoP9Y1KhgNhI/lR/VNAWuqGpJyG3pBHdVPn84qs
+rTqgqJyKyCI+bXHp2qqygH/XoQEW9i5M6r0CJ/1VVnJKP3Aw8QP4BvYf1RVh69K1lm0BMgXyQPL5
+moPJ50YDY+TCP1RQW+yRoaC44HhSygNXe99NbW7lCUKA48K3cdgbk8KDa5IAMcKj3XiklIMTSrj0
+zCppi8STwNAo5cEo4RXtq53pn1pke8NIUR5UoydWoA+lBMdrCdjSLr5G8yfCmJdWE6ZpNx2NiZoH
+4uZjlThKitJ35TUOl4DntUlZOgoiaAe9Nl+tnDbS1QAULdlzxHgftoMLdm4VzAmKKvTsvsrywUqS
+y8y42seBBBB9xoSWTa7hVwocGmyrh7qDZpUKFXzoptG77NdoFkwiXD7uFUvCMOu7+5SxbNLccO8A
+TRd6Nsq3eB5mbVezq9l7ThsCTEUBcQvhtuKdWy0qVJ40wS4NqcNOTvA+NBKpMokRSiFKnx8opo07
+KAAYNOGwrko0DlB/JO005Y0gTNNW9pkzTm3SDJJoHIQhIlKZ50IesgFDLdgYgC74/mmjC03KaEXW
+WBTlmykH/Ox+qqgJnVAV+8S3hMbOA78fnVVlZ1Oio5BbnkHI/tVVlAOOvSIbtlSN8QSD/Y0yyayh
+3Ldg4BBNs37+6KedfERaWqo2+UE/8E1FdGdwF5Yw0kgzatmPzRQWdhnRClCTWPAEHeKkENodbSpO
+xpBy3IJJjegjFwjh6U1eWSoxw50+uGQHPE86j7qEqiY9KBstYSe6B61gfLapB5QaaPOQs77V4twR
+wNA8TcFyPKsdd25UyS6RBAivXXJSDQOm1jyinlq/CoSY22qDDh1AVI22mUqmNqCpdI+GXOZb9Ng1
+I9jZL+qNiVSAPiKiMhZQFixcHGGgkvJPaJJ4JBolsKaTdO9zdTcKV4CdqalLSrtevcQEkeVA7ytl
+/CcOm7sLVKFugb+VSt06k3WgfSSO9HKss30NsagAkAd0GminCXSr8onegfNKSDO/nJp22tCSDUYF
+xEJpVDkkwDPrQTTCoGxBmnjDp4KG9QVu4rVxjepS3WY1KVPpQSbSiT508tBvBImo9hJICt96f2yS
+TwIigkWht6UHes8ojK9mJE+2Dj/UVRhaEpn7aDnWhH71LP8A/uj9VVATupmZyCiePzn/ABVVledT
+OfxfszG6XYj/AGyqygHfX3A+TrPmTiKZ3/1JqvdFX+h+EqPO2TU91+NrK1Mf/sER/YmoLopSpWR8
+II/9sPtNARbNSdAKdzHCtnY0Eq4zypnbFaEAqImnRUFt6p93OgjbyEEn6qhrxQUo+FTGIp7m23rU
+HeGKBi6E6orRcRtWyiSoxWp4ERQeoAI3NeLSnnW7Q2341s5skcOMUCSEpMk08to0gA/Gmu4HCvW1
+qoPMQWtOJIDR/I3HiKyycacfU4o86Y4qpxBcutiUphIpLLy1LGtSN1RsTwoLS0SQpx3uoG4HlSHa
+gq1JMSa9ullVulJAA5+dN0wYiKCQbcJ+lNLoVzmmjKoEmndudSoVAHpQOrbVq3mIqWtYME7Co9oE
+xHAeFPWVqCQY250EsyuBsSPWn7C1CI4+tRdq4F7gjzFSduoCBHoKCRYUYAG1CHrQJjKFqqB/nqf1
+FUWbck96NO3jNCXrPmMm23Ha9R+oqgJPUvM9H6PLtR/vTWVt1MkoT0eMaQAVpdUYPPtSPurKAb9f
+ZQNkwnf/AMwR/wAA1GdD7QOQMHUeduPtNSfX3H7n2y4mcSSPT5g1G9D69XR9gxCeFvHHzNBczGkR
+tWqXIBjlW4HdO0U1XqQsGOe9ApcAKTHEGoe7tyJqWWSBv61roQ6nvCgrD7cEyPqpNCYn0qavrMhW
+w5U1NsYAAoGLAEmaUcAinItSZ8R4VjjJ0Qo+tBHK+NeoI0kVXs3ZptMIS42wk3FwB9EGAPU0KMWz
+zma6uCtm67BoHZLSQEx6nc0BdzRfBq0UhKu8BwFI4BchttK1EyrlPCqjhmJOXlrbquHUPOrbBJJ5
+8+FaXC32NTguLi2MmCYU2fLjPxoCel0vDUFFQPCnSAQaH+Vc625fRhuKpbtX+CHAruL+6iC0QtKV
+JUFA8CKByyFE8KeI2CTzpG2E8h8acpQSRCRH20DxlRMGPqp2zOkJB2prZtlRIg/Gn7bSUwNW9A+t
+BAO4B8Kk7UeJmo20SUkmeHlUnbK2G1A+aiZjfxoS9Z8k5Ltwrleo4f1VUV0d6IG/OhR1mkg5HZkz
+/lqP1VUBI6l5H4vmRJOzvHl86aykOpOf3iEb/Sdj+0rKCgdfUj5NtxzGJI9/zBqP6Gkj8XuDDn2H
+/Malev6lHybakQD8oNg7f6hVMehVIV0dYMqJhj/mNBcm29Q24U3u2gDtUohvS2dtzTO6H+IoI93d
+PmBTcq0xBmnDoM7b7U1KZUQCBvQLKcQtuFRSCwAmBFIvBxJ2MGaVAUUhQIngaBNUg7kVV81YoUhV
+rbuweCiOPpUxmC9Rh9it5Su+dkDmTQ9Nz2q1PrO5JCd5JNAxxyztBaOOXyktsNp1rUefqaH7dpc5
+hvlDCLJabVJgOLG5H2D0q7YhhV5nDFrfBLYrTbA9reOAwAkHYep/YKL+X8qWGHYe1aWjCUBtMDQn
+c+cmgDtvlq7scEQ2grTdtnVqKdhVXx3EscYTpumoIkdogbKHmDXRWJ2NotlxpbjRUOYVJ9N6G+bc
+Mt3EdghaFx9IAUAXfxB25bIe0haOHd+qiR0QZ1dRdIwXFXdbayEsOqP0T/NNU/MuXH7Qret0KKUn
+dIE/CoCxUW3UnfcyCOINB2BaiRI8Nop+0gxP21R+iPHF45gCUPq1XVsezd347bGr8wlQERQLWp0r
+Gw41IoAKp2k0xQkg/Qnwp9bSYJSZFA7YSfjUhbN6uMAimTBIUCakbZW44UD1hkJAoUdZ9pKMgNqH
+H25H6qqLTR7oPOhZ1ngD0dSOIvGz9SqC4dSZJGQyeRU7H6YrK36lBH4vk7flvQfzxWUFE6/oBwm1
+Vz+UWh7+wVTboPSR0cYKkCf8nk/pGpHr/o/e9aLjf5SbH+4XTPoOUk9G2CyCT7PwA/pGgvaR3eAN
+M71E8qkkJBRsDvTa7QNBFBCuoHGKaqbGuZEU/udIERvTbUmSNO9A0ukjSTG9Nm1BC5PDnUg8pIHC
+ozEn0tMLcCeCSfqoB9nrElXOLi2bJ0IOkeE86rVzcBDKnAoBLRgCNyfL/HjXmO4koPaoPaOEmf5s
+86aYHbPY7mmywdk6mmyHbjwKZkz60Bl6NcGRhWX0XDrWm6vPnnIHeAPAe4VZx2iEFSVrKYgpI0/X
+UJeYuzhrae0Qt9SANmwdIH7ajh0j4WrW204dYEEE8NqCXW6+7dP2yUMNnTKdZJ24cao2ZGCVO9ol
+tzcmUnh/j7qXTm3tbC8xdcKQnsWRJ7pIUoq+qKqeMZ3YUp9xxxsJcUCQngOJ2+NAmptDxKHEqSCN
+vGhnnCxOHY8sISEpe76YECecfb76J2GXDV8wLj2a4DDhlDikEA+h8Kr/AEk4St7Chdtd4sHUI4xz
+Hw391At0JY38mZntmnHdLV38y4nlP5J/x4102ywVAEbiuLMDfcYeauGjpUlQUDHMGu1MpXPyll+x
+vkbh5hKifOKBZtuCRoiKWbQoGYpx2KkmYO/lSiGjI2EeYoMaQSSDTplJA2rxtJG1OmUgD/tQKM6h
+50MOsv3ujlwgzF02ftopJMAAxt5ULeslKujq5I/9w2frNBb+pM6Dkfsuep8n9NH31lJ9SUfvOBgb
+F/cf10VlBVuv7q/B61g7fKTW3/0Lpn0IjT0d4IN/82HPzNO+v8Vfg/apkR8osn/crpr0HEno5wUE
+cLeJnzNARGSCmCD8ab3IBkAcqctjbhwrV1oqTMAUEBdpABnemRAHvqYu7WASQDNMOw7xAKfHjQMn
+YjvDaq/m64RaYFevcNLSj9VWp+3IEyKo3S8U2+RcSXIkthI9SQKAKXV4u5ue21BLY3JngImrb0S2
+TrmCXuYVuKb9pfUkr0nZpA4Ty3maHDSXsQLOHW/8PdOJZbHLvH7AAfjXUOW8rWWGZbssMTZtutMt
+BGpbQMniVRE7mTQDzMfSJctYXcpwbLq30MoSFP3KinXO3cQBJ9SRQpu/lvE8WS+jCQXXVAyylaUu
+T68TXQubsNNpZK1YwLVsDV3mSSPSFCoPJeTH3rv5dvO2UiYtu1RpKxH0o4xvzNBHXuX0WnRpcGzQ
+supWlSyqZMjccfGN6FGG2+Iv4u5cW+ALxNLSoaZUfmwoc1Abq9Jrq3GsPtrbI12gcFNHaI3oJW9q
+7Z9qbEpSpapg+NA2ezHnJ4rZxLCrZLVshHYWzdutBXPECCoAjzqRtlKxNpxp22U0tSTLS4kCPXen
++XLW5xR2EN3iXE/SCmwUj3wD9tTSsHLPdUtK1cYiaDnq1bXa4jcWLuy23FAe7b7q616v1yb/ACFa
+JMyySg78INc19IViLHNAuG0hPa94jzTxo79Vq/DtjiWHDcJWHk+iuXxFAXrls7xO1etJkCCaevNy
+NXOvGGxG6QDQaIbM8AactohMRWyWyD605aSInwoGwTIgCD40MOsa3/4cXh8Hm/1qLSkhMqNDHrHN
+T0Y38Dg43+sKCd6k4H4EHxDj36yKyvOpTIyZpJPF79dFZQVLr/T+Dtt4DEmf+Cuk+g1uejfAzEk2
+wn4ml+v8EjK9sYknE2Y8vmXKT6BQ4OjTAyT/ACfh+caAiBoDYCaxxOlAGnjSralQJg7V4+sdmBtQ
+Rd2gaZM7nwqLdShCzE1NXS2wmJmot5CSdSQregY3KthE+kUNOnjtF5Hf7PVHatlfoFCim+hOkwDI
+86p3SRhyr/Kt/apaKipklIHGRuKDmjKV81ZZuwi6cWSlNzpE8BtE/EiuqLbMPtFi2WVDVA2rjXEy
+5bX1skApW2AQPA6ia6awJQNvavpSZWgKI5EEUBLwmwbxFSHMQYZd098FSQQD761GMWuK45eYPhYT
+cKsUJNw4ncIKphI84BJqs5hzKcPy4+q0lVypISNIkk8gPEkmKnsuZCvMHyEi3sMQFnmC6V7Re3Rb
+16nFcUnyTwHp50Epmxm3ssnrYeUe0uEHSIoIYg0cKS9cOW5Uw1ClqHEJ5n1q59JWKYhh7tthN0+u
+7et2NetKY1pHExyoft4lfYnir1u6sPWL+mUqT9HxE+HxoLhaXS2sND9k5rbdROpJ2INa212OzK1L
+nn3qhMtMPYU67hThKmEElon+YeHw4e6nFxPblQBKPyY/x5UAt6VLwPZoba3DYbn3knf6qLXVGbcV
+e4u8QezS02kEjmSTFDHNuXcRxnNDKrG3U4SnRJMSZ/711B0I5OcyhktuxvA37a4suPqRuJ5CecCg
+ujoEQfCtmOI2rZ0d2vEpEbcKBy2AVcvfS6UgbDnTNJPnSrRK5UmRQKqC9ESDvyoY9Y4q/FlfiQfn
+G/1hRPWogaYihh1joPRhfEj+Mb/WFBMdSon8DgDyL8emtFZWvUnCRlFYSRIL0+utP7IrKCr9f3/R
+q2PH90mR/uV170EKKejLAxAP+TbfpGvOv+CMsWpPPEmY/sV1r0Bk/izwPcx7Od/zjQEhlRUNk+6l
+XEymY5UmyDp4x50uggpHemgj3mZSSRA8aadmEqAVFS9yfmzHhUU8UzvyNB4tpBG4NMLy2YeZWFN8
+QeNPA4QdMkT9VIvOHSUhQ3oORulXJl7a5tvbq2tyq3U4FIQBvBAO310bMBsQcKw+4ZIIS2kKSR5V
+KZ9sPaWm1WqW37i2lSxH0gBBFUTLeamLW+Tg97cBt9StSEcAB/N9aAws4LgLCbTEbrSi3aWLo61d
+1Kk7ifQ7+6vXeke2uLdCsvYLf46tThSk2zKuzBH85wjSBVeTeu3rCMPNom/tnFauyUdKfzvETyqb
+fubpFs2wthtCUCENsSAPACKCk50xPMvt72L4nkt4Xa2SygtuIUkIIjeFHkfKh5huMWdk+W73Bb2y
+UDu6WSpER4iYq45rwS9duV3Nz7Ssap+ceJ5cAJquM9o2QEIJ7wkKPCgnsHxCzxa1F0w6haSkplJ5
+UjdH5tdwe4gCRJj/ABzpo0lq3X2rbKGVLBBKe7J9OdUnpQzULWx+R7R3/KHk6VkH6CefvNBMdEmc
+13PSMm1u3W12C3VBgFIlJ8Z84+uuvLIzaoKtyRXzpy7fXFjcpWypTa5ACk7Eedd99HmJt4zlHDbx
+D6XlKYSFrTwKgN/roJ1xHdFY2J3mt3EmIrUCO7FAqhAVsRtThIAiCD5UigGCZO1bpSqNiaBR3SpH
+CCKFvWNI/Fne7TLjY/vUTl6tJoXdY0KPRnfGP41sf3qCb6lP+h7h0gDW8J/PTWVp1Ilzk59E8HXf
+tRWUFX6/5P4NWoKhHyizt/8AS5SnV+bDnRpgaU7xb8PzjSH/AOQNUZfsk+OINH/dOUr1eFhHRlgg
+3nsJHl3lUBONvGxTBitUthPGZpVLx3kztz51p2xJ2igTuWu6ajLhvjvUs8XltEttqWI3IrZGFJWl
+K7l4hSk6uz578KCtLQrXEz4RUknAHT2a1uaFkayI29KnWcIsrZ/tN1rSJgqmo1u5vLpt1p1pTUPE
+srk7jjvQBvH04hhuarxzELlaGn0Q2hHBJ3gTXOefrwt5oadUl1p9h4HQvYxqkGuoOmF+1U0bJ25Z
+aeWsKMqEq8QJrm7F8PcxfPTLKy12LSzD6mtaFJBkBQHHwoDBaYld4c004SpbRAKVDiAeVWFrPNi1
+aAugrlOgEHcbc6jsv263sBZtbhse0sICXE6eGw5elRSsJtA4/rag8Y+6gaZqzheXlyvsSWrdHdSh
+KREcKgm8bt0IlxYTGxkQaZZvtW14u0D80yGwVRtJBqCxRy2ClOaU7bJkzvQSmI5mff2YgNjYeJNC
+/MTy38auHnFd7VzNWW9uDDVtbgLed2QmOE8SagMesfZr0NKJU5pBVvxJoNMLt3HvyoI70zw3/wC9
+dP8AQRnVzD7K3wm5uGlWSTpSeaT4ek0CMm4GL68asw65LpCNCB4kcffRz6JMAbX0iXOEBxsJsWUO
+IhI0qBH+PfQdDsrQ4RpcSraYml9AUkq2EVDW2GO2OKe0hsr1JIUUq2I9KmEPs6ezSpJWRtNAow2C
+jma2TqA7vCvGFOCQpPpW5PmRHGg8UCUgn30LusgI6Mr8CZ7Rs/3hRWOyBznwoXdZRMdGV/wMuNfr
+UDvqQz+CNwTw7Z0Ae9FZXvUiH70LiOAedjb/AGdZQC/riY9cZgybaXdx2QWnEGUKDaFIAUGV6tlC
+eYq1dX1BPRrgRHO3/wCY1WuubhNnhOUrC1sX3LhtF61LjiYUo9k5JMVaur4kDoxwLf8Ak5P95VAT
+A0rmB8a2w+1L9ylomATvHhSrTetMJBJPKp7B8P7BsFMdqrdRPKg8xK0Qzh6bdlYbkiSBvE71AnCc
+TxHGRcqAtrdteyie84ANtuQq6KZSmCoaz4mtVpII22oIe2wq2t2tMOOL3lajJ3qDzc37Jhb2lSkF
+YgL/AJm3GrhpUkn3VSOljMWDZawBy5xd4gOgpbQlGpSj5Cg59zRaP67w35beeZAUwp8gl1JO59QK
+p/RVgS2XL/GL5am+3LyWg2jVI4QBB47/AAqbxvNb2a1vMYQ+i11KSEQ0QVeEq4/DzqwdGjLlvlkL
+unHEPquH9bkAqRGqVAcyDvHlQXHEMETbu219aJKA+yhtxB5EDYn7KrGYLY21yEOoLayCFBSYNF+8
+w4sXFu+EqubNTRVp4qVpbBA8NzJk1TukZeMu4fdN4ZgaMTcaLIDIZDim9X0zMgwPf6UHP2e03Tt/
+bN2rS3HFpACW06lKPkBxppYdGmasQR7RiCRhtvpK4cV84QP6PL311ecm2jFs0xgybSxu1sIcDxZk
+lJ4jYgmD9tVvFsoWiQ45mDHVvW8QvfsEQDChtufpJI35Gg57Tl/C7ZDjdtcsWz7SE9s65K3EhXPh
+z4eFVvNtrg15b9ph63W32nEMtoKCS8Oa1K4DyFdE57s8k4060zqQbi2SG0sNgw547jYgcffQr6RL
+Sywu6ZFtbN2zCXO0LaR/CECdz/jlQVnLWH3RxRoWoLEaVLUtUwf5225H30XeifGlYJnO0tn0ds3f
+/MG6UgJgkyD6HhVUyW3b3l+wEpSxcusjSDJC522+HD0qy4Thq8VzrY2QcKmRqaeCO4oECAoBQmAR
+QdTWTYCAVLCieFe3WFWV4pKnWh2iFSlSTBFQfR85eHD3MOxLvXlkoNqWf4xP5KvfVwZbIIOxmgi1
+4elgSkrIPDypqtlQE1ZnW0qbg7yKjLxlTKVAJGmOJoItepKR3gDFC/rJKJ6Mr2AdJcb3P9aig4VE
+bDj4UMOsekp6ML2SILrUfpUEj1JmyjJrytiFOOnj5oH7KyvepPIyfcTzccP95IrKAb9dN9p7J1gb
+a+urtkXjPefIKpDTgIMbE93iKtHV1LKujjL5fC+z9mIMeSlVS+tZh2L2HRrZt47Zi1vTiTQKO11w
+OzcgzJnh4mip1YcNb/FTgDrqQrVbE7jh31UBOsLcdhrbZ0JiEDnUvbM9miIk86SsdK3VqaVqbTtE
+bTzp4J5UGq0kxArRxsngDTkkgbcBWizwmRQNXGjvFBHrLZYxXHUYabJhbzDetC0gx3jEb8uHOjq4
+ElMzNUHpcfScEFmq5RbofVC1qXpgDfY+M0HI/YYtl1a3bmyt7C3LnZ26zHarIMaoEmOfhRKyM8nE
+7Fm4tihLaFKWHNMBKtCiVkb89yKHXSo7a2+IlouqKWkKTDjsjURsB5wZnzq+9ByG2cCtmlKI1rB4
+mSSOBI5GfhQG51SMLwhd9d3wWtNqhaU6YTAG+mPEmn2AN2tvZm9cCW37sh1ZPiYAHwgU6Yw9rEcv
+27ToAOgBJT4D9hpZGHNuhDK4T2YiOQoEsVtnlLtvZ7QOuIUdLmqNCSNxHP0oZ490dXl5jryPlJ9F
+m6kOOslZWFKPE7kxwG1EZnGGnMSvLNKoatEEvOzEE8BNKNOYa7iSk2zrbtwWwpxSSVGOUnnQU+wy
+Rg+HIQpNqhSkc1JGxoE9OeErv82Iw9huO9Ko2CUwkn7a6rv7VTiDpMeBP21zn0j26lZuvLy3e1Ia
+CVSonhI24eCaCjWtk0xcW5xC8RbtWoCmm0nvEJMgGOG9WHo4dxRzOthdXGHutoQ+oF7V3QF8IJ3P
+ED31EvM/InbEqZccvHNRURqCWwIjf3mpfJ79xdZywtCriXC+lxSNR7yAdyYMAbbCg6WbZXa4na4m
+2SEqhm4HLSfon3GPjVub2G1QVtbC6wxbau6FoInwqTwl0uWqAsy433HPUUEnJg7CKbvoC0qB4U4k
+cfqpFwlSjHKgg7pstrIKNPhAoSdZhSm+jW4kDvXLSfrP3Uar5jtGpmKB3WkXp6O3GuZu2p+ugn+p
+aQrJTp0gaXHUkxx7yT+2sr3qWQMjL8S69z/pJrKCqdfhR/B21bIgG8ZIMcfm3Pvq79WMA9DeXdWw
+9nV8A4qqf19Qn8F7NYBKxetD3FDn3Vaeq68X+iDA0L/ikKSNuI1E/toDCNKW+7tXjbknTMKB3pNR
+BRusSRwpG1bQlZdTqle5M0D8pIRIMzWi0k7kVs26VARuOHpWy9kydtqBvyAoR9PDdy6W0NBOlLJV
+KuAMnf3UXlQTx40JOmrHLe1vfYLllLluGPnSeRPn8KAA4sxhGY0IwXEbdAcaKSxfJRpVpJAjVzTx
+MHwFGPLGXLXA+yw6zh1lgobSswZKYG9B3GUWzCEXdmXHGFPoShKR/Bq8FetHnJTbjlip1/6Yuz9L
+YjvJP7aC9ZYYPyUyoOiQkApSQUp8hG23Davc2JxC3wi7fwlkvXvZHs0AgFR8idp8JpbKaAjCGwmS
+EqKQeaoP0jtxPH31LPJ7VlSRsY29aClZTwFdnkq3axBhab19IuL8FYUpxziQo8Dvy4VL4Szh7XaN
+WduGlIjVCCBvy4fZUi2T2imjsJmDSVuy6h59bjoUCrupH5I8KBtjTybTCLl8qCSlohJP84jb6656
+zk0FX+IS0pbhW2lKUnc7EzRw6QHpwL2clZ7d1KShIOpQG8SOA2Ek+nOg3iuFrxBV+Ge0S4HHXAAd
++4Dtz5GgodlhTuLOXV9blt+3t1qTcIKtK2FA8DOxnl76neizALh3NTd4ppRJCVIIJ+jJ+HCvcq4h
+8nZdRamwLpecU4tLg1BaCd9W3Ab8TPCp/oVxC3s80Iw95d2lVwSlhLoEJgcoHODQdA2Dei1QmDJG
+9LJZ7N4vN7H8pP8AOrGNSedOAQU7+NB6l1KyIO8SKb3L4QstEwTBn1P214NFvreIJK+fgPCoyzWi
+/eXfPhaQh5SG0qVACdoMefH30Ek+4lCi2dwQTx50Detesfi6J4TeND6lUYbgqf7VsK0KSe799BDr
+VOLPR8pCjwvWgY9FUFr6k8fgSuP/AFHj/eR91ZXvUm/0GV/Xe/XTWUFc695P4NtJO6ResHj/AKty
+rb1YRo6IMvAAd5lZ9/aKqqdexKjgCIE/5Vbn+45Vs6ti9PRLl0Dj7Or/AIi6AqO6QtC1qjkBHE1o
+HCEKCdlQSJ4DwmtL3UGwtKgkpM1s22OzCn1pJMcOFBtgq1rtRJKlT3jBAJ8p4ipIokAz7qq+FYnj
+D+OutrtbVOFto2eQ4dQVJATEQdoPvqypdA3kEcaDCxJ5jehL0k4JbX+aF2D4Kn7tslsaZ2jaD4yf
+qovJeBA9aGGbcYUxmS4XcLW2GwopMDToHL12nagDeMWrOQ8Ut38Te+bfeShTDYSrWUwZUOW870UM
+MxBD2EX17bgFKrp4twdjCEkfZQn6VMHxDMuZrB/CXbd3tnA0u2dX88gkzqG26efjtRiwWy7DLhY0
+gr9ofQTp3nSdx+jQXTLLxUxdJVGpL6jtMQrcRPkR75qQQ+kKUkq3mq1ktSoeSlBQkttqEj6RKASf
+MST7wamXwWX+2EQdlDwoFApS7wmBoB2ptbG6TdvOOvsG3OzTSEFOnfz4+tN7fEW3seXZNmS0NawP
+yR51o9d3Vzj6mBZhq3aSfnCoFThkbwOAoIHON2HsXtbALWgBQVISd1KJgTwiEmR6VV8rgqexG9AB
+0WjzgJG0mpa5Wm4zViN8hQcRaoXJ0gAdmgAAEHeFFcnjO3KkujlTSbK+uX/4MMLCpH5Ox4e80Alv
+7O89rtLK0tlXK1EgISqEK27yp2HAbDyqU6MsLXhucEqWyHHGnNTbhSJIVPCCY9Kl8CtsYvbxzFuz
+bu7dLmi3t1PFGpO41pjhyEceNPMpl+zzkwzeKLr2IK+ilOlCAlJIgevPnQGFNwrsCofSArXA7x29
+Sokd1KiJ9K2QzpSNUwRxpLB22MPs3xqVHaqWeZMkmBQa4u+3fONWdqVrLdylL+gwWiBqTPiOHuNM
+ct3D14L9zswi1RcuIb1cVadp9Nqe4241h6HMQbMLQytao4KhOxPjwpnktl1rKNqHge2cRrcP9JW5
++2gmrRsItgTBJ8KA3WzRoyUFJ4KvG5+CqP6GyGUgAcKBnWzQT0epIH8ub+xVBYepbIyQ2AQR8/Pl
+84Kyk+pPq/A10HcBbo9O8jasoILr1ScBRBgh63j4O1YerOo/ilwHURs0sbf7VdV/r0j9wkqn8u3+
+1yrB1Z5X0TYFwjslgf2q6Aq3X+bSPCeNIYchNywQ5JTBETtTi4SVWy4HLamuEhSElJM78qB1CGG2
+7VpCUpnvnkkePrUQ/cXjOYmbFAUbd4FSf6McR9dSmINLUJCCptB1KTzX4D0mqhmPH3MvZgZvbxtR
+sy+1bKITMLcCt/iAKAjhsJQIiYoW9Ilp+6V44yg3DhQIQ2qTJ2I08440TWHw/bJdSkiUzvQ2xxdy
+xib7rDalvF8nYctXCaAMYfjWJp6Q8Js8SY1D2xBCwkoIIOwE8eKZo34OV+zW0pK+1vS4QFcAttdD
+XGMYwu66SLG4xKzadvWLhDbLikFKgeB9QJPwotYez2YwxI4FDK9/zx+2gTynCb20WVhRfsQAmBKA
+hRmY33Kvqqw4mlCLRxxwEpSJgbk1XsHhl7D5b0Bu5fZU6Y4EylBnfcmdvDzq4ONpXG21AN8i4ZmH
+D845gfxLsBZ3LiV2znaa3FbniD9EAbRw22qw2bTFniV865dXLj4BUpK1Hs+E90UviDqm8UJgCFAH
+zBH3iorPVwm1wdy5ceLZdSGUKClJAUo7GR/jhQVxntG8v4ze3LTSHnWtB0DYFaiYnme9x58aQwNt
+bWT8XdSrQPZwkq8CZn6op1i/zOT20JVPbuBSTxkAEj7BS+VOweyhiDlwvTbuFxKlEcEgQf20AJuF
+Y1eW9vb2jtxpYK3FqZdG0ERPDxiiV0WYWu8xNGP3ilpdb7gSsGCqIkVBXgwDCMFQ40t5Srx5QZd1
+hSVRAkDkB4bVa+iS+YcuHMMR2kBHtACySdzvx5cKAj3t8pi1WUtqWoJkAVC/L9naWKbh1Sbi8WUM
+9i2QopWQYkDcDjvUvi94zZYY/dOphttsqJAnYChDlN22ds14xiS/ZXrm+cuWme00rUClPZpUBvJA
+n86gvrz1zieBui4T2NzeOIY7IHZvUdwPQA1dLVkW7DbKYAQkCqlhlstzMOCWOgDskru3gTMHTpAJ
+9VH4VeFp+cMAbeNBouNEBRG24oJdbFH/AIcpM8b5refJVG90d0bbmgp1sEk9Hze8RfN7fmqoJTqU
+6fwKfE97tndp80VlJdSrbLFyJnvu/aisoIrrzpP4OoVy7S3+1ypjqvuT0SYIkk7B0f71dRnXlSDl
+hKo4Lt/f3nKfdV0a+inByPyQ6D69qugM6BLShHEUyw7a4dQDMHantuklBHKmjbfY3ijBAO9BviT7
+bPdeMJcQQY/Z9dQV6S4n2e+CXgp5tbIUmSUpgyfMGneeGFuYCt5DikKZUlyQJ2Bk+u01st1tWYbS
+zCNZuGlKkD+DSIG/hMigstuEhlO0gjaqRmppJvXvY3GW7pSiYdMJkDjPjV8Q2EtgDkIqi52tmlG4
+L3dE90zxJHDyoAnjDdwzm21uMX7uKXVwA2yIKUJTxWFDZRO3186NzjqWbXCVE8EIT8Ck/fQlunm3
+834azcWVtqt3VJQtLkqEjf7d/Si9i7KU4HaPJ4MupmPAgj7qBtdtqRbX51hPs16h8o2GuTATJ9QR
+5gVamHSS2VRCk+FRl9bj2m7HY9qLi2JCAY1KA2G+wM86c2F12+CW7y161pQNStpkcZjnQNcetwhZ
+dA7y1JHHjVTz5cLdcw2wYQHpd1OTuECISvbeZkDx34xVhz1iAscKNyYKUd4jVHI0PsOujfZgaxC5
+uVIbZSFFtPNOkOBRj1IA8vSgks4OMpNtZtAFthWohJHArA4egVTjCbFf4txag6XHWCtfA7rOoj6z
+UFfF66XcXUavablQSTP0Q2qOJ/pD3ir4y2G8Hbti1rBbCVDgIAoOfsaxFOG481hC3LZ21QrtWm1t
+hayrgrSeRIMRwgUR+ibCWkdpj3cC32g2hCTPZpHI+e31VRs2ZYtHLxePKJUwpxTSWUcdYPDUOCSI
+38iKu/RG4ub+xDehhhSVISeI1DfhtxFARnG27izW28gKQRBBHEUEsFwkXnTJcYeo6rSwWl9CI4bC
+J8d/so3gAtQKpOG4a3adIuL4i0nvOWaJ24qkxQWzKzYdxbEsVIOnULdonwTx+s/VU8rvKK5nwApD
+CrX2TDmrfYECVkCJJ3P106aSJ8qBEqIAEDhQZ61xB6PmduF83+qujY62nwoJ9a8j8XzQif8ALkfq
+roH3UsP72LoT/GO7e9FZWvUtIGWn0wZK3if0kVlAy68IJyxO8Tb/AK6/vpfqquBPRfhQJjvuiDz+
+cVSXXg3yuE+HYH++uvOqopA6M7DWB3XHY/TNAemBKCBSdy2lDqSY350lZ3KdxIn7a0xJ0ltCkmCn
+eg0x1CXMJfbIiUECee1UToGzLe461jFtibRVcWF4WUPkfwiOQnxEVaMRxGbRYWZEGo7oktLS0yym
+5YA1XTzjyz4kqNBeytU+NVvMFlbYip5t5TqVEBIUhUaTyPhU4t4p4c6qGbHL9DinLI/OIlQSTHLj
+QDfOeCsYNmBDlpZEvKR2i7kypSjqg78Ad55UUFLL2WlIUkmWwRtPDehPjeM3t04W7nuBtxsFA2JK
+l/ZANF7C1JVh7bSu8kpjc0Gl1iLbVrh12tQRKkoJJ2hXdH1kVtgbgFvdWyne1LbihqkHUPHbn4+d
+QePYbcYng7Fqw5p7J4FSvNCpA+IFLNXTeFIxS+dSUKTal5QJ2UUpMke/9njQD7NmNX+dM7KyvhyU
+2+HWw7R+4c37VKFaVCBwEggTE0/dbfQWbewabsy6pWs9nJDadjII2Jnlt4VZzY2eH4cvFbawtkYl
+fNN+0PBAG8TJ99Nra0Q7jTSyhLa3GktrK3ZEDcpTzJkn4cqDW8swi5wu23AbaU85pHFS1AD6pq2L
+HzACRAAqKdQh7FiscilA9E//AO1KuL1IMcAKATZrFthgdwnEb1NjhhJdU+133DqV3U6dJj135VNZ
+CxrK7PZ4Xg945erMBb3YmSTJ75gR68KpvSFdtXV3ibz60e0W1ou5LSRsEAqS2k+oMnwIqMyLauYM
+3heI3lk9bKfulWt20pWkgLIgn3EEUHQEd07kCq1lwPO5qxRTgGhCkBJ8QBP2mpe2eXZdnaP6ltaQ
+lp8qmT/NV4Hz5+tQVveLtMcu9AkuOoB9KAgNqUobEA1oHFa9JFJ27mod0+tK7SFzvzoN1lWmZ4UE
++taf3gsTzvkAforo0uujSQN6B/WqcKskWyRzvk/qroJfqYT+DTuw2U9+sisrfqZD97D3kp2fXUis
+oGHXc3y3Eb6WD/vF1TOr7mvCMJyFbWt3i1nbPJdc1IceCSJVI2NXTrrtzgIUebbH1OL++uNnEkHa
+RvQd42PSFlkAFWP4bI//AJKPvp09n/La298cw0+l0j764C7yTxPrXhKualUHcGN51wJVo6pvGcOJ
+0HhcI++k+jDOGCYZlJi1uscw8LC1qANygEBSiY4+dcSHUocTvXg17gK4UH0Jts/ZXUolzMOGJA8b
+pG/11Vcx59y6rGJbx2xcbKIMXSAkHj41xBqXEaj8a1UTO5NB1FjucsuqddGH3tj3HWe0V24AUe/M
+c1RIPGN6I+HdImVm7VtCswYYCEj+Up++uFIJ8ayFfzj4bUHdFp0j5TQ++hWYcNSgr1g+0p58ai84
+dIeUVWi1s47h90lbKmXbcXAhaVbevjw8fSuLAFR9I153juSZoO2nek3J5Nuk5isAlpIIT2oIGxHv
+2Net9KOUWFBacdwwlcqWQ6JnhwiuJAFExJisg8N6Dta16UMn9sFuZhsATqO7nifup4elTJn5OZLD
++1rhzSrzrIJTtsaDoPpMz1gb+NG7sL21uBd4Y7au6FA7laon0mansdz9lrEsh2S14tZ+2pQytbYW
+NWtIAPv2rl7SRsTNbBJ8aDtW36UMn3Fg2HcespU2nUC5wMVBPdIWWk4s1+7to4nWklwrHAHnXJAC
+toJrfccKDuWz6UsohO+YLEf/AGU5b6UMnkE/hDYE/wC0rhi1WG7hC3UFxCFAlMxqHhNSPttq46Sq
+1S2kuqXAPAEyEjyHCg7Vd6T8omNOYLDf/WULOsDmzBMeyqzbYdidtdOi7SvQ2qSBpUJ+uufFLK3V
+rQAlKjIA5DwpdqeZJoOu+pjtlh8Rtqd/WTWUr1N2+zyo54rLiuP9NI/ZWUDLrop1YAgaTu21B5fw
+iv8AtXHrzMmNJ25iuzuuIytzLgUB3UtN/Hta5DW1J+jQRBZAMwTWBid441KlqTFZ2IkkpE0EZ7OJ
+3FeKY7xhMVK9l4CvOy24cPKgiCx5D41qWoMQRUuWSREDjNeFkapIoEEi0Fro1Q4EJAIRzBnf69/S
+lXrlpSrwtdnp1ksgtjgVSeXhtXpYT/NE16GeGw28qDxm4bLlmXez0hep8dn4K25eG1aBVsm00KIL
+obUmQjiZOx90QeVO2WmFFKFMjUTx1QPftTj2FggkBgDURBe3+ygi7z2Zxp/sVJSlTiS03o3QnfaY
+9PXjSeG+zNsuJfSky82RIJOkTq5c5G3OpP2JorCAlkd0GS5sd/t+6tnbW3aX2iW0KSFboS7Mj4UE
+Y8m2DKezUndrTu3BCtczw8K2U1a+03L6XG9Dpc7NGg93+by293hSzrLanCW29KTwSTMV4liOVBHP
+WiUL0ocS5wkpBiffSaWNztvUr2G86a9DMDYR40EX2BE6RNbJYPGNvGpMMA8q9LJ8B8KBghnfhSoa
+QdwmDTzsiTukVuGjEQAaBu22EwIpw0gTsK3S0rbu07tmVTPCg606nrShk5bihA1OJHn3xWVKdUtC
+R0bJUEgEPLB24nWr9kfCsoNOtVaOXeUEttNqWsplISkknStBP1Sa5NXhS9KdVncSAdXcO/hX0PxG
+wssRt/Z7+0ZuWpnS6gKE+O9RRyblgmfka2HpI/bQcDDDGAynVZ3Xac1Rtz/7Ui9hqVbtMOpE7yJr
+6ADKGWxwwlj4q++tGcmZYZWtbWEMoKzKgFqgnxiYoPn/APJrv/oufCvBhywf4Je3ka+hP4L4BEfJ
+jUep++vBlfAAZGGNT6q++g+e3yavj2S/gaw4a4Y+Zc/RNfQo5XwAmThjU+p++vPwWy/M/JjXxV99
+B89Pkp4meycj+qa2GEvT/Aun8019ChlfAASRhrUnzV99bHLeBnjhrJ+P30Hz2ThL+/zDv6Ne/JFw
+Zlh39E19Ck5dwVPDDmR8aw5dwX/49r6/voPnl8kvD+Idn+qa1+S3Z/gnI80mvoactYESScOaJPmf
+vrwZZwEGRhjIPv8AvoPnmMLdH8W5+ia9+TF7y0ufSvoYct4EeOGMH1BNe/g7gn/xzP1/fQfPA4cs
+H6Cx5RXhw9QH0VV9DXcsYAv6WGMn4/fSasqZdJk4Wz8VffQfPgWPPQfSsNlvOk719Al5Qy2RvhLP
+xV99aLydlk8cIY+KvvoOChZ4fKtSbgbbcONYLO030h7yMV3j+BuWNR/ce34+f316jKGWwSBhLIB2
+O6vvoOE7e0swiXRcSBtoAiYP7Yq69FHRxdZ9xt2zs1qtLW3QHLi5WmQkEwEj+keXoTXXyMpZcSNK
+cJYA4wCfvp5Z4DhNqoqtrQNEiDoWoftoGeRMrYdlDBk4PhfaezoMhThBUSSSSSAPGsqfSAlISOAr
+KD//2Q==
+
+
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: image/jpeg
+Content-Disposition: inline; filename="custer.jpg"
+Content-Transfer-Encoding: base64
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4Q1kRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB
+AAAAyAAAABsBBQABAAAAbgAAACgBAwABAAAAAgAAADEBAgAOAAAAdgAAADIBAgAUAAAAhAAAABMC
+CQABAAAAAQAAAGmHBAABAAAAmAAAAOYAAADIAAAAAQAAAGd0aHVtYiAyLjExLjMAMjAwNTowMTox
+MCAwMDo1NzowMwAGAACQBwAEAAAAMDIyMQGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA
+//8AAAKgCQABAAAAyAAAAAOgCQABAAAA9gAAAAAAAAAGAAMBAwABAAAABgAAABoBCQABAAAASAAA
+ABsBCQABAAAASAAAACgBCQABAAAAAgAAAAECBAABAAAANAEAAAICBAABAAAAJwwAAAAAAAD/2P/g
+ABBKRklGAAEBAAABAAEAAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAk
+LicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAaAMBIgACEQED
+EQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0B
+AgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpD
+REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq
+srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEB
+AQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFR
+B2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVW
+V1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrC
+w8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/ANNRzUiIfSlR
+DmrUUfrXOajEiz2qZYPap0jqlrOr2+i2RkkZDMR+7jLYJPqfanYCd4OOlQGe2ibbLcQo/ozgGvPL
+vxRqE6kSXsj+yfu1/JeT+dYsl55m7JUZ6kD+vWiwrnsAlgfG2eI/RxUhjrxlLhgMLKcdua0YPE2q
+2mDHfSMB2c7h+tFgueqeVimmPiuO034hKWEepW4APHmw9vqK7G3ube9tlntZUliboymiwXECCgpU
+oSnGPNIZEqgUVKIz3opAVUXDVajFV4/vVT8R6yNC0SW6THnsfLhB/vHv+AyaaApeKPF0WjK1nZ7Z
+L0jk9RF9ff2rzC71Ge7naaeZpZWOSzHNVZriSaRndyzsdzMTkk1GFLHABP0qhEhnc5xWjpmg32q5
+8oY9M96hs7DzHG4MBjJJGK9j8HW1ta6ckqqHVunHQ0BY8wl8H6hDGWOcjttNYtxbzWxIcHivoXWN
+X0nS7YzXh5I+WMDJNeY+KTb3umHUotOlhgkfYjkfqaLhY4ESZrb8Oa/caLqCMrFrZyBLH2I9fqKx
+Y4mkcIilmJwABkk16R4V8BmDy9Q1dfnGGjtj292/woCx2qjIB9aftp+MUvFSMYFopxopDKCDDdK5
+L4i2V9dWdlJb28kkEJcylBnaTjBP68116ctWjanFUhHzpit7SLa0t1W7u5lCk4AxmvZbzwvoGoOZ
+rnS7dpDyXUbCfrjFeZ3WkW0OuXEcMMbQK52RljtA9jTESWNvDqExntAJDu2YEf3R6/413Xhq7itb
+IW88MgKMct2zmvN47h9C1WCS1uzGGbbIqnPynsexFelLMloUMjF4pVBEh75oAvX50q7+RIXknkwG
+Pt6c9q0bu00e/wBEk0m/kht4pE2KrOAwPYjPcVmWlza2TSXTbWwMgep9K5zWdavNSmISzS4bOMjA
+x6AUhm3pHhfRNFVZLG3EkmOLiQ72P0PQfhWnITUVjJNLYQPOmyUoN6+hxUjZzQAwr7UgWpKCMUgG
+7RRRzmikxlGNavwDiqiDB6VegGQOKpCJygkhaM9GBBrzHxFp50+5la8tnO4DbIg6LnqP0r0S/v5L
+K1MtvaS3bZKbYcHDDqD7+3WvONT1u+1W/jbUSVtY3yYEGAB396YjCtLQXt1mOBhAjZDPyT7V3GkX
+bJbtp92pe1I+V8ZMf19qzta8T6dounx6dplhbySkbi/U4PIJP9K4y88Q6reYMk2yPOQiKFU/h3oA
+73VLC6trFrmCVJ7dTn5X6fhWLo3ih31i2N3Bi1iYqWjHTPc+uKg0zxCb+2NrNaHj75hOFI9wKs6j
+FF9hf7CotmjAZWTj659aAPUQVdA6EMpGQR0IpjCuJ8GeKVNtJYXshd4xuiKLyw7jH9K7SK4guGdI
+nBePG9Dwy5GRkHkUDFA5HWnMBikIwaKkBo64FFAPNFIZVQVi+IvEk+mSJa2OVnxueQKDt9AM1uov
+NedeIJkufENyySEAEIMHjgY/mKpCZaOri/hjg1G3jnhRtylB5LofVWXHP1zWpqF1pt1pg8+4jmlS
+PbA6o32lj/02ydpGOMjr+lcsXliHOJF7jHNQSyAYmiPyA5I9DTEQnTVimMwywHOD6VoafJHp13Hc
+pZW92gJIhnGUORiljcSJnqDUMTBQ0fdf5UAaFhANDj1STUbP7PJN+9EQHHlnJUKPTJNSW4G4jqCB
+1qKKzu0EeoXV0k0N0CqiSXc4C5HI9OaSNsXCIvCg4oAzNbj1O01iDVXhS3km+dPKUKpHTOB61b0j
+xNLYayl3JGBE6COWNOAQOhFMv7QGCa7N5E489kFuZCXTHOcdhWMGBJJ6CgD3BJEliSVTlXAZT7Gn
+AA1xvgnXJrxX0+5fd5SAxEjnb0wf0rshjFSMTABopwxRSGVgDn615TNCTNMrctHIwP516xzuGK8z
+1FBa+I7yJgNrynH48/1qkJleOZo0GMstMlDy5MKxsx6g8GpGUxSnb9084p5RZUyuFb06UxGfazPE
+WicFWB4B9Kk3fv8Ad68VDOri4BaNt443D+tEchKsfegDQsVQy3DjJcR/d7dR+Va9lDGLR/NkLOzE
+gAenTn6iudsJtmoMeoaMg+3IrbtpJVjXa+O/NAGK4Q3M5bG7ewz+NZqAZcdlY5q3bxXOrasba0QN
+LNIxUFgo7nqa1PCPhttZ1a6S7Vha2rnz9p5Yj+EGgDLsZ7i0nS6t5WikU5BHp6V61o2pxatp8c6M
+vmYxIgP3W9KI/Bukyu0stmgZkwkSsVWMepx1NcOss/h3Wp47K4WRUfa+4cSD6UmM9IGM0VQ0zVrf
+VLbzImxIB88Z6r/9aipAn3ZNcB41tGj1gzKMGVFcH3HH9K7pDzXJeNCrTRuzHem1AueMNuOfzX9K
+pAznLa7juUCSEJMvr3qZwydRx6isi4ty3zJw1TWksjrs3kOOoJpiL0twwXlCwHoKxri6WFj8pG/k
+A1fe6vIODtKmu1+Gc9vcazNHeRxyZh+RXjVuc8nJ5oA4zwxp91rmpSW1nGGnZMZbgAZ5JP5V6Cvw
+213Jb7TYgbcKPMbr/wB816pGsESARpGi+iqAKT7VCPlMi5+tAHisPwc8SLJk39hH/tpI+R/47XWe
+FvBeqeGtHnhlENxO8hlYRSH5vQDIHP1xXoSzow4ZT9DSvIO1AHiepeMNUWaWCBPsgDFWDrl8jsc9
+PpXMajqdxe3BuLt90zAAsABkDp0rq/ixpTWeuwanbDC3kZDr/trwT+IIrziS6uUcJcqGWgDpPDOo
+pb+ILctJxITGQffp+uKKyNFtjc67YxqdyNMrfgDk/wAqKljR64vB5rK8Q6DaapCtxI0qXC7I0Kth
+eXAGR3+8fzrTDYNQavKRol465DpEZFI6gryP1FJMbPMEcqxRhytNkwrLInDDriqST/ay3z4lB3A+
+tSrIWQq3DjqDVEl9h9oiDIC2e1V4H1KxvEuIGe2KHPmZxtqkbuazYtC5FVJ767vG/eyEr6dKAPQr
+P4k6tZRHcy3KKeGlXJI98VuQ+OdM1EB3nubN3IASWM4Y+gIzXksN4YcB49yjsank1L7Q4ZvlVfuj
+3oC57To2vQX8jJBdJujO0hnCnP0ODXWxC6dBuLL6ZFeP2/izwp4hsIodejmtNREflvdRICjnszY5
+OepGOtcvc6vqeh3Ij0jXbnyifl8mZth+maLDPU/i9Cf+EOt53YB4rtcHvypBx+leILdgDbICwrqL
+7TvHPiVo7fUTdTxp8y+dIAg9+uM/rXS6D8PbDTQs+pst5cjkJ/yzU/T+L8fyp3AyfAmiXHn/ANrz
+I0duqkQhurk8Z+mM0V6BI+FCqAFHAA6CiobA/9kA/+EMRWh0dHA6Ly9ucy5hZG9iZS5jb20veGFw
+LzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi
+Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg
+NC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5
+LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgog
+ICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6dGlm
+Zj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczpleGlmPSJodHRwOi8v
+bnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUu
+Y29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50
+cy8xLjEvIgogICB4bXA6Q3JlYXRlRGF0ZT0iMjAwNS0wMS0xMFQwMDowNzoyNyswMTowMCIKICAg
+eG1wOk1vZGlmeURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpNZXRhZGF0
+YURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpDcmVhdG9yVG9vbD0iQWRv
+YmUgUGhvdG9zaG9wIENTIFdpbmRvd3MiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHRpZmY6
+WFJlc29sdXRpb249IjIwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIyMDAvMSIKICAgdGlmZjpS
+ZXNvbHV0aW9uVW5pdD0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSI0Mjk0OTY3Mjk1IgogICBleGlm
+OlBpeGVsWERpbWVuc2lvbj0iNzU1IgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iOTMwIgogICB4
+bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Zjg2ZTcwZTQtNjI5OC0xMWQ5
+LTllM2YtZDQyZjM0NjM5ZGJiIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ1dWlkOmY4NmU3MGU1LTYy
+OTgtMTFkOS05ZTNmLWQ0MmYzNDYzOWRiYiIKICAgZGM6Zm9ybWF0PSJpbWFnZS9qcGVnIi8+CiA8
+L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+IAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7/2wBDAAUDBAQEAwUEBAQFBQUG
+BwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUF
+BQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e
+Hh4eHh7/wAARCAD2AMgDASIAAhEBAxEB/8QAHQAAAAcBAQEAAAAAAAAAAAAAAAIDBAUGBwEICf/E
+AEIQAAIBAwIEBAQDBgUBCAMBAAECAwAEEQUhBhIxQQcTUWEicYGRFDKhI0JSscHRCBVicoLwFiQl
+M5KissJDU9Lh/8QAGQEBAQEBAQEAAAAAAAAAAAAAAAECAwQF/8QAGhEBAQEBAQEBAAAAAAAAAAAA
+AAERAjEhEv/aAAwDAQACEQMRAD8Am8Hl60dEGN+tAbnalEBIr5+PUIoGcmllXPSgkW9LxxntVBET
+2pVY/Y0tFEe+KcpFRDYRDHSjrD/pp4sdHWIUwMHhwOlJcpXr0qVeEEd6aXCxxRtLK6xou7MxwB8z
+TDSSpjelAtQFxxvwjaSmGXW7dnH/AOtWkH3UEUeLjjhGXATV0z7wyD/61cNTTDbG1AKcdKjBxXww
+RznWbUA9Mkj+lSem3+n6lE0mnXtvdopwzQyBgp9DjpTDXGQ0mY6fFD6Vzy8DpTDTMR7UOT3p0U9q
+KF3oaasm1F5PSnTr3pMAb1FJrGwGRRuX1o64xvRtqYG5X2oyp7UpgV1agCKAQSKFKZHLQozUEoNL
+Rj3oBd+lLxJsKrTsaEmnccW+d65Ehz0pzGp9KuI5HHv0pdU7Yrsa+1LqmSBitSAsabdKrXiLxfb8
+IabG6xR3V/O4EVsXweXuxx2/maX4w410PhmJknlWe7A2gRhkf7j2/nWCaxxK9/qs+ohea6mbJuJd
+2A7BR0UDoKuJaud54gcYX0Jk/wC46Lbno5T4sf8ALJ/Sqlq+sx3j/wDiOoahq7A5HmSFIwfYf2qA
+uLxpn55ZGkf1Y5ptLPynAxVxEsdQXcwWNrCvuvM33phM8jS+Z5rZBz1pn+JxuWxRGvxzbDI9aYbE
+rHfXgGFuX/T+1OrPXdZs3LWt/NCx6lTjP2xUGl3EO9HN3Gds/ah8XnTvEjiiywZLsXKj92UBs/ff
+9at+heL9jNiPV7FoG7vCdvsf71izzAocGkWkyD1phuPVuh63o+uRc+mX8NwQMlAcOPmp3p8UA7V5
+Gs725sp0mtLiSGRDlWRiCD7EVq3Afi5KrR2HFA8yPoLxB8S/7x+8Pcb/ADqYNfkA3pLlpWKWC6t4
+7m1lSaGReZJEbKsD3BoKtYWEwvSjhQe1GA9aOoFA2K4JoAb9Kdcq5zQEagnYVKpALkHrQpyAB0Az
+6UKioTlIpxCMikyMiloU2qwLRfMU6iGe4puin0p3Ah6kYHf2rUQsgVFLuyqoGSScACsj8TfFURPJ
+pXDEo2+GW8Hf2T29/t61D+MHiM+pTy6BoMpFhGeWedT/AOeR2B/h/n8qyiSQjuc961ImndzdzTyN
+NPK0sjHJLHNNZLk5601klY96SZqrOnRuipJApKWdn70iuTTqztWnkUAEg+gqp9JRrJIQEVmPsM0s
+LG9dsC2lyenwmtO4D4PZ3jmKeYp64GQPnWoDw8WeBZIrZUbqMj+dTVx5l/yvUACfw0gx1yKQaOaM
+kNGy/MV6UuuALhIyGRDj2qj8W8E3do3mCDIJ3wOgppjICzD2owmPepnWdKaAtlSCO2KgGBBwe1VM
+OBJnYUA2BTdTR1zQaD4UceXHDd+thfyNJpE74dTv5JP76+3qO/zr0PHiQB42DKwyrA5BHrXjtc16
+l8Lo75OBNIF+GE34cYDdeTJ5P/bisdRqLGq980cLjpXQpB60dQc1lonymuBd8UsFocveoogXahSm
+2+1CsqglI6E05twu1NU69KdW/XFaiHcYXOKznx44xOi6QvD2ny8t7fITOyneOE7Y+bbj5A+orRlK
+Rq0kjBUQFmJ6ADqa8l8ba5LxDxTf6vITi4lJjH8KDZR9ABW+WUY03KMCkC5cnJorHJoA4rQ6elBI
+2kOFGTjNGUcx32qZsdAubiESoC+eyjcUQwgspVTmZod+ilxk1a+EtC8/UIeZ+WOVSSmNw3/WKjG0
+60tpbeCZJTLIQVKHIO/Tb5Vb7Vbhbxr+1jES82Vjkckuo/d6bHHr7VBtXhrp8ttaxhreGKFfzcw/
+X1rTbdomQBQCpGxA61ROFW/EaBDcsQrcgyo3GcdKnLNTHmWV0VTjdTjb5f8A+UaieFjHK/mHBQZJ
+J6Yqr8XXPD8UDie9tkC5BzKPsKrfGnFGu6jM2k8OlxynlaY/EgPYbd/sKyjTOFeK+JeJmtdUSRgk
+gDM/wDrjI9fuaiadcY22k3EL3MbRgcpCuB136VjOoIou2ZNlJyK9NeKfhUR4fwS6aZDLYEtcIDu4
+I3b6YrzJdczTNzdtvtViEQoNKwxs7qiKWZjgADJJqS4b4e1fiC/Wy0ewmu5m68i/Co9WPQD3NejP
+C7wtseEuTU9UaK+1gj4SBmO3/wBuerf6vtVXFO8K/CR08nXOKoimCHgsGG59DJ//AD9/Sti5eu1O
+5zzHem/b3rPqyCgHNGUUXau5qK6a4a4DXcZqApOxwDQoxWhWK0gsdDinEGAelJMgxS0CgVqMmHHM
+7wcDa3LHs4sJsEdvgIryU29ez0tobmNre4iSWGVSkiMMhlIwQazXjHwGtLsyXfCt8LWQ7i0uCSny
+V+o+ufnXSMvPGN66BVl4m4H4p4clZdW0W7hQHaUJzxn5OuR+tV4Ieb8pG/pVE5w/odxcSiaWNhGm
+CB6g9/lWn6XprWmnk5jifPQ9MHPX5AVQtK1trTTrlDkyOnlg+m2/9BTF77W7iN3Ms5UAFiW6elRF
+6gsNQZ31aOS3gnjCrIzDnJU9cdh9KXKZto52hYWkcgAxj2OGx03qF4D1qOyv4oNSvVCznq+8cfpz
+fP8AStj/AOxtla6VJDaXLXMDp580gwU5zvzAg7/agsXBN7pqWqWYYK4QOQTsexq4wjTrpArMvKw+
+1ZXw1oyxW8byOfPyc4bOKmJWvLRSPMbH5gcYxmoq5axacP2ulSAzx28e55om5Wyfl3qJ4Jt9Gtbt
+ruCGeaRsBGlkJyM5O9VaOG8v7oJzFhnY9Rg1oGh6BIlp5WQVC9em+KKstmq3isvLEI5MgqNwfXtW
+O6j4EcH2/Fd7qF811PBNKZorQNyRIDuVyNzv7jtU5xBxbxhoDfh7Hh6wEEPWV7gF3/2ov9a5oPE2
+rcUomoXUFvBaRgoAHLSM/cEY+ED077VUSem2NhpNmLLS7K3srdOkcKBR+nU+9CQk96OxBpN8YqVT
+dzSecjpSjgE7UFUBTtvQJMoAz61xRntSpAoBRisqIF36Cjcu3SukUBt1oOcu9CjKAaFYq6r2AcZI
+pxEvTem4SnMKYrUZPrUbipa2bG9RNqN6koelbglElBXBGR6Go/VLezNlPKmnW0kgQkc0S7n7UvET
+iofjLVrjS9PDWyKZHzueigdT+o+9VHmziHSiuu3d6UjJmdZ1B/KBJvnHrnI9iDUqAlxZLplxp6lF
+AdnXClhk7j164ovF/Pa6v5v4ZppJCWl5JCNic8uOg3Pak5+JntoRaS2DRyQrkCQbgEk9vcn70ZV/
+XtFktLCS4ihcRscDKbjpt/Or34IW8eraeLS4vrkJbyeYsAlbkLDcZXoaoGtahf3/ADT80nk8u4J+
+EbdKmPCrU7jh/VI71gTaSEeZj933+VFbglytnfGKdVUMxOMGpaSS3uYEBQZGQDmm2o2FvxBpy3tn
+MAWXmSRNwfqKqY1HUdKumt7wFcbFlG30qYrQOHohBdBlwA24wOm+f+vrSfGfF8nOdN0+8MKQjMrq
+uSx9B/aqmeJkS2llRsyKhKgnHNjNQcU97qzNGzRKZy3MScg/P5bUDbiHjp7GZFt9OlupWBBeQkkD
+tnHqMnFO/Crij8fxBLbRwGOKaIyEZ3LZzzH7kU2nsI9Jljt5ktnuZRhZMZ39fUbZH2rQ+H+GtN0x
+zeW9nDFdSKFkdR1wP0+lBNk5FEkyRSpGBik2HzqBHFGA964QAcZoZHTNWtDADHWuMpxkGuLgnFK4
+GOlZoR5cbmisTncUs4xtSRHM1AF+VCjqu2TQrNVAxg9KdQj1pJBvTmMHpWoyc2y5bYU+jUimtuu1
+PIx9a0HMewFR/E1g2oaXJChxJjCt6Zp+uwBpxFvjeqjzxfF9O1r8XeIwYebHKh/ccMcbf7Sp981A
+Xer8P3Mt3eXrPJMIisUUSgZboC3etw8W4OFE0sXWs31tYXiDML8vNJJ/p5Ruw/lXn6/01dXuGvtO
+tMO8uJQgPIpYZH3wftREUbt7pvwFkk0dtLgSK+Pi9/b9auOkaekdqEIxt27Va+A/CfWbuwTU4tPE
+kL5BmdwoGOu27Y+QNPf8hcSyxxhAiNy87HkUn5tj+9Ax4M1274duxDcRmfT2OSq9Y/cf2q7cQaVb
+a1apcWEsTLIAUYbA59ao2qXXDehoX1PU43cDaKE4P1JGfsPrTXhjiXUtUMknDn+W21ssn/kTO3O5
+9cbkfeinXEXDuraYhafSJJ0XOJIGz+lUK/4ku9PZ1tw6GPJIdcEe1bXJxWLfTHg1WExl15SVBIBP
+p9axbibSdT4g1aX/ACyyldJGCmRl5VC+pJoENB41upOIrK/1W1W8tYHBeHJGR6/Mdd69P6Ve2Wp6
+bBfafKJLaZOZGH9fevMs3DtnoFsJNTW6ZCcGeJQVQ+46/fFX3wr4ls9Im/Cxail1pc7ZYdHgb+Ir
+6euD70I2Vl+HNJkUvsyBlIKkZBBpJhRSLYwaKuD1FKkY61zAA6Vm0EQKW2pZeXuKSU4pQnbaoorh
+TSZ5RR96IwqDqnOcChRosAdaFZqocYB6UrG2/Sk1BzS8Y3rcQ7tj7U9i3ppboaF1r3D+l+bFf6xY
+296q/s4ZSTgnozKu+PatodalfWmm2Zu76dIYV/ebv7AdSfYVmPFnirO4ktOHoDD+7+JlALf8V7fX
+7VfOEtW1K0sNSng4j0Xi7VrlwbeGS9NmkSY/IkfKwXt8+5rJ9S401nT9TOleLPAyzo7HlvIIBDOo
+/iR1+GQD2OKuMqNftPqd1JPqM81xNJ+aSVizH61evCS+s40l0q5jhWYDy/2oGCMgxsc9gwAPtUzx
+F4ZTDQouJeGmm1TRp4xMvNHyzRoRnJHRh7j7Vmmr+ZYTW+q2p/aQNhwD+de6mojWPETxMvdE0KG1
+1LmFxFzJDaKRGWYE/E6rsFHQD296wHiDjbiTXJna71KZUb/8UR5Fx6bdfrTXiC9u9b1iW/uA3NId
+lySFHTApyugO9mLiHJI3xVVXzzN8TMSfc0406/u9OnE9pO8TjuO9WCLR47q3DACGdR3GVb2P96tX
+iN4K8Q8G8LR8QXV5YzxYX8RBEx5oS3pnZgDtQJaD4j3z2fk6laC5jGzSR/mX3q26JxClzah47kTI
+35SVwe+29UfwY0NNQur2/uA4ihVY0IPVzv8AI7Dv61aNStX03U5YT5fKeZ4+VcdOQ7j1xn7VApqL
+/wCZWNzbSr+zmjZcHsazC202WwnVortlukJ/J0BHbPetK06QvEzEHIkI+hFI3fh1+O4e1Pia31uO
+G5t5ARZsm7DAyQc5/Sg0Dwd4ztdU4Y/C6jMyX1rJycnLn4NsfYnGPf2q6aVq2m6tC02n3ccyI5jf
+BwUYdQwO4Psa8kcLa3LpGssWeTyXfEgU4Jwa2vWLxdQ4e1oWGnabc6Zcwfio721YJcQuF5v2i7MQ
+G2BGwzvRZWsOuKTIIqgeC/FjaxpB0q+maS+tF+FnOWkj9fmOn2rQGO9ZsUl0augkVwjfOKAB9qiu
+52pNmzttRiDRGU5xQGQ9xQrqKcbihWaIxF70snWklB74paJcmtQRXHHEq8NaA90ih7uU8lup9cbs
+fYf2rDLvWJ9QvGur6WWWd2y7Mc5q9eOV60Op6XbqhYLA8jHtu2P6VQYruAkeZFsepxW2KfQTI35G
+Gau/DvG99aWo0zW401vR2xz211livujHdSO3p7VSLdrGX9/lPTI9KXaCXk/ZOjemaDW9M02NpbTi
+LT/FjVrPhaxbzf8AL7mTnmgYDaH4iQVxtuDt69aoviLrXD+u67LcaLpE1pBJnzWcgLMf4gmAVPr2
+/nVMnlBcRXCBJ13QkbA06065FxCWcYdG5WT0IoGI0eISMgUe2RR9OJt5Ws3xjtt1qUB518zG6HtU
+frScskVygIwd8UHZ7copUCjaxe8bcR6Yuj3V7NeW5OUiCc0jY3+ZG2aXibmUSb4YdRU3wbxqvA2q
+zak1i9z5kBjDkcxjOf5f2FTQXwqnWz4eOmvGY5orhxICuG5tuvv2ol9rcOs69d+UcpZOsRYfvFld
+f54qNtOJ5+Jtf1XVpMwySzI/L3/Ly/8A1FHg0aLS9aklslKw3flM8Z3AbzR09tzVEjYcoeULsGAc
+CmvEOnarqt1aWelreSySK3PBbgnnUYO4HYUdJkSWAKd+XlOPepJ+J9S4VhttZ01Od1k5JEJ/OpBy
+PuB9hQZLxHpk+n6sySQvE2fiR1wVYdQQad2F5eWkb/h5XiJjaNgDsVbZhj3qa4o1W64nu5tVvIDF
+M7c2CcmohlBJ2xlaB9wzrl1w/r8Go2ZUyxDHK35WB6g16R4T1634i0SHUYF8st8MkZOeRh1FeW/J
+WRw2enU1qvgnxTp+nR3Gj6lMtuZ5Q8EjbKSRjlJ7dBUpK2QkD0rg3zRGIJ2O1GSstlFwAc0VsA7V
+35UXBzUo6p2oV0KaFZVGKm9LxKM9M0T0NdklENvLLjPIhYD5CukRjfi1qCXHF88eQY7ZFgH2yf1J
++1VyGOORMYGKbNK97JPLcMWkkcyOT3J60haieGQqmeXO2a0wkJ7FGHwkKexBpBLqeycJOTy9m7U/
+tJo5sebGMinM8VpLGedMqN996COvF/Hw+ZFyyOo6A9fl71H2NwEvg2cGQcrjpuOmf+u1PxZRK7SW
+NyFx+5UfqyK3/eEBW4jPM4/jH96Im7d/jOGG/auXo57RlO/ptTHS7xJ4lIOT0p60mQ4GxopppdwG
+QwOfy7UtIshlFuInmD4AULnOegqOGI7wMo2JwfSn8mXQgNysR8LZ70Fp4h8ML/hWK01C51Czb8eq
+q0ETEtGx3HsR7im96wjWJFlVmjKq3fmxlz/8ahtK1TXdc4jjt9cvGljit2EQXbmIAAz9KXnYWVq8
+apjCSSDfJGwTr/yNAiMlFy/7oORvjepDihUk0CGFZFjDToDI/wCUZOMn7060vT7OXQVvpLadCwZY
+wzBEkPL8JJPQBs5OcHp61E8Xs3+TwW867mQcy47gGgl+P+CtL4d0qyv9L1w6glwgLq6gEZHUY/lv
+Wb3MmG5c4PKR9akYbaRinPczyRpuiO2QufTNRuqxNHdKvYsD/wBfagPFtEAegFFkbzcKq7A9aDDK
+igx8qHb8zHCiiPQfhJrX+bcJQiWQyT2rGFyeu3Q/aropX0rzp4e8YS8JXqxPCJ7O5I89R+ZcfvCv
+QOnXttqFlFeWkgkglUMjDuKzY1DvPegGoLRWqNFFbI7UK4g2O1CshgAcelGRFkUxt0YEH610jAo0
+LKD71qDzaI1g1Ke1JB5XZM/I4o1xGY2Db4FLcWRfgOML9OgS8kH0LH+4p75AmgDcwwa2wYYLplNi
+BtiixzzRH4gWA7EVyM/h5miz8NPIRHMOUAADvnFENpYrO7X87wSnoU/tTC80bUoR5kEgvUG+xww+
+h/pUxJawr8cec02FxJE27HlHrQQOkTG2lliZCuGyA2xHtUxFKzfER9qYaxBLPci9tD5g5cSRj8w9
+x6ijW8+YsZHTegPLvuMDBp2JAIlbbcY6U2tpPMV8/LOKAceXjB2oJ3hCeN9Z3UMywvjbp0rt9bmS
+OeeQZeQQrGN8AkFjt26ioLhed4uKIU5iOdXBH/GrLqDfEkaDc3BJ/wCKqv8ASirgbmxtLENZ6W7x
+ae37cMQFYc+Qq8wOSAASfXvvVP8AEdndhLJzeZ+LfmBbJyc7VJPdW0ziW6hilkAzzFFBHyqJ8RLt
+brTLec8gkacFiFwT8JoIK2lXlBYbVE8RMFaKT19/ejG5wAq0x1uVnhiB3IbagdwqHVc9MUSP9vdF
+v3E+FaPptvdX7Q2NhDJPdzEJHGgySTWq2/hOukcMvd6teSC/EZZUhwY1OM4JxknttjrQZqYlDGRx
+sOgNaX4M8Sw2Usmi6hOscUp5rcucAN3X60Xh3wf4k1e1ju7ua105GHMsUuWk/wCQH5flnNUnUrFo
+NVms5MB7VzGeX+JTg4+1QemVwRkYxRGJzWd+H3HtrJDBo+sytDdKOSOdz8MnoCexrRRg4IIIPTFZ
+aHRtutCiEkChWappJkLjJrkaEnJo8rCgjYxvW4MN8XrNYeNr3GR5ipJ09VFRWg3XnQlMnmTYg1dP
+G+z/APGbO8HSa25D81J/oRWXQztYXvm78j7PVYqw6hFGSJOXp1OKbpzKOZExipGIxXduGT4sjam3
+lmJzG+du4NULW84YBZCPfaiXtosykxt13xXFCEEDOR39aNDKUk/KceuelEVq/gu4JcRrICD13okr
+XBhE8x+IHDEdSOxNW55oHT4lXPqKjr+2je0kjHLlxjtRURZNiPYjfvXWbAIYCkbZuVeQnp1HvSU8
+yDIBG+2KIW055DxDp7W7AP5uCcA/Dj4v0zVnhdpZIWOxMZkP/Ni230IqrcMRo2tyTqCTDbyMBnbJ
+HKP/AJVaLGRBcXDkhkiYRqADsFGB/KosSyxQOg50JPuKqvH7CJrG2RsjDyN+gFS9veTT3W4+HPrV
+X48uObWljBwI4gMDsTRTbhvTbjXtfstHtZooprqURh5Gwq+5qa8ZeCLjgi6s7d9VttRSUkiSIcpV
+h1BXP61VrW4ltikttIYZo2DI69Qw70e9utS1/VrSPULlp5ZZVXJwOpAJrSN7/wAO3B6Wul/9qL5R
++JuU5bcMu6Reo92/lWo2GmS6hqBv78AJGSLe3JyFH8Te/f2+dNOHEkt+FtOtRcRmNY1AePBGw2pT
+U9RFoHSW9htI+txcu3KIl74z1JOwHr8qjUTkY/Fo0Vs7RW6bM46k98e9Y54+No6alYWtjbRw3MMZ
+82RVwFQ9Ax9Sd9/ep3WvFzQ7G3FhollcXap8IkJ8tSPXfcn3xVP408QdP4k4Yk0M6GLbmkEvmGfn
+JcHOTsPehqhpb2kjiTl53H72dqv/AADxs9hyaZquTaDaObmyY/Y+1Z000iAsRGF6DkJOPnSE1xME
+ODzKR2rNiPTqyxTQrLDIskbDKspyCKFZ14IX7XGgXFm0vP5MpIBOcA9PpQrNaXlmwBtXRkkGkQwz
+SqZyKSireMVh+I4TivAPitJ1JP8ApbY/ry1h+pW3OTgnHbevQPHsN3qOjwaDYcguNTnEILdAoBcn
+/wBtYMRzKFIww2atxmmGl6nPps3lOeaPO4qz2t5aXyghwD71WLy18zJC71FwzSW8uCSKrK+XFpyn
+nifJPXemp8xdgjZ+RqOtLucqG5+YEbUs2pyoCWJoHEqyn8sbkkdlNRV4uoKwPkuvtykVIwcQRK2G
+yKPNrEEzpySFGXpk/Cw9DRVT1qeW1lWQoyCUZx03HWo1LtpG3JrdvBySz1fxAtbDUeG7PUomVjI8
+8SukAxs45ts5GPXrXp+z4b4chAaHQ9MjI6FbVB/SkMeG/CzTdS1jiRNOtdPu5TduiGZISyRKrcxZ
+j6bV6l4a8FuDLSxWO7hvb+U7vJNM0ZLHr8KYx+taqkcUIVI41VewUYArsigSZGKuKoKeEHAMJDpo
+rhvX8VKf5tTW78DfDW+na5udDleV8Zb8ZMOnyatLyCu/pQQgDqKDNbPwJ8M7W6juU4e8xozkLLcy
+un1Utg/WlX8EfDsasurW+hm3u0bmUxXEiqp9lzj9K0gkAda7nK7VRVZeDdIeJwqzwzH8s6vzPGfU
+Bsr+lecPGPh/ifh/iBbLW9Sl1GxkzJY3LfCGGdwR0DDv8/evWb7HOdqpvjLwsvGHh9qGnxIDf26G
+4sX7iVRnl/5br9amFeQiOQ4yCPbrTeSLLl4zv1371DQX8yA+YCGHX2NBuJCshR0UgbdKiHdxd8jl
+dwfSm348xnm5Oai/ibe/bflRj0P96ZXHNBLyMpA96GtG8G9cS34rWDIWO9Qxuv8AqG6n+n1oVV/D
+S3a7490qNCwVZfNbHooJ/pQrnZjUejBjPSl12IG1Nhkt0pxGD1NSVcQ3F17HpeqcOalNJyRRaj5b
+tnAAdGWsIlYJql7CSMpcOvX0Y1vvG3DZ4q4Yn0qO4W3nLLJDKwyEZTnt7ZFYxxfwJq3DV9LNNfR6
+j+xWeeRFK8nM3IOvXfG9bjNRbJzHAxnGMUwurJJVbb608t5gFywJJ9KcZXBBGRVREabM1rP5E26n
+cGp0Q288PMNjTC/tkki5kwCB2613SblsCF2APT50AvNMj5TIuB3z3qNSMBvi7Hap65TbP9dqirhV
+QFsYOelNMW7wn1ltC4uspjPy28kgSTmOwB6H23r2Fpl+rwhieY8ucj+9fP1794mJBAxWhcEeJHF1
+vYCCDU3Fra/xgNk9lyRnHtRXtA3S8iuCppvd38ceDg7HtWD6B40W7Wwe9tQZlHxRBsBv9p7VcND4
+80ni+ynbTGkt7222ntJtnXPQjsw9xV1V8XX7U8o5XO5H8v70dNcsOflZ+VvQmszvG1Yc7RJzxk5y
+N+tIQX8sbjmkhBO551II+9T9GNfTUbaTHJMjfI04S4UjGay+zvp5CpJBP+kbCp2y1GRByFmPfc9K
+foxb2lDE7/ShDIomUZ2OxHzquG8nZQyinNpdyNgEYYGmmPH3jLoFpo3iVrulEGBBcmaEqNuST4wP
+pzY+lUu60i2EBImVpPbvWwf4z7Q23iBpeoRLgXenAE+pR2/owrCo7iUOAT9KrIvJJbyfDkYNSkFx
+Hd24jlxzr+U0QeVdDHMFIG5NMJk8p/gl+1X0aj4Eaf5vFNzeFdrW2IB92IA/QGhVn8BdNntOF59U
+uYyn46UeVnqyLtn5Ek/ahXLq/Wo0LlAbY0dAc9TSJOD70ojjY1iNU7iyM4yKqfF8IvNa1KyfcT8N
+3DKD3aN1dftirVDKp9Kz/wAbLq60pdO1ywmWN+SazfftIvp8s1uJWNluQq/NsRnrTtJkbBUjGKj7
+OSOeE2rN8QGVNNj51jNljzRnvW2U7JKrKQoyR1phM/lSK64z1NKQOsq8/MObrmkJjzty8h9M1BOW
+E63UIQMOalZdBkul+J8DtvVTE01pLzwyMuPQU4bi3UUQJkEjbJFMNSl1wzbwjnurxQvcLTC61OGO
+FdO08IkS+nf5moDUtbvr5irynHtSNoxiIbO9WRFnVWEBfm3A9abadJxE16l3pN7cWk8eyyxyFDj0
+yO3tSNpqSD4ZcYPWp231e0t7blgYA43FBYLbxG474Zt+eW/ttULYDrOhJP1BGftVguOK+MNV4cte
+I7LheG4sp8q01rO+Y5FPxKy9j36dMVlVzfSX1zsMhdzWm/4fePLPhniSTh7XJUj0bWCFLufggnH5
+XPoD+Un5HtRZVq4J8Q+GDpbza/FrMFzAc3Kxwq/kL/ER1K57gbbVpPDHFPAuuMo0jjbT5XbcQzv5
+T/8Apf8AtTTirwj0niC9N7a6oulXirmKSABiT/qGd1x2715q8SOAZ9D4gm0y4jWw1INlMHFtdL2a
+Mn8pP8JqZFe1raxkKjlaGZT3RhinSWLKdosZr532mu8UcPXLQ2uralp8sZwViuHT+Rqaj8XPEeKE
+xJxfqnKRjeXJ+53rWJrYv8cd9YC94ask5Wv44pXch90jJUAEe5B+1eaDKSMgkGlNV1HUNVvZL3Ur
+ye7uZDl5ZnLMfqaluDODeIeLLvydHsHkjU4kuH+GKP5sf5DJ9qvgghI4YkM2/vWreEXhhfcQSxaz
+r8Ulto6kMkbfC917DuF9+/b1GkcB+EPDvDix3mphdX1Jfi5pF/Yxn/Snf5tn5Cr9PP22A6Vi9Lhp
+NHFEkcEEaxRRqFRFGAoHQAUKTnkHPmhXOqTcnNBGNChWI0VjbvgVnX+IiMtwbZzKQDHeA/dWoUK6
+c+s1g0N2/mrMuzKas0YS/tQ7LguvN8qFCt1kwV2tJ/KU5XNO2cqSQBkj0oUKBheFmzvudyah51Oe
+tChViVyOFQfenCxKRvQoVUJSRgHrSTMQ2ATQoVYsOIZ3gBAJpve3Ek5BJ70KFIJXh/i/ibQZEl0r
+Wbu2KkEKJCV/9J2q73Pi3xPxRZ/5JqENhNPe8tsLqSLLICcbD60KFLCM+4gDw63cWUkjTpaSNApb
+qQpIrWPD/wAMeG+IuDLW/vWvYbqUNl4ZRjrtsQaFCs9XIs9XHhvwa4M04CS9hudVl5sg3EnKg/4r
+jP1zWhW8dvY2kdpZW8NtbxjCRRIFVR7AUKFc9tawSSViDTSRsnehQqKbyjLbUKFCs0j/2Q==
+
+Cheerio!
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1--
diff --git a/lib/tests/testdir2/Foo/new/.noindex b/lib/tests/testdir2/Foo/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/tests/testdir2/Foo/tmp/.noindex b/lib/tests/testdir2/Foo/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/tests/testdir2/bar/cur/181736.eml b/lib/tests/testdir2/bar/cur/181736.eml
new file mode 100644 (file)
index 0000000..56255c4
--- /dev/null
@@ -0,0 +1,42 @@
+Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail
+X-newsreader: xrn 9.03-beta-14-64bit
+Sender: jimbo@lews (Jimbo Foobarcuux)
+From: jimbo@slp53.sl.home (Jimbo Foobarcuux)
+Reply-To: slp53@pacbell.net
+Subject: Re: Are writes "atomic" to readers of the file?
+Newsgroups: comp.unix.programmer
+References: <e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt.googlegroups.com> <87hbblwelr.fsf@sapphire.mobileactivedefense.com> <pql248-4va.ln1@wilbur.25thandClement.com> <ikns6r$li3$1@Iltempo.Update.UU.SE> <8762s0jreh.fsf@sapphire.mobileactivedefense.com> <ikqqp1$jv0$1@Iltempo.Update.UU.SE> <87hbbjc5jt.fsf@sapphire.mobileactivedefense.com> <ikr0na$lru$1@Iltempo.Update.UU.SE> <tO8cp.1228$GE6.370@news.usenetserver.com> <ikr6ks$nlf$1@Iltempo.Update.UU.SE> <8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk>
+Organization: UseNetServer - www.usenetserver.com
+X-Complaints-To: abuse@usenetserver.com
+Message-ID: <oktdp.42997$Te.22361@news.usenetserver.com>
+Date: 08 Mar 2011 17:04:20 GMT
+Lines: 27
+Xref: uutiset.elisa.fi comp.unix.programmer:181736
+
+John Denver <jd@clare.See-My-Signature.invalid> writes:
+>Eric the Red wrote:
+>
+>>> There _IS_ a requirement that all reads and writes to regular files
+>>> be atomic.   There is also an ordering guarantee.   Any implementation
+>>> that doesn't provide both atomicity and ordering guarantees is broken.
+>>
+>> But where is it specified?
+>
+>The place where it is stated most explicitly is in XSH7 2.9.7
+>Thread Interactions with Regular File Operations:
+>
+>    All of the following functions shall be atomic with respect to each
+>    other in the effects specified in POSIX.1-2008 when they operate on
+>    regular files or symbolic links:
+>
+>    [List of functions that includes read() and write()]
+>
+>    If two threads each call one of these functions, each call shall
+>    either see all of the specified effects of the other call, or none
+>    of them.
+>
+
+And, for the purposes of this paragraph, the two threads need not be
+part of the same process.
+
+jimbo
diff --git a/lib/tests/testdir2/bar/cur/mail1 b/lib/tests/testdir2/bar/cur/mail1
new file mode 100644 (file)
index 0000000..56808c6
--- /dev/null
@@ -0,0 +1,38 @@
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "John Milton" <jm@example.com>
+Subject: Fere libenter homines id quod volunt credunt
+To: "Julius Caesar" <jc@example.com>
+Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+x-label: Paradise losT
+X-Keywords: milton,john
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+OF Mans First Disobedience, and the Fruit
+Of that Forbidden Tree, whose mortal tast
+Brought Death into the World, and all our woe,
+With loss of Eden, till one greater Man
+Restore us, and regain the blissful Seat, [ 5 ]
+Sing Heav'nly Muse,that on the secret top
+Of Oreb, or of Sinai, didst inspire
+That Shepherd, who first taught the chosen Seed,
+In the Beginning how the Heav'ns and Earth
+Rose out of Chaos: Or if Sion Hill [ 10 ]
+Delight thee more, and Siloa's Brook that flow'd
+Fast by the Oracle of God; I thence
+Invoke thy aid to my adventrous Song,
+That with no middle flight intends to soar
+Above th' Aonian Mount, while it pursues [ 15 ]
+Things unattempted yet in Prose or Rhime.
+And chiefly Thou O Spirit, that dost prefer
+Before all Temples th' upright heart and pure,
+Instruct me, for Thou know'st; Thou from the first
+Wast present, and with mighty wings outspread [ 20 ]
+Dove-like satst brooding on the vast Abyss
+And mad'st it pregnant: What in me is dark
+Illumin, what is low raise and support;
+That to the highth of this great Argument
+I may assert Eternal Providence, [ 25 ]
+And justifie the wayes of God to men. 
diff --git a/lib/tests/testdir2/bar/cur/mail2 b/lib/tests/testdir2/bar/cur/mail2
new file mode 100644 (file)
index 0000000..3799f30
--- /dev/null
@@ -0,0 +1,14 @@
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Socrates" <soc@example.com>
+Subject: cool stuff
+To: "Alcibiades" <alki@example.com>
+Message-id: <3BE9E6535E0D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+The hour of departure has arrived, and we go our ways—I to die, and you to
+live. Which is better God only knows.
+
+http-emacs
diff --git a/lib/tests/testdir2/bar/cur/mail3 b/lib/tests/testdir2/bar/cur/mail3
new file mode 100644 (file)
index 0000000..646365e
--- /dev/null
@@ -0,0 +1,34 @@
+From: Napoleon Bonaparte <nb@example.com>
+To: Edmond =?UTF-8?B?RGFudMOocw==?= <ed@example.com>
+Subject: rock on dude
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+Fcc: .sent
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Le 24 février 1815, la vigie de Notre-Dame de la Garde signala le trois-mâts
+le Pharaon, venant de Smyrne, Trieste et Naples.
+
+Comme d'habitude, un pilote côtier partit aussitôt du port, rasa le château
+d'If, et alla aborder le navire entre le cap de Morgion et l'île de Rion.
+
+Aussitôt, comme d'habitude encore, la plate-forme du fort Saint-Jean s'était
+couverte de curieux; car c'est toujours une grande affaire à Marseille que
+l'arrivée d'un bâtiment, surtout quand ce bâtiment, comme le Pharaon, a été
+construit, gréé, arrimé sur les chantiers de la vieille Phocée, et appartient
+à un armateur de la ville.
+
+Cependant ce bâtiment s'avançait; il avait heureusement franchi le détroit que
+quelque secousse volcanique a creusé entre l'île de Calasareigne et l'île de
+Jaros; il avait doublé Pomègue, et il s'avançait sous ses trois huniers, son
+grand foc et sa brigantine, mais si lentement et d'une allure si triste, que
+les curieux, avec cet instinct qui pressent un malheur, se demandaient quel
+accident pouvait être arrivé à bord. Néanmoins les experts en navigation
+reconnaissaient que si un accident était arrivé, ce ne pouvait être au
+bâtiment lui-même; car il s'avançait dans toutes les conditions d'un navire
+parfaitement gouverné: son ancre était en mouillage, ses haubans de beaupré
+décrochés; et près du pilote, qui s'apprêtait à diriger le Pharaon par
+l'étroite entrée du port de Marseille, était un jeune homme au geste rapide et
+à l'œil actif, qui surveillait chaque mouvement du navire et répétait chaque
+ordre du pilote.
diff --git a/lib/tests/testdir2/bar/cur/mail4 b/lib/tests/testdir2/bar/cur/mail4
new file mode 100644 (file)
index 0000000..4d21a48
--- /dev/null
@@ -0,0 +1,29 @@
+Return-Path: <foo@example.com>
+Delivered-To: foo@example.com
+Received: from [128.88.204.56] by freemailng0304.web.de with HTTP;
+       Mon, 07 May 2005 00:27:52 +0200
+Date: Mon, 07 May 2005 00:27:52 +0200
+Message-Id: <293847329847@web.de>
+MIME-Version: 1.0
+From: =?iso-8859-1?Q? "=F6tzi" ?= <oetzi@web.de>
+To: foo@example.com
+Subject: =?iso-8859-1?Q?Re:=20der=20b=E4r=20und=20das=20m=E4dchen?=
+Precedence: fm-user
+Organization: http://freemail.web.de/
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: 8bit
+X-MIME-Autoconverted: from quoted-printable to 8bit by mailhost6.ladot.com id j48MScQ30791
+X-Label: \backslash
+X-UIDL: 93h!!\i<!!L)l!!%_I!!
+X-Spam-Checker-Version: SpamAssassin 3.0.2 (2004-11-16) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-2.3 required=3.0 tests=AWL,BAYES_00 autolearn=ham 
+       version=3.0.2
+
+Viele liebe Gruesse aus der Stadt der Städte.. 
+__________________________________________________________
+Mit WEB.DE FreePhone mit hoechster Qualitaet ab 0 Ct./Min.
+weltweit telefonieren! http://freephone.web.de/?mc=021201
+
+
diff --git a/lib/tests/testdir2/bar/cur/mail5 b/lib/tests/testdir2/bar/cur/mail5
new file mode 100644 (file)
index 0000000..8ab972a
--- /dev/null
@@ -0,0 +1,7 @@
+Date: Mon, 13 Jun 2011 14:57:25 -0400
+From: xyz@123.xx
+Subject: abc 
+To: foo@bar.cx
+Message-id: <abc@def>
+
+123
diff --git a/lib/tests/testdir2/bar/cur/mail6 b/lib/tests/testdir2/bar/cur/mail6
new file mode 100644 (file)
index 0000000..c9b799b
--- /dev/null
@@ -0,0 +1,18 @@
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Geoff Tate" <jeff@example.com>
+Subject: eyes of a stranger
+To: "Enrico Fermi" <enrico@example.com>
+Message-id: <3BE9E6535E302944823E7A1A20D852173@msg.id>
+MIME-version: 1.0
+X-label: @NextActions operation:mindcrime Queensrÿche
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+And I raise my head and stare
+Into the eyes of a stranger
+I've always known that the mirror never lies
+People always turn away
+From the eyes of a stranger
+Afraid to know what
+Lies behind the stare
diff --git a/lib/tests/testdir2/bar/new/.noindex b/lib/tests/testdir2/bar/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/tests/testdir2/bar/tmp/.noindex b/lib/tests/testdir2/bar/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/tests/testdir2/wom_bat/cur/atomic b/lib/tests/testdir2/wom_bat/cur/atomic
new file mode 100644 (file)
index 0000000..c3c6792
--- /dev/null
@@ -0,0 +1,20 @@
+Date: Sat, 12 Nov 2011 12:06:23 -0400
+From: "Richard P. Feynman" <rpf@example.com>
+Subject: atoms
+To: "Democritus" <demo@example.com>
+Message-id: <3BE9E6535E302944823E7A1A20D852173@msg.id>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+If, in some cataclysm, all scientific knowledge were to be destroyed,
+and only one sentence passed on to the next generation of creatures,
+what statement would contain the most information in the fewest words?
+I believe it is the atomic hypothesis (or atomic fact, or whatever you
+wish to call it) that all things are made of atoms — little particles
+that move around in perpetual motion, attracting each other when they
+are a little distance apart, but repelling upon being squeezed into
+one another. In that one sentence you will see an enormous amount of
+information about the world, if just a little imagination and thinking
+are applied.
diff --git a/lib/tests/testdir2/wom_bat/cur/rfc822.1 b/lib/tests/testdir2/wom_bat/cur/rfc822.1
new file mode 100644 (file)
index 0000000..71c3107
--- /dev/null
@@ -0,0 +1,44 @@
+Return-Path: <foo@example.com>
+Subject: Fwd: rfc822 
+From: foobar <foo@example.com>
+To: martin
+Content-Type: multipart/mixed; boundary="=-XHhVx/BCC6tJB87HLPqF"
+Message-Id: <1077300332.871.27.camel@example.com>
+Mime-Version: 1.0
+X-Mailer: Ximian Evolution 1.4.5
+Date: Fri, 20 Feb 2004 19:05:33 +0100
+
+--=-XHhVx/BCC6tJB87HLPqF
+Content-Type: text/plain
+Content-Transfer-Encoding: 7bit
+
+Hello world, forwarding some RFC822 message
+
+--=-XHhVx/BCC6tJB87HLPqF
+Content-Disposition: inline
+Content-Type: message/rfc822
+
+Return-Path: <cuux@example.com>
+Message-ID: <9A01B19D0D605D478E8B72E1367C66340141B9C5@example.com>
+From: frob@example.com 
+To: foo@example.com
+Subject: hopjesvla
+Date: Sat, 13 Dec 2003 19:35:56 +0100
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: 7bit
+
+The ship drew on and had safely passed the strait, which some volcanic shock
+has made between the Calasareigne and Jaros islands; had doubled Pomegue, and
+approached the harbor under topsails, jib, and spanker, but so slowly and
+sedately that the idlers, with that instinct which is the forerunner of evil,
+asked one another what misfortune could have happened on board. However, those
+experienced in navigation saw plainly that if any accident had occurred, it was
+not to the vessel herself, for she bore down with all the evidence of being
+skilfully handled, the anchor a-cockbill, the jib-boom guys already eased off,
+and standing by the side of the pilot, who was steering the Pharaon towards the
+narrow entrance of the inner port, was a young man, who, with activity and
+vigilant eye, watched every motion of the ship, and repeated each direction of
+the pilot.
+
+--=-XHhVx/BCC6tJB87HLPqF--
diff --git a/lib/tests/testdir2/wom_bat/cur/rfc822.2 b/lib/tests/testdir2/wom_bat/cur/rfc822.2
new file mode 100644 (file)
index 0000000..316fa3f
--- /dev/null
@@ -0,0 +1,44 @@
+From: dwarf@siblings.net
+To: root@eruditorum.org
+Subject: Fwd: test abc
+References: <8639ddr9wu.fsf@cthulhu.djcbsoftware>
+User-agent: mu 0.98pre; emacs 24.0.91.9
+Date: Thu, 24 Nov 2011 14:24:00 +0200
+Message-ID: <861usxr9nj.fsf@cthulhu.djcbsoftware>
+Content-Type: multipart/mixed; boundary="=-=-="
+MIME-Version: 1.0
+
+--=-=-=
+Content-Type: text/plain
+
+Saw the website. Am willing to stipulate that you are not RIST 9E03. Suspect
+that you are the Dentist, who yearns for honest exchange of views. Anonymous,
+digitally signed e-mail is the only safe vehicle for same.
+
+If you want me to believe you are not the Dentist, provide plausible
+explanation for your question regarding why we are building the Crypt.
+
+Yours truly,
+
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: inline; filename=
+       "1322137188_3.11919.foo:2,S"
+Content-Description: rfc822
+
+From: dwarf@siblings.net
+To: root@eruditorum.org
+Subject: test abc      
+User-agent: mu 0.98pre; emacs 24.0.91.9
+Date: Thu, 24 Nov 2011 14:18:25 +0200
+Message-ID: <8639ddr9wu.fsf@cthulhu.djcbsoftware>
+Content-Type: text/plain
+MIME-Version: 1.0
+
+As I stepped on this unknown middle-aged Filipina's feet during an ill-advised
+ballroom dancing foray, she leaned close to me and uttered some latitude and
+longitude figures with a conspicuously large number of significant digits of
+precision, implying a maximum positional error on the order of the size of a
+dinner plate. Gosh, was I ever curious!
+
+--=-=-=--
diff --git a/lib/tests/testdir4/1220863042.12663_1.mindcrime!2,S b/lib/tests/testdir4/1220863042.12663_1.mindcrime!2,S
new file mode 100644 (file)
index 0000000..ab1500f
--- /dev/null
@@ -0,0 +1,146 @@
+Return-Path: <gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-4.9 required=3.0 tests=BAYES_00,DATE_IN_PAST_96_XX,
+       RCVD_IN_DNSWL_MED autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id 5123469CB3
+       for <xxxx@localhost>; Thu,  7 Aug 2008 08:10:19 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:19 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs39272wfh; Wed, 6 Aug 2008
+ 20:15:17 -0700 (PDT)
+Received: by 10.65.133.8 with SMTP id k8mr2071878qbn.7.1218078916289; Wed, 06
+ Aug 2008 20:15:16 -0700 (PDT)
+Received: from sourceware.org (sourceware.org [209.132.176.174]) by
+ mx.google.com with SMTP id 28si7904461qbw.0.2008.08.06.20.15.15; Wed, 06 Aug
+ 2008 20:15:16 -0700 (PDT)
+Received-SPF: neutral (google.com: 209.132.176.174 is neither permitted nor
+ denied by domain of gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org)
+ client-ip=209.132.176.174;
+Authentication-Results: mx.google.com; spf=neutral (google.com:
+ 209.132.176.174 is neither permitted nor denied by domain of
+ gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org)
+ smtp.mail=gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org
+Received: (qmail 13493 invoked by alias); 7 Aug 2008 03:15:13 -0000
+Received: (qmail 13485 invoked by uid 22791); 7 Aug 2008 03:15:12 -0000
+Received: from mailgw1a.lmco.com (HELO mailgw1a.lmco.com) (192.31.106.7)    
+ by sourceware.org (qpsmtpd/0.31) with ESMTP; Thu, 07 Aug 2008 03:14:27 +0000
+Received: from emss07g01.ems.lmco.com (relay5.ems.lmco.com [166.29.2.16])by
+ mailgw1a.lmco.com  (LM-6) with ESMTP id m773EPZH014730for
+ <gcc-help@gcc.gnu.org>; Wed, 6 Aug 2008 21:14:25 -0600 (MDT)
+Received: from CONVERSION2-DAEMON.lmco.com by lmco.com (PMDF V6.3-x14 #31428) 
+ id <0K5700601NO18J@lmco.com> for gcc-help@gcc.gnu.org; Wed, 06 Aug 2008
+ 21:14:25 -0600 (MDT)
+Received: from EMSS04I00.us.lmco.com ([166.17.13.135]) by lmco.com (PMDF
+ V6.3-x14 #31428)  with ESMTP id <0K5700H5MNNWGX@lmco.com> for
+ gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 21:14:20 -0600 (MDT)
+Received: from EMSS35M06.us.lmco.com ([158.187.107.143]) by
+ EMSS04I00.us.lmco.com with Microsoft SMTPSVC(5.0.2195.6713); Wed,  06 Aug
+ 2008 23:14:20 -0400
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Mickey Mouse" <anon@example.com>
+Subject: gcc include search order
+To: "Donald Duck" <gcc-help@gcc.gnu.org>
+Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Content-class: urn:content-classes:message
+Mailing-List: contact gcc-help-help@gcc.gnu.org; run by ezmlm
+Precedence: klub
+List-Id: <gcc-help.gcc.gnu.org>
+List-Unsubscribe: <mailto:gcc-help-unsubscribe-xxxx.klub=gmail.com@gcc.gnu.org>
+List-Archive: <http://gcc.gnu.org/ml/gcc-help/>
+List-Post: <mailto:gcc-help@gcc.gnu.org>
+List-Help: <mailto:gcc-help-help@gcc.gnu.org>
+Sender: gcc-help-owner@gcc.gnu.org
+Delivered-To: mailing list gcc-help@gcc.gnu.org
+Content-Length: 3024
+
+
+Hi.
+In my unit testing I need to change some header files (target is
+vxWorks, which supports some things that the sun does not). 
+So, what I do is fetch the development tree, and then in a new unit test
+directory I attempt to compile the unit under test. Since this is NOT
+vxworks, I use sed to change some of the .h files and put them in a
+./changed directory.
+
+When I try to compile the file, it is still using the .h file from the
+original location, even though I have listed the include path for
+./changed before the include path for the development tree.
+
+Here is a partial output from gcc using the -v option
+
+GNU CPP version 3.1 (cpplib) (sparc ELF)
+GNU C++ version 3.1 (sparc-sun-solaris2.8)
+        compiled by GNU C version 3.1.
+ignoring nonexistent directory "NONE/include"
+#include "..." search starts here:
+#include <...> search starts here:
+ .
+ changed
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/mp/interface
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/interface
+ /usr/local/include/g++-v3
+ /usr/local/include/g++-v3/sparc-sun-solaris2.8
+ /usr/local/include/g++-v3/backward
+ /usr/local/include
+ /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/3.1/include
+ /usr/local/sparc-sun-solaris2.8/include
+ /usr/include
+End of search list.
+
+I know the changed file is correct and that the include is not working
+as expected, because when I copy the file from ./changed, back into the
+development tree, the compilation works as expected.
+
+One more bit of information. The source that I cam compiling is in
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app
+And it is including files from
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+These include files should be including the files from ./changed (when
+they exist) but they are ignoring the .h files in the ./changed
+directory and are instead using other, unchanged files in the
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+directory.
+
+The gcc command line is something like
+
+  TEST_DIR="."
+  
+  CHANGED_DIR_NAME=changed
+  CHANGED_FILES_DIR=${TEST_DIR}/${CHANGED_DIR_NAME}
+
+  CICU_HEADER_FILES="-I ${AP_INTERFACE_FILES} -I ${AP_APP_FILES} -I
+${SHARED_COMMON_FILES} -I ${SHARED_INTERFACE_FILES}"
+  
+  HEADERS="-I ./ -I ${CHANGED_FILES_DIR} ${CICU_HEADER_FILES}"
+  DEFINES="-DSUNRUN -DA10_DEBUG -DJOETEST"
+  
+  CFLAGS="-v -c -g -O1 -pipe -Wformat -Wunused -Wuninitialized -Wshadow
+-Wmissing-prototypes -Wmissing-declarations"
+  
+  printf "Compiling the UUT File\n"
+  gcc -fprofile-arcs -ftest-coverage ${CFLAGS} ${HEADERS} ${DEFINES}
+${AP_APP_FILES}/unitUnderTest.cpp 
+
+
+I hope this explanation is clear. If anyone knows how to fix the command
+line so that it gets the .h files in the "changed" directory are used
+instead of files in the other include directories.
+
+Thanks
+Joe
+
+----------------------------------------------------
+Time Flies like an Arrow. Fruit Flies like a Banana
+
diff --git a/lib/tests/testdir4/1220863087.12663_19.mindcrime!2,S b/lib/tests/testdir4/1220863087.12663_19.mindcrime!2,S
new file mode 100644 (file)
index 0000000..78efa2a
--- /dev/null
@@ -0,0 +1,77 @@
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level: 
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+       version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+       by mail.xxxxsoftware.nl (Postfix) with ESMTP id C4D6569CB3
+       for <xxxx@localhost>; Thu,  7 Aug 2008 08:10:08 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+       by mindcrime with IMAP (fetchmail-6.3.8)
+       for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:08 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs34794wfh; Wed, 6 Aug 2008
+ 13:40:29 -0700 (PDT)
+Received: by 10.100.33.13 with SMTP id g13mr1093301ang.79.1218055228418; Wed,
+ 06 Aug 2008 13:40:28 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d19si15908789and.17.2008.08.06.13.40.27; Wed, 06 Aug 2008
+ 13:40:28 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:56316 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQpo3-0007Pc-Qk for
+ xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:40:27 -0400
+From: anon@example.com
+Newsgroups: gnu.emacs.help
+Date: Wed, 6 Aug 2008 20:38:35 +0100
+Message-ID: <r6bpm5-6n6.ln1@news.ducksburg.com>
+References: <55dbm5-qcl.ln1@news.ducksburg.com>
+ <mailman.15710.1217599959.18990.help-gnu-emacs@gnu.org>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+X-Trace: individual.net bABVU1hcJwWAuRwe/097AAoOXnGGeYR8G1In635iFGIyfDLPUv
+X-Orig-Path: news.ducksburg.com!news
+Cancel-Lock: sha1:wK7dsPRpNiVxpL/SfvmNzlvUR94=
+ sha1:oepBoM0tJBLN52DotWmBBvW5wbg=
+User-Agent: slrn/pre0.9.9-120/mm/ao (Ubuntu Hardy)
+Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!feeder.erje.net!proxad.net!feeder1-2.proxad.net!feed.ac-versailles.fr!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail
+Xref: news.stanford.edu gnu.emacs.help:160868
+To: help-gnu-emacs@gnu.org
+Subject: Re: Learning LISP; Scheme vs elisp.
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 417
+Lines: 11
+
+On 2008-08-01, Thien-Thi Nguyen wrote:
+
+> warriors attack, felling foe after foe,
+> few growing old til they realize: to know
+> what deceit is worth deflection;
+> such receipt reversed rejection!
+> then their heavy arms, e'er transformed to shields:
+> balanced hooked charms, ploughed deep, rich yields.
+
+Aha: the exercise for the reader is to place the parens correctly.
+Might take me a while to solve this puzzle.
+
diff --git a/lib/tests/testdir4/1252168370_3.14675.cthulhu!2,S b/lib/tests/testdir4/1252168370_3.14675.cthulhu!2,S
new file mode 100644 (file)
index 0000000..1e69622
--- /dev/null
@@ -0,0 +1,22 @@
+Return-Path: <dfgh@floppydisk.nl>
+X-Spam-Checker-Version: SpamAssassin 3.1.0 (2005-09-13) on mindcrime
+X-Spam-Level: 
+Delivered-To: dfgh@floppydisk.nl
+Message-ID: <43A09C49.9040902@euler.org>
+Date: Wed, 14 Dec 2005 23:27:21 +0100
+From: Fred Flintstone <fred@euler.org>
+User-Agent: Mozilla Thunderbird 1.0.7 (X11/20051010)
+X-Accept-Language: nl-NL, nl, en
+MIME-Version: 1.0
+To: dfgh@floppydisk.nl
+List-Id: =?utf-8?q?Example_of_List_Id?=
+Subject: Re: xyz
+References: <439C1136.90504@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439B41ED.2080402@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439A1E03.3090604@euler.org> <20051211184308.GB13513@gauss.org>
+In-Reply-To: <20051211184308.GB13513@gauss.org>
+X-Enigmail-Version: 0.92.0.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+X-UIDL: T<?"!%LG"!cAK"!_j(#!
+Content-Length: 1879
+
+Test 123.
diff --git a/lib/tests/testdir4/1283599333.1840_11.cthulhu!2, b/lib/tests/testdir4/1283599333.1840_11.cthulhu!2,
new file mode 100644 (file)
index 0000000..8349c3e
--- /dev/null
@@ -0,0 +1,15 @@
+From: Frodo Baggins <frodo@example.com>
+To: Bilbo Baggins <bilbo@anotherexample.com>
+Subject: Greetings from =?UTF-8?B?TG90aGzDs3JpZW4=?=
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+Fcc: .sent
+Organization: The Fellowship of the Ring
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+
+Let's write some fünkÿ text
+using umlauts.
+
+Foo.
diff --git a/lib/tests/testdir4/1305664394.2171_402.cthulhu!2, b/lib/tests/testdir4/1305664394.2171_402.cthulhu!2,
new file mode 100644 (file)
index 0000000..863f714
--- /dev/null
@@ -0,0 +1,17 @@
+From: =?UTF-8?B?TcO8?= <testmu@testmu.xx>
+To: Helmut =?UTF-8?B?S3LDtmdlcg==?= <hk@testmu.xxx>
+Subject: =?UTF-8?B?TW90w7ZyaGVhZA==?=
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+References: <non-exist-01@msg.id> <non-exist-02@msg.id> <non-exist-03@msg.id> <non-exist-04@msg.id>
+1n-Reply-To: <non-exist-04@msg.id>
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+
+Test for issue #38, where apparently searching for accented words in subject,
+to etc. fails.
+
+What about here? Queensrÿche. Mötley Crüe.
+
+
diff --git a/lib/tests/testdir4/181736.eml b/lib/tests/testdir4/181736.eml
new file mode 100644 (file)
index 0000000..56255c4
--- /dev/null
@@ -0,0 +1,42 @@
+Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail
+X-newsreader: xrn 9.03-beta-14-64bit
+Sender: jimbo@lews (Jimbo Foobarcuux)
+From: jimbo@slp53.sl.home (Jimbo Foobarcuux)
+Reply-To: slp53@pacbell.net
+Subject: Re: Are writes "atomic" to readers of the file?
+Newsgroups: comp.unix.programmer
+References: <e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt.googlegroups.com> <87hbblwelr.fsf@sapphire.mobileactivedefense.com> <pql248-4va.ln1@wilbur.25thandClement.com> <ikns6r$li3$1@Iltempo.Update.UU.SE> <8762s0jreh.fsf@sapphire.mobileactivedefense.com> <ikqqp1$jv0$1@Iltempo.Update.UU.SE> <87hbbjc5jt.fsf@sapphire.mobileactivedefense.com> <ikr0na$lru$1@Iltempo.Update.UU.SE> <tO8cp.1228$GE6.370@news.usenetserver.com> <ikr6ks$nlf$1@Iltempo.Update.UU.SE> <8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk>
+Organization: UseNetServer - www.usenetserver.com
+X-Complaints-To: abuse@usenetserver.com
+Message-ID: <oktdp.42997$Te.22361@news.usenetserver.com>
+Date: 08 Mar 2011 17:04:20 GMT
+Lines: 27
+Xref: uutiset.elisa.fi comp.unix.programmer:181736
+
+John Denver <jd@clare.See-My-Signature.invalid> writes:
+>Eric the Red wrote:
+>
+>>> There _IS_ a requirement that all reads and writes to regular files
+>>> be atomic.   There is also an ordering guarantee.   Any implementation
+>>> that doesn't provide both atomicity and ordering guarantees is broken.
+>>
+>> But where is it specified?
+>
+>The place where it is stated most explicitly is in XSH7 2.9.7
+>Thread Interactions with Regular File Operations:
+>
+>    All of the following functions shall be atomic with respect to each
+>    other in the effects specified in POSIX.1-2008 when they operate on
+>    regular files or symbolic links:
+>
+>    [List of functions that includes read() and write()]
+>
+>    If two threads each call one of these functions, each call shall
+>    either see all of the specified effects of the other call, or none
+>    of them.
+>
+
+And, for the purposes of this paragraph, the two threads need not be
+part of the same process.
+
+jimbo
diff --git a/lib/tests/testdir4/encrypted!2,S b/lib/tests/testdir4/encrypted!2,S
new file mode 100644 (file)
index 0000000..b6470e7
--- /dev/null
@@ -0,0 +1,57 @@
+Return-path: <>
+Envelope-to: peter@example.com
+Delivery-date: Fri, 11 May 2012 16:22:03 +0300
+Received: from localhost.example.com ([127.0.0.1] helo=borealis)
+       by borealis with esmtp (Exim 4.77)
+       id 1SSpnB-00038a-Ux
+       for djcb@localhost; Fri, 11 May 2012 16:21:58 +0300
+Delivered-To: peter@example.com 
+From: Brian <brian@example.com>
+To: Peter <peter@example.com>
+Subject: encrypted
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:21:42 +0300
+Message-ID: <877gwi97kp.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+hQQOA1T38TPQrHD6EA//YXkUB4Dy09ngCRyHWbXmV3XBjuKTr8xrak5ML1kwurav
+gyagOHKLMU+5CKvObChiKtXhtgU0od7IC8o+ALlHevQ0XXcqNYA2KUfX8R7akq7d
+Xx9mA6D8P7Y/P8juUCLBpfrCi2GC42DtvPZSUu3bL/ctUJ3InPHIfHibKF2HMm7/
+gUHAKY8VPJF39dLP8GLcfki6qFdeWbxgtzmuyzHfCBCLnDL0J9vpEQBpGDFMcc4v
+cCbmMJaiPOmRb6U4WOuRVnuXuTztLiIn0jMslzOSFDcLTVBAsrC01r71O+XZKfN4
+mIfcpcWJYKM2NQW8Jwf+8Hr84uznBqs8uTTlrmppjkAHZGqGMjiQDxLhDVaCQzMy
+O8PSV4xT6HPlKXOwV1OLc+vm0A0RAdSBctgZg40oFn4XdB1ur8edwAkLvc0hJKaz
+gyTQiPaXm2Uh2cDeEx4xNgXmwCKasqc9jAlnDC2QwA33+pw3OqgZT5h1obn0fAeR
+mgB+iW1503DIi/96p8HLZcr2EswLEH9ViHIEaFj/vlR5BaOncsLB0SsNV/MHRvym
+Xg5GUjzPIiyBZ3KaR9OIBiZ5eXw+bSrPAo/CAs0Zwxag7W3CH//oK39Qo1GnkYpc
+4IQxhx4IwkzqtCnripltV/kfpGu0yA/OdK8lOjkUqCwvL97o73utXIxm21Zd3mEP
+/iLNrduZjMCq+goz1pDAQa9Dez6VjwRuRPTqeAac8Fx/nzrVzIoIEAt36hpuaH1l
+KpbmHpKgsUWcrE5iYT0RRlRRtRF4PfJg8PUmP1hvw8TaEmNfT+0HgzcJB/gRsVdy
+gTzkzUDzGZLhRcpmM5eW4BkuUmIO7625pM6Jd3HOGyfCGSXyEZGYYeVKzv8xbzYf
+QM6YYKooRN9Ya2jdcWguW0sCSJO/RZ9eaORpTeOba2+Fp6w5L7lga+XM9GLfgref
+Cf39XX1RsmRBsrJTw0z5COf4bT8G3/IfQP0QyKWIFITiFjGmpZhLsKQ3KT4vSe/d
+gTY1xViVhkjvMFn3cgSOSrvktQpAhsXx0IRazN0T7pTU33a5K0SrZajY9ynFDIw9
+we7XYyVwZzYEXjGih5mTH1PhWYK5fZZEKKqaz5TyYv9SeWJ+8FrHeXUKD38SQEHM
+qkpl9Iv17RF4Qy9uASWwRoobhKO+GykTaBSTyw8R8ctG/hfAlnaZxQ3TwNyHWyvU
+9SVJsp27ulv/W9MLZtGpEMK0ckAR164Vyou1KOn200BqxbC2tJpegNeD2TP5ZtdY
+HIcxkgKr0haYcDnVEf1ulSxv23pZWIexbgvVCG7dRL0eB+6O28f9CWehle10MDyM
+0AYyw8Da2cu7PONMovqt4nayScyGTacFBp7c2KXR9DGZ0mcBwOjL/mGRKcVWN3MG
+2auCrwn2KVWmKZI3Jp0T8KhfGBnFs9lUElpDTOiED1/2bKz6Yoc385QtWx99DFMZ
+IWiH5wMxkWFpzjE+GHiJ09vSbTTL4JY9eu2n5nxQmtjYMBVxQm7S7qwH
+=0Paa
+-----END PGP MESSAGE-----
+--=-=-=--
+
diff --git a/lib/tests/testdir4/mail1 b/lib/tests/testdir4/mail1
new file mode 100644 (file)
index 0000000..a4e19c1
--- /dev/null
@@ -0,0 +1,38 @@
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "John Milton" <jm@example.com>
+Subject: Fere libenter homines id quod volunt credunt
+To: "Julius Caesar" <jc@example.com>
+Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+x-label: Paradise losT
+X-keywords: john, milton
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+OF Mans First Disobedience, and the Fruit
+Of that Forbidden Tree, whose mortal tast
+Brought Death into the World, and all our woe,
+With loss of Eden, till one greater Man
+Restore us, and regain the blissful Seat, [ 5 ]
+Sing Heav'nly Muse,that on the secret top
+Of Oreb, or of Sinai, didst inspire
+That Shepherd, who first taught the chosen Seed,
+In the Beginning how the Heav'ns and Earth
+Rose out of Chaos: Or if Sion Hill [ 10 ]
+Delight thee more, and Siloa's Brook that flow'd
+Fast by the Oracle of God; I thence
+Invoke thy aid to my adventrous Song,
+That with no middle flight intends to soar
+Above th' Aonian Mount, while it pursues [ 15 ]
+Things unattempted yet in Prose or Rhime.
+And chiefly Thou O Spirit, that dost prefer
+Before all Temples th' upright heart and pure,
+Instruct me, for Thou know'st; Thou from the first
+Wast present, and with mighty wings outspread [ 20 ]
+Dove-like satst brooding on the vast Abyss
+And mad'st it pregnant: What in me is dark
+Illumin, what is low raise and support;
+That to the highth of this great Argument
+I may assert Eternal Providence, [ 25 ]
+And justifie the wayes of God to men.
diff --git a/lib/tests/testdir4/mail5 b/lib/tests/testdir4/mail5
new file mode 100644 (file)
index 0000000..b12387a
--- /dev/null
@@ -0,0 +1,624 @@
+From: Sitting Bull <sb@example.com>
+To: George Custer <gac@example.com>
+Subject: pics for you 
+Mail-Reply-To: djcb@djcbsoftware.nl
+User-Agent: Hunkpapa/2.15.9 (Almost Unreal)
+Fcc: .sent
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: multipart/mixed;
+ boundary="Multipart_Sun_Oct_17_10:37:40_2010-1"
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: text/plain; charset=US-ASCII
+
+Dude! Here are some pics!
+
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: image/jpeg
+Content-Disposition: inline; filename="sittingbull.jpg"
+Content-Transfer-Encoding: base64
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4QvoRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB
+AAAASAAAABsBCQABAAAASAAAACgBCQABAAAAAgAAADEBAgAOAAAAbgAAADIBAgAUAAAAfAAAABMC
+CQABAAAAAQAAAGmHBAABAAAAkAAAAN4AAABndGh1bWIgMi4xMS4zADIwMTA6MTA6MTcgMTA6MzM6
+MzcABgAAkAcABAAAADAyMjEBkQcABAAAAAECAwAAoAcABAAAADAxMDABoAkAAQAAAAEAAAACoAkA
+AQAAAMgAAAADoAkAAQAAAGsBAAAAAAAABgADAQMAAQAAAAYAAAAaAQkAAQAAAEgAAAAbAQkAAQAA
+AEgAAAAoAQkAAQAAAAIAAAABAgQAAQAAACwBAAACAgQAAQAAALMKAAAAAAAA/9j/4AAQSkZJRgAB
+AQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwc
+KDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCACAAEcDASIAAhEBAxEB/8QAHwAA
+AQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIh
+MUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpT
+VFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5
+usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAA
+AAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEI
+FEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVm
+Z2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK
+0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDq77xdrX/CQ6laRXjRxQTF
+ECovA/EUg8Sa6W/5CUuP9xP8K5yWQnxjrw9Lwj9BWjkgZHFAG6mu6yV51OXP+4n/AMTUq61rBB/4
+mU2f9xP/AImsJJTuAJFW0YDnfmgCTUPFGqWFq882p3G1eyqmT/47VfRfGGpawkgGp3CyIeg2cj1+
+7XK+O7zybCGMNjzHyR6gD/69ZvgG8zqU67vvRZH4EUAesJe6m/XVLv8ANf8A4mpf7Qvl/wCX+6b6
+uP8ACs+ObKdaeh3Hg9aANTw/4gurjxTLpU7tIv2cTKzHpgkH+n5UVheHGI+KWzJwdNP/AKFRQBzD
+7f8AhMfEDEHH24j/AMdWrs0oCkDrVKJs+NfEsZ+79u/9kWrd5GqKTmgCstwwkyT0p5uzu61mOzbj
+zSFn3DmgDB8ePLPe2MEQZyykhRzk9/5Va8D6Vd2Mz3d3CYxJHiPd16+la0hhMybkUvxhj1HWr5uM
+uB0wMYoA3YJARjvV+DBPasC2lYsOuK3LVunWgCLQRj4sIPXTGP8A4/RSaF/yV2P30tv/AEOigDmY
+QD408UE9ftw/9AFXpv3iFT9Kgs4t3jXxV6C+H/oAq5cxkMcCgDFltXVyMVVv7iGwtzNcNsQfiT+F
+a8jbAxdgFAzmuTZZfEV81vG+xTyX/uIPT3P9aAIBr9vdNHcQI/lxk5DDBrfsLuK+jE0MqupPOOx9
+KzNY8L6fbaaYrdGXb3BOcnuT+FcpodzN4c8RRRyylrW4IDE9MdM/UUAes2wbOAK27PhRms6CJlwc
+VrWowRkUAV9CP/F3YffSm/8AQ6Kfo+P+FuWp9dLf/wBDooAxrH/kd/Ff/X8P/Ra1evUOcgVW01Qf
+G/izIz/py/8Aota2LqPK4xQBxniWc2mi3MxBGFA/Mgf1rmtEF/Z6HNqMNuzvPnY+7G1V6Hoe+T0r
+qfGmnT3Xhm8WNWJVQ+B/skH+lUPBt3d3PhuzXyBM6xBY0YfKDnALewxmgDE1BfEDaPaXNzMRJPIQ
++TjCgDHb69u9ZGt2Us2lrdNDtMLAgq27Kng84Fd74qnaMwWB8qWRTnzUcfePGSOx4ziuf1kzT6S9
+tuRHlVUG5sDJOMA+lAHofh5/tvh3T7k4ZnhXcfcDB/UVuRQEdqzvDelPo/hywsJGDSRRjeR0yeTj
+2ya3I8/3aAMXSU2/FmzJ/wCgbJ/6FRUunf8AJV7H/sGy/wDoQooAyNJXf448XYPS+X/0Wtb8ynyj
+0rm/DIll8W+KDKQ0pvF3FehPlr0rvINMzbfN8rsc7upH0oA5ie3mktZSI1ICn5W43e1ec6ZrDwax
+facIj9liUNtUcgE8j0IzXrHiqS20rQJbiadoyBsWQjc2T2HvXnvhbREuzeXTbvMlfILcsF6D6jFA
+GJr+pWE1ymFkwFzhlwo+i1xevazLd3Fva2+UiQhh7kdPyr0jVfA8t0BeXNybe35UK2EJAJwST/QG
+uS1Pw7HYalbKHUIxYxyDd8wHUnNAHsnhXVBrGhWkrBlmEYVww6sAATXQInA5rn/AOZtIa3mQHZI+
+xwfvAnJ6d8n9a6yazEKhlzgUAc1YAr8WbH302X/0IUU6xBPxYsSe2my/+hUUAV/Bdj5fi3xWJJDJ
+JHeopY8bj5a5OK9AUArwARXEeFjjxh4xbub5f/RYrsIZgJhGTjcuQMGgDnfHiwnw1KJoVkUuB8yg
+hfeuZ+HemTLpjx3OCZNzKUbPy54/Sut8Z263OlJE1wYgzkkjvgH86yfBb+XYWuIGiEithWzn9aAN
+loTcO0ctuGjV9oMg5JGCSOOnp9K8/wDH1qH1iERrukRAqqB3Jzj9BXpsk6F+oyCuRjJ54rhNcg+3
+Ge5XiUSL5ZGc87sdPagDQ+HlvJHoAdo9h85mUY7dK7WSRCoB6HiuV8IiW10JYs7yszDJ7fN/k1tG
+Rpb4xj7qpnj3Iwfx5oAwLMgfF+1UHI/suTH/AH3RTLJNnxltx2Olvj/vuigB3hgf8Vp4vH/T8v8A
+6AK6aRWFk2CA2CPSua8M4/4T3xcp/wCftD/45XR6q32e1JjUySCRdqA4J3HH9aAKHiJTceH4mliK
+r5e5lDfMpx2Iqp4eQR6Zp75Y4jX7xyfTn8q29djjbS/LMqxYGFdugNZWlskOh2pKgYj2AqO4OB/M
+0AW7+NLQ3Fwi/O6hsk5yRwOO3WuS1qGJtNuvN3iNJkX5e+EIxn8f1re1e4ubq8jSOMiBArZJ/wBY
+xOcfQcZ+tVNTsYh4dnjmG9PMJIP8XYUAQ20z2Hg6OeJGTYQzd+N3Le+RzXQ6TGwtjLLkuxAy3XAH
+f8Saw9Mlt7vwsI4yZI9m07xtyM/y5rqodqxIFAIx1oA5iDj4w2ZHfS3/APQjRSw8/GGzx20x/wD0
+I0UAee+I/GV/4S+IXiAWlvFKJ7gM28njC+1Ubn4v6xclC1hbAq6vwzdjn+lXviB4X1O88b6lPBYX
+EkUkgZXWJiDwPQVzH/CH61zjSbwj1EDf4UAbV78YdZvYPJbT7UA+7HP61HbfFXXLW3SFdOtSqZ67
+v8fesg+Ddbzn+yL3P/XBv8Kcvg3Xc5Oj3x/7YP8A4UAaY+KuvIQP7PtM5JXKt/jUF78Udcu7F7WS
+ytEVv4grZHPB61VPg/Ws/wDIGvs9v3T/AOFMPg7XcHOk32P+uD/4UAWLb4l6vb2zQJZ2m1gP4WGC
+FAz19q17f4va0sSobS04GB8rf41z3/CIayOuk3g/7d2/wqRfCWr8f8S27/78P/hQB33w78Q3fib4
+jR3l3HHG6WTxgR5xjOe/1oq78JvCmo6dq8+qXUBhhETQqJAVYsSDkA8496KAP//ZAP/bAEMABQME
+BAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4k
+HB4fHv/bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e
+Hh4eHh4eHh4eHh4eHh4eHv/AABEIAWsAyAMBIgACEQEDEQH/xAAdAAABBAMBAQAAAAAAAAAAAAAH
+AwQFBgACCAEJ/8QAVhAAAQMCBAIHAwYHCwoGAgMAAQIDEQAEBQYSITFBBwgTIlFhcRSBkRUjMqGx
+0RYXQoKSssElJjNDRFJicpOi8CQ0NmNzo7PC0uEnNVNUVYMJRWR08f/EABQBAQAAAAAAAAAAAAAA
+AAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDqjNudMEyw6lvE3HEqUnUS
+kDSmeEkkCTB241Aq6X8mgSL5J/PT99DPrX3S232GwohPbp5/6v8A70PMsrSuwYKgkgtjiONB0Ovp
+lykFQLhJ8+0Fefjoyh/6/wDfFBctsJWkpt0wRzSK9ft7cwVMNmf6IoDOOmjJ5/lB/SFejpmyeT/n
+I/TFBdFqwACGGR6IFOWbRhZ3Ya349wUBgT0y5QP8qT+n/wBq3T0x5OPG8A/OFCy3tbZCRpt2j+YK
+cotLMjvWjM/1BQEs9MWTR/LUn0VWfjiyb/72hki1sw5q9kaHL6ApZDFspQHsze3DuCgI6umLJqUg
++2Eg+AJ/ZSR6aMmj+UOfon7qozjTACUhlsCP5opo402AQWkeumgIK+mrKAA0uuK8e6R+ykj03ZU5
+JePx+6h6gaTGlPHmkUwzDjlrhDKVvtIWXVaUIjj4+6gKKOm7KZUQrtU+YCj/AMteHpuyvvpbeV7l
+f9NAHEc3Ls8ed9iYZLTZIAI4+vpRAy1i7WKYS3dhtIDghQHIjiKC9npwy3O1rcH3K/6a1V034Ef4
+OyfPqlf/AE1WG1KKxAAHhFO0LgTCfhQTf47cGPCwuJ/qr/6a9R004as7YXcn0bX/ANNRLbqhsAOF
+OWnVgEnh4UEm30wWbhhvBL5e3Jpz/ppwOlMLSC3lzElT4suD/kqHN642YT9tIu3T7h2UYHnQWW36
+S9YleXcQA8Qhf7U05b6SMPXiFpYGwfZfu19myl9XZ6leEkR9dVBy7XEFXLcUN+mPEn7VnCrplZQ6
+zclaFeCgAQfqoOqbO4L6VBxvsnURrRqConcbisqo9FOYE5mw1zGENdkLllpZQSCUkApO/wCbWUAY
+63i9N0x3v5SkHy+aqj5QBXhFqRzbFWrrjrWm5a1ER7Unh/saqOSXP3Bst/4pNBbWhLY1nvCt1NyZ
+mRWtuoKESCOdKjSOB99BiEjYHYU4ZQpKpTwpArTxpVt8g7RQPmlQkpP2UoFE8VkU0buEkK4A+NbN
+rCzHDagfQCmN69TtsPjTRLoBA1b0oHFaZPwoH6IPODFJPwOG9asklJJ228a8egp9aBspcHhvQf6T
+b55eZHWVu6m2kgISDsnaT76Kt46hlpx1WyUJKj7q59xzEFXV9dXC1krccK9/M0CybtYUTrMnj50R
+uiPGLl69OHBTaWG2isgjdRkffQgFyoqO441eOiW4WM1W8D6aVBXpFAfGCVGQmnDZO07jmaYW74G0
+yKfNuJUBB50D0ISAIAiNyK3UUkQIpDtgUhAAIApNToAKQACedAotaZgCfOvQrUCAIpsVAma31kEE
+bbUG60Dwn1oW9O6S3h2HqnYvK291E1SlEkzvQr6f1qRg1gVHc3B4+lAaeq2rXkFhcz3NPwcXWU06
+pzqnchtQ4CkBQjT/AKxVZQDXrpOEXLJO0XYj+xFVDI6lHAbEeLSfsq1dd2UPWyxMG7HHhPZCqtkX
+/wAhw9Svo9gj7KC4Wx2MCKdt7ncTTaxbGmBz3p4hISNzHhQJrQSYnjXiUkQOYpZzupJJpFK5g7n3
+0ChQSkbxSluSFDfam5WACokj1NetPAqA29KCXb0HdQ9TSwQlSduVR6XBpJnetkXWgCTQSTQCRpMk
+Uk8djzpsm9ExNaOP6pOqBQRWcblNtly/eI3SwoTPiIrnbEXAl7j4UaulrEHLbJ9z2ZEuKS2fQneg
+Fevy62ASQYoHCHkqXsqrl0U3aWs52mpelLgU3v5jah20/C9zO/hVmyc8Bj+HjfvXCB9dB09bq0mD
+zqQZUkAVDtriOdO7ZzSYO8UEoHBw3r0KB3mm6XUxERWdpO0UDhJBVvSulKh9kU2bUdzvvSzKu9tI
+mgXShOjfaTQk6xZ/cOw4GLk7/m0XlqlGxEcCDzoP9Y1KhgNhI/lR/VNAWuqGpJyG3pBHdVPn84qs
+rTqgqJyKyCI+bXHp2qqygH/XoQEW9i5M6r0CJ/1VVnJKP3Aw8QP4BvYf1RVh69K1lm0BMgXyQPL5
+moPJ50YDY+TCP1RQW+yRoaC44HhSygNXe99NbW7lCUKA48K3cdgbk8KDa5IAMcKj3XiklIMTSrj0
+zCppi8STwNAo5cEo4RXtq53pn1pke8NIUR5UoydWoA+lBMdrCdjSLr5G8yfCmJdWE6ZpNx2NiZoH
+4uZjlThKitJ35TUOl4DntUlZOgoiaAe9Nl+tnDbS1QAULdlzxHgftoMLdm4VzAmKKvTsvsrywUqS
+y8y42seBBBB9xoSWTa7hVwocGmyrh7qDZpUKFXzoptG77NdoFkwiXD7uFUvCMOu7+5SxbNLccO8A
+TRd6Nsq3eB5mbVezq9l7ThsCTEUBcQvhtuKdWy0qVJ40wS4NqcNOTvA+NBKpMokRSiFKnx8opo07
+KAAYNOGwrko0DlB/JO005Y0gTNNW9pkzTm3SDJJoHIQhIlKZ50IesgFDLdgYgC74/mmjC03KaEXW
+WBTlmykH/Ox+qqgJnVAV+8S3hMbOA78fnVVlZ1Oio5BbnkHI/tVVlAOOvSIbtlSN8QSD/Y0yyayh
+3Ldg4BBNs37+6KedfERaWqo2+UE/8E1FdGdwF5Yw0kgzatmPzRQWdhnRClCTWPAEHeKkENodbSpO
+xpBy3IJJjegjFwjh6U1eWSoxw50+uGQHPE86j7qEqiY9KBstYSe6B61gfLapB5QaaPOQs77V4twR
+wNA8TcFyPKsdd25UyS6RBAivXXJSDQOm1jyinlq/CoSY22qDDh1AVI22mUqmNqCpdI+GXOZb9Ng1
+I9jZL+qNiVSAPiKiMhZQFixcHGGgkvJPaJJ4JBolsKaTdO9zdTcKV4CdqalLSrtevcQEkeVA7ytl
+/CcOm7sLVKFugb+VSt06k3WgfSSO9HKss30NsagAkAd0GminCXSr8onegfNKSDO/nJp22tCSDUYF
+xEJpVDkkwDPrQTTCoGxBmnjDp4KG9QVu4rVxjepS3WY1KVPpQSbSiT508tBvBImo9hJICt96f2yS
+TwIigkWht6UHes8ojK9mJE+2Dj/UVRhaEpn7aDnWhH71LP8A/uj9VVATupmZyCiePzn/ABVVledT
+OfxfszG6XYj/AGyqygHfX3A+TrPmTiKZ3/1JqvdFX+h+EqPO2TU91+NrK1Mf/sER/YmoLopSpWR8
+II/9sPtNARbNSdAKdzHCtnY0Eq4zypnbFaEAqImnRUFt6p93OgjbyEEn6qhrxQUo+FTGIp7m23rU
+HeGKBi6E6orRcRtWyiSoxWp4ERQeoAI3NeLSnnW7Q2341s5skcOMUCSEpMk08to0gA/Gmu4HCvW1
+qoPMQWtOJIDR/I3HiKyycacfU4o86Y4qpxBcutiUphIpLLy1LGtSN1RsTwoLS0SQpx3uoG4HlSHa
+gq1JMSa9ullVulJAA5+dN0wYiKCQbcJ+lNLoVzmmjKoEmndudSoVAHpQOrbVq3mIqWtYME7Co9oE
+xHAeFPWVqCQY250EsyuBsSPWn7C1CI4+tRdq4F7gjzFSduoCBHoKCRYUYAG1CHrQJjKFqqB/nqf1
+FUWbck96NO3jNCXrPmMm23Ha9R+oqgJPUvM9H6PLtR/vTWVt1MkoT0eMaQAVpdUYPPtSPurKAb9f
+ZQNkwnf/AMwR/wAA1GdD7QOQMHUeduPtNSfX3H7n2y4mcSSPT5g1G9D69XR9gxCeFvHHzNBczGkR
+tWqXIBjlW4HdO0U1XqQsGOe9ApcAKTHEGoe7tyJqWWSBv61roQ6nvCgrD7cEyPqpNCYn0qavrMhW
+w5U1NsYAAoGLAEmaUcAinItSZ8R4VjjJ0Qo+tBHK+NeoI0kVXs3ZptMIS42wk3FwB9EGAPU0KMWz
+zma6uCtm67BoHZLSQEx6nc0BdzRfBq0UhKu8BwFI4BchttK1EyrlPCqjhmJOXlrbquHUPOrbBJJ5
+8+FaXC32NTguLi2MmCYU2fLjPxoCel0vDUFFQPCnSAQaH+Vc625fRhuKpbtX+CHAruL+6iC0QtKV
+JUFA8CKByyFE8KeI2CTzpG2E8h8acpQSRCRH20DxlRMGPqp2zOkJB2prZtlRIg/Gn7bSUwNW9A+t
+BAO4B8Kk7UeJmo20SUkmeHlUnbK2G1A+aiZjfxoS9Z8k5Ltwrleo4f1VUV0d6IG/OhR1mkg5HZkz
+/lqP1VUBI6l5H4vmRJOzvHl86aykOpOf3iEb/Sdj+0rKCgdfUj5NtxzGJI9/zBqP6Gkj8XuDDn2H
+/Malev6lHybakQD8oNg7f6hVMehVIV0dYMqJhj/mNBcm29Q24U3u2gDtUohvS2dtzTO6H+IoI93d
+PmBTcq0xBmnDoM7b7U1KZUQCBvQLKcQtuFRSCwAmBFIvBxJ2MGaVAUUhQIngaBNUg7kVV81YoUhV
+rbuweCiOPpUxmC9Rh9it5Su+dkDmTQ9Nz2q1PrO5JCd5JNAxxyztBaOOXyktsNp1rUefqaH7dpc5
+hvlDCLJabVJgOLG5H2D0q7YhhV5nDFrfBLYrTbA9reOAwAkHYep/YKL+X8qWGHYe1aWjCUBtMDQn
+c+cmgDtvlq7scEQ2grTdtnVqKdhVXx3EscYTpumoIkdogbKHmDXRWJ2NotlxpbjRUOYVJ9N6G+bc
+Mt3EdghaFx9IAUAXfxB25bIe0haOHd+qiR0QZ1dRdIwXFXdbayEsOqP0T/NNU/MuXH7Qret0KKUn
+dIE/CoCxUW3UnfcyCOINB2BaiRI8Nop+0gxP21R+iPHF45gCUPq1XVsezd347bGr8wlQERQLWp0r
+Gw41IoAKp2k0xQkg/Qnwp9bSYJSZFA7YSfjUhbN6uMAimTBIUCakbZW44UD1hkJAoUdZ9pKMgNqH
+H25H6qqLTR7oPOhZ1ngD0dSOIvGz9SqC4dSZJGQyeRU7H6YrK36lBH4vk7flvQfzxWUFE6/oBwm1
+Vz+UWh7+wVTboPSR0cYKkCf8nk/pGpHr/o/e9aLjf5SbH+4XTPoOUk9G2CyCT7PwA/pGgvaR3eAN
+M71E8qkkJBRsDvTa7QNBFBCuoHGKaqbGuZEU/udIERvTbUmSNO9A0ukjSTG9Nm1BC5PDnUg8pIHC
+ozEn0tMLcCeCSfqoB9nrElXOLi2bJ0IOkeE86rVzcBDKnAoBLRgCNyfL/HjXmO4koPaoPaOEmf5s
+86aYHbPY7mmywdk6mmyHbjwKZkz60Bl6NcGRhWX0XDrWm6vPnnIHeAPAe4VZx2iEFSVrKYgpI0/X
+UJeYuzhrae0Qt9SANmwdIH7ajh0j4WrW204dYEEE8NqCXW6+7dP2yUMNnTKdZJ24cao2ZGCVO9ol
+tzcmUnh/j7qXTm3tbC8xdcKQnsWRJ7pIUoq+qKqeMZ3YUp9xxxsJcUCQngOJ2+NAmptDxKHEqSCN
+vGhnnCxOHY8sISEpe76YECecfb76J2GXDV8wLj2a4DDhlDikEA+h8Kr/AEk4St7Chdtd4sHUI4xz
+Hw391At0JY38mZntmnHdLV38y4nlP5J/x4102ywVAEbiuLMDfcYeauGjpUlQUDHMGu1MpXPyll+x
+vkbh5hKifOKBZtuCRoiKWbQoGYpx2KkmYO/lSiGjI2EeYoMaQSSDTplJA2rxtJG1OmUgD/tQKM6h
+50MOsv3ujlwgzF02ftopJMAAxt5ULeslKujq5I/9w2frNBb+pM6Dkfsuep8n9NH31lJ9SUfvOBgb
+F/cf10VlBVuv7q/B61g7fKTW3/0Lpn0IjT0d4IN/82HPzNO+v8Vfg/apkR8osn/crpr0HEno5wUE
+cLeJnzNARGSCmCD8ab3IBkAcqctjbhwrV1oqTMAUEBdpABnemRAHvqYu7WASQDNMOw7xAKfHjQMn
+YjvDaq/m64RaYFevcNLSj9VWp+3IEyKo3S8U2+RcSXIkthI9SQKAKXV4u5ue21BLY3JngImrb0S2
+TrmCXuYVuKb9pfUkr0nZpA4Ty3maHDSXsQLOHW/8PdOJZbHLvH7AAfjXUOW8rWWGZbssMTZtutMt
+BGpbQMniVRE7mTQDzMfSJctYXcpwbLq30MoSFP3KinXO3cQBJ9SRQpu/lvE8WS+jCQXXVAyylaUu
+T68TXQubsNNpZK1YwLVsDV3mSSPSFCoPJeTH3rv5dvO2UiYtu1RpKxH0o4xvzNBHXuX0WnRpcGzQ
+supWlSyqZMjccfGN6FGG2+Iv4u5cW+ALxNLSoaZUfmwoc1Abq9Jrq3GsPtrbI12gcFNHaI3oJW9q
+7Z9qbEpSpapg+NA2ezHnJ4rZxLCrZLVshHYWzdutBXPECCoAjzqRtlKxNpxp22U0tSTLS4kCPXen
++XLW5xR2EN3iXE/SCmwUj3wD9tTSsHLPdUtK1cYiaDnq1bXa4jcWLuy23FAe7b7q616v1yb/ACFa
+JMyySg78INc19IViLHNAuG0hPa94jzTxo79Vq/DtjiWHDcJWHk+iuXxFAXrls7xO1etJkCCaevNy
+NXOvGGxG6QDQaIbM8AactohMRWyWyD605aSInwoGwTIgCD40MOsa3/4cXh8Hm/1qLSkhMqNDHrHN
+T0Y38Dg43+sKCd6k4H4EHxDj36yKyvOpTIyZpJPF79dFZQVLr/T+Dtt4DEmf+Cuk+g1uejfAzEk2
+wn4ml+v8EjK9sYknE2Y8vmXKT6BQ4OjTAyT/ACfh+caAiBoDYCaxxOlAGnjSralQJg7V4+sdmBtQ
+Rd2gaZM7nwqLdShCzE1NXS2wmJmot5CSdSQregY3KthE+kUNOnjtF5Hf7PVHatlfoFCim+hOkwDI
+86p3SRhyr/Kt/apaKipklIHGRuKDmjKV81ZZuwi6cWSlNzpE8BtE/EiuqLbMPtFi2WVDVA2rjXEy
+5bX1skApW2AQPA6ia6awJQNvavpSZWgKI5EEUBLwmwbxFSHMQYZd098FSQQD761GMWuK45eYPhYT
+cKsUJNw4ncIKphI84BJqs5hzKcPy4+q0lVypISNIkk8gPEkmKnsuZCvMHyEi3sMQFnmC6V7Re3Rb
+16nFcUnyTwHp50Epmxm3ssnrYeUe0uEHSIoIYg0cKS9cOW5Uw1ClqHEJ5n1q59JWKYhh7tthN0+u
+7et2NetKY1pHExyoft4lfYnir1u6sPWL+mUqT9HxE+HxoLhaXS2sND9k5rbdROpJ2INa212OzK1L
+nn3qhMtMPYU67hThKmEElon+YeHw4e6nFxPblQBKPyY/x5UAt6VLwPZoba3DYbn3knf6qLXVGbcV
+e4u8QezS02kEjmSTFDHNuXcRxnNDKrG3U4SnRJMSZ/711B0I5OcyhktuxvA37a4suPqRuJ5CecCg
+ujoEQfCtmOI2rZ0d2vEpEbcKBy2AVcvfS6UgbDnTNJPnSrRK5UmRQKqC9ESDvyoY9Y4q/FlfiQfn
+G/1hRPWogaYihh1joPRhfEj+Mb/WFBMdSon8DgDyL8emtFZWvUnCRlFYSRIL0+utP7IrKCr9f3/R
+q2PH90mR/uV170EKKejLAxAP+TbfpGvOv+CMsWpPPEmY/sV1r0Bk/izwPcx7Od/zjQEhlRUNk+6l
+XEymY5UmyDp4x50uggpHemgj3mZSSRA8aadmEqAVFS9yfmzHhUU8UzvyNB4tpBG4NMLy2YeZWFN8
+QeNPA4QdMkT9VIvOHSUhQ3oORulXJl7a5tvbq2tyq3U4FIQBvBAO310bMBsQcKw+4ZIIS2kKSR5V
+KZ9sPaWm1WqW37i2lSxH0gBBFUTLeamLW+Tg97cBt9StSEcAB/N9aAws4LgLCbTEbrSi3aWLo61d
+1Kk7ifQ7+6vXeke2uLdCsvYLf46tThSk2zKuzBH85wjSBVeTeu3rCMPNom/tnFauyUdKfzvETyqb
+fubpFs2wthtCUCENsSAPACKCk50xPMvt72L4nkt4Xa2SygtuIUkIIjeFHkfKh5huMWdk+W73Bb2y
+UDu6WSpER4iYq45rwS9duV3Nz7Ssap+ceJ5cAJquM9o2QEIJ7wkKPCgnsHxCzxa1F0w6haSkplJ5
+UjdH5tdwe4gCRJj/ABzpo0lq3X2rbKGVLBBKe7J9OdUnpQzULWx+R7R3/KHk6VkH6CefvNBMdEmc
+13PSMm1u3W12C3VBgFIlJ8Z84+uuvLIzaoKtyRXzpy7fXFjcpWypTa5ACk7Eedd99HmJt4zlHDbx
+D6XlKYSFrTwKgN/roJ1xHdFY2J3mt3EmIrUCO7FAqhAVsRtThIAiCD5UigGCZO1bpSqNiaBR3SpH
+CCKFvWNI/Fne7TLjY/vUTl6tJoXdY0KPRnfGP41sf3qCb6lP+h7h0gDW8J/PTWVp1Ilzk59E8HXf
+tRWUFX6/5P4NWoKhHyizt/8AS5SnV+bDnRpgaU7xb8PzjSH/AOQNUZfsk+OINH/dOUr1eFhHRlgg
+3nsJHl3lUBONvGxTBitUthPGZpVLx3kztz51p2xJ2igTuWu6ajLhvjvUs8XltEttqWI3IrZGFJWl
+K7l4hSk6uz578KCtLQrXEz4RUknAHT2a1uaFkayI29KnWcIsrZ/tN1rSJgqmo1u5vLpt1p1pTUPE
+srk7jjvQBvH04hhuarxzELlaGn0Q2hHBJ3gTXOefrwt5oadUl1p9h4HQvYxqkGuoOmF+1U0bJ25Z
+aeWsKMqEq8QJrm7F8PcxfPTLKy12LSzD6mtaFJBkBQHHwoDBaYld4c004SpbRAKVDiAeVWFrPNi1
+aAugrlOgEHcbc6jsv263sBZtbhse0sICXE6eGw5elRSsJtA4/rag8Y+6gaZqzheXlyvsSWrdHdSh
+KREcKgm8bt0IlxYTGxkQaZZvtW14u0D80yGwVRtJBqCxRy2ClOaU7bJkzvQSmI5mff2YgNjYeJNC
+/MTy38auHnFd7VzNWW9uDDVtbgLed2QmOE8SagMesfZr0NKJU5pBVvxJoNMLt3HvyoI70zw3/wC9
+dP8AQRnVzD7K3wm5uGlWSTpSeaT4ek0CMm4GL68asw65LpCNCB4kcffRz6JMAbX0iXOEBxsJsWUO
+IhI0qBH+PfQdDsrQ4RpcSraYml9AUkq2EVDW2GO2OKe0hsr1JIUUq2I9KmEPs6ezSpJWRtNAow2C
+jma2TqA7vCvGFOCQpPpW5PmRHGg8UCUgn30LusgI6Mr8CZ7Rs/3hRWOyBznwoXdZRMdGV/wMuNfr
+UDvqQz+CNwTw7Z0Ae9FZXvUiH70LiOAedjb/AGdZQC/riY9cZgybaXdx2QWnEGUKDaFIAUGV6tlC
+eYq1dX1BPRrgRHO3/wCY1WuubhNnhOUrC1sX3LhtF61LjiYUo9k5JMVaur4kDoxwLf8Ak5P95VAT
+A0rmB8a2w+1L9ylomATvHhSrTetMJBJPKp7B8P7BsFMdqrdRPKg8xK0Qzh6bdlYbkiSBvE71AnCc
+TxHGRcqAtrdteyie84ANtuQq6KZSmCoaz4mtVpII22oIe2wq2t2tMOOL3lajJ3qDzc37Jhb2lSkF
+YgL/AJm3GrhpUkn3VSOljMWDZawBy5xd4gOgpbQlGpSj5Cg59zRaP67w35beeZAUwp8gl1JO59QK
+p/RVgS2XL/GL5am+3LyWg2jVI4QBB47/AAqbxvNb2a1vMYQ+i11KSEQ0QVeEq4/DzqwdGjLlvlkL
+unHEPquH9bkAqRGqVAcyDvHlQXHEMETbu219aJKA+yhtxB5EDYn7KrGYLY21yEOoLayCFBSYNF+8
+w4sXFu+EqubNTRVp4qVpbBA8NzJk1TukZeMu4fdN4ZgaMTcaLIDIZDim9X0zMgwPf6UHP2e03Tt/
+bN2rS3HFpACW06lKPkBxppYdGmasQR7RiCRhtvpK4cV84QP6PL311ecm2jFs0xgybSxu1sIcDxZk
+lJ4jYgmD9tVvFsoWiQ45mDHVvW8QvfsEQDChtufpJI35Gg57Tl/C7ZDjdtcsWz7SE9s65K3EhXPh
+z4eFVvNtrg15b9ph63W32nEMtoKCS8Oa1K4DyFdE57s8k4060zqQbi2SG0sNgw547jYgcffQr6RL
+Sywu6ZFtbN2zCXO0LaR/CECdz/jlQVnLWH3RxRoWoLEaVLUtUwf5225H30XeifGlYJnO0tn0ds3f
+/MG6UgJgkyD6HhVUyW3b3l+wEpSxcusjSDJC522+HD0qy4Thq8VzrY2QcKmRqaeCO4oECAoBQmAR
+QdTWTYCAVLCieFe3WFWV4pKnWh2iFSlSTBFQfR85eHD3MOxLvXlkoNqWf4xP5KvfVwZbIIOxmgi1
+4elgSkrIPDypqtlQE1ZnW0qbg7yKjLxlTKVAJGmOJoItepKR3gDFC/rJKJ6Mr2AdJcb3P9aig4VE
+bDj4UMOsekp6ML2SILrUfpUEj1JmyjJrytiFOOnj5oH7KyvepPIyfcTzccP95IrKAb9dN9p7J1gb
+a+urtkXjPefIKpDTgIMbE93iKtHV1LKujjL5fC+z9mIMeSlVS+tZh2L2HRrZt47Zi1vTiTQKO11w
+OzcgzJnh4mip1YcNb/FTgDrqQrVbE7jh31UBOsLcdhrbZ0JiEDnUvbM9miIk86SsdK3VqaVqbTtE
+bTzp4J5UGq0kxArRxsngDTkkgbcBWizwmRQNXGjvFBHrLZYxXHUYabJhbzDetC0gx3jEb8uHOjq4
+ElMzNUHpcfScEFmq5RbofVC1qXpgDfY+M0HI/YYtl1a3bmyt7C3LnZ26zHarIMaoEmOfhRKyM8nE
+7Fm4tihLaFKWHNMBKtCiVkb89yKHXSo7a2+IlouqKWkKTDjsjURsB5wZnzq+9ByG2cCtmlKI1rB4
+mSSOBI5GfhQG51SMLwhd9d3wWtNqhaU6YTAG+mPEmn2AN2tvZm9cCW37sh1ZPiYAHwgU6Yw9rEcv
+27ToAOgBJT4D9hpZGHNuhDK4T2YiOQoEsVtnlLtvZ7QOuIUdLmqNCSNxHP0oZ490dXl5jryPlJ9F
+m6kOOslZWFKPE7kxwG1EZnGGnMSvLNKoatEEvOzEE8BNKNOYa7iSk2zrbtwWwpxSSVGOUnnQU+wy
+Rg+HIQpNqhSkc1JGxoE9OeErv82Iw9huO9Ko2CUwkn7a6rv7VTiDpMeBP21zn0j26lZuvLy3e1Ia
+CVSonhI24eCaCjWtk0xcW5xC8RbtWoCmm0nvEJMgGOG9WHo4dxRzOthdXGHutoQ+oF7V3QF8IJ3P
+ED31EvM/InbEqZccvHNRURqCWwIjf3mpfJ79xdZywtCriXC+lxSNR7yAdyYMAbbCg6WbZXa4na4m
+2SEqhm4HLSfon3GPjVub2G1QVtbC6wxbau6FoInwqTwl0uWqAsy433HPUUEnJg7CKbvoC0qB4U4k
+cfqpFwlSjHKgg7pstrIKNPhAoSdZhSm+jW4kDvXLSfrP3Uar5jtGpmKB3WkXp6O3GuZu2p+ugn+p
+aQrJTp0gaXHUkxx7yT+2sr3qWQMjL8S69z/pJrKCqdfhR/B21bIgG8ZIMcfm3Pvq79WMA9DeXdWw
+9nV8A4qqf19Qn8F7NYBKxetD3FDn3Vaeq68X+iDA0L/ikKSNuI1E/toDCNKW+7tXjbknTMKB3pNR
+BRusSRwpG1bQlZdTqle5M0D8pIRIMzWi0k7kVs26VARuOHpWy9kydtqBvyAoR9PDdy6W0NBOlLJV
+KuAMnf3UXlQTx40JOmrHLe1vfYLllLluGPnSeRPn8KAA4sxhGY0IwXEbdAcaKSxfJRpVpJAjVzTx
+MHwFGPLGXLXA+yw6zh1lgobSswZKYG9B3GUWzCEXdmXHGFPoShKR/Bq8FetHnJTbjlip1/6Yuz9L
+YjvJP7aC9ZYYPyUyoOiQkApSQUp8hG23Davc2JxC3wi7fwlkvXvZHs0AgFR8idp8JpbKaAjCGwmS
+EqKQeaoP0jtxPH31LPJ7VlSRsY29aClZTwFdnkq3axBhab19IuL8FYUpxziQo8Dvy4VL4Szh7XaN
+WduGlIjVCCBvy4fZUi2T2imjsJmDSVuy6h59bjoUCrupH5I8KBtjTybTCLl8qCSlohJP84jb6656
+zk0FX+IS0pbhW2lKUnc7EzRw6QHpwL2clZ7d1KShIOpQG8SOA2Ek+nOg3iuFrxBV+Ge0S4HHXAAd
++4Dtz5GgodlhTuLOXV9blt+3t1qTcIKtK2FA8DOxnl76neizALh3NTd4ppRJCVIIJ+jJ+HCvcq4h
+8nZdRamwLpecU4tLg1BaCd9W3Ab8TPCp/oVxC3s80Iw95d2lVwSlhLoEJgcoHODQdA2Dei1QmDJG
+9LJZ7N4vN7H8pP8AOrGNSedOAQU7+NB6l1KyIO8SKb3L4QstEwTBn1P214NFvreIJK+fgPCoyzWi
+/eXfPhaQh5SG0qVACdoMefH30Ek+4lCi2dwQTx50Detesfi6J4TeND6lUYbgqf7VsK0KSe799BDr
+VOLPR8pCjwvWgY9FUFr6k8fgSuP/AFHj/eR91ZXvUm/0GV/Xe/XTWUFc695P4NtJO6ResHj/AKty
+rb1YRo6IMvAAd5lZ9/aKqqdexKjgCIE/5Vbn+45Vs6ti9PRLl0Dj7Or/AIi6AqO6QtC1qjkBHE1o
+HCEKCdlQSJ4DwmtL3UGwtKgkpM1s22OzCn1pJMcOFBtgq1rtRJKlT3jBAJ8p4ipIokAz7qq+FYnj
+D+OutrtbVOFto2eQ4dQVJATEQdoPvqypdA3kEcaDCxJ5jehL0k4JbX+aF2D4Kn7tslsaZ2jaD4yf
+qovJeBA9aGGbcYUxmS4XcLW2GwopMDToHL12nagDeMWrOQ8Ut38Te+bfeShTDYSrWUwZUOW870UM
+MxBD2EX17bgFKrp4twdjCEkfZQn6VMHxDMuZrB/CXbd3tnA0u2dX88gkzqG26efjtRiwWy7DLhY0
+gr9ofQTp3nSdx+jQXTLLxUxdJVGpL6jtMQrcRPkR75qQQ+kKUkq3mq1ktSoeSlBQkttqEj6RKASf
+MST7wamXwWX+2EQdlDwoFApS7wmBoB2ptbG6TdvOOvsG3OzTSEFOnfz4+tN7fEW3seXZNmS0NawP
+yR51o9d3Vzj6mBZhq3aSfnCoFThkbwOAoIHON2HsXtbALWgBQVISd1KJgTwiEmR6VV8rgqexG9AB
+0WjzgJG0mpa5Wm4zViN8hQcRaoXJ0gAdmgAAEHeFFcnjO3KkujlTSbK+uX/4MMLCpH5Ox4e80Alv
+7O89rtLK0tlXK1EgISqEK27yp2HAbDyqU6MsLXhucEqWyHHGnNTbhSJIVPCCY9Kl8CtsYvbxzFuz
+bu7dLmi3t1PFGpO41pjhyEceNPMpl+zzkwzeKLr2IK+ilOlCAlJIgevPnQGFNwrsCofSArXA7x29
+Sokd1KiJ9K2QzpSNUwRxpLB22MPs3xqVHaqWeZMkmBQa4u+3fONWdqVrLdylL+gwWiBqTPiOHuNM
+ct3D14L9zswi1RcuIb1cVadp9Nqe4241h6HMQbMLQytao4KhOxPjwpnktl1rKNqHge2cRrcP9JW5
++2gmrRsItgTBJ8KA3WzRoyUFJ4KvG5+CqP6GyGUgAcKBnWzQT0epIH8ub+xVBYepbIyQ2AQR8/Pl
+84Kyk+pPq/A10HcBbo9O8jasoILr1ScBRBgh63j4O1YerOo/ilwHURs0sbf7VdV/r0j9wkqn8u3+
+1yrB1Z5X0TYFwjslgf2q6Aq3X+bSPCeNIYchNywQ5JTBETtTi4SVWy4HLamuEhSElJM78qB1CGG2
+7VpCUpnvnkkePrUQ/cXjOYmbFAUbd4FSf6McR9dSmINLUJCCptB1KTzX4D0mqhmPH3MvZgZvbxtR
+sy+1bKITMLcCt/iAKAjhsJQIiYoW9Ilp+6V44yg3DhQIQ2qTJ2I08440TWHw/bJdSkiUzvQ2xxdy
+xib7rDalvF8nYctXCaAMYfjWJp6Q8Js8SY1D2xBCwkoIIOwE8eKZo34OV+zW0pK+1vS4QFcAttdD
+XGMYwu66SLG4xKzadvWLhDbLikFKgeB9QJPwotYez2YwxI4FDK9/zx+2gTynCb20WVhRfsQAmBKA
+hRmY33Kvqqw4mlCLRxxwEpSJgbk1XsHhl7D5b0Bu5fZU6Y4EylBnfcmdvDzq4ONpXG21AN8i4ZmH
+D845gfxLsBZ3LiV2znaa3FbniD9EAbRw22qw2bTFniV865dXLj4BUpK1Hs+E90UviDqm8UJgCFAH
+zBH3iorPVwm1wdy5ceLZdSGUKClJAUo7GR/jhQVxntG8v4ze3LTSHnWtB0DYFaiYnme9x58aQwNt
+bWT8XdSrQPZwkq8CZn6op1i/zOT20JVPbuBSTxkAEj7BS+VOweyhiDlwvTbuFxKlEcEgQf20AJuF
+Y1eW9vb2jtxpYK3FqZdG0ERPDxiiV0WYWu8xNGP3ilpdb7gSsGCqIkVBXgwDCMFQ40t5Srx5QZd1
+hSVRAkDkB4bVa+iS+YcuHMMR2kBHtACySdzvx5cKAj3t8pi1WUtqWoJkAVC/L9naWKbh1Sbi8WUM
+9i2QopWQYkDcDjvUvi94zZYY/dOphttsqJAnYChDlN22ds14xiS/ZXrm+cuWme00rUClPZpUBvJA
+n86gvrz1zieBui4T2NzeOIY7IHZvUdwPQA1dLVkW7DbKYAQkCqlhlstzMOCWOgDskru3gTMHTpAJ
+9VH4VeFp+cMAbeNBouNEBRG24oJdbFH/AIcpM8b5refJVG90d0bbmgp1sEk9Hze8RfN7fmqoJTqU
+6fwKfE97tndp80VlJdSrbLFyJnvu/aisoIrrzpP4OoVy7S3+1ypjqvuT0SYIkk7B0f71dRnXlSDl
+hKo4Lt/f3nKfdV0a+inByPyQ6D69qugM6BLShHEUyw7a4dQDMHantuklBHKmjbfY3ijBAO9BviT7
+bPdeMJcQQY/Z9dQV6S4n2e+CXgp5tbIUmSUpgyfMGneeGFuYCt5DikKZUlyQJ2Bk+u01st1tWYbS
+zCNZuGlKkD+DSIG/hMigstuEhlO0gjaqRmppJvXvY3GW7pSiYdMJkDjPjV8Q2EtgDkIqi52tmlG4
+L3dE90zxJHDyoAnjDdwzm21uMX7uKXVwA2yIKUJTxWFDZRO3186NzjqWbXCVE8EIT8Ck/fQlunm3
+834azcWVtqt3VJQtLkqEjf7d/Si9i7KU4HaPJ4MupmPAgj7qBtdtqRbX51hPs16h8o2GuTATJ9QR
+5gVamHSS2VRCk+FRl9bj2m7HY9qLi2JCAY1KA2G+wM86c2F12+CW7y161pQNStpkcZjnQNcetwhZ
+dA7y1JHHjVTz5cLdcw2wYQHpd1OTuECISvbeZkDx34xVhz1iAscKNyYKUd4jVHI0PsOujfZgaxC5
+uVIbZSFFtPNOkOBRj1IA8vSgks4OMpNtZtAFthWohJHArA4egVTjCbFf4txag6XHWCtfA7rOoj6z
+UFfF66XcXUavablQSTP0Q2qOJ/pD3ir4y2G8Hbti1rBbCVDgIAoOfsaxFOG481hC3LZ21QrtWm1t
+hayrgrSeRIMRwgUR+ibCWkdpj3cC32g2hCTPZpHI+e31VRs2ZYtHLxePKJUwpxTSWUcdYPDUOCSI
+38iKu/RG4ub+xDehhhSVISeI1DfhtxFARnG27izW28gKQRBBHEUEsFwkXnTJcYeo6rSwWl9CI4bC
+J8d/so3gAtQKpOG4a3adIuL4i0nvOWaJ24qkxQWzKzYdxbEsVIOnULdonwTx+s/VU8rvKK5nwApD
+CrX2TDmrfYECVkCJJ3P106aSJ8qBEqIAEDhQZ61xB6PmduF83+qujY62nwoJ9a8j8XzQif8ALkfq
+roH3UsP72LoT/GO7e9FZWvUtIGWn0wZK3if0kVlAy68IJyxO8Tb/AK6/vpfqquBPRfhQJjvuiDz+
+cVSXXg3yuE+HYH++uvOqopA6M7DWB3XHY/TNAemBKCBSdy2lDqSY350lZ3KdxIn7a0xJ0ltCkmCn
+eg0x1CXMJfbIiUECee1UToGzLe461jFtibRVcWF4WUPkfwiOQnxEVaMRxGbRYWZEGo7oktLS0yym
+5YA1XTzjyz4kqNBeytU+NVvMFlbYip5t5TqVEBIUhUaTyPhU4t4p4c6qGbHL9DinLI/OIlQSTHLj
+QDfOeCsYNmBDlpZEvKR2i7kypSjqg78Ad55UUFLL2WlIUkmWwRtPDehPjeM3t04W7nuBtxsFA2JK
+l/ZANF7C1JVh7bSu8kpjc0Gl1iLbVrh12tQRKkoJJ2hXdH1kVtgbgFvdWyne1LbihqkHUPHbn4+d
+QePYbcYng7Fqw5p7J4FSvNCpA+IFLNXTeFIxS+dSUKTal5QJ2UUpMke/9njQD7NmNX+dM7KyvhyU
+2+HWw7R+4c37VKFaVCBwEggTE0/dbfQWbewabsy6pWs9nJDadjII2Jnlt4VZzY2eH4cvFbawtkYl
+fNN+0PBAG8TJ99Nra0Q7jTSyhLa3GktrK3ZEDcpTzJkn4cqDW8swi5wu23AbaU85pHFS1AD6pq2L
+HzACRAAqKdQh7FiscilA9E//AO1KuL1IMcAKATZrFthgdwnEb1NjhhJdU+133DqV3U6dJj135VNZ
+CxrK7PZ4Xg945erMBb3YmSTJ75gR68KpvSFdtXV3ibz60e0W1ou5LSRsEAqS2k+oMnwIqMyLauYM
+3heI3lk9bKfulWt20pWkgLIgn3EEUHQEd07kCq1lwPO5qxRTgGhCkBJ8QBP2mpe2eXZdnaP6ltaQ
+lp8qmT/NV4Hz5+tQVveLtMcu9AkuOoB9KAgNqUobEA1oHFa9JFJ27mod0+tK7SFzvzoN1lWmZ4UE
++taf3gsTzvkAforo0uujSQN6B/WqcKskWyRzvk/qroJfqYT+DTuw2U9+sisrfqZD97D3kp2fXUis
+oGHXc3y3Eb6WD/vF1TOr7mvCMJyFbWt3i1nbPJdc1IceCSJVI2NXTrrtzgIUebbH1OL++uNnEkHa
+RvQd42PSFlkAFWP4bI//AJKPvp09n/La298cw0+l0j764C7yTxPrXhKualUHcGN51wJVo6pvGcOJ
+0HhcI++k+jDOGCYZlJi1uscw8LC1qANygEBSiY4+dcSHUocTvXg17gK4UH0Jts/ZXUolzMOGJA8b
+pG/11Vcx59y6rGJbx2xcbKIMXSAkHj41xBqXEaj8a1UTO5NB1FjucsuqddGH3tj3HWe0V24AUe/M
+c1RIPGN6I+HdImVm7VtCswYYCEj+Up++uFIJ8ayFfzj4bUHdFp0j5TQ++hWYcNSgr1g+0p58ai84
+dIeUVWi1s47h90lbKmXbcXAhaVbevjw8fSuLAFR9I153juSZoO2nek3J5Nuk5isAlpIIT2oIGxHv
+2Net9KOUWFBacdwwlcqWQ6JnhwiuJAFExJisg8N6Dta16UMn9sFuZhsATqO7nifup4elTJn5OZLD
++1rhzSrzrIJTtsaDoPpMz1gb+NG7sL21uBd4Y7au6FA7laon0mansdz9lrEsh2S14tZ+2pQytbYW
+NWtIAPv2rl7SRsTNbBJ8aDtW36UMn3Fg2HcespU2nUC5wMVBPdIWWk4s1+7to4nWklwrHAHnXJAC
+toJrfccKDuWz6UsohO+YLEf/AGU5b6UMnkE/hDYE/wC0rhi1WG7hC3UFxCFAlMxqHhNSPttq46Sq
+1S2kuqXAPAEyEjyHCg7Vd6T8omNOYLDf/WULOsDmzBMeyqzbYdidtdOi7SvQ2qSBpUJ+uufFLK3V
+rQAlKjIA5DwpdqeZJoOu+pjtlh8Rtqd/WTWUr1N2+zyo54rLiuP9NI/ZWUDLrop1YAgaTu21B5fw
+iv8AtXHrzMmNJ25iuzuuIytzLgUB3UtN/Hta5DW1J+jQRBZAMwTWBid441KlqTFZ2IkkpE0EZ7OJ
+3FeKY7xhMVK9l4CvOy24cPKgiCx5D41qWoMQRUuWSREDjNeFkapIoEEi0Fro1Q4EJAIRzBnf69/S
+lXrlpSrwtdnp1ksgtjgVSeXhtXpYT/NE16GeGw28qDxm4bLlmXez0hep8dn4K25eG1aBVsm00KIL
+obUmQjiZOx90QeVO2WmFFKFMjUTx1QPftTj2FggkBgDURBe3+ygi7z2Zxp/sVJSlTiS03o3QnfaY
+9PXjSeG+zNsuJfSky82RIJOkTq5c5G3OpP2JorCAlkd0GS5sd/t+6tnbW3aX2iW0KSFboS7Mj4UE
+Y8m2DKezUndrTu3BCtczw8K2U1a+03L6XG9Dpc7NGg93+by293hSzrLanCW29KTwSTMV4liOVBHP
+WiUL0ocS5wkpBiffSaWNztvUr2G86a9DMDYR40EX2BE6RNbJYPGNvGpMMA8q9LJ8B8KBghnfhSoa
+QdwmDTzsiTukVuGjEQAaBu22EwIpw0gTsK3S0rbu07tmVTPCg606nrShk5bihA1OJHn3xWVKdUtC
+R0bJUEgEPLB24nWr9kfCsoNOtVaOXeUEttNqWsplISkknStBP1Sa5NXhS9KdVncSAdXcO/hX0PxG
+wssRt/Z7+0ZuWpnS6gKE+O9RRyblgmfka2HpI/bQcDDDGAynVZ3Xac1Rtz/7Ui9hqVbtMOpE7yJr
+6ADKGWxwwlj4q++tGcmZYZWtbWEMoKzKgFqgnxiYoPn/APJrv/oufCvBhywf4Je3ka+hP4L4BEfJ
+jUep++vBlfAAZGGNT6q++g+e3yavj2S/gaw4a4Y+Zc/RNfQo5XwAmThjU+p++vPwWy/M/JjXxV99
+B89Pkp4meycj+qa2GEvT/Aun8019ChlfAASRhrUnzV99bHLeBnjhrJ+P30Hz2ThL+/zDv6Ne/JFw
+Zlh39E19Ck5dwVPDDmR8aw5dwX/49r6/voPnl8kvD+Idn+qa1+S3Z/gnI80mvoactYESScOaJPmf
+vrwZZwEGRhjIPv8AvoPnmMLdH8W5+ia9+TF7y0ufSvoYct4EeOGMH1BNe/g7gn/xzP1/fQfPA4cs
+H6Cx5RXhw9QH0VV9DXcsYAv6WGMn4/fSasqZdJk4Wz8VffQfPgWPPQfSsNlvOk719Al5Qy2RvhLP
+xV99aLydlk8cIY+KvvoOChZ4fKtSbgbbcONYLO030h7yMV3j+BuWNR/ce34+f316jKGWwSBhLIB2
+O6vvoOE7e0swiXRcSBtoAiYP7Yq69FHRxdZ9xt2zs1qtLW3QHLi5WmQkEwEj+keXoTXXyMpZcSNK
+cJYA4wCfvp5Z4DhNqoqtrQNEiDoWoftoGeRMrYdlDBk4PhfaezoMhThBUSSSSSAPGsqfSAlISOAr
+KD//2Q==
+
+
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: image/jpeg
+Content-Disposition: inline; filename="custer.jpg"
+Content-Transfer-Encoding: base64
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4Q1kRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB
+AAAAyAAAABsBBQABAAAAbgAAACgBAwABAAAAAgAAADEBAgAOAAAAdgAAADIBAgAUAAAAhAAAABMC
+CQABAAAAAQAAAGmHBAABAAAAmAAAAOYAAADIAAAAAQAAAGd0aHVtYiAyLjExLjMAMjAwNTowMTox
+MCAwMDo1NzowMwAGAACQBwAEAAAAMDIyMQGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA
+//8AAAKgCQABAAAAyAAAAAOgCQABAAAA9gAAAAAAAAAGAAMBAwABAAAABgAAABoBCQABAAAASAAA
+ABsBCQABAAAASAAAACgBCQABAAAAAgAAAAECBAABAAAANAEAAAICBAABAAAAJwwAAAAAAAD/2P/g
+ABBKRklGAAEBAAABAAEAAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAk
+LicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAaAMBIgACEQED
+EQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0B
+AgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpD
+REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq
+srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEB
+AQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFR
+B2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVW
+V1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrC
+w8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/ANNRzUiIfSlR
+DmrUUfrXOajEiz2qZYPap0jqlrOr2+i2RkkZDMR+7jLYJPqfanYCd4OOlQGe2ibbLcQo/ozgGvPL
+vxRqE6kSXsj+yfu1/JeT+dYsl55m7JUZ6kD+vWiwrnsAlgfG2eI/RxUhjrxlLhgMLKcdua0YPE2q
+2mDHfSMB2c7h+tFgueqeVimmPiuO034hKWEepW4APHmw9vqK7G3ube9tlntZUliboymiwXECCgpU
+oSnGPNIZEqgUVKIz3opAVUXDVajFV4/vVT8R6yNC0SW6THnsfLhB/vHv+AyaaApeKPF0WjK1nZ7Z
+L0jk9RF9ff2rzC71Ge7naaeZpZWOSzHNVZriSaRndyzsdzMTkk1GFLHABP0qhEhnc5xWjpmg32q5
+8oY9M96hs7DzHG4MBjJJGK9j8HW1ta6ckqqHVunHQ0BY8wl8H6hDGWOcjttNYtxbzWxIcHivoXWN
+X0nS7YzXh5I+WMDJNeY+KTb3umHUotOlhgkfYjkfqaLhY4ESZrb8Oa/caLqCMrFrZyBLH2I9fqKx
+Y4mkcIilmJwABkk16R4V8BmDy9Q1dfnGGjtj292/woCx2qjIB9aftp+MUvFSMYFopxopDKCDDdK5
+L4i2V9dWdlJb28kkEJcylBnaTjBP68116ctWjanFUhHzpit7SLa0t1W7u5lCk4AxmvZbzwvoGoOZ
+rnS7dpDyXUbCfrjFeZ3WkW0OuXEcMMbQK52RljtA9jTESWNvDqExntAJDu2YEf3R6/413Xhq7itb
+IW88MgKMct2zmvN47h9C1WCS1uzGGbbIqnPynsexFelLMloUMjF4pVBEh75oAvX50q7+RIXknkwG
+Pt6c9q0bu00e/wBEk0m/kht4pE2KrOAwPYjPcVmWlza2TSXTbWwMgep9K5zWdavNSmISzS4bOMjA
+x6AUhm3pHhfRNFVZLG3EkmOLiQ72P0PQfhWnITUVjJNLYQPOmyUoN6+hxUjZzQAwr7UgWpKCMUgG
+7RRRzmikxlGNavwDiqiDB6VegGQOKpCJygkhaM9GBBrzHxFp50+5la8tnO4DbIg6LnqP0r0S/v5L
+K1MtvaS3bZKbYcHDDqD7+3WvONT1u+1W/jbUSVtY3yYEGAB396YjCtLQXt1mOBhAjZDPyT7V3GkX
+bJbtp92pe1I+V8ZMf19qzta8T6dounx6dplhbySkbi/U4PIJP9K4y88Q6reYMk2yPOQiKFU/h3oA
+73VLC6trFrmCVJ7dTn5X6fhWLo3ih31i2N3Bi1iYqWjHTPc+uKg0zxCb+2NrNaHj75hOFI9wKs6j
+FF9hf7CotmjAZWTj659aAPUQVdA6EMpGQR0IpjCuJ8GeKVNtJYXshd4xuiKLyw7jH9K7SK4guGdI
+nBePG9Dwy5GRkHkUDFA5HWnMBikIwaKkBo64FFAPNFIZVQVi+IvEk+mSJa2OVnxueQKDt9AM1uov
+NedeIJkufENyySEAEIMHjgY/mKpCZaOri/hjg1G3jnhRtylB5LofVWXHP1zWpqF1pt1pg8+4jmlS
+PbA6o32lj/02ydpGOMjr+lcsXliHOJF7jHNQSyAYmiPyA5I9DTEQnTVimMwywHOD6VoafJHp13Hc
+pZW92gJIhnGUORiljcSJnqDUMTBQ0fdf5UAaFhANDj1STUbP7PJN+9EQHHlnJUKPTJNSW4G4jqCB
+1qKKzu0EeoXV0k0N0CqiSXc4C5HI9OaSNsXCIvCg4oAzNbj1O01iDVXhS3km+dPKUKpHTOB61b0j
+xNLYayl3JGBE6COWNOAQOhFMv7QGCa7N5E489kFuZCXTHOcdhWMGBJJ6CgD3BJEliSVTlXAZT7Gn
+AA1xvgnXJrxX0+5fd5SAxEjnb0wf0rshjFSMTABopwxRSGVgDn615TNCTNMrctHIwP516xzuGK8z
+1FBa+I7yJgNrynH48/1qkJleOZo0GMstMlDy5MKxsx6g8GpGUxSnb9084p5RZUyuFb06UxGfazPE
+WicFWB4B9Kk3fv8Ad68VDOri4BaNt443D+tEchKsfegDQsVQy3DjJcR/d7dR+Va9lDGLR/NkLOzE
+gAenTn6iudsJtmoMeoaMg+3IrbtpJVjXa+O/NAGK4Q3M5bG7ewz+NZqAZcdlY5q3bxXOrasba0QN
+LNIxUFgo7nqa1PCPhttZ1a6S7Vha2rnz9p5Yj+EGgDLsZ7i0nS6t5WikU5BHp6V61o2pxatp8c6M
+vmYxIgP3W9KI/Bukyu0stmgZkwkSsVWMepx1NcOss/h3Wp47K4WRUfa+4cSD6UmM9IGM0VQ0zVrf
+VLbzImxIB88Z6r/9aipAn3ZNcB41tGj1gzKMGVFcH3HH9K7pDzXJeNCrTRuzHem1AueMNuOfzX9K
+pAznLa7juUCSEJMvr3qZwydRx6isi4ty3zJw1TWksjrs3kOOoJpiL0twwXlCwHoKxri6WFj8pG/k
+A1fe6vIODtKmu1+Gc9vcazNHeRxyZh+RXjVuc8nJ5oA4zwxp91rmpSW1nGGnZMZbgAZ5JP5V6Cvw
+213Jb7TYgbcKPMbr/wB816pGsESARpGi+iqAKT7VCPlMi5+tAHisPwc8SLJk39hH/tpI+R/47XWe
+FvBeqeGtHnhlENxO8hlYRSH5vQDIHP1xXoSzow4ZT9DSvIO1AHiepeMNUWaWCBPsgDFWDrl8jsc9
+PpXMajqdxe3BuLt90zAAsABkDp0rq/ixpTWeuwanbDC3kZDr/trwT+IIrziS6uUcJcqGWgDpPDOo
+pb+ILctJxITGQffp+uKKyNFtjc67YxqdyNMrfgDk/wAqKljR64vB5rK8Q6DaapCtxI0qXC7I0Kth
+eXAGR3+8fzrTDYNQavKRol465DpEZFI6gryP1FJMbPMEcqxRhytNkwrLInDDriqST/ay3z4lB3A+
+tSrIWQq3DjqDVEl9h9oiDIC2e1V4H1KxvEuIGe2KHPmZxtqkbuazYtC5FVJ767vG/eyEr6dKAPQr
+P4k6tZRHcy3KKeGlXJI98VuQ+OdM1EB3nubN3IASWM4Y+gIzXksN4YcB49yjsank1L7Q4ZvlVfuj
+3oC57To2vQX8jJBdJujO0hnCnP0ODXWxC6dBuLL6ZFeP2/izwp4hsIodejmtNREflvdRICjnszY5
+OepGOtcvc6vqeh3Ij0jXbnyifl8mZth+maLDPU/i9Cf+EOt53YB4rtcHvypBx+leILdgDbICwrqL
+7TvHPiVo7fUTdTxp8y+dIAg9+uM/rXS6D8PbDTQs+pst5cjkJ/yzU/T+L8fyp3AyfAmiXHn/ANrz
+I0duqkQhurk8Z+mM0V6BI+FCqAFHAA6CiobA/9kA/+EMRWh0dHA6Ly9ucy5hZG9iZS5jb20veGFw
+LzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi
+Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg
+NC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5
+LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgog
+ICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6dGlm
+Zj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczpleGlmPSJodHRwOi8v
+bnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUu
+Y29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50
+cy8xLjEvIgogICB4bXA6Q3JlYXRlRGF0ZT0iMjAwNS0wMS0xMFQwMDowNzoyNyswMTowMCIKICAg
+eG1wOk1vZGlmeURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpNZXRhZGF0
+YURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpDcmVhdG9yVG9vbD0iQWRv
+YmUgUGhvdG9zaG9wIENTIFdpbmRvd3MiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHRpZmY6
+WFJlc29sdXRpb249IjIwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIyMDAvMSIKICAgdGlmZjpS
+ZXNvbHV0aW9uVW5pdD0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSI0Mjk0OTY3Mjk1IgogICBleGlm
+OlBpeGVsWERpbWVuc2lvbj0iNzU1IgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iOTMwIgogICB4
+bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Zjg2ZTcwZTQtNjI5OC0xMWQ5
+LTllM2YtZDQyZjM0NjM5ZGJiIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ1dWlkOmY4NmU3MGU1LTYy
+OTgtMTFkOS05ZTNmLWQ0MmYzNDYzOWRiYiIKICAgZGM6Zm9ybWF0PSJpbWFnZS9qcGVnIi8+CiA8
+L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+IAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7/2wBDAAUDBAQEAwUEBAQFBQUG
+BwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUF
+BQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e
+Hh4eHh7/wAARCAD2AMgDASIAAhEBAxEB/8QAHQAAAAcBAQEAAAAAAAAAAAAAAAIDBAUGBwEICf/E
+AEIQAAIBAwIEBAQDBgUBCAMBAAECAwAEEQUhBhIxQQcTUWEicYGRFDKhI0JSscHRCBVicoLwFiQl
+M5KissJDU9Lh/8QAGQEBAQEBAQEAAAAAAAAAAAAAAAECAwQF/8QAGhEBAQEBAQEBAAAAAAAAAAAA
+AAERAjEhEv/aAAwDAQACEQMRAD8Am8Hl60dEGN+tAbnalEBIr5+PUIoGcmllXPSgkW9LxxntVBET
+2pVY/Y0tFEe+KcpFRDYRDHSjrD/pp4sdHWIUwMHhwOlJcpXr0qVeEEd6aXCxxRtLK6xou7MxwB8z
+TDSSpjelAtQFxxvwjaSmGXW7dnH/AOtWkH3UEUeLjjhGXATV0z7wyD/61cNTTDbG1AKcdKjBxXww
+RznWbUA9Mkj+lSem3+n6lE0mnXtvdopwzQyBgp9DjpTDXGQ0mY6fFD6Vzy8DpTDTMR7UOT3p0U9q
+KF3oaasm1F5PSnTr3pMAb1FJrGwGRRuX1o64xvRtqYG5X2oyp7UpgV1agCKAQSKFKZHLQozUEoNL
+Rj3oBd+lLxJsKrTsaEmnccW+d65Ehz0pzGp9KuI5HHv0pdU7Yrsa+1LqmSBitSAsabdKrXiLxfb8
+IabG6xR3V/O4EVsXweXuxx2/maX4w410PhmJknlWe7A2gRhkf7j2/nWCaxxK9/qs+ohea6mbJuJd
+2A7BR0UDoKuJaud54gcYX0Jk/wC46Lbno5T4sf8ALJ/Sqlq+sx3j/wDiOoahq7A5HmSFIwfYf2qA
+uLxpn55ZGkf1Y5ptLPynAxVxEsdQXcwWNrCvuvM33phM8jS+Z5rZBz1pn+JxuWxRGvxzbDI9aYbE
+rHfXgGFuX/T+1OrPXdZs3LWt/NCx6lTjP2xUGl3EO9HN3Gds/ah8XnTvEjiiywZLsXKj92UBs/ff
+9at+heL9jNiPV7FoG7vCdvsf71izzAocGkWkyD1phuPVuh63o+uRc+mX8NwQMlAcOPmp3p8UA7V5
+Gs725sp0mtLiSGRDlWRiCD7EVq3Afi5KrR2HFA8yPoLxB8S/7x+8Pcb/ADqYNfkA3pLlpWKWC6t4
+7m1lSaGReZJEbKsD3BoKtYWEwvSjhQe1GA9aOoFA2K4JoAb9Kdcq5zQEagnYVKpALkHrQpyAB0Az
+6UKioTlIpxCMikyMiloU2qwLRfMU6iGe4puin0p3Ah6kYHf2rUQsgVFLuyqoGSScACsj8TfFURPJ
+pXDEo2+GW8Hf2T29/t61D+MHiM+pTy6BoMpFhGeWedT/AOeR2B/h/n8qyiSQjuc961ImndzdzTyN
+NPK0sjHJLHNNZLk5601klY96SZqrOnRuipJApKWdn70iuTTqztWnkUAEg+gqp9JRrJIQEVmPsM0s
+LG9dsC2lyenwmtO4D4PZ3jmKeYp64GQPnWoDw8WeBZIrZUbqMj+dTVx5l/yvUACfw0gx1yKQaOaM
+kNGy/MV6UuuALhIyGRDj2qj8W8E3do3mCDIJ3wOgppjICzD2owmPepnWdKaAtlSCO2KgGBBwe1VM
+OBJnYUA2BTdTR1zQaD4UceXHDd+thfyNJpE74dTv5JP76+3qO/zr0PHiQB42DKwyrA5BHrXjtc16
+l8Lo75OBNIF+GE34cYDdeTJ5P/bisdRqLGq980cLjpXQpB60dQc1lonymuBd8UsFocveoogXahSm
+2+1CsqglI6E05twu1NU69KdW/XFaiHcYXOKznx44xOi6QvD2ny8t7fITOyneOE7Y+bbj5A+orRlK
+Rq0kjBUQFmJ6ADqa8l8ba5LxDxTf6vITi4lJjH8KDZR9ABW+WUY03KMCkC5cnJorHJoA4rQ6elBI
+2kOFGTjNGUcx32qZsdAubiESoC+eyjcUQwgspVTmZod+ilxk1a+EtC8/UIeZ+WOVSSmNw3/WKjG0
+60tpbeCZJTLIQVKHIO/Tb5Vb7Vbhbxr+1jES82Vjkckuo/d6bHHr7VBtXhrp8ttaxhreGKFfzcw/
+X1rTbdomQBQCpGxA61ROFW/EaBDcsQrcgyo3GcdKnLNTHmWV0VTjdTjb5f8A+UaieFjHK/mHBQZJ
+J6Yqr8XXPD8UDie9tkC5BzKPsKrfGnFGu6jM2k8OlxynlaY/EgPYbd/sKyjTOFeK+JeJmtdUSRgk
+gDM/wDrjI9fuaiadcY22k3EL3MbRgcpCuB136VjOoIou2ZNlJyK9NeKfhUR4fwS6aZDLYEtcIDu4
+I3b6YrzJdczTNzdtvtViEQoNKwxs7qiKWZjgADJJqS4b4e1fiC/Wy0ewmu5m68i/Co9WPQD3NejP
+C7wtseEuTU9UaK+1gj4SBmO3/wBuerf6vtVXFO8K/CR08nXOKoimCHgsGG59DJ//AD9/Sti5eu1O
+5zzHem/b3rPqyCgHNGUUXau5qK6a4a4DXcZqApOxwDQoxWhWK0gsdDinEGAelJMgxS0CgVqMmHHM
+7wcDa3LHs4sJsEdvgIryU29ez0tobmNre4iSWGVSkiMMhlIwQazXjHwGtLsyXfCt8LWQ7i0uCSny
+V+o+ufnXSMvPGN66BVl4m4H4p4clZdW0W7hQHaUJzxn5OuR+tV4Ieb8pG/pVE5w/odxcSiaWNhGm
+CB6g9/lWn6XprWmnk5jifPQ9MHPX5AVQtK1trTTrlDkyOnlg+m2/9BTF77W7iN3Ms5UAFiW6elRF
+6gsNQZ31aOS3gnjCrIzDnJU9cdh9KXKZto52hYWkcgAxj2OGx03qF4D1qOyv4oNSvVCznq+8cfpz
+fP8AStj/AOxtla6VJDaXLXMDp580gwU5zvzAg7/agsXBN7pqWqWYYK4QOQTsexq4wjTrpArMvKw+
+1ZXw1oyxW8byOfPyc4bOKmJWvLRSPMbH5gcYxmoq5axacP2ulSAzx28e55om5Wyfl3qJ4Jt9Gtbt
+ruCGeaRsBGlkJyM5O9VaOG8v7oJzFhnY9Rg1oGh6BIlp5WQVC9em+KKstmq3isvLEI5MgqNwfXtW
+O6j4EcH2/Fd7qF811PBNKZorQNyRIDuVyNzv7jtU5xBxbxhoDfh7Hh6wEEPWV7gF3/2ov9a5oPE2
+rcUomoXUFvBaRgoAHLSM/cEY+ED077VUSem2NhpNmLLS7K3srdOkcKBR+nU+9CQk96OxBpN8YqVT
+dzSecjpSjgE7UFUBTtvQJMoAz61xRntSpAoBRisqIF36Cjcu3SukUBt1oOcu9CjKAaFYq6r2AcZI
+pxEvTem4SnMKYrUZPrUbipa2bG9RNqN6koelbglElBXBGR6Go/VLezNlPKmnW0kgQkc0S7n7UvET
+iofjLVrjS9PDWyKZHzueigdT+o+9VHmziHSiuu3d6UjJmdZ1B/KBJvnHrnI9iDUqAlxZLplxp6lF
+AdnXClhk7j164ovF/Pa6v5v4ZppJCWl5JCNic8uOg3Pak5+JntoRaS2DRyQrkCQbgEk9vcn70ZV/
+XtFktLCS4ihcRscDKbjpt/Or34IW8eraeLS4vrkJbyeYsAlbkLDcZXoaoGtahf3/ADT80nk8u4J+
+EbdKmPCrU7jh/VI71gTaSEeZj933+VFbglytnfGKdVUMxOMGpaSS3uYEBQZGQDmm2o2FvxBpy3tn
+MAWXmSRNwfqKqY1HUdKumt7wFcbFlG30qYrQOHohBdBlwA24wOm+f+vrSfGfF8nOdN0+8MKQjMrq
+uSx9B/aqmeJkS2llRsyKhKgnHNjNQcU97qzNGzRKZy3MScg/P5bUDbiHjp7GZFt9OlupWBBeQkkD
+tnHqMnFO/Crij8fxBLbRwGOKaIyEZ3LZzzH7kU2nsI9Jljt5ktnuZRhZMZ39fUbZH2rQ+H+GtN0x
+zeW9nDFdSKFkdR1wP0+lBNk5FEkyRSpGBik2HzqBHFGA964QAcZoZHTNWtDADHWuMpxkGuLgnFK4
+GOlZoR5cbmisTncUs4xtSRHM1AF+VCjqu2TQrNVAxg9KdQj1pJBvTmMHpWoyc2y5bYU+jUimtuu1
+PIx9a0HMewFR/E1g2oaXJChxJjCt6Zp+uwBpxFvjeqjzxfF9O1r8XeIwYebHKh/ccMcbf7Sp981A
+Xer8P3Mt3eXrPJMIisUUSgZboC3etw8W4OFE0sXWs31tYXiDML8vNJJ/p5Ruw/lXn6/01dXuGvtO
+tMO8uJQgPIpYZH3wftREUbt7pvwFkk0dtLgSK+Pi9/b9auOkaekdqEIxt27Va+A/CfWbuwTU4tPE
+kL5BmdwoGOu27Y+QNPf8hcSyxxhAiNy87HkUn5tj+9Ax4M1274duxDcRmfT2OSq9Y/cf2q7cQaVb
+a1apcWEsTLIAUYbA59ao2qXXDehoX1PU43cDaKE4P1JGfsPrTXhjiXUtUMknDn+W21ssn/kTO3O5
+9cbkfeinXEXDuraYhafSJJ0XOJIGz+lUK/4ku9PZ1tw6GPJIdcEe1bXJxWLfTHg1WExl15SVBIBP
+p9axbibSdT4g1aX/ACyyldJGCmRl5VC+pJoENB41upOIrK/1W1W8tYHBeHJGR6/Mdd69P6Ve2Wp6
+bBfafKJLaZOZGH9fevMs3DtnoFsJNTW6ZCcGeJQVQ+46/fFX3wr4ls9Im/Cxail1pc7ZYdHgb+Ir
+6euD70I2Vl+HNJkUvsyBlIKkZBBpJhRSLYwaKuD1FKkY61zAA6Vm0EQKW2pZeXuKSU4pQnbaoorh
+TSZ5RR96IwqDqnOcChRosAdaFZqocYB6UrG2/Sk1BzS8Y3rcQ7tj7U9i3ppboaF1r3D+l+bFf6xY
+296q/s4ZSTgnozKu+PatodalfWmm2Zu76dIYV/ebv7AdSfYVmPFnirO4ktOHoDD+7+JlALf8V7fX
+7VfOEtW1K0sNSng4j0Xi7VrlwbeGS9NmkSY/IkfKwXt8+5rJ9S401nT9TOleLPAyzo7HlvIIBDOo
+/iR1+GQD2OKuMqNftPqd1JPqM81xNJ+aSVizH61evCS+s40l0q5jhWYDy/2oGCMgxsc9gwAPtUzx
+F4ZTDQouJeGmm1TRp4xMvNHyzRoRnJHRh7j7Vmmr+ZYTW+q2p/aQNhwD+de6mojWPETxMvdE0KG1
+1LmFxFzJDaKRGWYE/E6rsFHQD296wHiDjbiTXJna71KZUb/8UR5Fx6bdfrTXiC9u9b1iW/uA3NId
+lySFHTApyugO9mLiHJI3xVVXzzN8TMSfc0406/u9OnE9pO8TjuO9WCLR47q3DACGdR3GVb2P96tX
+iN4K8Q8G8LR8QXV5YzxYX8RBEx5oS3pnZgDtQJaD4j3z2fk6laC5jGzSR/mX3q26JxClzah47kTI
+35SVwe+29UfwY0NNQur2/uA4ihVY0IPVzv8AI7Dv61aNStX03U5YT5fKeZ4+VcdOQ7j1xn7VApqL
+/wCZWNzbSr+zmjZcHsazC202WwnVortlukJ/J0BHbPetK06QvEzEHIkI+hFI3fh1+O4e1Pia31uO
+G5t5ARZsm7DAyQc5/Sg0Dwd4ztdU4Y/C6jMyX1rJycnLn4NsfYnGPf2q6aVq2m6tC02n3ccyI5jf
+BwUYdQwO4Psa8kcLa3LpGssWeTyXfEgU4Jwa2vWLxdQ4e1oWGnabc6Zcwfio721YJcQuF5v2i7MQ
+G2BGwzvRZWsOuKTIIqgeC/FjaxpB0q+maS+tF+FnOWkj9fmOn2rQGO9ZsUl0augkVwjfOKAB9qiu
+52pNmzttRiDRGU5xQGQ9xQrqKcbihWaIxF70snWklB74paJcmtQRXHHEq8NaA90ih7uU8lup9cbs
+fYf2rDLvWJ9QvGur6WWWd2y7Mc5q9eOV60Op6XbqhYLA8jHtu2P6VQYruAkeZFsepxW2KfQTI35G
+Gau/DvG99aWo0zW401vR2xz211livujHdSO3p7VSLdrGX9/lPTI9KXaCXk/ZOjemaDW9M02NpbTi
+LT/FjVrPhaxbzf8AL7mTnmgYDaH4iQVxtuDt69aoviLrXD+u67LcaLpE1pBJnzWcgLMf4gmAVPr2
+/nVMnlBcRXCBJ13QkbA06065FxCWcYdG5WT0IoGI0eISMgUe2RR9OJt5Ws3xjtt1qUB518zG6HtU
+frScskVygIwd8UHZ7copUCjaxe8bcR6Yuj3V7NeW5OUiCc0jY3+ZG2aXibmUSb4YdRU3wbxqvA2q
+zak1i9z5kBjDkcxjOf5f2FTQXwqnWz4eOmvGY5orhxICuG5tuvv2ol9rcOs69d+UcpZOsRYfvFld
+f54qNtOJ5+Jtf1XVpMwySzI/L3/Ly/8A1FHg0aLS9aklslKw3flM8Z3AbzR09tzVEjYcoeULsGAc
+CmvEOnarqt1aWelreSySK3PBbgnnUYO4HYUdJkSWAKd+XlOPepJ+J9S4VhttZ01Od1k5JEJ/OpBy
+PuB9hQZLxHpk+n6sySQvE2fiR1wVYdQQad2F5eWkb/h5XiJjaNgDsVbZhj3qa4o1W64nu5tVvIDF
+M7c2CcmohlBJ2xlaB9wzrl1w/r8Go2ZUyxDHK35WB6g16R4T1634i0SHUYF8st8MkZOeRh1FeW/J
+WRw2enU1qvgnxTp+nR3Gj6lMtuZ5Q8EjbKSRjlJ7dBUpK2QkD0rg3zRGIJ2O1GSstlFwAc0VsA7V
+35UXBzUo6p2oV0KaFZVGKm9LxKM9M0T0NdklENvLLjPIhYD5CukRjfi1qCXHF88eQY7ZFgH2yf1J
++1VyGOORMYGKbNK97JPLcMWkkcyOT3J60haieGQqmeXO2a0wkJ7FGHwkKexBpBLqeycJOTy9m7U/
+tJo5sebGMinM8VpLGedMqN996COvF/Hw+ZFyyOo6A9fl71H2NwEvg2cGQcrjpuOmf+u1PxZRK7SW
+NyFx+5UfqyK3/eEBW4jPM4/jH96Im7d/jOGG/auXo57RlO/ptTHS7xJ4lIOT0p60mQ4GxopppdwG
+QwOfy7UtIshlFuInmD4AULnOegqOGI7wMo2JwfSn8mXQgNysR8LZ70Fp4h8ML/hWK01C51Czb8eq
+q0ETEtGx3HsR7im96wjWJFlVmjKq3fmxlz/8ahtK1TXdc4jjt9cvGljit2EQXbmIAAz9KXnYWVq8
+apjCSSDfJGwTr/yNAiMlFy/7oORvjepDihUk0CGFZFjDToDI/wCUZOMn7060vT7OXQVvpLadCwZY
+wzBEkPL8JJPQBs5OcHp61E8Xs3+TwW867mQcy47gGgl+P+CtL4d0qyv9L1w6glwgLq6gEZHUY/lv
+Wb3MmG5c4PKR9akYbaRinPczyRpuiO2QufTNRuqxNHdKvYsD/wBfagPFtEAegFFkbzcKq7A9aDDK
+igx8qHb8zHCiiPQfhJrX+bcJQiWQyT2rGFyeu3Q/aropX0rzp4e8YS8JXqxPCJ7O5I89R+ZcfvCv
+QOnXttqFlFeWkgkglUMjDuKzY1DvPegGoLRWqNFFbI7UK4g2O1CshgAcelGRFkUxt0YEH610jAo0
+LKD71qDzaI1g1Ke1JB5XZM/I4o1xGY2Db4FLcWRfgOML9OgS8kH0LH+4p75AmgDcwwa2wYYLplNi
+BtiixzzRH4gWA7EVyM/h5miz8NPIRHMOUAADvnFENpYrO7X87wSnoU/tTC80bUoR5kEgvUG+xww+
+h/pUxJawr8cec02FxJE27HlHrQQOkTG2lliZCuGyA2xHtUxFKzfER9qYaxBLPci9tD5g5cSRj8w9
+x6ijW8+YsZHTegPLvuMDBp2JAIlbbcY6U2tpPMV8/LOKAceXjB2oJ3hCeN9Z3UMywvjbp0rt9bmS
+OeeQZeQQrGN8AkFjt26ioLhed4uKIU5iOdXBH/GrLqDfEkaDc3BJ/wCKqv8ASirgbmxtLENZ6W7x
+ae37cMQFYc+Qq8wOSAASfXvvVP8AEdndhLJzeZ+LfmBbJyc7VJPdW0ziW6hilkAzzFFBHyqJ8RLt
+brTLec8gkacFiFwT8JoIK2lXlBYbVE8RMFaKT19/ejG5wAq0x1uVnhiB3IbagdwqHVc9MUSP9vdF
+v3E+FaPptvdX7Q2NhDJPdzEJHGgySTWq2/hOukcMvd6teSC/EZZUhwY1OM4JxknttjrQZqYlDGRx
+sOgNaX4M8Sw2Usmi6hOscUp5rcucAN3X60Xh3wf4k1e1ju7ua105GHMsUuWk/wCQH5flnNUnUrFo
+NVms5MB7VzGeX+JTg4+1QemVwRkYxRGJzWd+H3HtrJDBo+sytDdKOSOdz8MnoCexrRRg4IIIPTFZ
+aHRtutCiEkChWappJkLjJrkaEnJo8rCgjYxvW4MN8XrNYeNr3GR5ipJ09VFRWg3XnQlMnmTYg1dP
+G+z/APGbO8HSa25D81J/oRWXQztYXvm78j7PVYqw6hFGSJOXp1OKbpzKOZExipGIxXduGT4sjam3
+lmJzG+du4NULW84YBZCPfaiXtosykxt13xXFCEEDOR39aNDKUk/KceuelEVq/gu4JcRrICD13okr
+XBhE8x+IHDEdSOxNW55oHT4lXPqKjr+2je0kjHLlxjtRURZNiPYjfvXWbAIYCkbZuVeQnp1HvSU8
+yDIBG+2KIW055DxDp7W7AP5uCcA/Dj4v0zVnhdpZIWOxMZkP/Ni230IqrcMRo2tyTqCTDbyMBnbJ
+HKP/AJVaLGRBcXDkhkiYRqADsFGB/KosSyxQOg50JPuKqvH7CJrG2RsjDyN+gFS9veTT3W4+HPrV
+X48uObWljBwI4gMDsTRTbhvTbjXtfstHtZooprqURh5Gwq+5qa8ZeCLjgi6s7d9VttRSUkiSIcpV
+h1BXP61VrW4ltikttIYZo2DI69Qw70e9utS1/VrSPULlp5ZZVXJwOpAJrSN7/wAO3B6Wul/9qL5R
++JuU5bcMu6Reo92/lWo2GmS6hqBv78AJGSLe3JyFH8Te/f2+dNOHEkt+FtOtRcRmNY1AePBGw2pT
+U9RFoHSW9htI+txcu3KIl74z1JOwHr8qjUTkY/Fo0Vs7RW6bM46k98e9Y54+No6alYWtjbRw3MMZ
+82RVwFQ9Ax9Sd9/ep3WvFzQ7G3FhollcXap8IkJ8tSPXfcn3xVP408QdP4k4Yk0M6GLbmkEvmGfn
+JcHOTsPehqhpb2kjiTl53H72dqv/AADxs9hyaZquTaDaObmyY/Y+1Z000iAsRGF6DkJOPnSE1xME
+ODzKR2rNiPTqyxTQrLDIskbDKspyCKFZ14IX7XGgXFm0vP5MpIBOcA9PpQrNaXlmwBtXRkkGkQwz
+SqZyKSireMVh+I4TivAPitJ1JP8ApbY/ry1h+pW3OTgnHbevQPHsN3qOjwaDYcguNTnEILdAoBcn
+/wBtYMRzKFIww2atxmmGl6nPps3lOeaPO4qz2t5aXyghwD71WLy18zJC71FwzSW8uCSKrK+XFpyn
+nifJPXemp8xdgjZ+RqOtLucqG5+YEbUs2pyoCWJoHEqyn8sbkkdlNRV4uoKwPkuvtykVIwcQRK2G
+yKPNrEEzpySFGXpk/Cw9DRVT1qeW1lWQoyCUZx03HWo1LtpG3JrdvBySz1fxAtbDUeG7PUomVjI8
+8SukAxs45ts5GPXrXp+z4b4chAaHQ9MjI6FbVB/SkMeG/CzTdS1jiRNOtdPu5TduiGZISyRKrcxZ
+j6bV6l4a8FuDLSxWO7hvb+U7vJNM0ZLHr8KYx+taqkcUIVI41VewUYArsigSZGKuKoKeEHAMJDpo
+rhvX8VKf5tTW78DfDW+na5udDleV8Zb8ZMOnyatLyCu/pQQgDqKDNbPwJ8M7W6juU4e8xozkLLcy
+un1Utg/WlX8EfDsasurW+hm3u0bmUxXEiqp9lzj9K0gkAda7nK7VRVZeDdIeJwqzwzH8s6vzPGfU
+Bsr+lecPGPh/ifh/iBbLW9Sl1GxkzJY3LfCGGdwR0DDv8/evWb7HOdqpvjLwsvGHh9qGnxIDf26G
+4sX7iVRnl/5br9amFeQiOQ4yCPbrTeSLLl4zv1371DQX8yA+YCGHX2NBuJCshR0UgbdKiHdxd8jl
+dwfSm348xnm5Oai/ibe/bflRj0P96ZXHNBLyMpA96GtG8G9cS34rWDIWO9Qxuv8AqG6n+n1oVV/D
+S3a7490qNCwVZfNbHooJ/pQrnZjUejBjPSl12IG1Nhkt0pxGD1NSVcQ3F17HpeqcOalNJyRRaj5b
+tnAAdGWsIlYJql7CSMpcOvX0Y1vvG3DZ4q4Yn0qO4W3nLLJDKwyEZTnt7ZFYxxfwJq3DV9LNNfR6
+j+xWeeRFK8nM3IOvXfG9bjNRbJzHAxnGMUwurJJVbb608t5gFywJJ9KcZXBBGRVREabM1rP5E26n
+cGp0Q288PMNjTC/tkki5kwCB2613SblsCF2APT50AvNMj5TIuB3z3qNSMBvi7Hap65TbP9dqirhV
+QFsYOelNMW7wn1ltC4uspjPy28kgSTmOwB6H23r2Fpl+rwhieY8ucj+9fP1794mJBAxWhcEeJHF1
+vYCCDU3Fra/xgNk9lyRnHtRXtA3S8iuCppvd38ceDg7HtWD6B40W7Wwe9tQZlHxRBsBv9p7VcND4
+80ni+ynbTGkt7222ntJtnXPQjsw9xV1V8XX7U8o5XO5H8v70dNcsOflZ+VvQmszvG1Yc7RJzxk5y
+N+tIQX8sbjmkhBO551II+9T9GNfTUbaTHJMjfI04S4UjGay+zvp5CpJBP+kbCp2y1GRByFmPfc9K
+foxb2lDE7/ShDIomUZ2OxHzquG8nZQyinNpdyNgEYYGmmPH3jLoFpo3iVrulEGBBcmaEqNuST4wP
+pzY+lUu60i2EBImVpPbvWwf4z7Q23iBpeoRLgXenAE+pR2/owrCo7iUOAT9KrIvJJbyfDkYNSkFx
+Hd24jlxzr+U0QeVdDHMFIG5NMJk8p/gl+1X0aj4Eaf5vFNzeFdrW2IB92IA/QGhVn8BdNntOF59U
+uYyn46UeVnqyLtn5Ek/ahXLq/Wo0LlAbY0dAc9TSJOD70ojjY1iNU7iyM4yKqfF8IvNa1KyfcT8N
+3DKD3aN1dftirVDKp9Kz/wAbLq60pdO1ywmWN+SazfftIvp8s1uJWNluQq/NsRnrTtJkbBUjGKj7
+OSOeE2rN8QGVNNj51jNljzRnvW2U7JKrKQoyR1phM/lSK64z1NKQOsq8/MObrmkJjzty8h9M1BOW
+E63UIQMOalZdBkul+J8DtvVTE01pLzwyMuPQU4bi3UUQJkEjbJFMNSl1wzbwjnurxQvcLTC61OGO
+FdO08IkS+nf5moDUtbvr5irynHtSNoxiIbO9WRFnVWEBfm3A9abadJxE16l3pN7cWk8eyyxyFDj0
+yO3tSNpqSD4ZcYPWp231e0t7blgYA43FBYLbxG474Zt+eW/ttULYDrOhJP1BGftVguOK+MNV4cte
+I7LheG4sp8q01rO+Y5FPxKy9j36dMVlVzfSX1zsMhdzWm/4fePLPhniSTh7XJUj0bWCFLufggnH5
+XPoD+Un5HtRZVq4J8Q+GDpbza/FrMFzAc3Kxwq/kL/ER1K57gbbVpPDHFPAuuMo0jjbT5XbcQzv5
+T/8Apf8AtTTirwj0niC9N7a6oulXirmKSABiT/qGd1x2715q8SOAZ9D4gm0y4jWw1INlMHFtdL2a
+Mn8pP8JqZFe1raxkKjlaGZT3RhinSWLKdosZr532mu8UcPXLQ2uralp8sZwViuHT+Rqaj8XPEeKE
+xJxfqnKRjeXJ+53rWJrYv8cd9YC94ask5Wv44pXch90jJUAEe5B+1eaDKSMgkGlNV1HUNVvZL3Ur
+ye7uZDl5ZnLMfqaluDODeIeLLvydHsHkjU4kuH+GKP5sf5DJ9qvgghI4YkM2/vWreEXhhfcQSxaz
+r8Ulto6kMkbfC917DuF9+/b1GkcB+EPDvDix3mphdX1Jfi5pF/Yxn/Snf5tn5Cr9PP22A6Vi9Lhp
+NHFEkcEEaxRRqFRFGAoHQAUKTnkHPmhXOqTcnNBGNChWI0VjbvgVnX+IiMtwbZzKQDHeA/dWoUK6
+c+s1g0N2/mrMuzKas0YS/tQ7LguvN8qFCt1kwV2tJ/KU5XNO2cqSQBkj0oUKBheFmzvudyah51Oe
+tChViVyOFQfenCxKRvQoVUJSRgHrSTMQ2ATQoVYsOIZ3gBAJpve3Ek5BJ70KFIJXh/i/ibQZEl0r
+Wbu2KkEKJCV/9J2q73Pi3xPxRZ/5JqENhNPe8tsLqSLLICcbD60KFLCM+4gDw63cWUkjTpaSNApb
+qQpIrWPD/wAMeG+IuDLW/vWvYbqUNl4ZRjrtsQaFCs9XIs9XHhvwa4M04CS9hudVl5sg3EnKg/4r
+jP1zWhW8dvY2kdpZW8NtbxjCRRIFVR7AUKFc9tawSSViDTSRsnehQqKbyjLbUKFCs0j/2Q==
+
+Cheerio!
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1--
diff --git a/lib/tests/testdir4/multimime!2,FS b/lib/tests/testdir4/multimime!2,FS
new file mode 100644 (file)
index 0000000..84f85aa
--- /dev/null
@@ -0,0 +1,27 @@
+Return-path: <>
+Envelope-to: djcb@localhost
+Delivery-date: Sun, 20 May 2012 09:59:51 +0300
+From: Steve Jobs <jobs@example.com>
+To: Bill Gates <bg@example.com>
+Subject: multimime
+User-agent: mu4e 0.9.8.4; emacs 23.3.1
+Date: Sat, 19 May 2012 20:57:56 +0100
+Message-ID: <m2fwaw2baz.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+
+abc
+--=-=-=
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="test1.C"
+Content-Transfer-Encoding: base64
+
+aGVyZSBpcyBhIHNpbXBsZSB0ZXN0IGZpbGUuCg==
+--=-=-=
+Content-Type: text/plain
+
+def
+--=-=-=--
diff --git a/lib/tests/testdir4/signed!2,S b/lib/tests/testdir4/signed!2,S
new file mode 100644 (file)
index 0000000..7e1319a
--- /dev/null
@@ -0,0 +1,36 @@
+User-agent: mu4e 1.1.0; emacs 27.0.50
+From: Skipio <skipio@roma.net>
+To: Hannibal <hanni@carthago.net>
+Subject: test 123
+Date: Sun, 24 Mar 2019 11:50:42 +0200
+Message-ID: <87zhpky51p.fsf@djcbsoftware.nl>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-=";
+       micalg=pgp-sha256; protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+
+
+I am signed!
+
+--=-=-=
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAEBCAAdFiEEaYec7RdFk3UPFNqYEd3+qdzEoDYFAlyXUwAACgkQEd3+qdzE
+oDbjdw//dAosaEyqSfyUMXjS++iJEeDIwKwO6AjEI0xCbJjHmxq93PA61ApE/BS3
+d/sKa1dsfN+plRS+Fh3NNGSA7evar9dXtMBUr6hwL0VTmm5NDwedaPeuW6mgyVcB
+VNUn5x1e/QdnSClapnGd156sryfcM1pg/667fTHT6WC01Xe0sezpkV9l0j4pslYt
+y6ud/Hejszax+NcwQY7vkCcVWfB9K4zbiapdoCjHi78S4YAcsbd//KmePOqn04Sa
+Tg1XsmMzIh7L/3njkJdIOd9XctTwYEcN5geY1QKrHQ/3+gBeaEYvwsvrnqnVKqMY
+WCg/aYibuXl+xNkPMcKHIj1dXA3m5MkL77RrxODiAYz0YkiQx1/DLZs8PV3IVoB4
+f0GGDqyiOwSmSDa4iuCottwO4yG1WM1i7r6pir22qAekIt43wSdwakOrT1IkS8q2
+o0VGiQtEPy27D+ufiw06t02Ryf20Q7i2YcueZxYeRBq41m11M41DJ4wH7LQcJsww
+qG5iBOdwQFCTWpi1UrbbFjlxXXWvKMuIU+4k7nsamrEL4SDXmq1v13vtlcgJ6vnn
+v7c9+MF7laqdfI+BYnlD1v/9LosPbFTm0hPdvK4yIOORp8Iwj/1PGzTOz6SCUxzA
+kDu+Y+NN9/SM1ppStg1OikYPcfEXF8igWhuORwqcmpgHxVkIQ9I=
+=wnkU
+-----END PGP SIGNATURE-----
+--=-=-=--
diff --git a/lib/tests/testdir4/signed-bad!2,S b/lib/tests/testdir4/signed-bad!2,S
new file mode 100644 (file)
index 0000000..7a37ba9
--- /dev/null
@@ -0,0 +1,35 @@
+Return-path: <>
+Envelope-to: skipio@localhost
+Delivery-date: Fri, 11 May 2012 16:21:57 +0300
+Received: from localhost.roma.net([127.0.0.1] helo=borealis)
+       by borealis with esmtp (Exim 4.77)
+       id 1SSpnB-00038a-55
+       for djcb@localhost; Fri, 11 May 2012 16:21:57 +0300
+From: Skipio <skipio@roma.net>
+To: Hannibal <hanni@carthago.net> 
+Subject: signed
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:20:45 +0300
+Message-ID: <878vgy97ma.fsf@roma.net>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1;
+       protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+
+
+I am signed! But it's not good because I added this later
+
+--=-=-=
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+iEYEARECAAYFAk+tEi0ACgkQ6WrHoQF92jxTzACeKd/XxY+P7bpymWL3JBRHaW9p
+DpwAoKw7PDW4z/lNTkWjndVTjoO9jGhs
+=blXz
+-----END PGP SIGNATURE-----
+--=-=-=--
+
diff --git a/lib/tests/testdir4/signed-encrypted!2,S b/lib/tests/testdir4/signed-encrypted!2,S
new file mode 100644 (file)
index 0000000..a3910e6
--- /dev/null
@@ -0,0 +1,54 @@
+Return-path: <>
+Envelope-to: karjala@localhost
+Delivery-date: Fri, 11 May 2012 16:37:57 +0300
+From: karjala@example.com
+To: lapinkulta@example.com
+Subject: signed + encrypted
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:36:08 +0300
+Message-ID: <874nrm96wn.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+       protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+hQQOA1T38TPQrHD6EA/+K4kSpMa7zk+qihUkQnHSq28xYxisNQx6X5DVNjA/Qx16
+uZj/40ae+PoSMTVfklP+B2S/IomuTW6dwVqS7aQ3u4MTzi+YOi11k1lEMD7hR0Wb
+L0i48o3/iCPuCTpnOsaLZvRL06g+oTi0BF2pgz/YdsgsBTGrTb3pkDGSlLIhvh/J
+P8eE3OuzkXS6d8ymJKx2S2wQJrc1AFf1BgJfgc5T0iAvcV+zIMG+PIYcVd04zVpj
+cORFEfvGgfxWkeX+Ks3tu/l5PA1EesnoqFdNFZm+RKBg3RFsOm8tBlJ46xJjfeHg
+zLgifeSLy3tOX7CvWYs9torrx7s7UOI2gV8kzBqz+a7diyCMezceeQ9l0nIRybwW
+C9Egp8Bpfb02iXTOGdE/vRiNItQH14GKmXf4nCSwdtQUm3yzaqY9yL3xBxAlW53e
+YOFfPMESt+E7IlPn0c7llWGrcdrhJbUEoGOIPezES7kdeNPzi8G1lLtvT04/SSZJ
+QxPH5FNzSFaYFAQSdI7TR69P7L7vtLL8ndkjY49HfLFXochQQzsqrzVxzRCruHxA
+zbZSRptNf9SuXEaX9buO1vlFHheGvrCKzEWa6O7JD/DiyrE/zqy4jdlh9abMCouQ
+GWGSbn8jk6SMTQQ2Yv/VOyFqifHZp0UJD59tyIdenpxoYu5M0lwHLNVDlRjLEwUQ
+AIDz1tbLoM7lxs2FOKGr8QqbKIeMfL+NUmbvVIDc4mJrOlRnHh+cZYm4Z49iTl1v
+bYNMYgR5nY7W6rqh0ae7ZOW0h2NzpkAwTzuf1YrSjNavd9KBwOCFtAoZhRwfwFVx
+ju+ByHFNnf7g/R6DekHS0pSiatM0cPDJT05atEZb+13CRHHznonmLHi+VahXjrpg
+cIUA8Lhjdfm6Fsabo7gNZnTTRxNBqUXKK2vJF/XLbNrH5K2BH2dCCmUNtm3yFWiM
+DOzaw3665Y3S6MvZdyKpatbNrVoJdBpRgPxJ1YCSEituFUqHJBStay+aRb5fVkQR
+w3+9hWw+Ob0+2EumKbgfQ7iMwTZBCZP4VOxkoqdHvs9aWm4N7wHtXsyCew3icbJx
+lyUWsDx/FI+HlQRfOqeAMxmp8kKybmHNw8oGiw+uPPUHSD1NFYVm2DtwhYll3Fvs
+YY7r5s3yP1ZnwxMqWI3OsExVUXs8MS4UTAgO+cggO7YidPcANbBDihBFP8mTXtni
+Oo5n5v+/eRoLfHmnsGcaK8EkKsfFHpbqn4gxXGcBuHaTTJ/ZhbW6bi1WWZA9ExaJ
+IeTDtp5Bks1pJvTjCDacvgwl3rEBM6yaeIvB7575Y/GPMTOZhawhfOxV1smMmTKI
+JOWYb3+PuN2cvWetkjFgH8re4sRXq22DKBZHJEWYU8sH0sACAePnIr+pkrOtGeJB
+t1zBqZUnrupH6ptk9n/AjbQ+XSMTEKu55gSjYLAYx1EHApx52QLkdh+ej5xCIVeY
+6wS1Iipkoc6/r6F7CKctupXurNY2AlD4uQIOfD6kQgkqK4PY3hsRHQA+Zqj6oRfr
+kxysFJZvhgt26IeBVapFs10WuYt9iHfpbPUBQUIZCLyPAh08UdVW64Uc2DvUPy+I
+C+3RrmTHQPP/YNKgDQaZ3ySVEDkqjaDPmXr5K0Ibaib2dtPCLcA=
+=pv03
+-----END PGP MESSAGE-----
+--=-=-=--
+
diff --git a/lib/tests/testdir4/special!2,Sabc b/lib/tests/testdir4/special!2,Sabc
new file mode 100644 (file)
index 0000000..7f1de8e
--- /dev/null
@@ -0,0 +1,10 @@
+Date: Thu, 1 Jun 2012 14:57:25 -0200
+From: "Rocky Balboa" <rocky@example.com>
+To: "Ivan Drago" <ivan@example.com>
+Subject: currying and tail optimization
+Message-id: <3BE9E653ef345@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+
+Test 123. I'm a special message with special flags.
diff --git a/lib/thirdparty/Makefile.am b/lib/thirdparty/Makefile.am
new file mode 100644 (file)
index 0000000..7b3af9b
--- /dev/null
@@ -0,0 +1,22 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+EXTRA_DIST=            \
+       expected.hpp    \
+       optional.hpp    \
+       tabulate.hpp
diff --git a/lib/thirdparty/expected.hpp b/lib/thirdparty/expected.hpp
new file mode 100644 (file)
index 0000000..31b130a
--- /dev/null
@@ -0,0 +1,2326 @@
+///
+// expected - An implementation of std::expected with extensions
+// Written in 2017 by Simon Brand (simonrbrand@gmail.com, @TartanLlama)
+//
+// Documentation available at http://tl.tartanllama.xyz/
+//
+// To the extent possible under law, the author(s) have dedicated all
+// copyright and related and neighboring rights to this software to the
+// public domain worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication
+// along with this software. If not, see
+// <http://creativecommons.org/publicdomain/zero/1.0/>.
+///
+
+#ifndef TL_EXPECTED_HPP
+#define TL_EXPECTED_HPP
+
+#define TL_EXPECTED_VERSION_MAJOR 1
+#define TL_EXPECTED_VERSION_MINOR 0
+#define TL_EXPECTED_VERSION_PATCH 1
+
+#include <exception>
+#include <functional>
+#include <type_traits>
+#include <utility>
+
+#if defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#define TL_EXPECTED_EXCEPTIONS_ENABLED
+#endif
+
+#if (defined(_MSC_VER) && _MSC_VER == 1900)
+#define TL_EXPECTED_MSVC2015
+#define TL_EXPECTED_MSVC2015_CONSTEXPR
+#else
+#define TL_EXPECTED_MSVC2015_CONSTEXPR constexpr
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 &&              \
+     !defined(__clang__))
+#define TL_EXPECTED_GCC49
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 &&              \
+     !defined(__clang__))
+#define TL_EXPECTED_GCC54
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 5 &&              \
+     !defined(__clang__))
+#define TL_EXPECTED_GCC55
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 &&              \
+     !defined(__clang__))
+// GCC < 5 doesn't support overloading on const&& for member functions
+
+#define TL_EXPECTED_NO_CONSTRR
+// GCC < 5 doesn't support some standard C++11 type traits
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)                         \
+  std::has_trivial_copy_constructor<T>
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T)                            \
+  std::has_trivial_copy_assign<T>
+
+// This one will be different for GCC 5.7 if it's ever supported
+#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T)                               \
+  std::is_trivially_destructible<T>
+
+// GCC 5 < v < 8 has a bug in is_trivially_copy_constructible which breaks std::vector
+// for non-copyable types
+#elif (defined(__GNUC__) && __GNUC__ < 8 &&                                                \
+     !defined(__clang__))
+#ifndef TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+#define TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+namespace tl {
+  namespace detail {
+      template<class T>
+      struct is_trivially_copy_constructible : std::is_trivially_copy_constructible<T>{};
+#ifdef _GLIBCXX_VECTOR
+      template<class T, class A>
+      struct is_trivially_copy_constructible<std::vector<T,A>>
+          : std::false_type{};
+#endif
+  }
+}
+#endif
+
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)                                     \
+  tl::detail::is_trivially_copy_constructible<T>
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T)                                        \
+  std::is_trivially_copy_assignable<T>
+#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>
+#else
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)                         \
+  std::is_trivially_copy_constructible<T>
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T)                            \
+  std::is_trivially_copy_assignable<T>
+#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T)                               \
+  std::is_trivially_destructible<T>
+#endif
+
+#if __cplusplus > 201103L
+#define TL_EXPECTED_CXX14
+#endif
+
+#ifdef TL_EXPECTED_GCC49
+#define TL_EXPECTED_GCC49_CONSTEXPR
+#else
+#define TL_EXPECTED_GCC49_CONSTEXPR constexpr
+#endif
+
+#if (__cplusplus == 201103L || defined(TL_EXPECTED_MSVC2015) ||                \
+     defined(TL_EXPECTED_GCC49))
+#define TL_EXPECTED_11_CONSTEXPR
+#else
+#define TL_EXPECTED_11_CONSTEXPR constexpr
+#endif
+
+namespace tl {
+template <class T, class E> class expected;
+
+#ifndef TL_MONOSTATE_INPLACE_MUTEX
+#define TL_MONOSTATE_INPLACE_MUTEX
+class monostate {};
+
+struct in_place_t {
+  explicit in_place_t() = default;
+};
+static constexpr in_place_t in_place{};
+#endif
+
+template <class E> class unexpected {
+public:
+  static_assert(!std::is_same<E, void>::value, "E must not be void");
+
+  unexpected() = delete;
+  constexpr explicit unexpected(const E &e) : m_val(e) {}
+
+  constexpr explicit unexpected(E &&e) : m_val(std::move(e)) {}
+
+  constexpr const E &value() const & { return m_val; }
+  TL_EXPECTED_11_CONSTEXPR E &value() & { return m_val; }
+  TL_EXPECTED_11_CONSTEXPR E &&value() && { return std::move(m_val); }
+  constexpr const E &&value() const && { return std::move(m_val); }
+
+private:
+  E m_val;
+};
+
+template <class E>
+constexpr bool operator==(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+  return lhs.value() == rhs.value();
+}
+template <class E>
+constexpr bool operator!=(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+  return lhs.value() != rhs.value();
+}
+template <class E>
+constexpr bool operator<(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+  return lhs.value() < rhs.value();
+}
+template <class E>
+constexpr bool operator<=(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+  return lhs.value() <= rhs.value();
+}
+template <class E>
+constexpr bool operator>(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+  return lhs.value() > rhs.value();
+}
+template <class E>
+constexpr bool operator>=(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+  return lhs.value() >= rhs.value();
+}
+
+template <class E>
+unexpected<typename std::decay<E>::type> make_unexpected(E &&e) {
+  return unexpected<typename std::decay<E>::type>(std::forward<E>(e));
+}
+
+struct unexpect_t {
+  unexpect_t() = default;
+};
+static constexpr unexpect_t unexpect{};
+
+namespace detail {
+template<typename E>
+[[noreturn]] TL_EXPECTED_11_CONSTEXPR void throw_exception(E &&e) {
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+    throw std::forward<E>(e);
+#else
+  #ifdef _MSC_VER
+    __assume(0);
+  #else
+    __builtin_unreachable();
+  #endif
+#endif
+}
+
+#ifndef TL_TRAITS_MUTEX
+#define TL_TRAITS_MUTEX
+// C++14-style aliases for brevity
+template <class T> using remove_const_t = typename std::remove_const<T>::type;
+template <class T>
+using remove_reference_t = typename std::remove_reference<T>::type;
+template <class T> using decay_t = typename std::decay<T>::type;
+template <bool E, class T = void>
+using enable_if_t = typename std::enable_if<E, T>::type;
+template <bool B, class T, class F>
+using conditional_t = typename std::conditional<B, T, F>::type;
+
+// std::conjunction from C++17
+template <class...> struct conjunction : std::true_type {};
+template <class B> struct conjunction<B> : B {};
+template <class B, class... Bs>
+struct conjunction<B, Bs...>
+  : std::conditional<bool(B::value), conjunction<Bs...>, B>::type {};
+
+#if defined(_LIBCPP_VERSION) && __cplusplus == 201103L
+#define TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+#endif
+
+// In C++11 mode, there's an issue in libc++'s std::mem_fn
+// which results in a hard-error when using it in a noexcept expression
+// in some cases. This is a check to workaround the common failing case.
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+template <class T> struct is_pointer_to_non_const_member_func : std::false_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...)> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...)&> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...) &&> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...) volatile> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...) volatile &> : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret(T::*) (Args...) volatile &&> : std::true_type {};
+
+template <class T> struct is_const_or_const_ref : std::false_type {};
+template <class T> struct is_const_or_const_ref<T const&> : std::true_type {};
+template <class T> struct is_const_or_const_ref<T const> : std::true_type {};
+#endif
+
+// std::invoke from C++17
+// https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround
+template <typename Fn, typename... Args,
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+  typename = enable_if_t<!(is_pointer_to_non_const_member_func<Fn>::value
+    && is_const_or_const_ref<Args...>::value)>,
+#endif
+  typename = enable_if_t<std::is_member_pointer<decay_t<Fn>>::value>,
+  int = 0>
+  constexpr auto invoke(Fn && f, Args && ... args) noexcept(
+    noexcept(std::mem_fn(f)(std::forward<Args>(args)...)))
+  -> decltype(std::mem_fn(f)(std::forward<Args>(args)...)) {
+  return std::mem_fn(f)(std::forward<Args>(args)...);
+}
+
+template <typename Fn, typename... Args,
+  typename = enable_if_t<!std::is_member_pointer<decay_t<Fn>>::value>>
+  constexpr auto invoke(Fn && f, Args && ... args) noexcept(
+    noexcept(std::forward<Fn>(f)(std::forward<Args>(args)...)))
+  -> decltype(std::forward<Fn>(f)(std::forward<Args>(args)...)) {
+  return std::forward<Fn>(f)(std::forward<Args>(args)...);
+}
+
+// std::invoke_result from C++17
+template <class F, class, class... Us> struct invoke_result_impl;
+
+template <class F, class... Us>
+struct invoke_result_impl<
+  F, decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...), void()),
+  Us...> {
+  using type = decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...));
+};
+
+template <class F, class... Us>
+using invoke_result = invoke_result_impl<F, void, Us...>;
+
+template <class F, class... Us>
+using invoke_result_t = typename invoke_result<F, Us...>::type;
+
+#if defined(_MSC_VER) && _MSC_VER <= 1900
+// TODO make a version which works with MSVC 2015
+template <class T, class U = T> struct is_swappable : std::true_type {};
+
+template <class T, class U = T> struct is_nothrow_swappable : std::true_type {};
+#else
+// https://stackoverflow.com/questions/26744589/what-is-a-proper-way-to-implement-is-swappable-to-test-for-the-swappable-concept
+namespace swap_adl_tests {
+  // if swap ADL finds this then it would call std::swap otherwise (same
+  // signature)
+  struct tag {};
+
+  template <class T> tag swap(T&, T&);
+  template <class T, std::size_t N> tag swap(T(&a)[N], T(&b)[N]);
+
+  // helper functions to test if an unqualified swap is possible, and if it
+  // becomes std::swap
+  template <class, class> std::false_type can_swap(...) noexcept(false);
+  template <class T, class U,
+    class = decltype(swap(std::declval<T&>(), std::declval<U&>()))>
+    std::true_type can_swap(int) noexcept(noexcept(swap(std::declval<T&>(),
+      std::declval<U&>())));
+
+  template <class, class> std::false_type uses_std(...);
+  template <class T, class U>
+  std::is_same<decltype(swap(std::declval<T&>(), std::declval<U&>())), tag>
+    uses_std(int);
+
+  template <class T>
+  struct is_std_swap_noexcept
+    : std::integral_constant<bool,
+    std::is_nothrow_move_constructible<T>::value&&
+    std::is_nothrow_move_assignable<T>::value> {};
+
+  template <class T, std::size_t N>
+  struct is_std_swap_noexcept<T[N]> : is_std_swap_noexcept<T> {};
+
+  template <class T, class U>
+  struct is_adl_swap_noexcept
+    : std::integral_constant<bool, noexcept(can_swap<T, U>(0))> {};
+} // namespace swap_adl_tests
+
+template <class T, class U = T>
+struct is_swappable
+  : std::integral_constant<
+  bool,
+  decltype(detail::swap_adl_tests::can_swap<T, U>(0))::value &&
+  (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value ||
+  (std::is_move_assignable<T>::value &&
+    std::is_move_constructible<T>::value))> {};
+
+template <class T, std::size_t N>
+struct is_swappable<T[N], T[N]>
+  : std::integral_constant<
+  bool,
+  decltype(detail::swap_adl_tests::can_swap<T[N], T[N]>(0))::value &&
+  (!decltype(
+    detail::swap_adl_tests::uses_std<T[N], T[N]>(0))::value ||
+    is_swappable<T, T>::value)> {};
+
+template <class T, class U = T>
+struct is_nothrow_swappable
+  : std::integral_constant<
+  bool,
+  is_swappable<T, U>::value &&
+  ((decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value
+    && detail::swap_adl_tests::is_std_swap_noexcept<T>::value) ||
+    (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value &&
+      detail::swap_adl_tests::is_adl_swap_noexcept<T,
+      U>::value))> {
+};
+#endif
+#endif
+
+// Trait for checking if a type is a tl::expected
+template <class T> struct is_expected_impl : std::false_type {};
+template <class T, class E>
+struct is_expected_impl<expected<T, E>> : std::true_type {};
+template <class T> using is_expected = is_expected_impl<decay_t<T>>;
+
+template <class T, class E, class U>
+using expected_enable_forward_value = detail::enable_if_t<
+    std::is_constructible<T, U &&>::value &&
+    !std::is_same<detail::decay_t<U>, in_place_t>::value &&
+    !std::is_same<expected<T, E>, detail::decay_t<U>>::value &&
+    !std::is_same<unexpected<E>, detail::decay_t<U>>::value>;
+
+template <class T, class E, class U, class G, class UR, class GR>
+using expected_enable_from_other = detail::enable_if_t<
+    std::is_constructible<T, UR>::value &&
+    std::is_constructible<E, GR>::value &&
+    !std::is_constructible<T, expected<U, G> &>::value &&
+    !std::is_constructible<T, expected<U, G> &&>::value &&
+    !std::is_constructible<T, const expected<U, G> &>::value &&
+    !std::is_constructible<T, const expected<U, G> &&>::value &&
+    !std::is_convertible<expected<U, G> &, T>::value &&
+    !std::is_convertible<expected<U, G> &&, T>::value &&
+    !std::is_convertible<const expected<U, G> &, T>::value &&
+    !std::is_convertible<const expected<U, G> &&, T>::value>;
+
+template <class T, class U>
+using is_void_or = conditional_t<std::is_void<T>::value, std::true_type, U>;
+
+template <class T>
+using is_copy_constructible_or_void =
+    is_void_or<T, std::is_copy_constructible<T>>;
+
+template <class T>
+using is_move_constructible_or_void =
+    is_void_or<T, std::is_move_constructible<T>>;
+
+template <class T>
+using is_copy_assignable_or_void =
+    is_void_or<T, std::is_copy_assignable<T>>;
+
+
+template <class T>
+using is_move_assignable_or_void =
+    is_void_or<T, std::is_move_assignable<T>>;
+    
+
+} // namespace detail
+
+namespace detail {
+struct no_init_t {};
+static constexpr no_init_t no_init{};
+
+// Implements the storage of the values, and ensures that the destructor is
+// trivial if it can be.
+//
+// This specialization is for where neither `T` or `E` is trivially
+// destructible, so the destructors must be called on destruction of the
+// `expected`
+template <class T, class E, bool = std::is_trivially_destructible<T>::value,
+          bool = std::is_trivially_destructible<E>::value>
+struct expected_storage_base {
+  constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+  constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {}
+
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+                nullptr>
+  constexpr expected_storage_base(in_place_t, Args &&... args)
+      : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+                                  Args &&... args)
+      : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+                nullptr>
+  constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+      : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr explicit expected_storage_base(unexpect_t,
+                                           std::initializer_list<U> il,
+                                           Args &&... args)
+      : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+  ~expected_storage_base() {
+    if (m_has_val) {
+      m_val.~T();
+    } else {
+      m_unexpect.~unexpected<E>();
+    }
+  }
+  union {
+    T m_val;
+    unexpected<E> m_unexpect;
+    char m_no_init;
+  };
+  bool m_has_val;
+};
+
+// This specialization is for when both `T` and `E` are trivially-destructible,
+// so the destructor of the `expected` can be trivial.
+template <class T, class E> struct expected_storage_base<T, E, true, true> {
+  constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+  constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {}
+
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+                nullptr>
+  constexpr expected_storage_base(in_place_t, Args &&... args)
+      : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+                                  Args &&... args)
+      : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+                nullptr>
+  constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+      : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr explicit expected_storage_base(unexpect_t,
+                                           std::initializer_list<U> il,
+                                           Args &&... args)
+      : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+  ~expected_storage_base() = default;
+  union {
+    T m_val;
+    unexpected<E> m_unexpect;
+    char m_no_init;
+  };
+  bool m_has_val;
+};
+
+// T is trivial, E is not.
+template <class T, class E> struct expected_storage_base<T, E, true, false> {
+  constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+  TL_EXPECTED_MSVC2015_CONSTEXPR expected_storage_base(no_init_t)
+      : m_no_init(), m_has_val(false) {}
+
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+                nullptr>
+  constexpr expected_storage_base(in_place_t, Args &&... args)
+      : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+                                  Args &&... args)
+      : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+                nullptr>
+  constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+      : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr explicit expected_storage_base(unexpect_t,
+                                           std::initializer_list<U> il,
+                                           Args &&... args)
+      : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+  ~expected_storage_base() {
+    if (!m_has_val) {
+      m_unexpect.~unexpected<E>();
+    }
+  }
+
+  union {
+    T m_val;
+    unexpected<E> m_unexpect;
+    char m_no_init;
+  };
+  bool m_has_val;
+};
+
+// E is trivial, T is not.
+template <class T, class E> struct expected_storage_base<T, E, false, true> {
+  constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+  constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {}
+
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+                nullptr>
+  constexpr expected_storage_base(in_place_t, Args &&... args)
+      : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+                                  Args &&... args)
+      : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+                nullptr>
+  constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+      : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr explicit expected_storage_base(unexpect_t,
+                                           std::initializer_list<U> il,
+                                           Args &&... args)
+      : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+  ~expected_storage_base() {
+    if (m_has_val) {
+      m_val.~T();
+    }
+  }
+  union {
+    T m_val;
+    unexpected<E> m_unexpect;
+    char m_no_init;
+  };
+  bool m_has_val;
+};
+
+// `T` is `void`, `E` is trivially-destructible
+template <class E> struct expected_storage_base<void, E, false, true> {
+  TL_EXPECTED_MSVC2015_CONSTEXPR expected_storage_base() : m_has_val(true) {}
+  constexpr expected_storage_base(no_init_t) : m_val(), m_has_val(false) {}
+
+  constexpr expected_storage_base(in_place_t) : m_has_val(true) {}
+
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+                nullptr>
+  constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+      : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr explicit expected_storage_base(unexpect_t,
+                                           std::initializer_list<U> il,
+                                           Args &&... args)
+      : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+  ~expected_storage_base() = default;
+  struct dummy {};
+  union {
+    unexpected<E> m_unexpect;
+    dummy m_val;
+  };
+  bool m_has_val;
+};
+
+// `T` is `void`, `E` is not trivially-destructible
+template <class E> struct expected_storage_base<void, E, false, false> {
+  constexpr expected_storage_base() : m_dummy(), m_has_val(true) {}
+  constexpr expected_storage_base(no_init_t) : m_dummy(), m_has_val(false) {}
+
+  constexpr expected_storage_base(in_place_t) : m_dummy(), m_has_val(true) {}
+
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+                nullptr>
+  constexpr explicit expected_storage_base(unexpect_t, Args &&... args)
+      : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr explicit expected_storage_base(unexpect_t,
+                                           std::initializer_list<U> il,
+                                           Args &&... args)
+      : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+  ~expected_storage_base() {
+    if (!m_has_val) {
+      m_unexpect.~unexpected<E>();
+    }
+  }
+
+  union {
+    unexpected<E> m_unexpect;
+    char m_dummy;
+  };
+  bool m_has_val;
+};
+
+// This base class provides some handy member functions which can be used in
+// further derived classes
+template <class T, class E>
+struct expected_operations_base : expected_storage_base<T, E> {
+  using expected_storage_base<T, E>::expected_storage_base;
+
+  template <class... Args> void construct(Args &&... args) noexcept {
+    new (std::addressof(this->m_val)) T(std::forward<Args>(args)...);
+    this->m_has_val = true;
+  }
+
+  template <class Rhs> void construct_with(Rhs &&rhs) noexcept {
+    new (std::addressof(this->m_val)) T(std::forward<Rhs>(rhs).get());
+    this->m_has_val = true;
+  }
+
+  template <class... Args> void construct_error(Args &&... args) noexcept {
+    new (std::addressof(this->m_unexpect))
+        unexpected<E>(std::forward<Args>(args)...);
+    this->m_has_val = false;
+  }
+
+  #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+
+  // These assign overloads ensure that the most efficient assignment
+  // implementation is used while maintaining the strong exception guarantee.
+  // The problematic case is where rhs has a value, but *this does not.
+  //
+  // This overload handles the case where we can just copy-construct `T`
+  // directly into place without throwing.
+  template <class U = T,
+            detail::enable_if_t<std::is_nothrow_copy_constructible<U>::value>
+                * = nullptr>
+  void assign(const expected_operations_base &rhs) noexcept {
+    if (!this->m_has_val && rhs.m_has_val) {
+      geterr().~unexpected<E>();
+      construct(rhs.get());
+    } else {
+      assign_common(rhs);
+    }
+  }
+
+  // This overload handles the case where we can attempt to create a copy of
+  // `T`, then no-throw move it into place if the copy was successful.
+  template <class U = T,
+            detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value &&
+                                std::is_nothrow_move_constructible<U>::value>
+                * = nullptr>
+  void assign(const expected_operations_base &rhs) noexcept {
+    if (!this->m_has_val && rhs.m_has_val) {
+      T tmp = rhs.get();
+      geterr().~unexpected<E>();
+      construct(std::move(tmp));
+    } else {
+      assign_common(rhs);
+    }
+  }
+
+  // This overload is the worst-case, where we have to move-construct the
+  // unexpected value into temporary storage, then try to copy the T into place.
+  // If the construction succeeds, then everything is fine, but if it throws,
+  // then we move the old unexpected value back into place before rethrowing the
+  // exception.
+  template <class U = T,
+            detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value &&
+                                !std::is_nothrow_move_constructible<U>::value>
+                * = nullptr>
+  void assign(const expected_operations_base &rhs) {
+    if (!this->m_has_val && rhs.m_has_val) {
+      auto tmp = std::move(geterr());
+      geterr().~unexpected<E>();
+
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+      try {
+        construct(rhs.get());
+      } catch (...) {
+        geterr() = std::move(tmp);
+        throw;
+      }
+#else
+      construct(rhs.get());
+#endif
+    } else {
+      assign_common(rhs);
+    }
+  }
+
+  // These overloads do the same as above, but for rvalues
+  template <class U = T,
+            detail::enable_if_t<std::is_nothrow_move_constructible<U>::value>
+                * = nullptr>
+  void assign(expected_operations_base &&rhs) noexcept {
+    if (!this->m_has_val && rhs.m_has_val) {
+      geterr().~unexpected<E>();
+      construct(std::move(rhs).get());
+    } else {
+      assign_common(std::move(rhs));
+    }
+  }
+
+  template <class U = T,
+            detail::enable_if_t<!std::is_nothrow_move_constructible<U>::value>
+                * = nullptr>
+  void assign(expected_operations_base &&rhs) {
+    if (!this->m_has_val && rhs.m_has_val) {
+      auto tmp = std::move(geterr());
+      geterr().~unexpected<E>();
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+      try {
+        construct(std::move(rhs).get());
+      } catch (...) {
+        geterr() = std::move(tmp);
+        throw;
+      }
+#else
+      construct(std::move(rhs).get());
+#endif
+    } else {
+      assign_common(std::move(rhs));
+    }
+  }
+
+  #else
+
+  // If exceptions are disabled then we can just copy-construct
+  void assign(const expected_operations_base &rhs) noexcept {
+    if (!this->m_has_val && rhs.m_has_val) {
+      geterr().~unexpected<E>();
+      construct(rhs.get());
+    } else {
+      assign_common(rhs);
+    }
+  }
+
+  void assign(expected_operations_base &&rhs) noexcept {
+    if (!this->m_has_val && rhs.m_has_val) {
+      geterr().~unexpected<E>();
+      construct(std::move(rhs).get());
+    } else {
+      assign_common(rhs);
+    }
+  }
+
+  #endif
+
+  // The common part of move/copy assigning
+  template <class Rhs> void assign_common(Rhs &&rhs) {
+    if (this->m_has_val) {
+      if (rhs.m_has_val) {
+        get() = std::forward<Rhs>(rhs).get();
+      } else {
+               destroy_val();
+        construct_error(std::forward<Rhs>(rhs).geterr());
+      }
+    } else {
+      if (!rhs.m_has_val) {
+        geterr() = std::forward<Rhs>(rhs).geterr();
+      }
+    }
+  }
+
+  bool has_value() const { return this->m_has_val; }
+
+  TL_EXPECTED_11_CONSTEXPR T &get() & { return this->m_val; }
+  constexpr const T &get() const & { return this->m_val; }
+  TL_EXPECTED_11_CONSTEXPR T &&get() && { return std::move(this->m_val); }
+#ifndef TL_EXPECTED_NO_CONSTRR
+  constexpr const T &&get() const && { return std::move(this->m_val); }
+#endif
+
+  TL_EXPECTED_11_CONSTEXPR unexpected<E> &geterr() & {
+    return this->m_unexpect;
+  }
+  constexpr const unexpected<E> &geterr() const & { return this->m_unexpect; }
+  TL_EXPECTED_11_CONSTEXPR unexpected<E> &&geterr() && {
+    return std::move(this->m_unexpect);
+  }
+#ifndef TL_EXPECTED_NO_CONSTRR
+  constexpr const unexpected<E> &&geterr() const && {
+    return std::move(this->m_unexpect);
+  }
+#endif
+
+  TL_EXPECTED_11_CONSTEXPR void destroy_val() {
+       get().~T();
+  }
+};
+
+// This base class provides some handy member functions which can be used in
+// further derived classes
+template <class E>
+struct expected_operations_base<void, E> : expected_storage_base<void, E> {
+  using expected_storage_base<void, E>::expected_storage_base;
+
+  template <class... Args> void construct() noexcept { this->m_has_val = true; }
+
+  // This function doesn't use its argument, but needs it so that code in
+  // levels above this can work independently of whether T is void
+  template <class Rhs> void construct_with(Rhs &&) noexcept {
+    this->m_has_val = true;
+  }
+
+  template <class... Args> void construct_error(Args &&... args) noexcept {
+    new (std::addressof(this->m_unexpect))
+        unexpected<E>(std::forward<Args>(args)...);
+    this->m_has_val = false;
+  }
+
+  template <class Rhs> void assign(Rhs &&rhs) noexcept {
+    if (!this->m_has_val) {
+      if (rhs.m_has_val) {
+        geterr().~unexpected<E>();
+        construct();
+      } else {
+        geterr() = std::forward<Rhs>(rhs).geterr();
+      }
+    } else {
+      if (!rhs.m_has_val) {
+        construct_error(std::forward<Rhs>(rhs).geterr());
+      }
+    }
+  }
+
+  bool has_value() const { return this->m_has_val; }
+
+  TL_EXPECTED_11_CONSTEXPR unexpected<E> &geterr() & {
+    return this->m_unexpect;
+  }
+  constexpr const unexpected<E> &geterr() const & { return this->m_unexpect; }
+  TL_EXPECTED_11_CONSTEXPR unexpected<E> &&geterr() && {
+    return std::move(this->m_unexpect);
+  }
+#ifndef TL_EXPECTED_NO_CONSTRR
+  constexpr const unexpected<E> &&geterr() const && {
+    return std::move(this->m_unexpect);
+  }
+#endif
+
+  TL_EXPECTED_11_CONSTEXPR void destroy_val() {
+         //no-op
+  }
+};
+
+// This class manages conditionally having a trivial copy constructor
+// This specialization is for when T and E are trivially copy constructible
+template <class T, class E,
+          bool = is_void_or<T, TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)>::
+              value &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value>
+struct expected_copy_base : expected_operations_base<T, E> {
+  using expected_operations_base<T, E>::expected_operations_base;
+};
+
+// This specialization is for when T or E are not trivially copy constructible
+template <class T, class E>
+struct expected_copy_base<T, E, false> : expected_operations_base<T, E> {
+  using expected_operations_base<T, E>::expected_operations_base;
+
+  expected_copy_base() = default;
+  expected_copy_base(const expected_copy_base &rhs)
+      : expected_operations_base<T, E>(no_init) {
+    if (rhs.has_value()) {
+      this->construct_with(rhs);
+    } else {
+      this->construct_error(rhs.geterr());
+    }
+  }
+
+  expected_copy_base(expected_copy_base &&rhs) = default;
+  expected_copy_base &operator=(const expected_copy_base &rhs) = default;
+  expected_copy_base &operator=(expected_copy_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move constructor
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_constructible. We
+// have to make do with a non-trivial move constructor even if T is trivially
+// move constructible
+#ifndef TL_EXPECTED_GCC49
+template <class T, class E,
+          bool = is_void_or<T, std::is_trivially_move_constructible<T>>::value
+              &&std::is_trivially_move_constructible<E>::value>
+struct expected_move_base : expected_copy_base<T, E> {
+  using expected_copy_base<T, E>::expected_copy_base;
+};
+#else
+template <class T, class E, bool = false> struct expected_move_base;
+#endif
+template <class T, class E>
+struct expected_move_base<T, E, false> : expected_copy_base<T, E> {
+  using expected_copy_base<T, E>::expected_copy_base;
+
+  expected_move_base() = default;
+  expected_move_base(const expected_move_base &rhs) = default;
+
+  expected_move_base(expected_move_base &&rhs) noexcept(
+      std::is_nothrow_move_constructible<T>::value)
+      : expected_copy_base<T, E>(no_init) {
+    if (rhs.has_value()) {
+      this->construct_with(std::move(rhs));
+    } else {
+      this->construct_error(std::move(rhs.geterr()));
+    }
+  }
+  expected_move_base &operator=(const expected_move_base &rhs) = default;
+  expected_move_base &operator=(expected_move_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial copy assignment operator
+template <class T, class E,
+          bool = is_void_or<
+              T, conjunction<TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T),
+                             TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T),
+                             TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T)>>::value
+              &&TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(E)::value
+                  &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value
+                      &&TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(E)::value>
+struct expected_copy_assign_base : expected_move_base<T, E> {
+  using expected_move_base<T, E>::expected_move_base;
+};
+
+template <class T, class E>
+struct expected_copy_assign_base<T, E, false> : expected_move_base<T, E> {
+  using expected_move_base<T, E>::expected_move_base;
+
+  expected_copy_assign_base() = default;
+  expected_copy_assign_base(const expected_copy_assign_base &rhs) = default;
+
+  expected_copy_assign_base(expected_copy_assign_base &&rhs) = default;
+  expected_copy_assign_base &operator=(const expected_copy_assign_base &rhs) {
+    this->assign(rhs);
+    return *this;
+  }
+  expected_copy_assign_base &
+  operator=(expected_copy_assign_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move assignment operator
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_assignable. We have
+// to make do with a non-trivial move assignment operator even if T is trivially
+// move assignable
+#ifndef TL_EXPECTED_GCC49
+template <class T, class E,
+          bool =
+              is_void_or<T, conjunction<std::is_trivially_destructible<T>,
+                                        std::is_trivially_move_constructible<T>,
+                                        std::is_trivially_move_assignable<T>>>::
+                  value &&std::is_trivially_destructible<E>::value
+                      &&std::is_trivially_move_constructible<E>::value
+                          &&std::is_trivially_move_assignable<E>::value>
+struct expected_move_assign_base : expected_copy_assign_base<T, E> {
+  using expected_copy_assign_base<T, E>::expected_copy_assign_base;
+};
+#else
+template <class T, class E, bool = false> struct expected_move_assign_base;
+#endif
+
+template <class T, class E>
+struct expected_move_assign_base<T, E, false>
+    : expected_copy_assign_base<T, E> {
+  using expected_copy_assign_base<T, E>::expected_copy_assign_base;
+
+  expected_move_assign_base() = default;
+  expected_move_assign_base(const expected_move_assign_base &rhs) = default;
+
+  expected_move_assign_base(expected_move_assign_base &&rhs) = default;
+
+  expected_move_assign_base &
+  operator=(const expected_move_assign_base &rhs) = default;
+
+  expected_move_assign_base &
+  operator=(expected_move_assign_base &&rhs) noexcept(
+      std::is_nothrow_move_constructible<T>::value
+          &&std::is_nothrow_move_assignable<T>::value) {
+    this->assign(std::move(rhs));
+    return *this;
+  }
+};
+
+// expected_delete_ctor_base will conditionally delete copy and move
+// constructors depending on whether T is copy/move constructible
+template <class T, class E,
+          bool EnableCopy = (is_copy_constructible_or_void<T>::value &&
+                             std::is_copy_constructible<E>::value),
+          bool EnableMove = (is_move_constructible_or_void<T>::value &&
+                             std::is_move_constructible<E>::value)>
+struct expected_delete_ctor_base {
+  expected_delete_ctor_base() = default;
+  expected_delete_ctor_base(const expected_delete_ctor_base &) = default;
+  expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default;
+  expected_delete_ctor_base &
+  operator=(const expected_delete_ctor_base &) = default;
+  expected_delete_ctor_base &
+  operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_ctor_base<T, E, true, false> {
+  expected_delete_ctor_base() = default;
+  expected_delete_ctor_base(const expected_delete_ctor_base &) = default;
+  expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete;
+  expected_delete_ctor_base &
+  operator=(const expected_delete_ctor_base &) = default;
+  expected_delete_ctor_base &
+  operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_ctor_base<T, E, false, true> {
+  expected_delete_ctor_base() = default;
+  expected_delete_ctor_base(const expected_delete_ctor_base &) = delete;
+  expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default;
+  expected_delete_ctor_base &
+  operator=(const expected_delete_ctor_base &) = default;
+  expected_delete_ctor_base &
+  operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_ctor_base<T, E, false, false> {
+  expected_delete_ctor_base() = default;
+  expected_delete_ctor_base(const expected_delete_ctor_base &) = delete;
+  expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete;
+  expected_delete_ctor_base &
+  operator=(const expected_delete_ctor_base &) = default;
+  expected_delete_ctor_base &
+  operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+// expected_delete_assign_base will conditionally delete copy and move
+// constructors depending on whether T and E are copy/move constructible +
+// assignable
+template <class T, class E,
+          bool EnableCopy = (is_copy_constructible_or_void<T>::value &&
+                             std::is_copy_constructible<E>::value &&
+                             is_copy_assignable_or_void<T>::value &&
+                             std::is_copy_assignable<E>::value),
+          bool EnableMove = (is_move_constructible_or_void<T>::value &&
+                             std::is_move_constructible<E>::value &&
+                             is_move_assignable_or_void<T>::value &&
+                             std::is_move_assignable<E>::value)>
+struct expected_delete_assign_base {
+  expected_delete_assign_base() = default;
+  expected_delete_assign_base(const expected_delete_assign_base &) = default;
+  expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+      default;
+  expected_delete_assign_base &
+  operator=(const expected_delete_assign_base &) = default;
+  expected_delete_assign_base &
+  operator=(expected_delete_assign_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_assign_base<T, E, true, false> {
+  expected_delete_assign_base() = default;
+  expected_delete_assign_base(const expected_delete_assign_base &) = default;
+  expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+      default;
+  expected_delete_assign_base &
+  operator=(const expected_delete_assign_base &) = default;
+  expected_delete_assign_base &
+  operator=(expected_delete_assign_base &&) noexcept = delete;
+};
+
+template <class T, class E>
+struct expected_delete_assign_base<T, E, false, true> {
+  expected_delete_assign_base() = default;
+  expected_delete_assign_base(const expected_delete_assign_base &) = default;
+  expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+      default;
+  expected_delete_assign_base &
+  operator=(const expected_delete_assign_base &) = delete;
+  expected_delete_assign_base &
+  operator=(expected_delete_assign_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_assign_base<T, E, false, false> {
+  expected_delete_assign_base() = default;
+  expected_delete_assign_base(const expected_delete_assign_base &) = default;
+  expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+      default;
+  expected_delete_assign_base &
+  operator=(const expected_delete_assign_base &) = delete;
+  expected_delete_assign_base &
+  operator=(expected_delete_assign_base &&) noexcept = delete;
+};
+
+// This is needed to be able to construct the expected_default_ctor_base which
+// follows, while still conditionally deleting the default constructor.
+struct default_constructor_tag {
+  explicit constexpr default_constructor_tag() = default;
+};
+
+// expected_default_ctor_base will ensure that expected has a deleted default
+// consturctor if T is not default constructible.
+// This specialization is for when T is default constructible
+template <class T, class E,
+          bool Enable =
+              std::is_default_constructible<T>::value || std::is_void<T>::value>
+struct expected_default_ctor_base {
+  constexpr expected_default_ctor_base() noexcept = default;
+  constexpr expected_default_ctor_base(
+      expected_default_ctor_base const &) noexcept = default;
+  constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept =
+      default;
+  expected_default_ctor_base &
+  operator=(expected_default_ctor_base const &) noexcept = default;
+  expected_default_ctor_base &
+  operator=(expected_default_ctor_base &&) noexcept = default;
+
+  constexpr explicit expected_default_ctor_base(default_constructor_tag) {}
+};
+
+// This specialization is for when T is not default constructible
+template <class T, class E> struct expected_default_ctor_base<T, E, false> {
+  constexpr expected_default_ctor_base() noexcept = delete;
+  constexpr expected_default_ctor_base(
+      expected_default_ctor_base const &) noexcept = default;
+  constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept =
+      default;
+  expected_default_ctor_base &
+  operator=(expected_default_ctor_base const &) noexcept = default;
+  expected_default_ctor_base &
+  operator=(expected_default_ctor_base &&) noexcept = default;
+
+  constexpr explicit expected_default_ctor_base(default_constructor_tag) {}
+};
+} // namespace detail
+
+template <class E> class bad_expected_access : public std::exception {
+public:
+  explicit bad_expected_access(E e) : m_val(std::move(e)) {}
+
+  virtual const char *what() const noexcept override {
+    return "Bad expected access";
+  }
+
+  const E &error() const & { return m_val; }
+  E &error() & { return m_val; }
+  const E &&error() const && { return std::move(m_val); }
+  E &&error() && { return std::move(m_val); }
+
+private:
+  E m_val;
+};
+
+/// An `expected<T, E>` object is an object that contains the storage for
+/// another object and manages the lifetime of this contained object `T`.
+/// Alternatively it could contain the storage for another unexpected object
+/// `E`. The contained object may not be initialized after the expected object
+/// has been initialized, and may not be destroyed before the expected object
+/// has been destroyed. The initialization state of the contained object is
+/// tracked by the expected object.
+template <class T, class E>
+class expected : private detail::expected_move_assign_base<T, E>,
+                 private detail::expected_delete_ctor_base<T, E>,
+                 private detail::expected_delete_assign_base<T, E>,
+                 private detail::expected_default_ctor_base<T, E> {
+  static_assert(!std::is_reference<T>::value, "T must not be a reference");
+  static_assert(!std::is_same<T, std::remove_cv<in_place_t>::type>::value,
+                "T must not be in_place_t");
+  static_assert(!std::is_same<T, std::remove_cv<unexpect_t>::type>::value,
+                "T must not be unexpect_t");
+  static_assert(!std::is_same<T, typename std::remove_cv<unexpected<E>>::type>::value,
+                "T must not be unexpected<E>");
+  static_assert(!std::is_reference<E>::value, "E must not be a reference");
+
+  T *valptr() { return std::addressof(this->m_val); }
+  const T *valptr() const { return std::addressof(this->m_val); }    
+  unexpected<E> *errptr() { return std::addressof(this->m_unexpect); }
+  const unexpected<E> *errptr() const { return std::addressof(this->m_unexpect); }    
+
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  TL_EXPECTED_11_CONSTEXPR U &val() {
+    return this->m_val;
+  }
+  TL_EXPECTED_11_CONSTEXPR unexpected<E> &err() { return this->m_unexpect; }
+
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  constexpr const U &val() const {
+    return this->m_val;
+  }
+  constexpr const unexpected<E> &err() const { return this->m_unexpect; }
+
+  using impl_base = detail::expected_move_assign_base<T, E>;
+  using ctor_base = detail::expected_default_ctor_base<T, E>;
+
+public:
+  typedef T value_type;
+  typedef E error_type;
+  typedef unexpected<E> unexpected_type;
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) &&               \
+    !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+  template <class F> TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) & {
+    return and_then_impl(*this, std::forward<F>(f));
+  }
+  template <class F> TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) && {
+    return and_then_impl(std::move(*this), std::forward<F>(f));
+  }
+  template <class F> constexpr auto and_then(F &&f) const & {
+    return and_then_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+  template <class F> constexpr auto and_then(F &&f) const && {
+    return and_then_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+
+#else
+  template <class F>
+  TL_EXPECTED_11_CONSTEXPR auto
+  and_then(F &&f) & -> decltype(and_then_impl(std::declval<expected&>(), std::forward<F>(f))) {
+    return and_then_impl(*this, std::forward<F>(f));
+  }
+  template <class F>
+  TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) && -> decltype(
+      and_then_impl(std::declval<expected&&>(), std::forward<F>(f))) {
+    return and_then_impl(std::move(*this), std::forward<F>(f));
+  }
+  template <class F>
+  constexpr auto and_then(F &&f) const & -> decltype(
+      and_then_impl(std::declval<expected const&>(), std::forward<F>(f))) {
+    return and_then_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+  template <class F>
+  constexpr auto and_then(F &&f) const && -> decltype(
+      and_then_impl(std::declval<expected const&&>(), std::forward<F>(f))) {
+    return and_then_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) &&               \
+    !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+    template <class F> TL_EXPECTED_11_CONSTEXPR auto map(F &&f) & {
+    return expected_map_impl(*this, std::forward<F>(f));
+  }
+  template <class F> TL_EXPECTED_11_CONSTEXPR auto map(F &&f) && {
+    return expected_map_impl(std::move(*this), std::forward<F>(f));
+  }
+  template <class F> constexpr auto map(F &&f) const & {
+    return expected_map_impl(*this, std::forward<F>(f));
+  }
+  template <class F> constexpr auto map(F &&f) const && {
+    return expected_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#else
+  template <class F>
+  TL_EXPECTED_11_CONSTEXPR decltype(
+      expected_map_impl(std::declval<expected &>(), std::declval<F &&>()))
+  map(F &&f) & {
+    return expected_map_impl(*this, std::forward<F>(f));
+  }
+  template <class F>
+  TL_EXPECTED_11_CONSTEXPR decltype(
+      expected_map_impl(std::declval<expected>(), std::declval<F &&>()))
+  map(F &&f) && {
+    return expected_map_impl(std::move(*this), std::forward<F>(f));
+  }
+  template <class F>
+  constexpr decltype(expected_map_impl(std::declval<const expected &>(),
+                                       std::declval<F &&>()))
+  map(F &&f) const & {
+    return expected_map_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+  template <class F>
+  constexpr decltype(expected_map_impl(std::declval<const expected &&>(),
+                                       std::declval<F &&>()))
+  map(F &&f) const && {
+    return expected_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) &&               \
+    !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+    template <class F> TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) & {
+    return expected_map_impl(*this, std::forward<F>(f));
+  }
+  template <class F> TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) && {
+    return expected_map_impl(std::move(*this), std::forward<F>(f));
+  }
+  template <class F> constexpr auto transform(F &&f) const & {
+    return expected_map_impl(*this, std::forward<F>(f));
+  }
+  template <class F> constexpr auto transform(F &&f) const && {
+    return expected_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#else
+    template <class F>
+  TL_EXPECTED_11_CONSTEXPR decltype(
+      expected_map_impl(std::declval<expected &>(), std::declval<F &&>()))
+  transform(F &&f) & {
+    return expected_map_impl(*this, std::forward<F>(f));
+  }
+  template <class F>
+  TL_EXPECTED_11_CONSTEXPR decltype(
+      expected_map_impl(std::declval<expected>(), std::declval<F &&>()))
+  transform(F &&f) && {
+    return expected_map_impl(std::move(*this), std::forward<F>(f));
+  }
+  template <class F>
+  constexpr decltype(expected_map_impl(std::declval<const expected &>(),
+                                       std::declval<F &&>()))
+  transform(F &&f) const & {
+    return expected_map_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+  template <class F>
+  constexpr decltype(expected_map_impl(std::declval<const expected &&>(),
+                                       std::declval<F &&>()))
+  transform(F &&f) const && {
+    return expected_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) &&               \
+    !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+  template <class F> TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) & {
+    return map_error_impl(*this, std::forward<F>(f));
+  }
+  template <class F> TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) && {
+    return map_error_impl(std::move(*this), std::forward<F>(f));
+  }
+  template <class F> constexpr auto map_error(F &&f) const & {
+    return map_error_impl(*this, std::forward<F>(f));
+  }
+  template <class F> constexpr auto map_error(F &&f) const && {
+    return map_error_impl(std::move(*this), std::forward<F>(f));
+  }
+#else
+  template <class F>
+  TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval<expected &>(),
+                                                   std::declval<F &&>()))
+  map_error(F &&f) & {
+    return map_error_impl(*this, std::forward<F>(f));
+  }
+  template <class F>
+  TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval<expected &&>(),
+                                                   std::declval<F &&>()))
+  map_error(F &&f) && {
+    return map_error_impl(std::move(*this), std::forward<F>(f));
+  }
+  template <class F>
+  constexpr decltype(map_error_impl(std::declval<const expected &>(),
+                                    std::declval<F &&>()))
+  map_error(F &&f) const & {
+    return map_error_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+  template <class F>
+  constexpr decltype(map_error_impl(std::declval<const expected &&>(),
+                                    std::declval<F &&>()))
+  map_error(F &&f) const && {
+    return map_error_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+#endif
+  template <class F> expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) & {
+    return or_else_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F> expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) && {
+    return or_else_impl(std::move(*this), std::forward<F>(f));
+  }
+
+  template <class F> expected constexpr or_else(F &&f) const & {
+    return or_else_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+  template <class F> expected constexpr or_else(F &&f) const && {
+    return or_else_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+  constexpr expected() = default;
+  constexpr expected(const expected &rhs) = default;
+  constexpr expected(expected &&rhs) = default;
+  expected &operator=(const expected &rhs) = default;
+  expected &operator=(expected &&rhs) = default;
+
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+                nullptr>
+  constexpr expected(in_place_t, Args &&... args)
+      : impl_base(in_place, std::forward<Args>(args)...),
+        ctor_base(detail::default_constructor_tag{}) {}
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr expected(in_place_t, std::initializer_list<U> il, Args &&... args)
+      : impl_base(in_place, il, std::forward<Args>(args)...),
+        ctor_base(detail::default_constructor_tag{}) {}
+
+  template <class G = E,
+            detail::enable_if_t<std::is_constructible<E, const G &>::value> * =
+                nullptr,
+            detail::enable_if_t<!std::is_convertible<const G &, E>::value> * =
+                nullptr>
+  explicit constexpr expected(const unexpected<G> &e)
+      : impl_base(unexpect, e.value()),
+        ctor_base(detail::default_constructor_tag{}) {}
+
+    template <
+      class G = E,
+      detail::enable_if_t<std::is_constructible<E, const G &>::value> * =
+          nullptr,
+      detail::enable_if_t<std::is_convertible<const G &, E>::value> * = nullptr>
+  constexpr expected(unexpected<G> const &e)
+      : impl_base(unexpect, e.value()),
+        ctor_base(detail::default_constructor_tag{}) {}
+
+  template <
+      class G = E,
+      detail::enable_if_t<std::is_constructible<E, G &&>::value> * = nullptr,
+      detail::enable_if_t<!std::is_convertible<G &&, E>::value> * = nullptr>
+  explicit constexpr expected(unexpected<G> &&e) noexcept(
+      std::is_nothrow_constructible<E, G &&>::value)
+      : impl_base(unexpect, std::move(e.value())),
+        ctor_base(detail::default_constructor_tag{}) {}
+
+    template <
+      class G = E,
+      detail::enable_if_t<std::is_constructible<E, G &&>::value> * = nullptr,
+      detail::enable_if_t<std::is_convertible<G &&, E>::value> * = nullptr>
+  constexpr expected(unexpected<G> &&e) noexcept(
+      std::is_nothrow_constructible<E, G &&>::value)
+      : impl_base(unexpect, std::move(e.value())),
+        ctor_base(detail::default_constructor_tag{}) {}
+
+  template <class... Args,
+            detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+                nullptr>
+  constexpr explicit expected(unexpect_t, Args &&... args)
+      : impl_base(unexpect, std::forward<Args>(args)...),
+        ctor_base(detail::default_constructor_tag{}) {}
+
+    template <class U, class... Args,
+            detail::enable_if_t<std::is_constructible<
+                E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  constexpr explicit expected(unexpect_t, std::initializer_list<U> il,
+                              Args &&... args)
+      : impl_base(unexpect, il, std::forward<Args>(args)...),
+        ctor_base(detail::default_constructor_tag{}) {}
+
+  template <class U, class G,
+            detail::enable_if_t<!(std::is_convertible<U const &, T>::value &&
+                                  std::is_convertible<G const &, E>::value)> * =
+                nullptr,
+            detail::expected_enable_from_other<T, E, U, G, const U &, const G &>
+                * = nullptr>
+  explicit TL_EXPECTED_11_CONSTEXPR expected(const expected<U, G> &rhs)
+      : ctor_base(detail::default_constructor_tag{}) {
+    if (rhs.has_value()) {
+      this->construct(*rhs);
+    } else {
+      this->construct_error(rhs.error());        
+    }
+  }
+
+    template <class U, class G,
+            detail::enable_if_t<(std::is_convertible<U const &, T>::value &&
+                                 std::is_convertible<G const &, E>::value)> * =
+                nullptr,
+            detail::expected_enable_from_other<T, E, U, G, const U &, const G &>
+                * = nullptr>
+  TL_EXPECTED_11_CONSTEXPR expected(const expected<U, G> &rhs)
+      : ctor_base(detail::default_constructor_tag{}) {
+    if (rhs.has_value()) {
+      this->construct(*rhs);
+    } else {
+      this->construct_error(rhs.error());        
+    }      
+  }
+
+  template <
+      class U, class G,
+      detail::enable_if_t<!(std::is_convertible<U &&, T>::value &&
+                            std::is_convertible<G &&, E>::value)> * = nullptr,
+      detail::expected_enable_from_other<T, E, U, G, U &&, G &&> * = nullptr>
+  explicit TL_EXPECTED_11_CONSTEXPR expected(expected<U, G> &&rhs)
+      : ctor_base(detail::default_constructor_tag{}) {
+    if (rhs.has_value()) {
+      this->construct(std::move(*rhs));
+    } else {
+      this->construct_error(std::move(rhs.error()));        
+    }            
+  }
+
+    template <
+      class U, class G,
+      detail::enable_if_t<(std::is_convertible<U &&, T>::value &&
+                           std::is_convertible<G &&, E>::value)> * = nullptr,
+      detail::expected_enable_from_other<T, E, U, G, U &&, G &&> * = nullptr>
+  TL_EXPECTED_11_CONSTEXPR expected(expected<U, G> &&rhs)
+      : ctor_base(detail::default_constructor_tag{}) {
+    if (rhs.has_value()) {
+      this->construct(std::move(*rhs));
+    } else {
+      this->construct_error(std::move(rhs.error()));        
+    }                  
+  }
+
+  template <
+      class U = T,
+      detail::enable_if_t<!std::is_convertible<U &&, T>::value> * = nullptr,
+      detail::expected_enable_forward_value<T, E, U> * = nullptr>
+  explicit TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v)
+      : expected(in_place, std::forward<U>(v)) {}
+
+    template <
+      class U = T,
+      detail::enable_if_t<std::is_convertible<U &&, T>::value> * = nullptr,
+      detail::expected_enable_forward_value<T, E, U> * = nullptr>
+  TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v)
+      : expected(in_place, std::forward<U>(v)) {}
+
+  template <
+      class U = T, class G = T,
+      detail::enable_if_t<std::is_nothrow_constructible<T, U &&>::value> * =
+          nullptr,
+      detail::enable_if_t<!std::is_void<G>::value> * = nullptr,
+      detail::enable_if_t<
+          (!std::is_same<expected<T, E>, detail::decay_t<U>>::value &&
+           !detail::conjunction<std::is_scalar<T>,
+                                std::is_same<T, detail::decay_t<U>>>::value &&
+           std::is_constructible<T, U>::value &&
+           std::is_assignable<G &, U>::value &&
+           std::is_nothrow_move_constructible<E>::value)> * = nullptr>
+  expected &operator=(U &&v) {
+    if (has_value()) {
+      val() = std::forward<U>(v);
+    } else {
+      err().~unexpected<E>();
+      ::new (valptr()) T(std::forward<U>(v));
+      this->m_has_val = true;
+    }
+
+    return *this;
+  }
+
+    template <
+      class U = T, class G = T,
+      detail::enable_if_t<!std::is_nothrow_constructible<T, U &&>::value> * =
+          nullptr,
+      detail::enable_if_t<!std::is_void<U>::value> * = nullptr,
+      detail::enable_if_t<
+          (!std::is_same<expected<T, E>, detail::decay_t<U>>::value &&
+           !detail::conjunction<std::is_scalar<T>,
+                                std::is_same<T, detail::decay_t<U>>>::value &&
+           std::is_constructible<T, U>::value &&
+           std::is_assignable<G &, U>::value &&
+           std::is_nothrow_move_constructible<E>::value)> * = nullptr>
+  expected &operator=(U &&v) {
+    if (has_value()) {
+      val() = std::forward<U>(v);
+    } else {
+      auto tmp = std::move(err());
+      err().~unexpected<E>();
+
+      #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+      try {
+        ::new (valptr()) T(std::forward<U>(v));
+        this->m_has_val = true;
+      } catch (...) {
+        err() = std::move(tmp);
+        throw;
+      }
+      #else
+        ::new (valptr()) T(std::forward<U>(v));
+        this->m_has_val = true;
+      #endif
+    }
+
+    return *this;
+  }
+
+  template <class G = E,
+            detail::enable_if_t<std::is_nothrow_copy_constructible<G>::value &&
+                                std::is_assignable<G &, G>::value> * = nullptr>
+  expected &operator=(const unexpected<G> &rhs) {
+    if (!has_value()) {
+      err() = rhs;
+    } else {
+      this->destroy_val();
+      ::new (errptr()) unexpected<E>(rhs);
+      this->m_has_val = false;
+    }
+
+    return *this;
+  }
+
+  template <class G = E,
+            detail::enable_if_t<std::is_nothrow_move_constructible<G>::value &&
+                                std::is_move_assignable<G>::value> * = nullptr>
+  expected &operator=(unexpected<G> &&rhs) noexcept {
+    if (!has_value()) {
+      err() = std::move(rhs);
+    } else {
+      this->destroy_val();
+      ::new (errptr()) unexpected<E>(std::move(rhs));
+      this->m_has_val = false;
+    }
+
+    return *this;
+  }
+
+  template <class... Args, detail::enable_if_t<std::is_nothrow_constructible<
+                               T, Args &&...>::value> * = nullptr>
+  void emplace(Args &&... args) {
+    if (has_value()) {
+      val() = T(std::forward<Args>(args)...);
+    } else {
+      err().~unexpected<E>();
+      ::new (valptr()) T(std::forward<Args>(args)...);
+      this->m_has_val = true;
+    }
+  }
+
+    template <class... Args, detail::enable_if_t<!std::is_nothrow_constructible<
+                               T, Args &&...>::value> * = nullptr>
+  void emplace(Args &&... args) {
+    if (has_value()) {
+      val() = T(std::forward<Args>(args)...);
+    } else {
+      auto tmp = std::move(err());
+      err().~unexpected<E>();
+
+      #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+      try {
+        ::new (valptr()) T(std::forward<Args>(args)...);
+        this->m_has_val = true;
+      } catch (...) {
+        err() = std::move(tmp);
+        throw;
+      }
+      #else
+      ::new (valptr()) T(std::forward<Args>(args)...);
+      this->m_has_val = true;
+      #endif
+    }
+  }
+
+  template <class U, class... Args,
+            detail::enable_if_t<std::is_nothrow_constructible<
+                T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  void emplace(std::initializer_list<U> il, Args &&... args) {
+    if (has_value()) {
+      T t(il, std::forward<Args>(args)...);
+      val() = std::move(t);
+    } else {
+      err().~unexpected<E>();
+      ::new (valptr()) T(il, std::forward<Args>(args)...);
+      this->m_has_val = true;
+    }
+  }
+
+    template <class U, class... Args,
+            detail::enable_if_t<!std::is_nothrow_constructible<
+                T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+  void emplace(std::initializer_list<U> il, Args &&... args) {
+    if (has_value()) {
+      T t(il, std::forward<Args>(args)...);
+      val() = std::move(t);
+    } else {
+      auto tmp = std::move(err());
+      err().~unexpected<E>();
+
+      #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+      try {
+        ::new (valptr()) T(il, std::forward<Args>(args)...);
+        this->m_has_val = true;
+      } catch (...) {
+        err() = std::move(tmp);
+        throw;
+      }
+      #else
+      ::new (valptr()) T(il, std::forward<Args>(args)...);
+      this->m_has_val = true;
+      #endif
+    }
+  }
+
+private:
+  using t_is_void = std::true_type;
+  using t_is_not_void = std::false_type;
+  using t_is_nothrow_move_constructible = std::true_type;
+  using move_constructing_t_can_throw = std::false_type;
+  using e_is_nothrow_move_constructible = std::true_type;
+  using move_constructing_e_can_throw = std::false_type;
+
+  void swap_where_both_have_value(expected &/*rhs*/ , t_is_void) noexcept {
+    // swapping void is a no-op
+  }
+
+  void swap_where_both_have_value(expected &rhs, t_is_not_void) {
+    using std::swap;
+    swap(val(), rhs.val());
+  }
+
+  void swap_where_only_one_has_value(expected &rhs, t_is_void) noexcept(
+      std::is_nothrow_move_constructible<E>::value) {
+    ::new (errptr()) unexpected_type(std::move(rhs.err()));
+    rhs.err().~unexpected_type();
+    std::swap(this->m_has_val, rhs.m_has_val);
+  }
+
+  void swap_where_only_one_has_value(expected &rhs, t_is_not_void) {
+    swap_where_only_one_has_value_and_t_is_not_void(
+        rhs, typename std::is_nothrow_move_constructible<T>::type{},
+        typename std::is_nothrow_move_constructible<E>::type{});
+  }
+
+  void swap_where_only_one_has_value_and_t_is_not_void(
+      expected &rhs, t_is_nothrow_move_constructible,
+      e_is_nothrow_move_constructible) noexcept {
+    auto temp = std::move(val());
+    val().~T();
+    ::new (errptr()) unexpected_type(std::move(rhs.err()));
+    rhs.err().~unexpected_type();
+    ::new (rhs.valptr()) T(std::move(temp));
+    std::swap(this->m_has_val, rhs.m_has_val);
+  }
+
+  void swap_where_only_one_has_value_and_t_is_not_void(
+      expected &rhs, t_is_nothrow_move_constructible,
+      move_constructing_e_can_throw) {
+    auto temp = std::move(val());
+    val().~T();
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+    try {
+      ::new (errptr()) unexpected_type(std::move(rhs.err()));
+      rhs.err().~unexpected_type();
+      ::new (rhs.valptr()) T(std::move(temp));
+      std::swap(this->m_has_val, rhs.m_has_val);
+    } catch (...) {
+      val() = std::move(temp);
+      throw;
+    }
+#else
+    ::new (errptr()) unexpected_type(std::move(rhs.err()));
+    rhs.err().~unexpected_type();
+    ::new (rhs.valptr()) T(std::move(temp));
+    std::swap(this->m_has_val, rhs.m_has_val);
+#endif
+  }
+
+  void swap_where_only_one_has_value_and_t_is_not_void(
+      expected &rhs, move_constructing_t_can_throw,
+      t_is_nothrow_move_constructible) {
+    auto temp = std::move(rhs.err());
+    rhs.err().~unexpected_type();
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+    try {
+      ::new (rhs.valptr()) T(val());
+      val().~T();
+      ::new (errptr()) unexpected_type(std::move(temp));
+      std::swap(this->m_has_val, rhs.m_has_val);
+    } catch (...) {
+      rhs.err() = std::move(temp);
+      throw;
+    }
+#else
+    ::new (rhs.valptr()) T(val());
+    val().~T();
+    ::new (errptr()) unexpected_type(std::move(temp));
+    std::swap(this->m_has_val, rhs.m_has_val);
+#endif
+  }
+
+public:
+  template <class OT = T, class OE = E>
+  detail::enable_if_t<detail::is_swappable<OT>::value &&
+                      detail::is_swappable<OE>::value &&
+                      (std::is_nothrow_move_constructible<OT>::value ||
+                       std::is_nothrow_move_constructible<OE>::value)>
+  swap(expected &rhs) noexcept(
+      std::is_nothrow_move_constructible<T>::value
+          &&detail::is_nothrow_swappable<T>::value
+              &&std::is_nothrow_move_constructible<E>::value
+                  &&detail::is_nothrow_swappable<E>::value) {
+    if (has_value() && rhs.has_value()) {
+      swap_where_both_have_value(rhs, typename std::is_void<T>::type{});
+    } else if (!has_value() && rhs.has_value()) {
+      rhs.swap(*this);
+    } else if (has_value()) {
+      swap_where_only_one_has_value(rhs, typename std::is_void<T>::type{});
+    } else {
+      using std::swap;
+      swap(err(), rhs.err());
+    }
+  }
+
+  constexpr const T *operator->() const { return valptr(); }
+  TL_EXPECTED_11_CONSTEXPR T *operator->() { return valptr(); }
+
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  constexpr const U &operator*() const & {
+    return val();
+  }
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  TL_EXPECTED_11_CONSTEXPR U &operator*() & {
+    return val();
+  }
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  constexpr const U &&operator*() const && {
+    return std::move(val());
+  }
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  TL_EXPECTED_11_CONSTEXPR U &&operator*() && {
+    return std::move(val());
+  }
+
+  constexpr bool has_value() const noexcept { return this->m_has_val; }
+  constexpr explicit operator bool() const noexcept { return this->m_has_val; }
+
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  TL_EXPECTED_11_CONSTEXPR const U &value() const & {
+    if (!has_value())
+      detail::throw_exception(bad_expected_access<E>(err().value()));
+    return val();
+  }
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  TL_EXPECTED_11_CONSTEXPR U &value() & {
+    if (!has_value())
+      detail::throw_exception(bad_expected_access<E>(err().value()));
+    return val();
+  }
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  TL_EXPECTED_11_CONSTEXPR const U &&value() const && {
+    if (!has_value())
+      detail::throw_exception(bad_expected_access<E>(std::move(err()).value()));
+    return std::move(val());
+  }
+  template <class U = T,
+            detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+  TL_EXPECTED_11_CONSTEXPR U &&value() && {
+    if (!has_value())
+      detail::throw_exception(bad_expected_access<E>(std::move(err()).value()));
+    return std::move(val());
+  }
+
+  constexpr const E &error() const & { return err().value(); }
+  TL_EXPECTED_11_CONSTEXPR E &error() & { return err().value(); }
+  constexpr const E &&error() const && { return std::move(err().value()); }
+  TL_EXPECTED_11_CONSTEXPR E &&error() && { return std::move(err().value()); }
+
+  template <class U> constexpr T value_or(U &&v) const & {
+    static_assert(std::is_copy_constructible<T>::value &&
+                      std::is_convertible<U &&, T>::value,
+                  "T must be copy-constructible and convertible to from U&&");
+    return bool(*this) ? **this : static_cast<T>(std::forward<U>(v));
+  }
+  template <class U> TL_EXPECTED_11_CONSTEXPR T value_or(U &&v) && {
+    static_assert(std::is_move_constructible<T>::value &&
+                      std::is_convertible<U &&, T>::value,
+                  "T must be move-constructible and convertible to from U&&");
+    return bool(*this) ? std::move(**this) : static_cast<T>(std::forward<U>(v));
+  }
+};
+
+namespace detail {
+template <class Exp> using exp_t = typename detail::decay_t<Exp>::value_type;
+template <class Exp> using err_t = typename detail::decay_t<Exp>::error_type;
+template <class Exp, class Ret> using ret_t = expected<Ret, err_t<Exp>>;
+
+#ifdef TL_EXPECTED_CXX14
+template <class Exp, class F,
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Exp>()))>
+constexpr auto and_then_impl(Exp &&exp, F &&f) {
+  static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+  return exp.has_value()
+             ? detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp))
+             : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+          class Ret = decltype(detail::invoke(std::declval<F>()))>
+constexpr auto and_then_impl(Exp &&exp, F &&f) {
+  static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+  return exp.has_value() ? detail::invoke(std::forward<F>(f))
+                         : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+#else
+template <class> struct TC;
+template <class Exp, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Exp>())),
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr>
+auto and_then_impl(Exp &&exp, F &&f) -> Ret {
+  static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+  return exp.has_value()
+             ? detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp))
+             : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>())),
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr>
+constexpr auto and_then_impl(Exp &&exp, F &&f) -> Ret {
+  static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+  return exp.has_value() ? detail::invoke(std::forward<F>(f))
+                         : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+#endif
+
+#ifdef TL_EXPECTED_CXX14
+template <class Exp, class F,
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,          
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Exp>())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto expected_map_impl(Exp &&exp, F &&f) {
+  using result = ret_t<Exp, detail::decay_t<Ret>>;
+  return exp.has_value() ? result(detail::invoke(std::forward<F>(f),
+                                                 *std::forward<Exp>(exp)))
+                         : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,          
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Exp>())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto expected_map_impl(Exp &&exp, F &&f) {
+  using result = expected<void, err_t<Exp>>;
+  if (exp.has_value()) {
+    detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp));
+    return result();
+  }
+
+  return result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+          class Ret = decltype(detail::invoke(std::declval<F>())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto expected_map_impl(Exp &&exp, F &&f) {
+  using result = ret_t<Exp, detail::decay_t<Ret>>;
+  return exp.has_value() ? result(detail::invoke(std::forward<F>(f)))
+                         : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,          
+          class Ret = decltype(detail::invoke(std::declval<F>())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto expected_map_impl(Exp &&exp, F &&f) {
+  using result = expected<void, err_t<Exp>>;
+  if (exp.has_value()) {
+    detail::invoke(std::forward<F>(f));
+    return result();
+  }
+
+  return result(unexpect, std::forward<Exp>(exp).error());
+}    
+#else
+template <class Exp, class F,
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,          
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Exp>())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+
+constexpr auto expected_map_impl(Exp &&exp, F &&f)
+    -> ret_t<Exp, detail::decay_t<Ret>> {
+  using result = ret_t<Exp, detail::decay_t<Ret>>;
+
+  return exp.has_value() ? result(detail::invoke(std::forward<F>(f),
+                                                 *std::forward<Exp>(exp)))
+                         : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,                    
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Exp>())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+
+auto expected_map_impl(Exp &&exp, F &&f) -> expected<void, err_t<Exp>> {
+  if (exp.has_value()) {
+    detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp));
+    return {};
+  }
+
+  return unexpected<err_t<Exp>>(std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,                              
+          class Ret = decltype(detail::invoke(std::declval<F>())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+
+constexpr auto expected_map_impl(Exp &&exp, F &&f)
+    -> ret_t<Exp, detail::decay_t<Ret>> {
+  using result = ret_t<Exp, detail::decay_t<Ret>>;
+
+  return exp.has_value() ? result(detail::invoke(std::forward<F>(f)))
+                         : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,                                        
+          class Ret = decltype(detail::invoke(std::declval<F>())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+
+auto expected_map_impl(Exp &&exp, F &&f) -> expected<void, err_t<Exp>> {
+  if (exp.has_value()) {
+    detail::invoke(std::forward<F>(f));
+    return {};
+  }
+
+  return unexpected<err_t<Exp>>(std::forward<Exp>(exp).error());
+}    
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) &&               \
+    !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+template <class Exp, class F,
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,          
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f) {
+  using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+  return exp.has_value()
+             ? result(*std::forward<Exp>(exp))
+             : result(unexpect, detail::invoke(std::forward<F>(f),
+                                               std::forward<Exp>(exp).error()));
+}
+template <class Exp, class F,
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,                    
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) {
+  using result = expected<exp_t<Exp>, monostate>;
+  if (exp.has_value()) {
+    return result(*std::forward<Exp>(exp));
+  }
+
+  detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+  return result(unexpect, monostate{});
+}
+template <class Exp, class F,
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,          
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f) {
+  using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+  return exp.has_value()
+             ? result()
+             : result(unexpect, detail::invoke(std::forward<F>(f),
+                                               std::forward<Exp>(exp).error()));
+}
+template <class Exp, class F,
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,                    
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) {
+  using result = expected<exp_t<Exp>, monostate>;
+  if (exp.has_value()) {
+    return result();
+  }
+
+  detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+  return result(unexpect, monostate{});
+}    
+#else
+template <class Exp, class F,
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,                              
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f)
+    -> expected<exp_t<Exp>, detail::decay_t<Ret>> {
+  using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+
+  return exp.has_value()
+             ? result(*std::forward<Exp>(exp))
+             : result(unexpect, detail::invoke(std::forward<F>(f),
+                                               std::forward<Exp>(exp).error()));
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,                                        
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) -> expected<exp_t<Exp>, monostate> {
+  using result = expected<exp_t<Exp>, monostate>;
+  if (exp.has_value()) {
+    return result(*std::forward<Exp>(exp));
+  }
+
+  detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+  return result(unexpect, monostate{});
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,                              
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f)
+    -> expected<exp_t<Exp>, detail::decay_t<Ret>> {
+  using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+
+  return exp.has_value()
+             ? result()
+             : result(unexpect, detail::invoke(std::forward<F>(f),
+                                               std::forward<Exp>(exp).error()));
+}
+
+template <class Exp, class F,
+          detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,                                        
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) -> expected<exp_t<Exp>, monostate> {
+  using result = expected<exp_t<Exp>, monostate>;
+  if (exp.has_value()) {
+    return result();
+  }
+
+  detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+  return result(unexpect, monostate{});
+}    
+#endif
+
+#ifdef TL_EXPECTED_CXX14
+template <class Exp, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto or_else_impl(Exp &&exp, F &&f) {
+  static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+  return exp.has_value()
+  ? std::forward<Exp>(exp)
+  : detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+detail::decay_t<Exp> or_else_impl(Exp &&exp, F &&f) {
+  return exp.has_value()
+  ? std::forward<Exp>(exp)
+  : (detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error()),
+    std::forward<Exp>(exp));
+}
+#else
+template <class Exp, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+  detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+auto or_else_impl(Exp &&exp, F &&f) -> Ret {
+  static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+  return exp.has_value()
+         ? std::forward<Exp>(exp)
+         : detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              std::declval<Exp>().error())),
+  detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+detail::decay_t<Exp> or_else_impl(Exp &&exp, F &&f) {
+  return exp.has_value()
+         ? std::forward<Exp>(exp)
+         : (detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error()),
+            std::forward<Exp>(exp));
+}
+#endif
+} // namespace detail
+
+template <class T, class E, class U, class F>
+constexpr bool operator==(const expected<T, E> &lhs,
+                          const expected<U, F> &rhs) {
+  return (lhs.has_value() != rhs.has_value())
+             ? false
+             : (!lhs.has_value() ? lhs.error() == rhs.error() : *lhs == *rhs);
+}
+template <class T, class E, class U, class F>
+constexpr bool operator!=(const expected<T, E> &lhs,
+                          const expected<U, F> &rhs) {
+  return (lhs.has_value() != rhs.has_value())
+             ? true
+             : (!lhs.has_value() ? lhs.error() != rhs.error() : *lhs != *rhs);
+}
+
+template <class T, class E, class U>
+constexpr bool operator==(const expected<T, E> &x, const U &v) {
+  return x.has_value() ? *x == v : false;
+}
+template <class T, class E, class U>
+constexpr bool operator==(const U &v, const expected<T, E> &x) {
+  return x.has_value() ? *x == v : false;
+}
+template <class T, class E, class U>
+constexpr bool operator!=(const expected<T, E> &x, const U &v) {
+  return x.has_value() ? *x != v : true;
+}
+template <class T, class E, class U>
+constexpr bool operator!=(const U &v, const expected<T, E> &x) {
+  return x.has_value() ? *x != v : true;
+}
+
+template <class T, class E>
+constexpr bool operator==(const expected<T, E> &x, const unexpected<E> &e) {
+  return x.has_value() ? false : x.error() == e.value();
+}
+template <class T, class E>
+constexpr bool operator==(const unexpected<E> &e, const expected<T, E> &x) {
+  return x.has_value() ? false : x.error() == e.value();
+}
+template <class T, class E>
+constexpr bool operator!=(const expected<T, E> &x, const unexpected<E> &e) {
+  return x.has_value() ? true : x.error() != e.value();
+}
+template <class T, class E>
+constexpr bool operator!=(const unexpected<E> &e, const expected<T, E> &x) {
+  return x.has_value() ? true : x.error() != e.value();
+}
+
+template <class T, class E,
+          detail::enable_if_t<(std::is_void<T>::value ||
+                               std::is_move_constructible<T>::value) &&
+                              detail::is_swappable<T>::value &&
+                              std::is_move_constructible<E>::value &&
+                              detail::is_swappable<E>::value> * = nullptr>
+void swap(expected<T, E> &lhs,
+          expected<T, E> &rhs) noexcept(noexcept(lhs.swap(rhs))) {
+  lhs.swap(rhs);
+}
+} // namespace tl
+
+#endif
diff --git a/lib/thirdparty/optional.hpp b/lib/thirdparty/optional.hpp
new file mode 100644 (file)
index 0000000..37b774a
--- /dev/null
@@ -0,0 +1,2063 @@
+
+///
+// optional - An implementation of std::optional with extensions
+// Written in 2017 by Sy Brand (tartanllama@gmail.com, @TartanLlama)
+//
+// Documentation available at https://tl.tartanllama.xyz/
+//
+// To the extent possible under law, the author(s) have dedicated all
+// copyright and related and neighboring rights to this software to the
+// public domain worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication
+// along with this software. If not, see
+// <http://creativecommons.org/publicdomain/zero/1.0/>.
+///
+
+#ifndef TL_OPTIONAL_HPP
+#define TL_OPTIONAL_HPP
+
+#define TL_OPTIONAL_VERSION_MAJOR 1
+#define TL_OPTIONAL_VERSION_MINOR 0
+#define TL_OPTIONAL_VERSION_PATCH 0
+
+#include <exception>
+#include <functional>
+#include <new>
+#include <type_traits>
+#include <utility>
+
+#if (defined(_MSC_VER) && _MSC_VER == 1900)
+#define TL_OPTIONAL_MSVC2015
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 &&              \
+     !defined(__clang__))
+#define TL_OPTIONAL_GCC49
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 &&              \
+     !defined(__clang__))
+#define TL_OPTIONAL_GCC54
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 5 &&              \
+     !defined(__clang__))
+#define TL_OPTIONAL_GCC55
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 &&              \
+     !defined(__clang__))
+// GCC < 5 doesn't support overloading on const&& for member functions
+#define TL_OPTIONAL_NO_CONSTRR
+
+// GCC < 5 doesn't support some standard C++11 type traits
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)                                     \
+  std::has_trivial_copy_constructor<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) std::has_trivial_copy_assign<T>::value
+
+// This one will be different for GCC 5.7 if it's ever supported
+#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>::value
+
+// GCC 5 < v < 8 has a bug in is_trivially_copy_constructible which breaks std::vector
+// for non-copyable types
+#elif (defined(__GNUC__) && __GNUC__ < 8 &&                                                \
+     !defined(__clang__))
+#ifndef TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+#define TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+namespace tl {
+  namespace detail {
+      template<class T>
+      struct is_trivially_copy_constructible : std::is_trivially_copy_constructible<T>{};
+#ifdef _GLIBCXX_VECTOR
+      template<class T, class A>
+      struct is_trivially_copy_constructible<std::vector<T,A>>
+          : std::is_trivially_copy_constructible<T>{};
+#endif      
+  }
+}
+#endif
+
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)                                     \
+    tl::detail::is_trivially_copy_constructible<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T)                                        \
+  std::is_trivially_copy_assignable<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>::value
+#else
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)                                     \
+  std::is_trivially_copy_constructible<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T)                                        \
+  std::is_trivially_copy_assignable<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>::value
+#endif
+
+#if __cplusplus > 201103L
+#define TL_OPTIONAL_CXX14
+#endif
+
+// constexpr implies const in C++11, not C++14
+#if (__cplusplus == 201103L || defined(TL_OPTIONAL_MSVC2015) ||                \
+     defined(TL_OPTIONAL_GCC49))
+#define TL_OPTIONAL_11_CONSTEXPR
+#else
+#define TL_OPTIONAL_11_CONSTEXPR constexpr
+#endif
+
+namespace tl {
+#ifndef TL_MONOSTATE_INPLACE_MUTEX
+#define TL_MONOSTATE_INPLACE_MUTEX
+/// Used to represent an optional with no data; essentially a bool
+class monostate {};
+
+///  A tag type to tell optional to construct its value in-place
+struct in_place_t {
+  explicit in_place_t() = default;
+};
+/// A tag to tell optional to construct its value in-place
+static constexpr in_place_t in_place{};
+#endif
+
+template <class T> class optional;
+
+namespace detail {
+#ifndef TL_TRAITS_MUTEX
+#define TL_TRAITS_MUTEX
+// C++14-style aliases for brevity
+template <class T> using remove_const_t = typename std::remove_const<T>::type;
+template <class T>
+using remove_reference_t = typename std::remove_reference<T>::type;
+template <class T> using decay_t = typename std::decay<T>::type;
+template <bool E, class T = void>
+using enable_if_t = typename std::enable_if<E, T>::type;
+template <bool B, class T, class F>
+using conditional_t = typename std::conditional<B, T, F>::type;
+
+// std::conjunction from C++17
+template <class...> struct conjunction : std::true_type {};
+template <class B> struct conjunction<B> : B {};
+template <class B, class... Bs>
+struct conjunction<B, Bs...>
+    : std::conditional<bool(B::value), conjunction<Bs...>, B>::type {};
+
+#if defined(_LIBCPP_VERSION) && __cplusplus == 201103L
+#define TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+#endif
+
+// In C++11 mode, there's an issue in libc++'s std::mem_fn
+// which results in a hard-error when using it in a noexcept expression
+// in some cases. This is a check to workaround the common failing case.
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+template <class T> struct is_pointer_to_non_const_member_func : std::false_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...)> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...)&> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...)&&> : std::true_type{};        
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...) volatile> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...) volatile&> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...) volatile&&> : std::true_type{};        
+
+template <class T> struct is_const_or_const_ref : std::false_type{};
+template <class T> struct is_const_or_const_ref<T const&> : std::true_type{};
+template <class T> struct is_const_or_const_ref<T const> : std::true_type{};    
+#endif
+
+// std::invoke from C++17
+// https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround
+template <typename Fn, typename... Args,
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+          typename = enable_if_t<!(is_pointer_to_non_const_member_func<Fn>::value 
+                                 && is_const_or_const_ref<Args...>::value)>, 
+#endif
+          typename = enable_if_t<std::is_member_pointer<decay_t<Fn>>::value>,
+          int = 0>
+constexpr auto invoke(Fn &&f, Args &&... args) noexcept(
+    noexcept(std::mem_fn(f)(std::forward<Args>(args)...)))
+    -> decltype(std::mem_fn(f)(std::forward<Args>(args)...)) {
+  return std::mem_fn(f)(std::forward<Args>(args)...);
+}
+
+template <typename Fn, typename... Args,
+          typename = enable_if_t<!std::is_member_pointer<decay_t<Fn>>::value>>
+constexpr auto invoke(Fn &&f, Args &&... args) noexcept(
+    noexcept(std::forward<Fn>(f)(std::forward<Args>(args)...)))
+    -> decltype(std::forward<Fn>(f)(std::forward<Args>(args)...)) {
+  return std::forward<Fn>(f)(std::forward<Args>(args)...);
+}
+
+// std::invoke_result from C++17
+template <class F, class, class... Us> struct invoke_result_impl;
+
+template <class F, class... Us>
+struct invoke_result_impl<
+    F, decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...), void()),
+    Us...> {
+  using type = decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...));
+};
+
+template <class F, class... Us>
+using invoke_result = invoke_result_impl<F, void, Us...>;
+
+template <class F, class... Us>
+using invoke_result_t = typename invoke_result<F, Us...>::type;
+
+#if defined(_MSC_VER) && _MSC_VER <= 1900
+// TODO make a version which works with MSVC 2015
+template <class T, class U = T> struct is_swappable : std::true_type {};
+
+template <class T, class U = T> struct is_nothrow_swappable : std::true_type {};
+#else
+// https://stackoverflow.com/questions/26744589/what-is-a-proper-way-to-implement-is-swappable-to-test-for-the-swappable-concept
+namespace swap_adl_tests {
+// if swap ADL finds this then it would call std::swap otherwise (same
+// signature)
+struct tag {};
+
+template <class T> tag swap(T &, T &);
+template <class T, std::size_t N> tag swap(T (&a)[N], T (&b)[N]);
+
+// helper functions to test if an unqualified swap is possible, and if it
+// becomes std::swap
+template <class, class> std::false_type can_swap(...) noexcept(false);
+template <class T, class U,
+          class = decltype(swap(std::declval<T &>(), std::declval<U &>()))>
+std::true_type can_swap(int) noexcept(noexcept(swap(std::declval<T &>(),
+                                                    std::declval<U &>())));
+
+template <class, class> std::false_type uses_std(...);
+template <class T, class U>
+std::is_same<decltype(swap(std::declval<T &>(), std::declval<U &>())), tag>
+uses_std(int);
+
+template <class T>
+struct is_std_swap_noexcept
+    : std::integral_constant<bool,
+                             std::is_nothrow_move_constructible<T>::value &&
+                                 std::is_nothrow_move_assignable<T>::value> {};
+
+template <class T, std::size_t N>
+struct is_std_swap_noexcept<T[N]> : is_std_swap_noexcept<T> {};
+
+template <class T, class U>
+struct is_adl_swap_noexcept
+    : std::integral_constant<bool, noexcept(can_swap<T, U>(0))> {};
+} // namespace swap_adl_tests
+
+template <class T, class U = T>
+struct is_swappable
+    : std::integral_constant<
+          bool,
+          decltype(detail::swap_adl_tests::can_swap<T, U>(0))::value &&
+              (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value ||
+               (std::is_move_assignable<T>::value &&
+                std::is_move_constructible<T>::value))> {};
+
+template <class T, std::size_t N>
+struct is_swappable<T[N], T[N]>
+    : std::integral_constant<
+          bool,
+          decltype(detail::swap_adl_tests::can_swap<T[N], T[N]>(0))::value &&
+              (!decltype(
+                   detail::swap_adl_tests::uses_std<T[N], T[N]>(0))::value ||
+               is_swappable<T, T>::value)> {};
+
+template <class T, class U = T>
+struct is_nothrow_swappable
+    : std::integral_constant<
+          bool,
+          is_swappable<T, U>::value &&
+              ((decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value
+                    &&detail::swap_adl_tests::is_std_swap_noexcept<T>::value) ||
+               (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value &&
+                    detail::swap_adl_tests::is_adl_swap_noexcept<T,
+                                                                 U>::value))> {
+};
+#endif
+#endif
+
+// std::void_t from C++17
+template <class...> struct voider { using type = void; };
+template <class... Ts> using void_t = typename voider<Ts...>::type;
+
+// Trait for checking if a type is a tl::optional
+template <class T> struct is_optional_impl : std::false_type {};
+template <class T> struct is_optional_impl<optional<T>> : std::true_type {};
+template <class T> using is_optional = is_optional_impl<decay_t<T>>;
+
+// Change void to tl::monostate
+template <class U>
+using fixup_void = conditional_t<std::is_void<U>::value, monostate, U>;
+
+template <class F, class U, class = invoke_result_t<F, U>>
+using get_map_return = optional<fixup_void<invoke_result_t<F, U>>>;
+
+// Check if invoking F for some Us returns void
+template <class F, class = void, class... U> struct returns_void_impl;
+template <class F, class... U>
+struct returns_void_impl<F, void_t<invoke_result_t<F, U...>>, U...>
+    : std::is_void<invoke_result_t<F, U...>> {};
+template <class F, class... U>
+using returns_void = returns_void_impl<F, void, U...>;
+
+template <class T, class... U>
+using enable_if_ret_void = enable_if_t<returns_void<T &&, U...>::value>;
+
+template <class T, class... U>
+using disable_if_ret_void = enable_if_t<!returns_void<T &&, U...>::value>;
+
+template <class T, class U>
+using enable_forward_value =
+    detail::enable_if_t<std::is_constructible<T, U &&>::value &&
+                        !std::is_same<detail::decay_t<U>, in_place_t>::value &&
+                        !std::is_same<optional<T>, detail::decay_t<U>>::value>;
+
+template <class T, class U, class Other>
+using enable_from_other = detail::enable_if_t<
+    std::is_constructible<T, Other>::value &&
+    !std::is_constructible<T, optional<U> &>::value &&
+    !std::is_constructible<T, optional<U> &&>::value &&
+    !std::is_constructible<T, const optional<U> &>::value &&
+    !std::is_constructible<T, const optional<U> &&>::value &&
+    !std::is_convertible<optional<U> &, T>::value &&
+    !std::is_convertible<optional<U> &&, T>::value &&
+    !std::is_convertible<const optional<U> &, T>::value &&
+    !std::is_convertible<const optional<U> &&, T>::value>;
+
+template <class T, class U>
+using enable_assign_forward = detail::enable_if_t<
+    !std::is_same<optional<T>, detail::decay_t<U>>::value &&
+    !detail::conjunction<std::is_scalar<T>,
+                         std::is_same<T, detail::decay_t<U>>>::value &&
+    std::is_constructible<T, U>::value && std::is_assignable<T &, U>::value>;
+
+template <class T, class U, class Other>
+using enable_assign_from_other = detail::enable_if_t<
+    std::is_constructible<T, Other>::value &&
+    std::is_assignable<T &, Other>::value &&
+    !std::is_constructible<T, optional<U> &>::value &&
+    !std::is_constructible<T, optional<U> &&>::value &&
+    !std::is_constructible<T, const optional<U> &>::value &&
+    !std::is_constructible<T, const optional<U> &&>::value &&
+    !std::is_convertible<optional<U> &, T>::value &&
+    !std::is_convertible<optional<U> &&, T>::value &&
+    !std::is_convertible<const optional<U> &, T>::value &&
+    !std::is_convertible<const optional<U> &&, T>::value &&
+    !std::is_assignable<T &, optional<U> &>::value &&
+    !std::is_assignable<T &, optional<U> &&>::value &&
+    !std::is_assignable<T &, const optional<U> &>::value &&
+    !std::is_assignable<T &, const optional<U> &&>::value>;
+
+// The storage base manages the actual storage, and correctly propagates
+// trivial destruction from T. This case is for when T is not trivially
+// destructible.
+template <class T, bool = ::std::is_trivially_destructible<T>::value>
+struct optional_storage_base {
+  TL_OPTIONAL_11_CONSTEXPR optional_storage_base() noexcept
+      : m_dummy(), m_has_value(false) {}
+
+  template <class... U>
+  TL_OPTIONAL_11_CONSTEXPR optional_storage_base(in_place_t, U &&... u)
+      : m_value(std::forward<U>(u)...), m_has_value(true) {}
+
+  ~optional_storage_base() {
+    if (m_has_value) {
+      m_value.~T();
+      m_has_value = false;
+    }
+  }
+
+  struct dummy {};
+  union {
+    dummy m_dummy;
+    T m_value;
+  };
+
+  bool m_has_value;
+};
+
+// This case is for when T is trivially destructible.
+template <class T> struct optional_storage_base<T, true> {
+  TL_OPTIONAL_11_CONSTEXPR optional_storage_base() noexcept
+      : m_dummy(), m_has_value(false) {}
+
+  template <class... U>
+  TL_OPTIONAL_11_CONSTEXPR optional_storage_base(in_place_t, U &&... u)
+      : m_value(std::forward<U>(u)...), m_has_value(true) {}
+
+  // No destructor, so this class is trivially destructible
+
+  struct dummy {};
+  union {
+    dummy m_dummy;
+    T m_value;
+  };
+
+  bool m_has_value = false;
+};
+
+// This base class provides some handy member functions which can be used in
+// further derived classes
+template <class T> struct optional_operations_base : optional_storage_base<T> {
+  using optional_storage_base<T>::optional_storage_base;
+
+  void hard_reset() noexcept {
+    get().~T();
+    this->m_has_value = false;
+  }
+
+  template <class... Args> void construct(Args &&... args) noexcept {
+    new (std::addressof(this->m_value)) T(std::forward<Args>(args)...);
+    this->m_has_value = true;
+  }
+
+  template <class Opt> void assign(Opt &&rhs) {
+    if (this->has_value()) {
+      if (rhs.has_value()) {
+        this->m_value = std::forward<Opt>(rhs).get();
+      } else {
+        this->m_value.~T();
+        this->m_has_value = false;
+      }
+    }
+
+    else if (rhs.has_value()) {
+      construct(std::forward<Opt>(rhs).get());
+    }
+  }
+
+  bool has_value() const { return this->m_has_value; }
+
+  TL_OPTIONAL_11_CONSTEXPR T &get() & { return this->m_value; }
+  TL_OPTIONAL_11_CONSTEXPR const T &get() const & { return this->m_value; }
+  TL_OPTIONAL_11_CONSTEXPR T &&get() && { return std::move(this->m_value); }
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  constexpr const T &&get() const && { return std::move(this->m_value); }
+#endif
+};
+
+// This class manages conditionally having a trivial copy constructor
+// This specialization is for when T is trivially copy constructible
+template <class T, bool = TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)>
+struct optional_copy_base : optional_operations_base<T> {
+  using optional_operations_base<T>::optional_operations_base;
+};
+
+// This specialization is for when T is not trivially copy constructible
+template <class T>
+struct optional_copy_base<T, false> : optional_operations_base<T> {
+  using optional_operations_base<T>::optional_operations_base;
+
+  optional_copy_base() = default;
+  optional_copy_base(const optional_copy_base &rhs)
+  : optional_operations_base<T>() {
+    if (rhs.has_value()) {
+      this->construct(rhs.get());
+    } else {
+      this->m_has_value = false;
+    }
+  }
+
+  optional_copy_base(optional_copy_base &&rhs) = default;
+  optional_copy_base &operator=(const optional_copy_base &rhs) = default;
+  optional_copy_base &operator=(optional_copy_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move constructor
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_constructible. We
+// have to make do with a non-trivial move constructor even if T is trivially
+// move constructible
+#ifndef TL_OPTIONAL_GCC49
+template <class T, bool = std::is_trivially_move_constructible<T>::value>
+struct optional_move_base : optional_copy_base<T> {
+  using optional_copy_base<T>::optional_copy_base;
+};
+#else
+template <class T, bool = false> struct optional_move_base;
+#endif
+template <class T> struct optional_move_base<T, false> : optional_copy_base<T> {
+  using optional_copy_base<T>::optional_copy_base;
+
+  optional_move_base() = default;
+  optional_move_base(const optional_move_base &rhs) = default;
+
+  optional_move_base(optional_move_base &&rhs) noexcept(
+      std::is_nothrow_move_constructible<T>::value) {
+    if (rhs.has_value()) {
+      this->construct(std::move(rhs.get()));
+    } else {
+      this->m_has_value = false;
+    }
+  }
+  optional_move_base &operator=(const optional_move_base &rhs) = default;
+  optional_move_base &operator=(optional_move_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial copy assignment operator
+template <class T, bool = TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) &&
+                          TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) &&
+                          TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T)>
+struct optional_copy_assign_base : optional_move_base<T> {
+  using optional_move_base<T>::optional_move_base;
+};
+
+template <class T>
+struct optional_copy_assign_base<T, false> : optional_move_base<T> {
+  using optional_move_base<T>::optional_move_base;
+
+  optional_copy_assign_base() = default;
+  optional_copy_assign_base(const optional_copy_assign_base &rhs) = default;
+
+  optional_copy_assign_base(optional_copy_assign_base &&rhs) = default;
+  optional_copy_assign_base &operator=(const optional_copy_assign_base &rhs) {
+    this->assign(rhs);
+    return *this;
+  }
+  optional_copy_assign_base &
+  operator=(optional_copy_assign_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move assignment operator
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_assignable. We have
+// to make do with a non-trivial move assignment operator even if T is trivially
+// move assignable
+#ifndef TL_OPTIONAL_GCC49
+template <class T, bool = std::is_trivially_destructible<T>::value
+                       &&std::is_trivially_move_constructible<T>::value
+                           &&std::is_trivially_move_assignable<T>::value>
+struct optional_move_assign_base : optional_copy_assign_base<T> {
+  using optional_copy_assign_base<T>::optional_copy_assign_base;
+};
+#else
+template <class T, bool = false> struct optional_move_assign_base;
+#endif
+
+template <class T>
+struct optional_move_assign_base<T, false> : optional_copy_assign_base<T> {
+  using optional_copy_assign_base<T>::optional_copy_assign_base;
+
+  optional_move_assign_base() = default;
+  optional_move_assign_base(const optional_move_assign_base &rhs) = default;
+
+  optional_move_assign_base(optional_move_assign_base &&rhs) = default;
+
+  optional_move_assign_base &
+  operator=(const optional_move_assign_base &rhs) = default;
+
+  optional_move_assign_base &
+  operator=(optional_move_assign_base &&rhs) noexcept(
+      std::is_nothrow_move_constructible<T>::value
+          &&std::is_nothrow_move_assignable<T>::value) {
+    this->assign(std::move(rhs));
+    return *this;
+  }
+};
+
+// optional_delete_ctor_base will conditionally delete copy and move
+// constructors depending on whether T is copy/move constructible
+template <class T, bool EnableCopy = std::is_copy_constructible<T>::value,
+          bool EnableMove = std::is_move_constructible<T>::value>
+struct optional_delete_ctor_base {
+  optional_delete_ctor_base() = default;
+  optional_delete_ctor_base(const optional_delete_ctor_base &) = default;
+  optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = default;
+  optional_delete_ctor_base &
+  operator=(const optional_delete_ctor_base &) = default;
+  optional_delete_ctor_base &
+  operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_ctor_base<T, true, false> {
+  optional_delete_ctor_base() = default;
+  optional_delete_ctor_base(const optional_delete_ctor_base &) = default;
+  optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = delete;
+  optional_delete_ctor_base &
+  operator=(const optional_delete_ctor_base &) = default;
+  optional_delete_ctor_base &
+  operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_ctor_base<T, false, true> {
+  optional_delete_ctor_base() = default;
+  optional_delete_ctor_base(const optional_delete_ctor_base &) = delete;
+  optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = default;
+  optional_delete_ctor_base &
+  operator=(const optional_delete_ctor_base &) = default;
+  optional_delete_ctor_base &
+  operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_ctor_base<T, false, false> {
+  optional_delete_ctor_base() = default;
+  optional_delete_ctor_base(const optional_delete_ctor_base &) = delete;
+  optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = delete;
+  optional_delete_ctor_base &
+  operator=(const optional_delete_ctor_base &) = default;
+  optional_delete_ctor_base &
+  operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+// optional_delete_assign_base will conditionally delete copy and move
+// constructors depending on whether T is copy/move constructible + assignable
+template <class T,
+          bool EnableCopy = (std::is_copy_constructible<T>::value &&
+                             std::is_copy_assignable<T>::value),
+          bool EnableMove = (std::is_move_constructible<T>::value &&
+                             std::is_move_assignable<T>::value)>
+struct optional_delete_assign_base {
+  optional_delete_assign_base() = default;
+  optional_delete_assign_base(const optional_delete_assign_base &) = default;
+  optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+      default;
+  optional_delete_assign_base &
+  operator=(const optional_delete_assign_base &) = default;
+  optional_delete_assign_base &
+  operator=(optional_delete_assign_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_assign_base<T, true, false> {
+  optional_delete_assign_base() = default;
+  optional_delete_assign_base(const optional_delete_assign_base &) = default;
+  optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+      default;
+  optional_delete_assign_base &
+  operator=(const optional_delete_assign_base &) = default;
+  optional_delete_assign_base &
+  operator=(optional_delete_assign_base &&) noexcept = delete;
+};
+
+template <class T> struct optional_delete_assign_base<T, false, true> {
+  optional_delete_assign_base() = default;
+  optional_delete_assign_base(const optional_delete_assign_base &) = default;
+  optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+      default;
+  optional_delete_assign_base &
+  operator=(const optional_delete_assign_base &) = delete;
+  optional_delete_assign_base &
+  operator=(optional_delete_assign_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_assign_base<T, false, false> {
+  optional_delete_assign_base() = default;
+  optional_delete_assign_base(const optional_delete_assign_base &) = default;
+  optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+      default;
+  optional_delete_assign_base &
+  operator=(const optional_delete_assign_base &) = delete;
+  optional_delete_assign_base &
+  operator=(optional_delete_assign_base &&) noexcept = delete;
+};
+
+} // namespace detail
+
+/// A tag type to represent an empty optional
+struct nullopt_t {
+  struct do_not_use {};
+  constexpr explicit nullopt_t(do_not_use, do_not_use) noexcept {}
+};
+/// Represents an empty optional
+static constexpr nullopt_t nullopt{nullopt_t::do_not_use{},
+                                   nullopt_t::do_not_use{}};
+
+class bad_optional_access : public std::exception {
+public:
+  bad_optional_access() = default;
+  const char *what() const noexcept { return "Optional has no value"; }
+};
+
+/// An optional object is an object that contains the storage for another
+/// object and manages the lifetime of this contained object, if any. The
+/// contained object may be initialized after the optional object has been
+/// initialized, and may be destroyed before the optional object has been
+/// destroyed. The initialization state of the contained object is tracked by
+/// the optional object.
+template <class T>
+class optional : private detail::optional_move_assign_base<T>,
+                 private detail::optional_delete_ctor_base<T>,
+                 private detail::optional_delete_assign_base<T> {
+  using base = detail::optional_move_assign_base<T>;
+
+  static_assert(!std::is_same<T, in_place_t>::value,
+                "instantiation of optional with in_place_t is ill-formed");
+  static_assert(!std::is_same<detail::decay_t<T>, nullopt_t>::value,
+                "instantiation of optional with nullopt_t is ill-formed");
+
+public:
+// The different versions for C++14 and 11 are needed because deduced return
+// types are not SFINAE-safe. This provides better support for things like
+// generic lambdas. C.f.
+// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0826r0.html
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) &&               \
+    !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+  /// Carries out some operation which returns an optional on the stored
+  /// object if there is one.
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) & {
+    using result = detail::invoke_result_t<F, T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) && {
+    using result = detail::invoke_result_t<F, T &&>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : result(nullopt);
+  }
+
+  template <class F> constexpr auto and_then(F &&f) const & {
+    using result = detail::invoke_result_t<F, const T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F> constexpr auto and_then(F &&f) const && {
+    using result = detail::invoke_result_t<F, const T &&>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : result(nullopt);
+  }
+#endif
+#else
+  /// Carries out some operation which returns an optional on the stored
+  /// object if there is one.
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &> and_then(F &&f) & {
+    using result = detail::invoke_result_t<F, T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &&> and_then(F &&f) && {
+    using result = detail::invoke_result_t<F, T &&>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : result(nullopt);
+  }
+
+  template <class F>
+  constexpr detail::invoke_result_t<F, const T &> and_then(F &&f) const & {
+    using result = detail::invoke_result_t<F, const T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F>
+  constexpr detail::invoke_result_t<F, const T &&> and_then(F &&f) const && {
+    using result = detail::invoke_result_t<F, const T &&>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : result(nullopt);
+  }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) &&               \
+    !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+  /// Carries out some operation on the stored object if there is one.
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) & {
+    return optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) && {
+    return optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+
+  template <class F> constexpr auto map(F &&f) const & {
+    return optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F> constexpr auto map(F &&f) const && {
+    return optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#else
+  /// Carries out some operation on the stored object if there is one.
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional &>(),
+                                             std::declval<F &&>()))
+  map(F &&f) & {
+    return optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional &&>(),
+                                             std::declval<F &&>()))
+  map(F &&f) && {
+    return optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+
+  template <class F>
+  constexpr decltype(optional_map_impl(std::declval<const optional &>(),
+                              std::declval<F &&>()))
+  map(F &&f) const & {
+    return optional_map_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F>
+  constexpr decltype(optional_map_impl(std::declval<const optional &&>(),
+                              std::declval<F &&>()))
+  map(F &&f) const && {
+    return optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) &&               \
+    !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+  /// Carries out some operation on the stored object if there is one.
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) & {
+    return optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) && {
+    return optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+
+  template <class F> constexpr auto transform(F&& f) const & {
+    return optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F> constexpr auto transform(F&& f) const && {
+    return optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#else
+  /// Carries out some operation on the stored object if there is one.
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional&>(),
+    std::declval<F&&>()))
+    transform(F&& f) & {
+    return optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional&&>(),
+    std::declval<F&&>()))
+    transform(F&& f) && {
+    return optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+
+  template <class F>
+  constexpr decltype(optional_map_impl(std::declval<const optional&>(),
+    std::declval<F&&>()))
+    transform(F&& f) const & {
+    return optional_map_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F>
+  constexpr decltype(optional_map_impl(std::declval<const optional&&>(),
+    std::declval<F&&>()))
+    transform(F&& f) const && {
+    return optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+#endif
+
+  /// Calls `f` if the optional is empty
+  template <class F, detail::enable_if_ret_void<F> * = nullptr>
+  optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+    if (has_value())
+      return *this;
+
+    std::forward<F>(f)();
+    return nullopt;
+  }
+
+  template <class F, detail::disable_if_ret_void<F> * = nullptr>
+  optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+    return has_value() ? *this : std::forward<F>(f)();
+  }
+
+  template <class F, detail::enable_if_ret_void<F> * = nullptr>
+  optional<T> or_else(F &&f) && {
+    if (has_value())
+      return std::move(*this);
+
+    std::forward<F>(f)();
+    return nullopt;
+  }
+
+  template <class F, detail::disable_if_ret_void<F> * = nullptr>
+  optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) && {
+    return has_value() ? std::move(*this) : std::forward<F>(f)();
+  }
+
+  template <class F, detail::enable_if_ret_void<F> * = nullptr>
+  optional<T> or_else(F &&f) const & {
+    if (has_value())
+      return *this;
+
+    std::forward<F>(f)();
+    return nullopt;
+  }
+
+  template <class F, detail::disable_if_ret_void<F> * = nullptr>
+  optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) const & {
+    return has_value() ? *this : std::forward<F>(f)();
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F, detail::enable_if_ret_void<F> * = nullptr>
+  optional<T> or_else(F &&f) const && {
+    if (has_value())
+      return std::move(*this);
+
+    std::forward<F>(f)();
+    return nullopt;
+  }
+
+  template <class F, detail::disable_if_ret_void<F> * = nullptr>
+  optional<T> or_else(F &&f) const && {
+    return has_value() ? std::move(*this) : std::forward<F>(f)();
+  }
+#endif
+
+  /// Maps the stored value with `f` if there is one, otherwise returns `u`.
+  template <class F, class U> U map_or(F &&f, U &&u) & {
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : std::forward<U>(u);
+  }
+
+  template <class F, class U> U map_or(F &&f, U &&u) && {
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : std::forward<U>(u);
+  }
+
+  template <class F, class U> U map_or(F &&f, U &&u) const & {
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : std::forward<U>(u);
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F, class U> U map_or(F &&f, U &&u) const && {
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : std::forward<U>(u);
+  }
+#endif
+
+  /// Maps the stored value with `f` if there is one, otherwise calls
+  /// `u` and returns the result.
+  template <class F, class U>
+  detail::invoke_result_t<U> map_or_else(F &&f, U &&u) & {
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : std::forward<U>(u)();
+  }
+
+  template <class F, class U>
+  detail::invoke_result_t<U> map_or_else(F &&f, U &&u) && {
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : std::forward<U>(u)();
+  }
+
+  template <class F, class U>
+  detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const & {
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : std::forward<U>(u)();
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F, class U>
+  detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const && {
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : std::forward<U>(u)();
+  }
+#endif
+
+  /// Returns `u` if `*this` has a value, otherwise an empty optional.
+  template <class U>
+  constexpr optional<typename std::decay<U>::type> conjunction(U &&u) const {
+    using result = optional<detail::decay_t<U>>;
+    return has_value() ? result{u} : result{nullopt};
+  }
+
+  /// Returns `rhs` if `*this` is empty, otherwise the current value.
+  TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) & {
+    return has_value() ? *this : rhs;
+  }
+
+  constexpr optional disjunction(const optional &rhs) const & {
+    return has_value() ? *this : rhs;
+  }
+
+  TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) && {
+    return has_value() ? std::move(*this) : rhs;
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  constexpr optional disjunction(const optional &rhs) const && {
+    return has_value() ? std::move(*this) : rhs;
+  }
+#endif
+
+  TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) & {
+    return has_value() ? *this : std::move(rhs);
+  }
+
+  constexpr optional disjunction(optional &&rhs) const & {
+    return has_value() ? *this : std::move(rhs);
+  }
+
+  TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) && {
+    return has_value() ? std::move(*this) : std::move(rhs);
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  constexpr optional disjunction(optional &&rhs) const && {
+    return has_value() ? std::move(*this) : std::move(rhs);
+  }
+#endif
+
+  /// Takes the value out of the optional, leaving it empty
+  optional take() {
+    optional ret = std::move(*this);
+    reset();
+    return ret;
+  }
+
+  using value_type = T;
+
+  /// Constructs an optional that does not contain a value.
+  constexpr optional() noexcept = default;
+
+  constexpr optional(nullopt_t) noexcept {}
+
+  /// Copy constructor
+  ///
+  /// If `rhs` contains a value, the stored value is direct-initialized with
+  /// it. Otherwise, the constructed optional is empty.
+  TL_OPTIONAL_11_CONSTEXPR optional(const optional &rhs) = default;
+
+  /// Move constructor
+  ///
+  /// If `rhs` contains a value, the stored value is direct-initialized with
+  /// it. Otherwise, the constructed optional is empty.
+  TL_OPTIONAL_11_CONSTEXPR optional(optional &&rhs) = default;
+
+  /// Constructs the stored value in-place using the given arguments.
+ template <class... Args>
+  constexpr explicit optional(
+      detail::enable_if_t<std::is_constructible<T, Args...>::value, in_place_t>,
+      Args &&... args)
+      : base(in_place, std::forward<Args>(args)...) {}
+
+  template <class U, class... Args>
+  TL_OPTIONAL_11_CONSTEXPR explicit optional(
+      detail::enable_if_t<std::is_constructible<T, std::initializer_list<U> &,
+                                                Args &&...>::value,
+                          in_place_t>,
+      std::initializer_list<U> il, Args &&... args) {
+    this->construct(il, std::forward<Args>(args)...);
+  }
+
+  /// Constructs the stored value with `u`.
+  template <
+      class U = T,
+      detail::enable_if_t<std::is_convertible<U &&, T>::value> * = nullptr,
+      detail::enable_forward_value<T, U> * = nullptr>
+  constexpr optional(U &&u) : base(in_place, std::forward<U>(u)) {}
+
+  template <
+      class U = T,
+      detail::enable_if_t<!std::is_convertible<U &&, T>::value> * = nullptr,
+      detail::enable_forward_value<T, U> * = nullptr>
+  constexpr explicit optional(U &&u) : base(in_place, std::forward<U>(u)) {}
+
+  /// Converting copy constructor.
+  template <
+      class U, detail::enable_from_other<T, U, const U &> * = nullptr,
+      detail::enable_if_t<std::is_convertible<const U &, T>::value> * = nullptr>
+  optional(const optional<U> &rhs) {
+    if (rhs.has_value()) {
+      this->construct(*rhs);
+    }
+  }
+
+  template <class U, detail::enable_from_other<T, U, const U &> * = nullptr,
+            detail::enable_if_t<!std::is_convertible<const U &, T>::value> * =
+                nullptr>
+  explicit optional(const optional<U> &rhs) {
+    if (rhs.has_value()) {
+      this->construct(*rhs);
+    }
+  }
+
+  /// Converting move constructor.
+  template <
+      class U, detail::enable_from_other<T, U, U &&> * = nullptr,
+      detail::enable_if_t<std::is_convertible<U &&, T>::value> * = nullptr>
+  optional(optional<U> &&rhs) {
+    if (rhs.has_value()) {
+      this->construct(std::move(*rhs));
+    }
+  }
+
+  template <
+      class U, detail::enable_from_other<T, U, U &&> * = nullptr,
+      detail::enable_if_t<!std::is_convertible<U &&, T>::value> * = nullptr>
+  explicit optional(optional<U> &&rhs) {
+    if (rhs.has_value()) {
+      this->construct(std::move(*rhs));
+    }
+  }
+
+  /// Destroys the stored value if there is one.
+  ~optional() = default;
+
+  /// Assignment to empty.
+  ///
+  /// Destroys the current value if there is one.
+  optional &operator=(nullopt_t) noexcept {
+    if (has_value()) {
+      this->m_value.~T();
+      this->m_has_value = false;
+    }
+
+    return *this;
+  }
+
+  /// Copy assignment.
+  ///
+  /// Copies the value from `rhs` if there is one. Otherwise resets the stored
+  /// value in `*this`.
+  optional &operator=(const optional &rhs) = default;
+
+  /// Move assignment.
+  ///
+  /// Moves the value from `rhs` if there is one. Otherwise resets the stored
+  /// value in `*this`.
+  optional &operator=(optional &&rhs) = default;
+
+  /// Assigns the stored value from `u`, destroying the old value if there was
+  /// one.
+  template <class U = T, detail::enable_assign_forward<T, U> * = nullptr>
+  optional &operator=(U &&u) {
+    if (has_value()) {
+      this->m_value = std::forward<U>(u);
+    } else {
+      this->construct(std::forward<U>(u));
+    }
+
+    return *this;
+  }
+
+  /// Converting copy assignment operator.
+  ///
+  /// Copies the value from `rhs` if there is one. Otherwise resets the stored
+  /// value in `*this`.
+  template <class U,
+            detail::enable_assign_from_other<T, U, const U &> * = nullptr>
+  optional &operator=(const optional<U> &rhs) {
+    if (has_value()) {
+      if (rhs.has_value()) {
+        this->m_value = *rhs;
+      } else {
+        this->hard_reset();
+      }
+    }
+
+    if (rhs.has_value()) {
+      this->construct(*rhs);
+    }
+
+    return *this;
+  }
+
+  // TODO check exception guarantee
+  /// Converting move assignment operator.
+  ///
+  /// Moves the value from `rhs` if there is one. Otherwise resets the stored
+  /// value in `*this`.
+  template <class U, detail::enable_assign_from_other<T, U, U> * = nullptr>
+  optional &operator=(optional<U> &&rhs) {
+    if (has_value()) {
+      if (rhs.has_value()) {
+        this->m_value = std::move(*rhs);
+      } else {
+        this->hard_reset();
+      }
+    }
+
+    if (rhs.has_value()) {
+      this->construct(std::move(*rhs));
+    }
+
+    return *this;
+  }
+
+  /// Constructs the value in-place, destroying the current one if there is
+  /// one.
+  template <class... Args> T &emplace(Args &&... args) {
+    static_assert(std::is_constructible<T, Args &&...>::value,
+                  "T must be constructible with Args");
+
+    *this = nullopt;
+    this->construct(std::forward<Args>(args)...);
+    return value();
+  }
+
+  template <class U, class... Args>
+  detail::enable_if_t<
+      std::is_constructible<T, std::initializer_list<U> &, Args &&...>::value,
+      T &>
+  emplace(std::initializer_list<U> il, Args &&... args) {
+    *this = nullopt;
+    this->construct(il, std::forward<Args>(args)...);
+    return value();    
+  }
+
+  /// Swaps this optional with the other.
+  ///
+  /// If neither optionals have a value, nothing happens.
+  /// If both have a value, the values are swapped.
+  /// If one has a value, it is moved to the other and the movee is left
+  /// valueless.
+  void
+  swap(optional &rhs) noexcept(std::is_nothrow_move_constructible<T>::value
+                                   &&detail::is_nothrow_swappable<T>::value) {
+    using std::swap;
+    if (has_value()) {
+      if (rhs.has_value()) {
+        swap(**this, *rhs);
+      } else {
+        new (std::addressof(rhs.m_value)) T(std::move(this->m_value));
+        this->m_value.T::~T();
+      }
+    } else if (rhs.has_value()) {
+      new (std::addressof(this->m_value)) T(std::move(rhs.m_value));
+      rhs.m_value.T::~T();
+    }
+    swap(this->m_has_value, rhs.m_has_value);
+  }
+
+  /// Returns a pointer to the stored value
+  constexpr const T *operator->() const {
+    return std::addressof(this->m_value);
+  }
+
+  TL_OPTIONAL_11_CONSTEXPR T *operator->() {
+    return std::addressof(this->m_value);
+  }
+
+  /// Returns the stored value
+  TL_OPTIONAL_11_CONSTEXPR T &operator*() & { return this->m_value; }
+
+  constexpr const T &operator*() const & { return this->m_value; }
+
+  TL_OPTIONAL_11_CONSTEXPR T &&operator*() && {
+    return std::move(this->m_value);
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  constexpr const T &&operator*() const && { return std::move(this->m_value); }
+#endif
+
+  /// Returns whether or not the optional has a value
+  constexpr bool has_value() const noexcept { return this->m_has_value; }
+
+  constexpr explicit operator bool() const noexcept {
+    return this->m_has_value;
+  }
+
+  /// Returns the contained value if there is one, otherwise throws bad_optional_access
+  TL_OPTIONAL_11_CONSTEXPR T &value() & {
+    if (has_value())
+      return this->m_value;
+    throw bad_optional_access();
+  }
+  TL_OPTIONAL_11_CONSTEXPR const T &value() const & {
+    if (has_value())
+      return this->m_value;
+    throw bad_optional_access();
+  }
+  TL_OPTIONAL_11_CONSTEXPR T &&value() && {
+    if (has_value())
+      return std::move(this->m_value);
+    throw bad_optional_access();
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  TL_OPTIONAL_11_CONSTEXPR const T &&value() const && {
+    if (has_value())
+      return std::move(this->m_value);
+    throw bad_optional_access();
+  }
+#endif
+
+  /// Returns the stored value if there is one, otherwise returns `u`
+  template <class U> constexpr T value_or(U &&u) const & {
+    static_assert(std::is_copy_constructible<T>::value &&
+                      std::is_convertible<U &&, T>::value,
+                  "T must be copy constructible and convertible from U");
+    return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+  }
+
+  template <class U> TL_OPTIONAL_11_CONSTEXPR T value_or(U &&u) && {
+    static_assert(std::is_move_constructible<T>::value &&
+                      std::is_convertible<U &&, T>::value,
+                  "T must be move constructible and convertible from U");
+    return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+  }
+
+  /// Destroys the stored value if one exists, making the optional empty
+  void reset() noexcept {
+    if (has_value()) {
+      this->m_value.~T();
+      this->m_has_value = false;
+    }
+  }
+}; // namespace tl
+
+/// Compares two optional objects
+template <class T, class U>
+inline constexpr bool operator==(const optional<T> &lhs,
+                                 const optional<U> &rhs) {
+  return lhs.has_value() == rhs.has_value() &&
+         (!lhs.has_value() || *lhs == *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator!=(const optional<T> &lhs,
+                                 const optional<U> &rhs) {
+  return lhs.has_value() != rhs.has_value() ||
+         (lhs.has_value() && *lhs != *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator<(const optional<T> &lhs,
+                                const optional<U> &rhs) {
+  return rhs.has_value() && (!lhs.has_value() || *lhs < *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator>(const optional<T> &lhs,
+                                const optional<U> &rhs) {
+  return lhs.has_value() && (!rhs.has_value() || *lhs > *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator<=(const optional<T> &lhs,
+                                 const optional<U> &rhs) {
+  return !lhs.has_value() || (rhs.has_value() && *lhs <= *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator>=(const optional<T> &lhs,
+                                 const optional<U> &rhs) {
+  return !rhs.has_value() || (lhs.has_value() && *lhs >= *rhs);
+}
+
+/// Compares an optional to a `nullopt`
+template <class T>
+inline constexpr bool operator==(const optional<T> &lhs, nullopt_t) noexcept {
+  return !lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator==(nullopt_t, const optional<T> &rhs) noexcept {
+  return !rhs.has_value();
+}
+template <class T>
+inline constexpr bool operator!=(const optional<T> &lhs, nullopt_t) noexcept {
+  return lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator!=(nullopt_t, const optional<T> &rhs) noexcept {
+  return rhs.has_value();
+}
+template <class T>
+inline constexpr bool operator<(const optional<T> &, nullopt_t) noexcept {
+  return false;
+}
+template <class T>
+inline constexpr bool operator<(nullopt_t, const optional<T> &rhs) noexcept {
+  return rhs.has_value();
+}
+template <class T>
+inline constexpr bool operator<=(const optional<T> &lhs, nullopt_t) noexcept {
+  return !lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator<=(nullopt_t, const optional<T> &) noexcept {
+  return true;
+}
+template <class T>
+inline constexpr bool operator>(const optional<T> &lhs, nullopt_t) noexcept {
+  return lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator>(nullopt_t, const optional<T> &) noexcept {
+  return false;
+}
+template <class T>
+inline constexpr bool operator>=(const optional<T> &, nullopt_t) noexcept {
+  return true;
+}
+template <class T>
+inline constexpr bool operator>=(nullopt_t, const optional<T> &rhs) noexcept {
+  return !rhs.has_value();
+}
+
+/// Compares the optional with a value.
+template <class T, class U>
+inline constexpr bool operator==(const optional<T> &lhs, const U &rhs) {
+  return lhs.has_value() ? *lhs == rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator==(const U &lhs, const optional<T> &rhs) {
+  return rhs.has_value() ? lhs == *rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator!=(const optional<T> &lhs, const U &rhs) {
+  return lhs.has_value() ? *lhs != rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator!=(const U &lhs, const optional<T> &rhs) {
+  return rhs.has_value() ? lhs != *rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator<(const optional<T> &lhs, const U &rhs) {
+  return lhs.has_value() ? *lhs < rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator<(const U &lhs, const optional<T> &rhs) {
+  return rhs.has_value() ? lhs < *rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator<=(const optional<T> &lhs, const U &rhs) {
+  return lhs.has_value() ? *lhs <= rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator<=(const U &lhs, const optional<T> &rhs) {
+  return rhs.has_value() ? lhs <= *rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator>(const optional<T> &lhs, const U &rhs) {
+  return lhs.has_value() ? *lhs > rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator>(const U &lhs, const optional<T> &rhs) {
+  return rhs.has_value() ? lhs > *rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator>=(const optional<T> &lhs, const U &rhs) {
+  return lhs.has_value() ? *lhs >= rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator>=(const U &lhs, const optional<T> &rhs) {
+  return rhs.has_value() ? lhs >= *rhs : true;
+}
+
+template <class T,
+          detail::enable_if_t<std::is_move_constructible<T>::value> * = nullptr,
+          detail::enable_if_t<detail::is_swappable<T>::value> * = nullptr>
+void swap(optional<T> &lhs,
+          optional<T> &rhs) noexcept(noexcept(lhs.swap(rhs))) {
+  return lhs.swap(rhs);
+}
+
+namespace detail {
+struct i_am_secret {};
+} // namespace detail
+
+template <class T = detail::i_am_secret, class U,
+          class Ret =
+              detail::conditional_t<std::is_same<T, detail::i_am_secret>::value,
+                                    detail::decay_t<U>, T>>
+inline constexpr optional<Ret> make_optional(U &&v) {
+  return optional<Ret>(std::forward<U>(v));
+}
+
+template <class T, class... Args>
+inline constexpr optional<T> make_optional(Args &&... args) {
+  return optional<T>(in_place, std::forward<Args>(args)...);
+}
+template <class T, class U, class... Args>
+inline constexpr optional<T> make_optional(std::initializer_list<U> il,
+                                           Args &&... args) {
+  return optional<T>(in_place, il, std::forward<Args>(args)...);
+}
+
+#if __cplusplus >= 201703L
+template <class T> optional(T)->optional<T>;
+#endif
+
+/// \exclude
+namespace detail {
+#ifdef TL_OPTIONAL_CXX14
+template <class Opt, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Opt>())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto optional_map_impl(Opt &&opt, F &&f) {
+  return opt.has_value()
+             ? detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt))
+             : optional<Ret>(nullopt);
+}
+
+template <class Opt, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Opt>())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto optional_map_impl(Opt &&opt, F &&f) {
+  if (opt.has_value()) {
+    detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt));
+    return make_optional(monostate{});
+  }
+
+  return optional<monostate>(nullopt);
+}
+#else
+template <class Opt, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Opt>())),
+          detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+
+constexpr auto optional_map_impl(Opt &&opt, F &&f) -> optional<Ret> {
+  return opt.has_value()
+             ? detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt))
+             : optional<Ret>(nullopt);
+}
+
+template <class Opt, class F,
+          class Ret = decltype(detail::invoke(std::declval<F>(),
+                                              *std::declval<Opt>())),
+          detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+
+auto optional_map_impl(Opt &&opt, F &&f) -> optional<monostate> {
+  if (opt.has_value()) {
+    detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt));
+    return monostate{};
+  }
+
+  return nullopt;
+}
+#endif
+} // namespace detail
+
+/// Specialization for when `T` is a reference. `optional<T&>` acts similarly
+/// to a `T*`, but provides more operations and shows intent more clearly.
+template <class T> class optional<T &> {
+public:
+// The different versions for C++14 and 11 are needed because deduced return
+// types are not SFINAE-safe. This provides better support for things like
+// generic lambdas. C.f.
+// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0826r0.html
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) &&               \
+    !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+
+  /// Carries out some operation which returns an optional on the stored
+  /// object if there is one.
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) & {
+    using result = detail::invoke_result_t<F, T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) && {
+    using result = detail::invoke_result_t<F, T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+  template <class F> constexpr auto and_then(F &&f) const & {
+    using result = detail::invoke_result_t<F, const T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F> constexpr auto and_then(F &&f) const && {
+    using result = detail::invoke_result_t<F, const T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+#endif
+#else
+  /// Carries out some operation which returns an optional on the stored
+  /// object if there is one.
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &> and_then(F &&f) & {
+    using result = detail::invoke_result_t<F, T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &> and_then(F &&f) && {
+    using result = detail::invoke_result_t<F, T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+  template <class F>
+  constexpr detail::invoke_result_t<F, const T &> and_then(F &&f) const & {
+    using result = detail::invoke_result_t<F, const T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F>
+  constexpr detail::invoke_result_t<F, const T &> and_then(F &&f) const && {
+    using result = detail::invoke_result_t<F, const T &>;
+    static_assert(detail::is_optional<result>::value,
+                  "F must return an optional");
+
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : result(nullopt);
+  }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) &&               \
+    !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+  /// Carries out some operation on the stored object if there is one.
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) & {
+    return detail::optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) && {
+    return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+
+  template <class F> constexpr auto map(F &&f) const & {
+    return detail::optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F> constexpr auto map(F &&f) const && {
+    return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#else
+  /// Carries out some operation on the stored object if there is one.
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional &>(),
+                                                     std::declval<F &&>()))
+  map(F &&f) & {
+    return detail::optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional &&>(),
+                                                     std::declval<F &&>()))
+  map(F &&f) && {
+    return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+
+  template <class F>
+  constexpr decltype(detail::optional_map_impl(std::declval<const optional &>(),
+                                      std::declval<F &&>()))
+  map(F &&f) const & {
+    return detail::optional_map_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F>
+  constexpr decltype(detail::optional_map_impl(std::declval<const optional &&>(),
+                                      std::declval<F &&>()))
+  map(F &&f) const && {
+    return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) &&               \
+    !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+  /// Carries out some operation on the stored object if there is one.
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) & {
+    return detail::optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) && {
+    return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+
+  template <class F> constexpr auto transform(F&& f) const & {
+    return detail::optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  template <class F> constexpr auto transform(F&& f) const && {
+    return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#else
+  /// Carries out some operation on the stored object if there is one.
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional&>(),
+    std::declval<F&&>()))
+    transform(F&& f) & {
+    return detail::optional_map_impl(*this, std::forward<F>(f));
+  }
+
+  /// \group map
+  /// \synopsis template <class F> auto transform(F &&f) &&;
+  template <class F>
+  TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional&&>(),
+    std::declval<F&&>()))
+    transform(F&& f) && {
+    return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+
+  template <class F>
+  constexpr decltype(detail::optional_map_impl(std::declval<const optional&>(),
+    std::declval<F&&>()))
+    transform(F&& f) const & {
+    return detail::optional_map_impl(*this, std::forward<F>(f));
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F>
+  constexpr decltype(detail::optional_map_impl(std::declval<const optional&&>(),
+    std::declval<F&&>()))
+    transform(F&& f) const && {
+    return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+  }
+#endif
+#endif
+
+  /// Calls `f` if the optional is empty
+  template <class F, detail::enable_if_ret_void<F> * = nullptr>
+  optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+    if (has_value())
+      return *this;
+
+    std::forward<F>(f)();
+    return nullopt;
+  }
+
+  template <class F, detail::disable_if_ret_void<F> * = nullptr>
+  optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+    return has_value() ? *this : std::forward<F>(f)();
+  }
+
+  template <class F, detail::enable_if_ret_void<F> * = nullptr>
+  optional<T> or_else(F &&f) && {
+    if (has_value())
+      return std::move(*this);
+
+    std::forward<F>(f)();
+    return nullopt;
+  }
+
+  template <class F, detail::disable_if_ret_void<F> * = nullptr>
+  optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) && {
+    return has_value() ? std::move(*this) : std::forward<F>(f)();
+  }
+
+  template <class F, detail::enable_if_ret_void<F> * = nullptr>
+  optional<T> or_else(F &&f) const & {
+    if (has_value())
+      return *this;
+
+    std::forward<F>(f)();
+    return nullopt;
+  }
+
+  template <class F, detail::disable_if_ret_void<F> * = nullptr>
+  optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) const & {
+    return has_value() ? *this : std::forward<F>(f)();
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F, detail::enable_if_ret_void<F> * = nullptr>
+  optional<T> or_else(F &&f) const && {
+    if (has_value())
+      return std::move(*this);
+
+    std::forward<F>(f)();
+    return nullopt;
+  }
+
+  template <class F, detail::disable_if_ret_void<F> * = nullptr>
+  optional<T> or_else(F &&f) const && {
+    return has_value() ? std::move(*this) : std::forward<F>(f)();
+  }
+#endif
+
+  /// Maps the stored value with `f` if there is one, otherwise returns `u`
+  template <class F, class U> U map_or(F &&f, U &&u) & {
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : std::forward<U>(u);
+  }
+
+  template <class F, class U> U map_or(F &&f, U &&u) && {
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : std::forward<U>(u);
+  }
+
+  template <class F, class U> U map_or(F &&f, U &&u) const & {
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : std::forward<U>(u);
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F, class U> U map_or(F &&f, U &&u) const && {
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : std::forward<U>(u);
+  }
+#endif
+
+  /// Maps the stored value with `f` if there is one, otherwise calls
+  /// `u` and returns the result.
+  template <class F, class U>
+  detail::invoke_result_t<U> map_or_else(F &&f, U &&u) & {
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : std::forward<U>(u)();
+  }
+
+  template <class F, class U>
+  detail::invoke_result_t<U> map_or_else(F &&f, U &&u) && {
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : std::forward<U>(u)();
+  }
+
+  template <class F, class U>
+  detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const & {
+    return has_value() ? detail::invoke(std::forward<F>(f), **this)
+                       : std::forward<U>(u)();
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  template <class F, class U>
+  detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const && {
+    return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+                       : std::forward<U>(u)();
+  }
+#endif
+
+  /// Returns `u` if `*this` has a value, otherwise an empty optional.
+  template <class U>
+  constexpr optional<typename std::decay<U>::type> conjunction(U &&u) const {
+    using result = optional<detail::decay_t<U>>;
+    return has_value() ? result{u} : result{nullopt};
+  }
+
+  /// Returns `rhs` if `*this` is empty, otherwise the current value.
+  TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) & {
+    return has_value() ? *this : rhs;
+  }
+
+  constexpr optional disjunction(const optional &rhs) const & {
+    return has_value() ? *this : rhs;
+  }
+
+  TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) && {
+    return has_value() ? std::move(*this) : rhs;
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  constexpr optional disjunction(const optional &rhs) const && {
+    return has_value() ? std::move(*this) : rhs;
+  }
+#endif
+
+  TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) & {
+    return has_value() ? *this : std::move(rhs);
+  }
+
+  constexpr optional disjunction(optional &&rhs) const & {
+    return has_value() ? *this : std::move(rhs);
+  }
+
+  TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) && {
+    return has_value() ? std::move(*this) : std::move(rhs);
+  }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+  constexpr optional disjunction(optional &&rhs) const && {
+    return has_value() ? std::move(*this) : std::move(rhs);
+  }
+#endif
+
+  /// Takes the value out of the optional, leaving it empty
+  optional take() {
+    optional ret = std::move(*this);
+    reset();
+    return ret;
+  }
+
+  using value_type = T &;
+
+  /// Constructs an optional that does not contain a value.
+  constexpr optional() noexcept : m_value(nullptr) {}
+
+  constexpr optional(nullopt_t) noexcept : m_value(nullptr) {}
+
+  /// Copy constructor
+  ///
+  /// If `rhs` contains a value, the stored value is direct-initialized with
+  /// it. Otherwise, the constructed optional is empty.
+  TL_OPTIONAL_11_CONSTEXPR optional(const optional &rhs) noexcept = default;
+
+  /// Move constructor
+  ///
+  /// If `rhs` contains a value, the stored value is direct-initialized with
+  /// it. Otherwise, the constructed optional is empty.
+  TL_OPTIONAL_11_CONSTEXPR optional(optional &&rhs) = default;
+
+  /// Constructs the stored value with `u`.
+  template <class U = T,
+            detail::enable_if_t<!detail::is_optional<detail::decay_t<U>>::value>
+                * = nullptr>
+  constexpr optional(U &&u)  noexcept : m_value(std::addressof(u)) {
+    static_assert(std::is_lvalue_reference<U>::value, "U must be an lvalue");
+  }
+
+  template <class U>
+  constexpr explicit optional(const optional<U> &rhs) noexcept : optional(*rhs) {}
+
+  /// No-op
+  ~optional() = default;
+
+  /// Assignment to empty.
+  ///
+  /// Destroys the current value if there is one.
+  optional &operator=(nullopt_t) noexcept {
+    m_value = nullptr;
+    return *this;
+  }
+
+  /// Copy assignment.
+  ///
+  /// Rebinds this optional to the referee of `rhs` if there is one. Otherwise
+  /// resets the stored value in `*this`.
+  optional &operator=(const optional &rhs) = default;
+
+  /// Rebinds this optional to `u`.
+  template <class U = T,
+            detail::enable_if_t<!detail::is_optional<detail::decay_t<U>>::value>
+                * = nullptr>
+  optional &operator=(U &&u) {
+    static_assert(std::is_lvalue_reference<U>::value, "U must be an lvalue");
+    m_value = std::addressof(u);
+    return *this;
+  }
+
+  /// Converting copy assignment operator.
+  ///
+  /// Rebinds this optional to the referee of `rhs` if there is one. Otherwise
+  /// resets the stored value in `*this`.
+  template <class U> optional &operator=(const optional<U> &rhs) noexcept {
+    m_value = std::addressof(rhs.value());
+    return *this;
+  }
+
+  /// Rebinds this optional to `u`.
+  template <class U = T,
+            detail::enable_if_t<!detail::is_optional<detail::decay_t<U>>::value>
+                * = nullptr>
+  optional &emplace(U &&u) noexcept {
+    return *this = std::forward<U>(u);
+  }
+
+  void swap(optional &rhs) noexcept { std::swap(m_value, rhs.m_value); }
+
+  /// Returns a pointer to the stored value
+  constexpr const T *operator->() const noexcept { return m_value; }
+
+  TL_OPTIONAL_11_CONSTEXPR T *operator->() noexcept { return m_value; }
+
+  /// Returns the stored value
+  TL_OPTIONAL_11_CONSTEXPR T &operator*() noexcept { return *m_value; }
+
+  constexpr const T &operator*() const noexcept { return *m_value; }
+
+  constexpr bool has_value() const noexcept { return m_value != nullptr; }
+
+  constexpr explicit operator bool() const noexcept {
+    return m_value != nullptr;
+  }
+
+  /// Returns the contained value if there is one, otherwise throws bad_optional_access
+  TL_OPTIONAL_11_CONSTEXPR T &value() {
+    if (has_value())
+      return *m_value;
+    throw bad_optional_access();
+  }
+  TL_OPTIONAL_11_CONSTEXPR const T &value() const {
+    if (has_value())
+      return *m_value;
+    throw bad_optional_access();
+  }
+
+  /// Returns the stored value if there is one, otherwise returns `u`
+  template <class U> constexpr T value_or(U &&u) const & noexcept {
+    static_assert(std::is_copy_constructible<T>::value &&
+                      std::is_convertible<U &&, T>::value,
+                  "T must be copy constructible and convertible from U");
+    return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+  }
+
+  /// \group value_or
+  template <class U> TL_OPTIONAL_11_CONSTEXPR T value_or(U &&u) && noexcept {
+    static_assert(std::is_move_constructible<T>::value &&
+                      std::is_convertible<U &&, T>::value,
+                  "T must be move constructible and convertible from U");
+    return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+  }
+
+  /// Destroys the stored value if one exists, making the optional empty
+  void reset() noexcept { m_value = nullptr; }
+
+private:
+  T *m_value;
+}; // namespace tl
+
+
+
+} // namespace tl
+
+namespace std {
+// TODO SFINAE
+template <class T> struct hash<tl::optional<T>> {
+  ::std::size_t operator()(const tl::optional<T> &o) const {
+    if (!o.has_value())
+      return 0;
+
+    return std::hash<tl::detail::remove_const_t<T>>()(*o);
+  }
+};
+} // namespace std
+
+#endif
diff --git a/lib/thirdparty/tabulate.hpp b/lib/thirdparty/tabulate.hpp
new file mode 100644 (file)
index 0000000..ef217aa
--- /dev/null
@@ -0,0 +1,9235 @@
+// Copyright 2016-2018 by Martin Moene
+//
+// https://github.com/martinmoene/variant-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_VARIANT_LITE_HPP
+#define NONSTD_VARIANT_LITE_HPP
+
+#define variant_lite_MAJOR 1
+#define variant_lite_MINOR 2
+#define variant_lite_PATCH 2
+
+#define variant_lite_VERSION                                                                       \
+  variant_STRINGIFY(variant_lite_MAJOR) "." variant_STRINGIFY(                                     \
+      variant_lite_MINOR) "." variant_STRINGIFY(variant_lite_PATCH)
+
+#define variant_STRINGIFY(x) variant_STRINGIFY_(x)
+#define variant_STRINGIFY_(x) #x
+
+// variant-lite configuration:
+
+#define variant_VARIANT_DEFAULT 0
+#define variant_VARIANT_NONSTD 1
+#define variant_VARIANT_STD 2
+
+#if !defined(variant_CONFIG_SELECT_VARIANT)
+#define variant_CONFIG_SELECT_VARIANT                                                              \
+  (variant_HAVE_STD_VARIANT ? variant_VARIANT_STD : variant_VARIANT_NONSTD)
+#endif
+
+#ifndef variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO
+#define variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO 0
+#endif
+
+#ifndef variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO
+#define variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO 0
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef variant_CONFIG_NO_EXCEPTIONS
+#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#define variant_CONFIG_NO_EXCEPTIONS 0
+#else
+#define variant_CONFIG_NO_EXCEPTIONS 1
+#endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef variant_CPLUSPLUS
+#if defined(_MSVC_LANG) && !defined(__clang__)
+#define variant_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
+#else
+#define variant_CPLUSPLUS __cplusplus
+#endif
+#endif
+
+#define variant_CPP98_OR_GREATER (variant_CPLUSPLUS >= 199711L)
+#define variant_CPP11_OR_GREATER (variant_CPLUSPLUS >= 201103L)
+#define variant_CPP11_OR_GREATER_ (variant_CPLUSPLUS >= 201103L)
+#define variant_CPP14_OR_GREATER (variant_CPLUSPLUS >= 201402L)
+#define variant_CPP17_OR_GREATER (variant_CPLUSPLUS >= 201703L)
+#define variant_CPP20_OR_GREATER (variant_CPLUSPLUS >= 202000L)
+
+// Use C++17 std::variant if available and requested:
+
+#if variant_CPP17_OR_GREATER && defined(__has_include)
+#if __has_include(<variant> )
+#define variant_HAVE_STD_VARIANT 1
+#else
+#define variant_HAVE_STD_VARIANT 0
+#endif
+#else
+#define variant_HAVE_STD_VARIANT 0
+#endif
+
+#define variant_USES_STD_VARIANT                                                                   \
+  ((variant_CONFIG_SELECT_VARIANT == variant_VARIANT_STD) ||                                       \
+   ((variant_CONFIG_SELECT_VARIANT == variant_VARIANT_DEFAULT) && variant_HAVE_STD_VARIANT))
+
+//
+// in_place: code duplicated in any-lite, expected-lite, optional-lite, value-ptr-lite,
+// variant-lite:
+//
+
+#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES
+#define nonstd_lite_HAVE_IN_PLACE_TYPES 1
+
+// C++17 std::in_place in <utility>:
+
+#if variant_CPP17_OR_GREATER
+
+#include <utility>
+
+namespace nonstd {
+
+using std::in_place;
+using std::in_place_index;
+using std::in_place_index_t;
+using std::in_place_t;
+using std::in_place_type;
+using std::in_place_type_t;
+
+#define nonstd_lite_in_place_t(T) std::in_place_t
+#define nonstd_lite_in_place_type_t(T) std::in_place_type_t<T>
+#define nonstd_lite_in_place_index_t(K) std::in_place_index_t<K>
+
+#define nonstd_lite_in_place(T)                                                                    \
+  std::in_place_t {}
+#define nonstd_lite_in_place_type(T)                                                               \
+  std::in_place_type_t<T> {}
+#define nonstd_lite_in_place_index(K)                                                              \
+  std::in_place_index_t<K> {}
+
+} // namespace nonstd
+
+#else // variant_CPP17_OR_GREATER
+
+#include <cstddef>
+
+namespace nonstd {
+namespace detail {
+
+template <class T> struct in_place_type_tag {};
+
+template <std::size_t K> struct in_place_index_tag {};
+
+} // namespace detail
+
+struct in_place_t {};
+
+template <class T>
+inline in_place_t in_place(detail::in_place_type_tag<T> = detail::in_place_type_tag<T>()) {
+  return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t in_place(detail::in_place_index_tag<K> = detail::in_place_index_tag<K>()) {
+  return in_place_t();
+}
+
+template <class T>
+inline in_place_t in_place_type(detail::in_place_type_tag<T> = detail::in_place_type_tag<T>()) {
+  return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t in_place_index(detail::in_place_index_tag<K> = detail::in_place_index_tag<K>()) {
+  return in_place_t();
+}
+
+// mimic templated typedef:
+
+#define nonstd_lite_in_place_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_type_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_index_t(K)                                                            \
+  nonstd::in_place_t (&)(nonstd::detail::in_place_index_tag<K>)
+
+#define nonstd_lite_in_place(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_type(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_index(K) nonstd::in_place_index<K>
+
+} // namespace nonstd
+
+#endif // variant_CPP17_OR_GREATER
+#endif // nonstd_lite_HAVE_IN_PLACE_TYPES
+
+//
+// Use C++17 std::variant:
+//
+
+#if variant_USES_STD_VARIANT
+
+#include <functional> // std::hash<>
+#include <variant>
+
+#if !variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO
+#define variant_size_V(T) nonstd::variant_size<T>::value
+#endif
+
+#if !variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO
+#define variant_alternative_T(K, T) typename nonstd::variant_alternative<K, T>::type
+#endif
+
+namespace nonstd {
+
+using std::bad_variant_access;
+using std::hash;
+using std::monostate;
+using std::variant;
+using std::variant_alternative;
+using std::variant_alternative_t;
+using std::variant_size;
+using std::variant_size_v;
+
+using std::get;
+using std::get_if;
+using std::holds_alternative;
+using std::visit;
+using std::operator==;
+using std::operator!=;
+using std::operator<;
+using std::operator<=;
+using std::operator>;
+using std::operator>=;
+using std::swap;
+
+constexpr auto variant_npos = std::variant_npos;
+} // namespace nonstd
+
+#else // variant_USES_STD_VARIANT
+
+#include <cstddef>
+#include <limits>
+#include <new>
+#include <utility>
+
+#if variant_CONFIG_NO_EXCEPTIONS
+#include <cassert>
+#else
+#include <stdexcept>
+#endif
+
+// variant-lite type and visitor argument count configuration (script/generate_header.py):
+
+#define variant_CONFIG_MAX_TYPE_COUNT 16
+#define variant_CONFIG_MAX_VISITOR_ARG_COUNT 5
+
+// variant-lite alignment configuration:
+
+#ifndef variant_CONFIG_MAX_ALIGN_HACK
+#define variant_CONFIG_MAX_ALIGN_HACK 0
+#endif
+
+#ifndef variant_CONFIG_ALIGN_AS
+// no default, used in #if defined()
+#endif
+
+#ifndef variant_CONFIG_ALIGN_AS_FALLBACK
+#define variant_CONFIG_ALIGN_AS_FALLBACK double
+#endif
+
+// half-open range [lo..hi):
+#define variant_BETWEEN(v, lo, hi) ((lo) <= (v) && (v) < (hi))
+
+// Compiler versions:
+//
+// MSVC++  6.0  _MSC_VER == 1200  variant_COMPILER_MSVC_VERSION ==  60  (Visual Studio 6.0)
+// MSVC++  7.0  _MSC_VER == 1300  variant_COMPILER_MSVC_VERSION ==  70  (Visual Studio .NET 2002)
+// MSVC++  7.1  _MSC_VER == 1310  variant_COMPILER_MSVC_VERSION ==  71  (Visual Studio .NET 2003)
+// MSVC++  8.0  _MSC_VER == 1400  variant_COMPILER_MSVC_VERSION ==  80  (Visual Studio 2005)
+// MSVC++  9.0  _MSC_VER == 1500  variant_COMPILER_MSVC_VERSION ==  90  (Visual Studio 2008)
+// MSVC++ 10.0  _MSC_VER == 1600  variant_COMPILER_MSVC_VERSION == 100  (Visual Studio 2010)
+// MSVC++ 11.0  _MSC_VER == 1700  variant_COMPILER_MSVC_VERSION == 110  (Visual Studio 2012)
+// MSVC++ 12.0  _MSC_VER == 1800  variant_COMPILER_MSVC_VERSION == 120  (Visual Studio 2013)
+// MSVC++ 14.0  _MSC_VER == 1900  variant_COMPILER_MSVC_VERSION == 140  (Visual Studio 2015)
+// MSVC++ 14.1  _MSC_VER >= 1910  variant_COMPILER_MSVC_VERSION == 141  (Visual Studio 2017)
+// MSVC++ 14.2  _MSC_VER >= 1920  variant_COMPILER_MSVC_VERSION == 142  (Visual Studio 2019)
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define variant_COMPILER_MSVC_VER (_MSC_VER)
+#define variant_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * (5 + (_MSC_VER < 1900)))
+#else
+#define variant_COMPILER_MSVC_VER 0
+#define variant_COMPILER_MSVC_VERSION 0
+#endif
+
+#define variant_COMPILER_VERSION(major, minor, patch) (10 * (10 * (major) + (minor)) + (patch))
+
+#if defined(__clang__)
+#define variant_COMPILER_CLANG_VERSION                                                             \
+  variant_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+#define variant_COMPILER_CLANG_VERSION 0
+#endif
+
+#if defined(__GNUC__) && !defined(__clang__)
+#define variant_COMPILER_GNUC_VERSION                                                              \
+  variant_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+#define variant_COMPILER_GNUC_VERSION 0
+#endif
+
+#if variant_BETWEEN(variant_COMPILER_MSVC_VER, 1300, 1900)
+#pragma warning(push)
+#pragma warning(disable : 4345) // initialization behavior changed
+#endif
+
+// Presence of language and library features:
+
+#define variant_HAVE(feature) (variant_HAVE_##feature)
+
+#ifdef _HAS_CPP0X
+#define variant_HAS_CPP0X _HAS_CPP0X
+#else
+#define variant_HAS_CPP0X 0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for variant-lite:
+
+#if variant_COMPILER_MSVC_VER >= 1900
+#undef variant_CPP11_OR_GREATER
+#define variant_CPP11_OR_GREATER 1
+#endif
+
+#define variant_CPP11_90 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1500)
+#define variant_CPP11_100 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1600)
+#define variant_CPP11_110 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1700)
+#define variant_CPP11_120 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1800)
+#define variant_CPP11_140 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1900)
+#define variant_CPP11_141 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1910)
+
+#define variant_CPP14_000 (variant_CPP14_OR_GREATER)
+#define variant_CPP17_000 (variant_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+#define variant_HAVE_CONSTEXPR_11 variant_CPP11_140
+#define variant_HAVE_INITIALIZER_LIST variant_CPP11_120
+#define variant_HAVE_NOEXCEPT variant_CPP11_140
+#define variant_HAVE_NULLPTR variant_CPP11_100
+#define variant_HAVE_OVERRIDE variant_CPP11_140
+
+// Presence of C++14 language features:
+
+#define variant_HAVE_CONSTEXPR_14 variant_CPP14_000
+
+// Presence of C++17 language features:
+
+// no flag
+
+// Presence of C++ library features:
+
+#define variant_HAVE_CONDITIONAL variant_CPP11_120
+#define variant_HAVE_REMOVE_CV variant_CPP11_120
+#define variant_HAVE_STD_ADD_POINTER variant_CPP11_90
+#define variant_HAVE_TYPE_TRAITS variant_CPP11_90
+
+#define variant_HAVE_TR1_TYPE_TRAITS (!!variant_COMPILER_GNUC_VERSION)
+#define variant_HAVE_TR1_ADD_POINTER (!!variant_COMPILER_GNUC_VERSION)
+
+// C++ feature usage:
+
+#if variant_HAVE_CONSTEXPR_11
+#define variant_constexpr constexpr
+#else
+#define variant_constexpr /*constexpr*/
+#endif
+
+#if variant_HAVE_CONSTEXPR_14
+#define variant_constexpr14 constexpr
+#else
+#define variant_constexpr14 /*constexpr*/
+#endif
+
+#if variant_HAVE_NOEXCEPT
+#define variant_noexcept noexcept
+#else
+#define variant_noexcept /*noexcept*/
+#endif
+
+#if variant_HAVE_NULLPTR
+#define variant_nullptr nullptr
+#else
+#define variant_nullptr NULL
+#endif
+
+#if variant_HAVE_OVERRIDE
+#define variant_override override
+#else
+#define variant_override /*override*/
+#endif
+
+// additional includes:
+
+#if variant_CPP11_OR_GREATER
+#include <functional> // std::hash
+#endif
+
+#if variant_HAVE_INITIALIZER_LIST
+#include <initializer_list>
+#endif
+
+#if variant_HAVE_TYPE_TRAITS
+#include <type_traits>
+#elif variant_HAVE_TR1_TYPE_TRAITS
+#include <tr1/type_traits>
+#endif
+
+// Method enabling
+
+#if variant_CPP11_OR_GREATER
+
+#define variant_REQUIRES_0(...)                                                                    \
+  template <bool B = (__VA_ARGS__), typename std::enable_if<B, int>::type = 0>
+
+#define variant_REQUIRES_T(...) , typename std::enable_if<(__VA_ARGS__), int>::type = 0
+
+#define variant_REQUIRES_R(R, ...) typename std::enable_if<(__VA_ARGS__), R>::type
+
+#define variant_REQUIRES_A(...) , typename std::enable_if<(__VA_ARGS__), void *>::type = nullptr
+
+#endif
+
+//
+// variant:
+//
+
+namespace nonstd {
+namespace variants {
+
+// C++11 emulation:
+
+namespace std11 {
+
+#if variant_HAVE_STD_ADD_POINTER
+
+using std::add_pointer;
+
+#elif variant_HAVE_TR1_ADD_POINTER
+
+using std::tr1::add_pointer;
+
+#else
+
+template <class T> struct remove_reference { typedef T type; };
+template <class T> struct remove_reference<T &> { typedef T type; };
+
+template <class T> struct add_pointer { typedef typename remove_reference<T>::type *type; };
+
+#endif // variant_HAVE_STD_ADD_POINTER
+
+#if variant_HAVE_REMOVE_CV
+
+using std::remove_cv;
+
+#else
+
+template <class T> struct remove_const { typedef T type; };
+template <class T> struct remove_const<const T> { typedef T type; };
+
+template <class T> struct remove_volatile { typedef T type; };
+template <class T> struct remove_volatile<volatile T> { typedef T type; };
+
+template <class T> struct remove_cv {
+  typedef typename remove_volatile<typename remove_const<T>::type>::type type;
+};
+
+#endif // variant_HAVE_REMOVE_CV
+
+#if variant_HAVE_CONDITIONAL
+
+using std::conditional;
+
+#else
+
+template <bool Cond, class Then, class Else> struct conditional;
+
+template <class Then, class Else> struct conditional<true, Then, Else> { typedef Then type; };
+
+template <class Then, class Else> struct conditional<false, Then, Else> { typedef Else type; };
+
+#endif // variant_HAVE_CONDITIONAL
+
+} // namespace std11
+
+/// type traits C++17:
+
+namespace std17 {
+
+#if variant_CPP17_OR_GREATER
+
+using std::is_nothrow_swappable;
+using std::is_swappable;
+
+#elif variant_CPP11_OR_GREATER
+
+namespace detail {
+
+using std::swap;
+
+struct is_swappable {
+  template <typename T, typename = decltype(swap(std::declval<T &>(), std::declval<T &>()))>
+  static std::true_type test(int);
+
+  template <typename> static std::false_type test(...);
+};
+
+struct is_nothrow_swappable {
+  // wrap noexcept(epr) in separate function as work-around for VC140 (VS2015):
+
+  template <typename T> static constexpr bool test() {
+    return noexcept(swap(std::declval<T &>(), std::declval<T &>()));
+  }
+
+  template <typename T> static auto test(int) -> std::integral_constant<bool, test<T>()> {}
+
+  template <typename> static std::false_type test(...);
+};
+
+} // namespace detail
+
+// is [nothow] swappable:
+
+template <typename T> struct is_swappable : decltype(detail::is_swappable::test<T>(0)) {};
+
+template <typename T>
+struct is_nothrow_swappable : decltype(detail::is_nothrow_swappable::test<T>(0)) {};
+
+#endif // variant_CPP17_OR_GREATER
+
+} // namespace std17
+
+// detail:
+
+namespace detail {
+
+// typelist:
+
+#define variant_TL1(T1) detail::typelist<T1, detail::nulltype>
+#define variant_TL2(T1, T2) detail::typelist<T1, variant_TL1(T2)>
+#define variant_TL3(T1, T2, T3) detail::typelist<T1, variant_TL2(T2, T3)>
+#define variant_TL4(T1, T2, T3, T4) detail::typelist<T1, variant_TL3(T2, T3, T4)>
+#define variant_TL5(T1, T2, T3, T4, T5) detail::typelist<T1, variant_TL4(T2, T3, T4, T5)>
+#define variant_TL6(T1, T2, T3, T4, T5, T6) detail::typelist<T1, variant_TL5(T2, T3, T4, T5, T6)>
+#define variant_TL7(T1, T2, T3, T4, T5, T6, T7)                                                    \
+  detail::typelist<T1, variant_TL6(T2, T3, T4, T5, T6, T7)>
+#define variant_TL8(T1, T2, T3, T4, T5, T6, T7, T8)                                                \
+  detail::typelist<T1, variant_TL7(T2, T3, T4, T5, T6, T7, T8)>
+#define variant_TL9(T1, T2, T3, T4, T5, T6, T7, T8, T9)                                            \
+  detail::typelist<T1, variant_TL8(T2, T3, T4, T5, T6, T7, T8, T9)>
+#define variant_TL10(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10)                                      \
+  detail::typelist<T1, variant_TL9(T2, T3, T4, T5, T6, T7, T8, T9, T10)>
+#define variant_TL11(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)                                 \
+  detail::typelist<T1, variant_TL10(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)>
+#define variant_TL12(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)                            \
+  detail::typelist<T1, variant_TL11(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)>
+#define variant_TL13(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)                       \
+  detail::typelist<T1, variant_TL12(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)>
+#define variant_TL14(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)                  \
+  detail::typelist<T1, variant_TL13(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)>
+#define variant_TL15(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)             \
+  detail::typelist<T1, variant_TL14(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)>
+#define variant_TL16(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16)        \
+  detail::typelist<T1, variant_TL15(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15,  \
+                                    T16)>
+
+// variant parameter unused type tags:
+
+template <class T> struct TX : T {
+  inline TX<T> operator+() const { return TX<T>(); }
+  inline TX<T> operator-() const { return TX<T>(); }
+
+  inline TX<T> operator!() const { return TX<T>(); }
+  inline TX<T> operator~() const { return TX<T>(); }
+
+  inline TX<T> *operator&() const { return variant_nullptr; }
+
+  template <class U> inline TX<T> operator*(U const &)const { return TX<T>(); }
+  template <class U> inline TX<T> operator/(U const &) const { return TX<T>(); }
+
+  template <class U> inline TX<T> operator%(U const &) const { return TX<T>(); }
+  template <class U> inline TX<T> operator+(U const &) const { return TX<T>(); }
+  template <class U> inline TX<T> operator-(U const &) const { return TX<T>(); }
+
+  template <class U> inline TX<T> operator<<(U const &) const { return TX<T>(); }
+  template <class U> inline TX<T> operator>>(U const &) const { return TX<T>(); }
+
+  inline bool operator==(T const &) const { return false; }
+  inline bool operator<(T const &) const { return false; }
+
+  template <class U> inline TX<T> operator&(U const &)const { return TX<T>(); }
+  template <class U> inline TX<T> operator|(U const &) const { return TX<T>(); }
+  template <class U> inline TX<T> operator^(U const &) const { return TX<T>(); }
+
+  template <class U> inline TX<T> operator&&(U const &) const { return TX<T>(); }
+  template <class U> inline TX<T> operator||(U const &) const { return TX<T>(); }
+};
+
+struct S0 {};
+typedef TX<S0> T0;
+struct S1 {};
+typedef TX<S1> T1;
+struct S2 {};
+typedef TX<S2> T2;
+struct S3 {};
+typedef TX<S3> T3;
+struct S4 {};
+typedef TX<S4> T4;
+struct S5 {};
+typedef TX<S5> T5;
+struct S6 {};
+typedef TX<S6> T6;
+struct S7 {};
+typedef TX<S7> T7;
+struct S8 {};
+typedef TX<S8> T8;
+struct S9 {};
+typedef TX<S9> T9;
+struct S10 {};
+typedef TX<S10> T10;
+struct S11 {};
+typedef TX<S11> T11;
+struct S12 {};
+typedef TX<S12> T12;
+struct S13 {};
+typedef TX<S13> T13;
+struct S14 {};
+typedef TX<S14> T14;
+struct S15 {};
+typedef TX<S15> T15;
+
+struct nulltype {};
+
+template <class Head, class Tail> struct typelist {
+  typedef Head head;
+  typedef Tail tail;
+};
+
+// typelist max element size:
+
+template <class List> struct typelist_max;
+
+template <> struct typelist_max<nulltype> {
+  enum V { value = 0 };
+  typedef void type;
+};
+
+template <class Head, class Tail> struct typelist_max<typelist<Head, Tail>> {
+private:
+  enum TV { tail_value = size_t(typelist_max<Tail>::value) };
+
+  typedef typename typelist_max<Tail>::type tail_type;
+
+public:
+  enum V { value = (sizeof(Head) > tail_value) ? sizeof(Head) : std::size_t(tail_value) };
+
+  typedef typename std11::conditional<(sizeof(Head) > tail_value), Head, tail_type>::type type;
+};
+
+#if variant_CPP11_OR_GREATER
+
+// typelist max alignof element type:
+
+template <class List> struct typelist_max_alignof;
+
+template <> struct typelist_max_alignof<nulltype> {
+  enum V { value = 0 };
+};
+
+template <class Head, class Tail> struct typelist_max_alignof<typelist<Head, Tail>> {
+private:
+  enum TV { tail_value = size_t(typelist_max_alignof<Tail>::value) };
+
+public:
+  enum V { value = (alignof(Head) > tail_value) ? alignof(Head) : std::size_t(tail_value) };
+};
+
+#endif
+
+// typelist size (length):
+
+template <class List> struct typelist_size {
+  enum V { value = 1 };
+};
+
+template <> struct typelist_size<T0> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T1> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T2> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T3> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T4> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T5> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T6> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T7> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T8> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T9> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T10> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T11> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T12> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T13> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T14> {
+  enum V { value = 0 };
+};
+template <> struct typelist_size<T15> {
+  enum V { value = 0 };
+};
+
+template <> struct typelist_size<nulltype> {
+  enum V { value = 0 };
+};
+
+template <class Head, class Tail> struct typelist_size<typelist<Head, Tail>> {
+  enum V { value = typelist_size<Head>::value + typelist_size<Tail>::value };
+};
+
+// typelist index of type:
+
+template <class List, class T> struct typelist_index_of;
+
+template <class T> struct typelist_index_of<nulltype, T> {
+  enum V { value = -1 };
+};
+
+template <class Tail, class T> struct typelist_index_of<typelist<T, Tail>, T> {
+  enum V { value = 0 };
+};
+
+template <class Head, class Tail, class T> struct typelist_index_of<typelist<Head, Tail>, T> {
+private:
+  enum TV { nextVal = typelist_index_of<Tail, T>::value };
+
+public:
+  enum V { value = nextVal == -1 ? -1 : 1 + nextVal };
+};
+
+// typelist type at index:
+
+template <class List, std::size_t i> struct typelist_type_at;
+
+template <class Head, class Tail> struct typelist_type_at<typelist<Head, Tail>, 0> {
+  typedef Head type;
+};
+
+template <class Head, class Tail, std::size_t i> struct typelist_type_at<typelist<Head, Tail>, i> {
+  typedef typename typelist_type_at<Tail, i - 1>::type type;
+};
+
+#if variant_CONFIG_MAX_ALIGN_HACK
+
+// Max align, use most restricted type for alignment:
+
+#define variant_UNIQUE(name) variant_UNIQUE2(name, __LINE__)
+#define variant_UNIQUE2(name, line) variant_UNIQUE3(name, line)
+#define variant_UNIQUE3(name, line) name##line
+
+#define variant_ALIGN_TYPE(type)                                                                   \
+  type variant_UNIQUE(_t);                                                                         \
+  struct_t<type> variant_UNIQUE(_st)
+
+template <class T> struct struct_t { T _; };
+
+union max_align_t {
+  variant_ALIGN_TYPE(char);
+  variant_ALIGN_TYPE(short int);
+  variant_ALIGN_TYPE(int);
+  variant_ALIGN_TYPE(long int);
+  variant_ALIGN_TYPE(float);
+  variant_ALIGN_TYPE(double);
+  variant_ALIGN_TYPE(long double);
+  variant_ALIGN_TYPE(char *);
+  variant_ALIGN_TYPE(short int *);
+  variant_ALIGN_TYPE(int *);
+  variant_ALIGN_TYPE(long int *);
+  variant_ALIGN_TYPE(float *);
+  variant_ALIGN_TYPE(double *);
+  variant_ALIGN_TYPE(long double *);
+  variant_ALIGN_TYPE(void *);
+
+#ifdef HAVE_LONG_LONG
+  variant_ALIGN_TYPE(long long);
+#endif
+
+  struct Unknown;
+
+  Unknown (*variant_UNIQUE(_))(Unknown);
+  Unknown *Unknown::*variant_UNIQUE(_);
+  Unknown (Unknown::*variant_UNIQUE(_))(Unknown);
+
+  struct_t<Unknown (*)(Unknown)> variant_UNIQUE(_);
+  struct_t<Unknown * Unknown::*> variant_UNIQUE(_);
+  struct_t<Unknown (Unknown::*)(Unknown)> variant_UNIQUE(_);
+};
+
+#undef variant_UNIQUE
+#undef variant_UNIQUE2
+#undef variant_UNIQUE3
+
+#undef variant_ALIGN_TYPE
+
+#elif defined(variant_CONFIG_ALIGN_AS) // variant_CONFIG_MAX_ALIGN_HACK
+
+// Use user-specified type for alignment:
+
+#define variant_ALIGN_AS(unused) variant_CONFIG_ALIGN_AS
+
+#else // variant_CONFIG_MAX_ALIGN_HACK
+
+// Determine POD type to use for alignment:
+
+#define variant_ALIGN_AS(to_align)                                                                 \
+  typename detail::type_of_size<detail::alignment_types,                                           \
+                                detail::alignment_of<to_align>::value>::type
+
+template <typename T> struct alignment_of;
+
+template <typename T> struct alignment_of_hack {
+  char c;
+  T t;
+  alignment_of_hack();
+};
+
+template <size_t A, size_t S> struct alignment_logic {
+  enum V { value = A < S ? A : S };
+};
+
+template <typename T> struct alignment_of {
+  enum V { value = alignment_logic<sizeof(alignment_of_hack<T>) - sizeof(T), sizeof(T)>::value };
+};
+
+template <typename List, size_t N> struct type_of_size {
+  typedef
+      typename std11::conditional<N == sizeof(typename List::head), typename List::head,
+                                  typename type_of_size<typename List::tail, N>::type>::type type;
+};
+
+template <size_t N> struct type_of_size<nulltype, N> {
+  typedef variant_CONFIG_ALIGN_AS_FALLBACK type;
+};
+
+template <typename T> struct struct_t { T _; };
+
+#define variant_ALIGN_TYPE(type) typelist < type, typelist < struct_t<type>
+
+struct Unknown;
+
+typedef variant_ALIGN_TYPE(char), variant_ALIGN_TYPE(short), variant_ALIGN_TYPE(int),
+    variant_ALIGN_TYPE(long), variant_ALIGN_TYPE(float), variant_ALIGN_TYPE(double),
+    variant_ALIGN_TYPE(long double),
+
+    variant_ALIGN_TYPE(char *), variant_ALIGN_TYPE(short *), variant_ALIGN_TYPE(int *),
+    variant_ALIGN_TYPE(long *), variant_ALIGN_TYPE(float *), variant_ALIGN_TYPE(double *),
+    variant_ALIGN_TYPE(long double *),
+
+    variant_ALIGN_TYPE(Unknown (*)(Unknown)), variant_ALIGN_TYPE(Unknown *Unknown::*),
+    variant_ALIGN_TYPE(Unknown (Unknown::*)(Unknown)),
+
+    nulltype >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> alignment_types;
+
+#undef variant_ALIGN_TYPE
+
+#endif // variant_CONFIG_MAX_ALIGN_HACK
+
+#if variant_CPP11_OR_GREATER
+
+template <typename T> inline std::size_t hash(T const &v) { return std::hash<T>()(v); }
+
+inline std::size_t hash(T0 const &) { return 0; }
+inline std::size_t hash(T1 const &) { return 0; }
+inline std::size_t hash(T2 const &) { return 0; }
+inline std::size_t hash(T3 const &) { return 0; }
+inline std::size_t hash(T4 const &) { return 0; }
+inline std::size_t hash(T5 const &) { return 0; }
+inline std::size_t hash(T6 const &) { return 0; }
+inline std::size_t hash(T7 const &) { return 0; }
+inline std::size_t hash(T8 const &) { return 0; }
+inline std::size_t hash(T9 const &) { return 0; }
+inline std::size_t hash(T10 const &) { return 0; }
+inline std::size_t hash(T11 const &) { return 0; }
+inline std::size_t hash(T12 const &) { return 0; }
+inline std::size_t hash(T13 const &) { return 0; }
+inline std::size_t hash(T14 const &) { return 0; }
+inline std::size_t hash(T15 const &) { return 0; }
+
+#endif // variant_CPP11_OR_GREATER
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+struct helper {
+  typedef signed char type_index_t;
+  typedef variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+                       T15) variant_types;
+
+  template <class U> static U *as(void *data) { return reinterpret_cast<U *>(data); }
+
+  template <class U> static U const *as(void const *data) {
+    return reinterpret_cast<const U *>(data);
+  }
+
+  static type_index_t to_index_t(std::size_t index) { return static_cast<type_index_t>(index); }
+
+  static void destroy(type_index_t index, void *data) {
+    switch (index) {
+    case 0:
+      as<T0>(data)->~T0();
+      break;
+    case 1:
+      as<T1>(data)->~T1();
+      break;
+    case 2:
+      as<T2>(data)->~T2();
+      break;
+    case 3:
+      as<T3>(data)->~T3();
+      break;
+    case 4:
+      as<T4>(data)->~T4();
+      break;
+    case 5:
+      as<T5>(data)->~T5();
+      break;
+    case 6:
+      as<T6>(data)->~T6();
+      break;
+    case 7:
+      as<T7>(data)->~T7();
+      break;
+    case 8:
+      as<T8>(data)->~T8();
+      break;
+    case 9:
+      as<T9>(data)->~T9();
+      break;
+    case 10:
+      as<T10>(data)->~T10();
+      break;
+    case 11:
+      as<T11>(data)->~T11();
+      break;
+    case 12:
+      as<T12>(data)->~T12();
+      break;
+    case 13:
+      as<T13>(data)->~T13();
+      break;
+    case 14:
+      as<T14>(data)->~T14();
+      break;
+    case 15:
+      as<T15>(data)->~T15();
+      break;
+    }
+  }
+
+#if variant_CPP11_OR_GREATER
+  template <class T, class... Args> static type_index_t construct_t(void *data, Args &&... args) {
+    new (data) T(std::forward<Args>(args)...);
+
+    return to_index_t(detail::typelist_index_of<variant_types, T>::value);
+  }
+
+  template <std::size_t K, class... Args>
+  static type_index_t construct_i(void *data, Args &&... args) {
+    using type = typename detail::typelist_type_at<variant_types, K>::type;
+
+    construct_t<type>(data, std::forward<Args>(args)...);
+
+    return to_index_t(K);
+  }
+
+  static type_index_t move_construct(type_index_t const from_index, void *from_value,
+                                     void *to_value) {
+    switch (from_index) {
+    case 0:
+      new (to_value) T0(std::move(*as<T0>(from_value)));
+      break;
+    case 1:
+      new (to_value) T1(std::move(*as<T1>(from_value)));
+      break;
+    case 2:
+      new (to_value) T2(std::move(*as<T2>(from_value)));
+      break;
+    case 3:
+      new (to_value) T3(std::move(*as<T3>(from_value)));
+      break;
+    case 4:
+      new (to_value) T4(std::move(*as<T4>(from_value)));
+      break;
+    case 5:
+      new (to_value) T5(std::move(*as<T5>(from_value)));
+      break;
+    case 6:
+      new (to_value) T6(std::move(*as<T6>(from_value)));
+      break;
+    case 7:
+      new (to_value) T7(std::move(*as<T7>(from_value)));
+      break;
+    case 8:
+      new (to_value) T8(std::move(*as<T8>(from_value)));
+      break;
+    case 9:
+      new (to_value) T9(std::move(*as<T9>(from_value)));
+      break;
+    case 10:
+      new (to_value) T10(std::move(*as<T10>(from_value)));
+      break;
+    case 11:
+      new (to_value) T11(std::move(*as<T11>(from_value)));
+      break;
+    case 12:
+      new (to_value) T12(std::move(*as<T12>(from_value)));
+      break;
+    case 13:
+      new (to_value) T13(std::move(*as<T13>(from_value)));
+      break;
+    case 14:
+      new (to_value) T14(std::move(*as<T14>(from_value)));
+      break;
+    case 15:
+      new (to_value) T15(std::move(*as<T15>(from_value)));
+      break;
+    }
+    return from_index;
+  }
+
+  static type_index_t move_assign(type_index_t const from_index, void *from_value, void *to_value) {
+    switch (from_index) {
+    case 0:
+      *as<T0>(to_value) = std::move(*as<T0>(from_value));
+      break;
+    case 1:
+      *as<T1>(to_value) = std::move(*as<T1>(from_value));
+      break;
+    case 2:
+      *as<T2>(to_value) = std::move(*as<T2>(from_value));
+      break;
+    case 3:
+      *as<T3>(to_value) = std::move(*as<T3>(from_value));
+      break;
+    case 4:
+      *as<T4>(to_value) = std::move(*as<T4>(from_value));
+      break;
+    case 5:
+      *as<T5>(to_value) = std::move(*as<T5>(from_value));
+      break;
+    case 6:
+      *as<T6>(to_value) = std::move(*as<T6>(from_value));
+      break;
+    case 7:
+      *as<T7>(to_value) = std::move(*as<T7>(from_value));
+      break;
+    case 8:
+      *as<T8>(to_value) = std::move(*as<T8>(from_value));
+      break;
+    case 9:
+      *as<T9>(to_value) = std::move(*as<T9>(from_value));
+      break;
+    case 10:
+      *as<T10>(to_value) = std::move(*as<T10>(from_value));
+      break;
+    case 11:
+      *as<T11>(to_value) = std::move(*as<T11>(from_value));
+      break;
+    case 12:
+      *as<T12>(to_value) = std::move(*as<T12>(from_value));
+      break;
+    case 13:
+      *as<T13>(to_value) = std::move(*as<T13>(from_value));
+      break;
+    case 14:
+      *as<T14>(to_value) = std::move(*as<T14>(from_value));
+      break;
+    case 15:
+      *as<T15>(to_value) = std::move(*as<T15>(from_value));
+      break;
+    }
+    return from_index;
+  }
+#endif
+
+  static type_index_t copy_construct(type_index_t const from_index, const void *from_value,
+                                     void *to_value) {
+    switch (from_index) {
+    case 0:
+      new (to_value) T0(*as<T0>(from_value));
+      break;
+    case 1:
+      new (to_value) T1(*as<T1>(from_value));
+      break;
+    case 2:
+      new (to_value) T2(*as<T2>(from_value));
+      break;
+    case 3:
+      new (to_value) T3(*as<T3>(from_value));
+      break;
+    case 4:
+      new (to_value) T4(*as<T4>(from_value));
+      break;
+    case 5:
+      new (to_value) T5(*as<T5>(from_value));
+      break;
+    case 6:
+      new (to_value) T6(*as<T6>(from_value));
+      break;
+    case 7:
+      new (to_value) T7(*as<T7>(from_value));
+      break;
+    case 8:
+      new (to_value) T8(*as<T8>(from_value));
+      break;
+    case 9:
+      new (to_value) T9(*as<T9>(from_value));
+      break;
+    case 10:
+      new (to_value) T10(*as<T10>(from_value));
+      break;
+    case 11:
+      new (to_value) T11(*as<T11>(from_value));
+      break;
+    case 12:
+      new (to_value) T12(*as<T12>(from_value));
+      break;
+    case 13:
+      new (to_value) T13(*as<T13>(from_value));
+      break;
+    case 14:
+      new (to_value) T14(*as<T14>(from_value));
+      break;
+    case 15:
+      new (to_value) T15(*as<T15>(from_value));
+      break;
+    }
+    return from_index;
+  }
+
+  static type_index_t copy_assign(type_index_t const from_index, const void *from_value,
+                                  void *to_value) {
+    switch (from_index) {
+    case 0:
+      *as<T0>(to_value) = *as<T0>(from_value);
+      break;
+    case 1:
+      *as<T1>(to_value) = *as<T1>(from_value);
+      break;
+    case 2:
+      *as<T2>(to_value) = *as<T2>(from_value);
+      break;
+    case 3:
+      *as<T3>(to_value) = *as<T3>(from_value);
+      break;
+    case 4:
+      *as<T4>(to_value) = *as<T4>(from_value);
+      break;
+    case 5:
+      *as<T5>(to_value) = *as<T5>(from_value);
+      break;
+    case 6:
+      *as<T6>(to_value) = *as<T6>(from_value);
+      break;
+    case 7:
+      *as<T7>(to_value) = *as<T7>(from_value);
+      break;
+    case 8:
+      *as<T8>(to_value) = *as<T8>(from_value);
+      break;
+    case 9:
+      *as<T9>(to_value) = *as<T9>(from_value);
+      break;
+    case 10:
+      *as<T10>(to_value) = *as<T10>(from_value);
+      break;
+    case 11:
+      *as<T11>(to_value) = *as<T11>(from_value);
+      break;
+    case 12:
+      *as<T12>(to_value) = *as<T12>(from_value);
+      break;
+    case 13:
+      *as<T13>(to_value) = *as<T13>(from_value);
+      break;
+    case 14:
+      *as<T14>(to_value) = *as<T14>(from_value);
+      break;
+    case 15:
+      *as<T15>(to_value) = *as<T15>(from_value);
+      break;
+    }
+    return from_index;
+  }
+};
+
+} // namespace detail
+
+//
+// Variant:
+//
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+class variant;
+
+// 19.7.8 Class monostate
+
+class monostate {};
+
+// 19.7.9 monostate relational operators
+
+inline variant_constexpr bool operator<(monostate, monostate) variant_noexcept { return false; }
+inline variant_constexpr bool operator>(monostate, monostate) variant_noexcept { return false; }
+inline variant_constexpr bool operator<=(monostate, monostate) variant_noexcept { return true; }
+inline variant_constexpr bool operator>=(monostate, monostate) variant_noexcept { return true; }
+inline variant_constexpr bool operator==(monostate, monostate) variant_noexcept { return true; }
+inline variant_constexpr bool operator!=(monostate, monostate) variant_noexcept { return false; }
+
+// 19.7.4 variant helper classes
+
+// obtain the size of the variant's list of alternatives at compile time
+
+template <class T> struct variant_size; /* undefined */
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+struct variant_size<variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>> {
+  enum _ {
+    value = detail::typelist_size<variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11,
+                                               T12, T13, T14, T15)>::value
+  };
+};
+
+#if variant_CPP14_OR_GREATER
+template <class T> constexpr std::size_t variant_size_v = variant_size<T>::value;
+#endif
+
+#if !variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO
+#define variant_size_V(T) nonstd::variant_size<T>::value
+#endif
+
+// obtain the type of the alternative specified by its index, at compile time:
+
+template <std::size_t K, class T> struct variant_alternative; /* undefined */
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+          class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+          class T15>
+struct variant_alternative<
+    K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>> {
+  typedef typename detail::typelist_type_at<variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9,
+                                                         T10, T11, T12, T13, T14, T15),
+                                            K>::type type;
+};
+
+#if variant_CPP11_OR_GREATER
+template <std::size_t K, class T>
+using variant_alternative_t = typename variant_alternative<K, T>::type;
+#endif
+
+#if !variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO
+#define variant_alternative_T(K, T) typename nonstd::variant_alternative<K, T>::type
+#endif
+
+// NTS:implement specializes the std::uses_allocator type trait
+// std::uses_allocator<nonstd::variant>
+
+// index of the variant in the invalid state (constant)
+
+#if variant_CPP11_OR_GREATER
+variant_constexpr std::size_t variant_npos = static_cast<std::size_t>(-1);
+#else
+static const std::size_t variant_npos = static_cast<std::size_t>(-1);
+#endif
+
+#if !variant_CONFIG_NO_EXCEPTIONS
+
+// 19.7.11 Class bad_variant_access
+
+class bad_variant_access : public std::exception {
+public:
+#if variant_CPP11_OR_GREATER
+  virtual const char *what() const variant_noexcept variant_override
+#else
+  virtual const char *what() const throw()
+#endif
+  {
+    return "bad variant access";
+  }
+};
+
+#endif // variant_CONFIG_NO_EXCEPTIONS
+
+// 19.7.3 Class template variant
+
+template <class T0, class T1 = detail::T1, class T2 = detail::T2, class T3 = detail::T3,
+          class T4 = detail::T4, class T5 = detail::T5, class T6 = detail::T6,
+          class T7 = detail::T7, class T8 = detail::T8, class T9 = detail::T9,
+          class T10 = detail::T10, class T11 = detail::T11, class T12 = detail::T12,
+          class T13 = detail::T13, class T14 = detail::T14, class T15 = detail::T15>
+class variant {
+  typedef detail::helper<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
+      helper_type;
+  typedef variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+                       T15) variant_types;
+
+public:
+  // 19.7.3.1 Constructors
+
+  variant() : type_index(0) { new (ptr()) T0(); }
+
+  variant(T0 const &t0) : type_index(0) { new (ptr()) T0(t0); }
+  variant(T1 const &t1) : type_index(1) { new (ptr()) T1(t1); }
+  variant(T2 const &t2) : type_index(2) { new (ptr()) T2(t2); }
+  variant(T3 const &t3) : type_index(3) { new (ptr()) T3(t3); }
+  variant(T4 const &t4) : type_index(4) { new (ptr()) T4(t4); }
+  variant(T5 const &t5) : type_index(5) { new (ptr()) T5(t5); }
+  variant(T6 const &t6) : type_index(6) { new (ptr()) T6(t6); }
+  variant(T7 const &t7) : type_index(7) { new (ptr()) T7(t7); }
+  variant(T8 const &t8) : type_index(8) { new (ptr()) T8(t8); }
+  variant(T9 const &t9) : type_index(9) { new (ptr()) T9(t9); }
+  variant(T10 const &t10) : type_index(10) { new (ptr()) T10(t10); }
+  variant(T11 const &t11) : type_index(11) { new (ptr()) T11(t11); }
+  variant(T12 const &t12) : type_index(12) { new (ptr()) T12(t12); }
+  variant(T13 const &t13) : type_index(13) { new (ptr()) T13(t13); }
+  variant(T14 const &t14) : type_index(14) { new (ptr()) T14(t14); }
+  variant(T15 const &t15) : type_index(15) { new (ptr()) T15(t15); }
+
+#if variant_CPP11_OR_GREATER
+  variant(T0 &&t0) : type_index(0) { new (ptr()) T0(std::move(t0)); }
+  variant(T1 &&t1) : type_index(1) { new (ptr()) T1(std::move(t1)); }
+  variant(T2 &&t2) : type_index(2) { new (ptr()) T2(std::move(t2)); }
+  variant(T3 &&t3) : type_index(3) { new (ptr()) T3(std::move(t3)); }
+  variant(T4 &&t4) : type_index(4) { new (ptr()) T4(std::move(t4)); }
+  variant(T5 &&t5) : type_index(5) { new (ptr()) T5(std::move(t5)); }
+  variant(T6 &&t6) : type_index(6) { new (ptr()) T6(std::move(t6)); }
+  variant(T7 &&t7) : type_index(7) { new (ptr()) T7(std::move(t7)); }
+  variant(T8 &&t8) : type_index(8) { new (ptr()) T8(std::move(t8)); }
+  variant(T9 &&t9) : type_index(9) { new (ptr()) T9(std::move(t9)); }
+  variant(T10 &&t10) : type_index(10) { new (ptr()) T10(std::move(t10)); }
+  variant(T11 &&t11) : type_index(11) { new (ptr()) T11(std::move(t11)); }
+  variant(T12 &&t12) : type_index(12) { new (ptr()) T12(std::move(t12)); }
+  variant(T13 &&t13) : type_index(13) { new (ptr()) T13(std::move(t13)); }
+  variant(T14 &&t14) : type_index(14) { new (ptr()) T14(std::move(t14)); }
+  variant(T15 &&t15) : type_index(15) { new (ptr()) T15(std::move(t15)); }
+
+#endif
+
+  variant(variant const &other) : type_index(other.type_index) {
+    (void)helper_type::copy_construct(other.type_index, other.ptr(), ptr());
+  }
+
+#if variant_CPP11_OR_GREATER
+
+  variant(variant &&other) noexcept(
+      std::is_nothrow_move_constructible<T0>::value &&std::is_nothrow_move_constructible<T1>::value
+          &&std::is_nothrow_move_constructible<T2>::value &&std::is_nothrow_move_constructible<
+              T3>::value &&std::is_nothrow_move_constructible<T4>::value
+              &&std::is_nothrow_move_constructible<T5>::value &&std::is_nothrow_move_constructible<
+                  T6>::value &&std::is_nothrow_move_constructible<T7>::value
+                  &&std::is_nothrow_move_constructible<T8>::value
+                      &&std::is_nothrow_move_constructible<T9>::value
+                          &&std::is_nothrow_move_constructible<T10>::value
+                              &&std::is_nothrow_move_constructible<T11>::value
+                                  &&std::is_nothrow_move_constructible<T12>::value
+                                      &&std::is_nothrow_move_constructible<T13>::value
+                                          &&std::is_nothrow_move_constructible<T14>::value
+                                              &&std::is_nothrow_move_constructible<T15>::value)
+      : type_index(other.type_index) {
+    (void)helper_type::move_construct(other.type_index, other.ptr(), ptr());
+  }
+
+  template <std::size_t K>
+  using type_at_t = typename detail::typelist_type_at<variant_types, K>::type;
+
+  template <class T, class... Args variant_REQUIRES_T(std::is_constructible<T, Args...>::value)>
+  explicit variant(nonstd_lite_in_place_type_t(T), Args &&... args) {
+    type_index = variant_npos_internal();
+    type_index = helper_type::template construct_t<T>(ptr(), std::forward<Args>(args)...);
+  }
+
+  template <class T, class U,
+            class... Args variant_REQUIRES_T(
+                std::is_constructible<T, std::initializer_list<U> &, Args...>::value)>
+  explicit variant(nonstd_lite_in_place_type_t(T), std::initializer_list<U> il, Args &&... args) {
+    type_index = variant_npos_internal();
+    type_index = helper_type::template construct_t<T>(ptr(), il, std::forward<Args>(args)...);
+  }
+
+  template <std::size_t K,
+            class... Args variant_REQUIRES_T(std::is_constructible<type_at_t<K>, Args...>::value)>
+  explicit variant(nonstd_lite_in_place_index_t(K), Args &&... args) {
+    type_index = variant_npos_internal();
+    type_index = helper_type::template construct_i<K>(ptr(), std::forward<Args>(args)...);
+  }
+
+  template <size_t K, class U,
+            class... Args variant_REQUIRES_T(
+                std::is_constructible<type_at_t<K>, std::initializer_list<U> &, Args...>::value)>
+  explicit variant(nonstd_lite_in_place_index_t(K), std::initializer_list<U> il, Args &&... args) {
+    type_index = variant_npos_internal();
+    type_index = helper_type::template construct_i<K>(ptr(), il, std::forward<Args>(args)...);
+  }
+
+#endif // variant_CPP11_OR_GREATER
+
+  // 19.7.3.2 Destructor
+
+  ~variant() {
+    if (!valueless_by_exception()) {
+      helper_type::destroy(type_index, ptr());
+    }
+  }
+
+  // 19.7.3.3 Assignment
+
+  variant &operator=(variant const &other) { return copy_assign(other); }
+
+#if variant_CPP11_OR_GREATER
+
+  variant &operator=(variant &&other) noexcept(
+      std::is_nothrow_move_assignable<T0>::value &&std::is_nothrow_move_assignable<T1>::value
+          &&std::is_nothrow_move_assignable<T2>::value &&std::is_nothrow_move_assignable<T3>::value
+              &&std::is_nothrow_move_assignable<T4>::value &&std::is_nothrow_move_assignable<
+                  T5>::value &&std::is_nothrow_move_assignable<T6>::value
+                  &&std::is_nothrow_move_assignable<T7>::value &&std::is_nothrow_move_assignable<
+                      T8>::value &&std::is_nothrow_move_assignable<T9>::value &&
+                      std::is_nothrow_move_assignable<T10>::value &&std::is_nothrow_move_assignable<
+                          T11>::value &&std::is_nothrow_move_assignable<T12>::value
+                          &&std::is_nothrow_move_assignable<T13>::value
+                              &&std::is_nothrow_move_assignable<T14>::value
+                                  &&std::is_nothrow_move_assignable<T15>::value) {
+    return move_assign(std::move(other));
+  }
+
+  variant &operator=(T0 &&t0) { return assign_value<0>(std::move(t0)); }
+  variant &operator=(T1 &&t1) { return assign_value<1>(std::move(t1)); }
+  variant &operator=(T2 &&t2) { return assign_value<2>(std::move(t2)); }
+  variant &operator=(T3 &&t3) { return assign_value<3>(std::move(t3)); }
+  variant &operator=(T4 &&t4) { return assign_value<4>(std::move(t4)); }
+  variant &operator=(T5 &&t5) { return assign_value<5>(std::move(t5)); }
+  variant &operator=(T6 &&t6) { return assign_value<6>(std::move(t6)); }
+  variant &operator=(T7 &&t7) { return assign_value<7>(std::move(t7)); }
+  variant &operator=(T8 &&t8) { return assign_value<8>(std::move(t8)); }
+  variant &operator=(T9 &&t9) { return assign_value<9>(std::move(t9)); }
+  variant &operator=(T10 &&t10) { return assign_value<10>(std::move(t10)); }
+  variant &operator=(T11 &&t11) { return assign_value<11>(std::move(t11)); }
+  variant &operator=(T12 &&t12) { return assign_value<12>(std::move(t12)); }
+  variant &operator=(T13 &&t13) { return assign_value<13>(std::move(t13)); }
+  variant &operator=(T14 &&t14) { return assign_value<14>(std::move(t14)); }
+  variant &operator=(T15 &&t15) { return assign_value<15>(std::move(t15)); }
+
+#endif
+
+  variant &operator=(T0 const &t0) { return assign_value<0>(t0); }
+  variant &operator=(T1 const &t1) { return assign_value<1>(t1); }
+  variant &operator=(T2 const &t2) { return assign_value<2>(t2); }
+  variant &operator=(T3 const &t3) { return assign_value<3>(t3); }
+  variant &operator=(T4 const &t4) { return assign_value<4>(t4); }
+  variant &operator=(T5 const &t5) { return assign_value<5>(t5); }
+  variant &operator=(T6 const &t6) { return assign_value<6>(t6); }
+  variant &operator=(T7 const &t7) { return assign_value<7>(t7); }
+  variant &operator=(T8 const &t8) { return assign_value<8>(t8); }
+  variant &operator=(T9 const &t9) { return assign_value<9>(t9); }
+  variant &operator=(T10 const &t10) { return assign_value<10>(t10); }
+  variant &operator=(T11 const &t11) { return assign_value<11>(t11); }
+  variant &operator=(T12 const &t12) { return assign_value<12>(t12); }
+  variant &operator=(T13 const &t13) { return assign_value<13>(t13); }
+  variant &operator=(T14 const &t14) { return assign_value<14>(t14); }
+  variant &operator=(T15 const &t15) { return assign_value<15>(t15); }
+
+  std::size_t index() const {
+    return variant_npos_internal() == type_index ? variant_npos
+                                                 : static_cast<std::size_t>(type_index);
+  }
+
+  // 19.7.3.4 Modifiers
+
+#if variant_CPP11_OR_GREATER
+  template <class T, class... Args variant_REQUIRES_T(std::is_constructible<T, Args...>::value)>
+  T &emplace(Args &&... args) {
+    helper_type::destroy(type_index, ptr());
+    type_index = variant_npos_internal();
+    type_index = helper_type::template construct_t<T>(ptr(), std::forward<Args>(args)...);
+
+    return *as<T>();
+  }
+
+  template <class T, class U,
+            class... Args variant_REQUIRES_T(
+                std::is_constructible<T, std::initializer_list<U> &, Args...>::value)>
+  T &emplace(std::initializer_list<U> il, Args &&... args) {
+    helper_type::destroy(type_index, ptr());
+    type_index = variant_npos_internal();
+    type_index = helper_type::template construct_t<T>(ptr(), il, std::forward<Args>(args)...);
+
+    return *as<T>();
+  }
+
+  template <size_t K,
+            class... Args variant_REQUIRES_T(std::is_constructible<type_at_t<K>, Args...>::value)>
+  variant_alternative_t<K, variant> &emplace(Args &&... args) {
+    return this->template emplace<type_at_t<K>>(std::forward<Args>(args)...);
+  }
+
+  template <size_t K, class U,
+            class... Args variant_REQUIRES_T(
+                std::is_constructible<type_at_t<K>, std::initializer_list<U> &, Args...>::value)>
+  variant_alternative_t<K, variant> &emplace(std::initializer_list<U> il, Args &&... args) {
+    return this->template emplace<type_at_t<K>>(il, std::forward<Args>(args)...);
+  }
+
+#endif // variant_CPP11_OR_GREATER
+
+  // 19.7.3.5 Value status
+
+  bool valueless_by_exception() const { return type_index == variant_npos_internal(); }
+
+  // 19.7.3.6 Swap
+
+  void swap(variant &other)
+#if variant_CPP11_OR_GREATER
+      noexcept(
+          std::is_nothrow_move_constructible<T0>::value &&std17::is_nothrow_swappable<
+              T0>::value &&std::is_nothrow_move_constructible<T1>::value
+              &&std17::is_nothrow_swappable<T1>::value &&std::is_nothrow_move_constructible<
+                  T2>::value &&std17::is_nothrow_swappable<T2>::value
+                  &&std::is_nothrow_move_constructible<T3>::value &&std17::is_nothrow_swappable<
+                      T3>::value &&std::is_nothrow_move_constructible<T4>::value
+                      &&std17::is_nothrow_swappable<T4>::value &&std::is_nothrow_move_constructible<
+                          T5>::value &&std17::is_nothrow_swappable<T5>::value &&std::
+                          is_nothrow_move_constructible<T6>::value &&std17::is_nothrow_swappable<
+                              T6>::value &&std::is_nothrow_move_constructible<T7>::value &&std17::
+                              is_nothrow_swappable<T7>::value &&std::is_nothrow_move_constructible<
+                                  T8>::value &&std17::is_nothrow_swappable<T8>::value
+                                  &&std::is_nothrow_move_constructible<
+                                      T9>::value &&std17::is_nothrow_swappable<T9>::value
+                                      &&std::is_nothrow_move_constructible<
+                                          T10>::value &&std17::is_nothrow_swappable<T10>::value
+                                          &&std::is_nothrow_move_constructible<
+                                              T11>::value &&std17::is_nothrow_swappable<T11>::value
+                                              &&std::is_nothrow_move_constructible<T12>::value
+                                                  &&std17::is_nothrow_swappable<T12>::value &&
+                                                      std::is_nothrow_move_constructible<T13>::value
+                                                          &&std17::is_nothrow_swappable<T13>::value
+                                                              &&std::is_nothrow_move_constructible<
+                                                                  T14>::value
+                                                                  &&std17::is_nothrow_swappable<
+                                                                      T14>::value &&std::
+                                                                      is_nothrow_move_constructible<
+                                                                          T15>::value &&std17::
+                                                                          is_nothrow_swappable<
+                                                                              T15>::value
+
+      )
+#endif
+  {
+    if (valueless_by_exception() && other.valueless_by_exception()) {
+      // no effect
+    } else if (type_index == other.type_index) {
+      this->swap_value(type_index, other);
+    } else {
+#if variant_CPP11_OR_GREATER
+      variant tmp(std::move(*this));
+      *this = std::move(other);
+      other = std::move(tmp);
+#else
+      variant tmp(*this);
+      *this = other;
+      other = tmp;
+#endif
+    }
+  }
+
+  //
+  // non-standard:
+  //
+
+  template <class T> static variant_constexpr std::size_t index_of() variant_noexcept {
+    return to_size_t(
+        detail::typelist_index_of<variant_types, typename std11::remove_cv<T>::type>::value);
+  }
+
+  template <class T> T &get() {
+#if variant_CONFIG_NO_EXCEPTIONS
+    assert(index_of<T>() == index());
+#else
+    if (index_of<T>() != index()) {
+      throw bad_variant_access();
+    }
+#endif
+    return *as<T>();
+  }
+
+  template <class T> T const &get() const {
+#if variant_CONFIG_NO_EXCEPTIONS
+    assert(index_of<T>() == index());
+#else
+    if (index_of<T>() != index()) {
+      throw bad_variant_access();
+    }
+#endif
+    return *as<const T>();
+  }
+
+  template <std::size_t K> typename variant_alternative<K, variant>::type &get() {
+    return this->template get<typename detail::typelist_type_at<variant_types, K>::type>();
+  }
+
+  template <std::size_t K> typename variant_alternative<K, variant>::type const &get() const {
+    return this->template get<typename detail::typelist_type_at<variant_types, K>::type>();
+  }
+
+private:
+  typedef typename helper_type::type_index_t type_index_t;
+
+  void *ptr() variant_noexcept { return &data; }
+
+  void const *ptr() const variant_noexcept { return &data; }
+
+  template <class U> U *as() { return reinterpret_cast<U *>(ptr()); }
+
+  template <class U> U const *as() const { return reinterpret_cast<U const *>(ptr()); }
+
+  template <class U> static variant_constexpr std::size_t to_size_t(U index) {
+    return static_cast<std::size_t>(index);
+  }
+
+  variant_constexpr type_index_t variant_npos_internal() const variant_noexcept {
+    return static_cast<type_index_t>(-1);
+  }
+
+  variant &copy_assign(variant const &other) {
+    if (valueless_by_exception() && other.valueless_by_exception()) {
+      // no effect
+    } else if (!valueless_by_exception() && other.valueless_by_exception()) {
+      helper_type::destroy(type_index, ptr());
+      type_index = variant_npos_internal();
+    } else if (index() == other.index()) {
+      type_index = helper_type::copy_assign(other.type_index, other.ptr(), ptr());
+    } else {
+      helper_type::destroy(type_index, ptr());
+      type_index = variant_npos_internal();
+      type_index = helper_type::copy_construct(other.type_index, other.ptr(), ptr());
+    }
+    return *this;
+  }
+
+#if variant_CPP11_OR_GREATER
+
+  variant &move_assign(variant &&other) {
+    if (valueless_by_exception() && other.valueless_by_exception()) {
+      // no effect
+    } else if (!valueless_by_exception() && other.valueless_by_exception()) {
+      helper_type::destroy(type_index, ptr());
+      type_index = variant_npos_internal();
+    } else if (index() == other.index()) {
+      type_index = helper_type::move_assign(other.type_index, other.ptr(), ptr());
+    } else {
+      helper_type::destroy(type_index, ptr());
+      type_index = variant_npos_internal();
+      type_index = helper_type::move_construct(other.type_index, other.ptr(), ptr());
+    }
+    return *this;
+  }
+
+  template <std::size_t K, class T> variant &assign_value(T &&value) {
+    if (index() == K) {
+      *as<T>() = std::forward<T>(value);
+    } else {
+      helper_type::destroy(type_index, ptr());
+      type_index = variant_npos_internal();
+      new (ptr()) T(std::forward<T>(value));
+      type_index = K;
+    }
+    return *this;
+  }
+
+#endif // variant_CPP11_OR_GREATER
+
+  template <std::size_t K, class T> variant &assign_value(T const &value) {
+    if (index() == K) {
+      *as<T>() = value;
+    } else {
+      helper_type::destroy(type_index, ptr());
+      type_index = variant_npos_internal();
+      new (ptr()) T(value);
+      type_index = K;
+    }
+    return *this;
+  }
+
+  void swap_value(type_index_t index, variant &other) {
+    using std::swap;
+    switch (index) {
+    case 0:
+      swap(this->get<0>(), other.get<0>());
+      break;
+    case 1:
+      swap(this->get<1>(), other.get<1>());
+      break;
+    case 2:
+      swap(this->get<2>(), other.get<2>());
+      break;
+    case 3:
+      swap(this->get<3>(), other.get<3>());
+      break;
+    case 4:
+      swap(this->get<4>(), other.get<4>());
+      break;
+    case 5:
+      swap(this->get<5>(), other.get<5>());
+      break;
+    case 6:
+      swap(this->get<6>(), other.get<6>());
+      break;
+    case 7:
+      swap(this->get<7>(), other.get<7>());
+      break;
+    case 8:
+      swap(this->get<8>(), other.get<8>());
+      break;
+    case 9:
+      swap(this->get<9>(), other.get<9>());
+      break;
+    case 10:
+      swap(this->get<10>(), other.get<10>());
+      break;
+    case 11:
+      swap(this->get<11>(), other.get<11>());
+      break;
+    case 12:
+      swap(this->get<12>(), other.get<12>());
+      break;
+    case 13:
+      swap(this->get<13>(), other.get<13>());
+      break;
+    case 14:
+      swap(this->get<14>(), other.get<14>());
+      break;
+    case 15:
+      swap(this->get<15>(), other.get<15>());
+      break;
+    }
+  }
+
+private:
+  enum { data_size = detail::typelist_max<variant_types>::value };
+
+#if variant_CPP11_OR_GREATER
+
+  enum { data_align = detail::typelist_max_alignof<variant_types>::value };
+
+  using aligned_storage_t = typename std::aligned_storage<data_size, data_align>::type;
+  aligned_storage_t data;
+
+#elif variant_CONFIG_MAX_ALIGN_HACK
+
+  typedef union {
+    unsigned char data[data_size];
+  } aligned_storage_t;
+
+  detail::max_align_t hack;
+  aligned_storage_t data;
+
+#else
+  typedef typename detail::typelist_max<variant_types>::type max_type;
+
+  typedef variant_ALIGN_AS(max_type) align_as_type;
+
+  typedef union {
+    align_as_type data[1 + (data_size - 1) / sizeof(align_as_type)];
+  } aligned_storage_t;
+  aligned_storage_t data;
+
+  // #   undef variant_ALIGN_AS
+
+#endif // variant_CONFIG_MAX_ALIGN_HACK
+
+  type_index_t type_index;
+};
+
+// 19.7.5 Value access
+
+template <class T, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+          class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool holds_alternative(
+    variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v)
+    variant_noexcept {
+  return v.index() == variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+                              T15>::template index_of<T>();
+}
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+          class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R &get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &v,
+              nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+  return v.template get<R>();
+}
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+          class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R const &
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+    nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+  return v.template get<R>();
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+          class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+          class T15>
+inline typename variant_alternative<
+    K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type &
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &v,
+    nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+  assert(K == v.index());
+#else
+  if (K != v.index()) {
+    throw bad_variant_access();
+  }
+#endif
+  return v.template get<K>();
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+          class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+          class T15>
+inline typename variant_alternative<
+    K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type const &
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+    nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+  assert(K == v.index());
+#else
+  if (K != v.index()) {
+    throw bad_variant_access();
+  }
+#endif
+  return v.template get<K>();
+}
+
+#if variant_CPP11_OR_GREATER
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+          class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R &&get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &&v,
+               nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+  return std::move(v.template get<R>());
+}
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+          class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R const &&
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &&v,
+    nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+  return std::move(v.template get<R>());
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+          class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+          class T15>
+inline typename variant_alternative<
+    K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type &&
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &&v,
+    nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+  assert(K == v.index());
+#else
+  if (K != v.index()) {
+    throw bad_variant_access();
+  }
+#endif
+  return std::move(v.template get<K>());
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+          class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+          class T15>
+inline typename variant_alternative<
+    K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type const &&
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &&v,
+    nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+  assert(K == v.index());
+#else
+  if (K != v.index()) {
+    throw bad_variant_access();
+  }
+#endif
+  return std::move(v.template get<K>());
+}
+
+#endif // variant_CPP11_OR_GREATER
+
+template <class T, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+          class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline typename std11::add_pointer<T>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> *pv,
+       nonstd_lite_in_place_type_t(T) = nonstd_lite_in_place_type(T)) {
+  return (pv->index() == variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+                                 T15>::template index_of<T>())
+             ? &get<T>(*pv)
+             : variant_nullptr;
+}
+
+template <class T, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+          class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline typename std11::add_pointer<const T>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const *pv,
+       nonstd_lite_in_place_type_t(T) = nonstd_lite_in_place_type(T)) {
+  return (pv->index() == variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+                                 T15>::template index_of<T>())
+             ? &get<T>(*pv)
+             : variant_nullptr;
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+          class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+          class T15>
+inline typename std11::add_pointer<typename variant_alternative<
+    K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> *pv,
+       nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+  return (pv->index() == K) ? &get<K>(*pv) : variant_nullptr;
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+          class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+          class T15>
+inline typename std11::add_pointer<const typename variant_alternative<
+    K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const *pv,
+       nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+  return (pv->index() == K) ? &get<K>(*pv) : variant_nullptr;
+}
+
+// 19.7.10 Specialized algorithms
+
+template <
+    class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+    class T9, class T10, class T11, class T12, class T13, class T14,
+    class T15
+#if variant_CPP11_OR_GREATER
+        variant_REQUIRES_T(
+            std::is_move_constructible<T0>::value &&std17::is_swappable<
+                T0>::value &&std::is_move_constructible<T1>::value &&std17::is_swappable<T1>::value
+                &&std::is_move_constructible<T2>::value &&std17::is_swappable<
+                    T2>::value &&std::is_move_constructible<T3>::value &&std17::is_swappable<T3>::
+                    value &&std::is_move_constructible<T4>::value &&std17::is_swappable<T4>::value
+                        &&std::is_move_constructible<T5>::value &&std17::is_swappable<
+                            T5>::value &&std::is_move_constructible<T6>::value
+                            &&std17::is_swappable<T6>::value &&std::is_move_constructible<
+                                T7>::value &&std17::is_swappable<T7>::value
+                                &&std::is_move_constructible<T8>::value &&std17::is_swappable<
+                                    T8>::value &&std::is_move_constructible<T9>::value
+                                    &&std17::is_swappable<T9>::value &&std::is_move_constructible<
+                                        T10>::value &&std17::is_swappable<T10>::value &&std::
+                                        is_move_constructible<T11>::value &&std17::is_swappable<
+                                            T11>::value &&std::is_move_constructible<T12>::value
+                                            &&std17::is_swappable<
+                                                T12>::value &&std::is_move_constructible<T13>::value
+                                                &&std17::is_swappable<T13>::value
+                                                    &&std::is_move_constructible<T14>::value
+                                                        &&std17::is_swappable<T14>::value
+                                                            &&std::is_move_constructible<T15>::value
+                                                                &&std17::is_swappable<T15>::value)
+#endif
+    >
+inline void swap(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &a,
+                 variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &b)
+#if variant_CPP11_OR_GREATER
+    noexcept(noexcept(a.swap(b)))
+#endif
+{
+  a.swap(b);
+}
+
+// 19.7.7 Visitation
+
+// Variant 'visitor' implementation
+
+namespace detail {
+
+template <typename R, typename VT> struct VisitorApplicatorImpl {
+  template <typename Visitor, typename T> static R apply(Visitor const &v, T const &arg) {
+    return v(arg);
+  }
+};
+
+template <typename R, typename VT> struct VisitorApplicatorImpl<R, TX<VT>> {
+  template <typename Visitor, typename T> static R apply(Visitor const &, T) {
+    // prevent default construction of a const reference, see issue #39:
+    std::terminate();
+  }
+};
+
+template <typename R> struct VisitorApplicator;
+
+template <typename R, typename Visitor, typename V1> struct VisitorUnwrapper;
+
+#if variant_CPP11_OR_GREATER
+template <size_t NumVars, typename R, typename Visitor, typename... T>
+#else
+template <size_t NumVars, typename R, typename Visitor, typename T1, typename T2 = S0,
+          typename T3 = S0, typename T4 = S0, typename T5 = S0>
+#endif
+struct TypedVisitorUnwrapper;
+
+template <typename R, typename Visitor, typename T2>
+struct TypedVisitorUnwrapper<2, R, Visitor, T2> {
+  const Visitor &visitor;
+  T2 const &val2;
+
+  TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_)
+      : visitor(visitor_), val2(val2_)
+
+  {}
+
+  template <typename T> R operator()(const T &val1) const { return visitor(val1, val2); }
+};
+
+template <typename R, typename Visitor, typename T2, typename T3>
+struct TypedVisitorUnwrapper<3, R, Visitor, T2, T3> {
+  const Visitor &visitor;
+  T2 const &val2;
+  T3 const &val3;
+
+  TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_)
+      : visitor(visitor_), val2(val2_), val3(val3_)
+
+  {}
+
+  template <typename T> R operator()(const T &val1) const { return visitor(val1, val2, val3); }
+};
+
+template <typename R, typename Visitor, typename T2, typename T3, typename T4>
+struct TypedVisitorUnwrapper<4, R, Visitor, T2, T3, T4> {
+  const Visitor &visitor;
+  T2 const &val2;
+  T3 const &val3;
+  T4 const &val4;
+
+  TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_, T4 const &val4_)
+      : visitor(visitor_), val2(val2_), val3(val3_), val4(val4_)
+
+  {}
+
+  template <typename T> R operator()(const T &val1) const {
+    return visitor(val1, val2, val3, val4);
+  }
+};
+
+template <typename R, typename Visitor, typename T2, typename T3, typename T4, typename T5>
+struct TypedVisitorUnwrapper<5, R, Visitor, T2, T3, T4, T5> {
+  const Visitor &visitor;
+  T2 const &val2;
+  T3 const &val3;
+  T4 const &val4;
+  T5 const &val5;
+
+  TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_, T4 const &val4_,
+                        T5 const &val5_)
+      : visitor(visitor_), val2(val2_), val3(val3_), val4(val4_), val5(val5_)
+
+  {}
+
+  template <typename T> R operator()(const T &val1) const {
+    return visitor(val1, val2, val3, val4, val5);
+  }
+};
+
+template <typename R, typename Visitor, typename V2> struct VisitorUnwrapper {
+  const Visitor &visitor;
+  const V2 &r;
+
+  VisitorUnwrapper(const Visitor &visitor_, const V2 &r_) : visitor(visitor_), r(r_) {}
+
+  template <typename T1> R operator()(T1 const &val1) const {
+    typedef TypedVisitorUnwrapper<2, R, Visitor, T1> visitor_type;
+    return VisitorApplicator<R>::apply(visitor_type(visitor, val1), r);
+  }
+
+  template <typename T1, typename T2> R operator()(T1 const &val1, T2 const &val2) const {
+    typedef TypedVisitorUnwrapper<3, R, Visitor, T1, T2> visitor_type;
+    return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2), r);
+  }
+
+  template <typename T1, typename T2, typename T3>
+  R operator()(T1 const &val1, T2 const &val2, T3 const &val3) const {
+    typedef TypedVisitorUnwrapper<4, R, Visitor, T1, T2, T3> visitor_type;
+    return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2, val3), r);
+  }
+
+  template <typename T1, typename T2, typename T3, typename T4>
+  R operator()(T1 const &val1, T2 const &val2, T3 const &val3, T4 const &val4) const {
+    typedef TypedVisitorUnwrapper<5, R, Visitor, T1, T2, T3, T4> visitor_type;
+    return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2, val3, val4), r);
+  }
+
+  template <typename T1, typename T2, typename T3, typename T4, typename T5>
+  R operator()(T1 const &val1, T2 const &val2, T3 const &val3, T4 const &val4,
+               T5 const &val5) const {
+    typedef TypedVisitorUnwrapper<6, R, Visitor, T1, T2, T3, T4, T5> visitor_type;
+    return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2, val3, val4, val5), r);
+  }
+};
+
+template <typename R> struct VisitorApplicator {
+  template <typename Visitor, typename V1> static R apply(const Visitor &v, const V1 &arg) {
+    switch (arg.index()) {
+    case 0:
+      return apply_visitor<0>(v, arg);
+    case 1:
+      return apply_visitor<1>(v, arg);
+    case 2:
+      return apply_visitor<2>(v, arg);
+    case 3:
+      return apply_visitor<3>(v, arg);
+    case 4:
+      return apply_visitor<4>(v, arg);
+    case 5:
+      return apply_visitor<5>(v, arg);
+    case 6:
+      return apply_visitor<6>(v, arg);
+    case 7:
+      return apply_visitor<7>(v, arg);
+    case 8:
+      return apply_visitor<8>(v, arg);
+    case 9:
+      return apply_visitor<9>(v, arg);
+    case 10:
+      return apply_visitor<10>(v, arg);
+    case 11:
+      return apply_visitor<11>(v, arg);
+    case 12:
+      return apply_visitor<12>(v, arg);
+    case 13:
+      return apply_visitor<13>(v, arg);
+    case 14:
+      return apply_visitor<14>(v, arg);
+    case 15:
+      return apply_visitor<15>(v, arg);
+
+    // prevent default construction of a const reference, see issue #39:
+    default:
+      std::terminate();
+    }
+  }
+
+  template <size_t Idx, typename Visitor, typename V1>
+  static R apply_visitor(const Visitor &v, const V1 &arg) {
+
+#if variant_CPP11_OR_GREATER
+    typedef typename variant_alternative<Idx, typename std::decay<V1>::type>::type value_type;
+#else
+    typedef typename variant_alternative<Idx, V1>::type value_type;
+#endif
+    return VisitorApplicatorImpl<R, value_type>::apply(v, get<Idx>(arg));
+  }
+
+#if variant_CPP11_OR_GREATER
+  template <typename Visitor, typename V1, typename V2, typename... V>
+  static R apply(const Visitor &v, const V1 &arg1, const V2 &arg2, const V... args) {
+    typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+    Unwrapper unwrapper(v, arg1);
+    return apply(unwrapper, arg2, args...);
+  }
+#else
+
+  template <typename Visitor, typename V1, typename V2>
+  static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2) {
+    typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+    Unwrapper unwrapper(v, arg1);
+    return apply(unwrapper, arg2);
+  }
+
+  template <typename Visitor, typename V1, typename V2, typename V3>
+  static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3) {
+    typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+    Unwrapper unwrapper(v, arg1);
+    return apply(unwrapper, arg2, arg3);
+  }
+
+  template <typename Visitor, typename V1, typename V2, typename V3, typename V4>
+  static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4) {
+    typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+    Unwrapper unwrapper(v, arg1);
+    return apply(unwrapper, arg2, arg3, arg4);
+  }
+
+  template <typename Visitor, typename V1, typename V2, typename V3, typename V4, typename V5>
+  static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4,
+                 V5 const &arg5) {
+    typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+    Unwrapper unwrapper(v, arg1);
+    return apply(unwrapper, arg2, arg3, arg4, arg5);
+  }
+
+#endif
+};
+
+#if variant_CPP11_OR_GREATER
+template <size_t NumVars, typename Visitor, typename... V> struct VisitorImpl {
+  typedef decltype(
+      std::declval<Visitor>()(get<0>(static_cast<const V &>(std::declval<V>()))...)) result_type;
+  typedef VisitorApplicator<result_type> applicator_type;
+};
+#endif
+} // namespace detail
+
+#if variant_CPP11_OR_GREATER
+// No perfect forwarding here in order to simplify code
+template <typename Visitor, typename... V>
+inline auto visit(Visitor const &v, V const &... vars) ->
+    typename detail::VisitorImpl<sizeof...(V), Visitor, V...>::result_type {
+  typedef detail::VisitorImpl<sizeof...(V), Visitor, V...> impl_type;
+  return impl_type::applicator_type::apply(v, vars...);
+}
+#else
+
+template <typename R, typename Visitor, typename V1>
+inline R visit(const Visitor &v, V1 const &arg1) {
+  return detail::VisitorApplicator<R>::apply(v, arg1);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2) {
+  return detail::VisitorApplicator<R>::apply(v, arg1, arg2);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2, typename V3>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3) {
+  return detail::VisitorApplicator<R>::apply(v, arg1, arg2, arg3);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2, typename V3, typename V4>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4) {
+  return detail::VisitorApplicator<R>::apply(v, arg1, arg2, arg3, arg4);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2, typename V3, typename V4,
+          typename V5>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4,
+               V5 const &arg5) {
+  return detail::VisitorApplicator<R>::apply(v, arg1, arg2, arg3, arg4, arg5);
+}
+
+#endif
+
+// 19.7.6 Relational operators
+
+namespace detail {
+
+template <class Variant> struct Comparator {
+  static inline bool equal(Variant const &v, Variant const &w) {
+    switch (v.index()) {
+    case 0:
+      return get<0>(v) == get<0>(w);
+    case 1:
+      return get<1>(v) == get<1>(w);
+    case 2:
+      return get<2>(v) == get<2>(w);
+    case 3:
+      return get<3>(v) == get<3>(w);
+    case 4:
+      return get<4>(v) == get<4>(w);
+    case 5:
+      return get<5>(v) == get<5>(w);
+    case 6:
+      return get<6>(v) == get<6>(w);
+    case 7:
+      return get<7>(v) == get<7>(w);
+    case 8:
+      return get<8>(v) == get<8>(w);
+    case 9:
+      return get<9>(v) == get<9>(w);
+    case 10:
+      return get<10>(v) == get<10>(w);
+    case 11:
+      return get<11>(v) == get<11>(w);
+    case 12:
+      return get<12>(v) == get<12>(w);
+    case 13:
+      return get<13>(v) == get<13>(w);
+    case 14:
+      return get<14>(v) == get<14>(w);
+    case 15:
+      return get<15>(v) == get<15>(w);
+
+    default:
+      return false;
+    }
+  }
+
+  static inline bool less_than(Variant const &v, Variant const &w) {
+    switch (v.index()) {
+    case 0:
+      return get<0>(v) < get<0>(w);
+    case 1:
+      return get<1>(v) < get<1>(w);
+    case 2:
+      return get<2>(v) < get<2>(w);
+    case 3:
+      return get<3>(v) < get<3>(w);
+    case 4:
+      return get<4>(v) < get<4>(w);
+    case 5:
+      return get<5>(v) < get<5>(w);
+    case 6:
+      return get<6>(v) < get<6>(w);
+    case 7:
+      return get<7>(v) < get<7>(w);
+    case 8:
+      return get<8>(v) < get<8>(w);
+    case 9:
+      return get<9>(v) < get<9>(w);
+    case 10:
+      return get<10>(v) < get<10>(w);
+    case 11:
+      return get<11>(v) < get<11>(w);
+    case 12:
+      return get<12>(v) < get<12>(w);
+    case 13:
+      return get<13>(v) < get<13>(w);
+    case 14:
+      return get<14>(v) < get<14>(w);
+    case 15:
+      return get<15>(v) < get<15>(w);
+
+    default:
+      return false;
+    }
+  }
+};
+
+} // namespace detail
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator==(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+           variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+  if (v.index() != w.index())
+    return false;
+  else if (v.valueless_by_exception())
+    return true;
+  else
+    return detail::Comparator<
+        variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::equal(v, w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator!=(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+           variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+  return !(v == w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator<(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+          variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+  if (w.valueless_by_exception())
+    return false;
+  else if (v.valueless_by_exception())
+    return true;
+  else if (v.index() < w.index())
+    return true;
+  else if (v.index() > w.index())
+    return false;
+  else
+    return detail::Comparator<variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
+                                      T14, T15>>::less_than(v, w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator>(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+          variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+  return w < v;
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator<=(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+           variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+  return !(v > w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator>=(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+           variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+  return !(v < w);
+}
+
+} // namespace variants
+
+using namespace variants;
+
+} // namespace nonstd
+
+#if variant_CPP11_OR_GREATER
+
+// 19.7.12 Hash support
+
+namespace std {
+
+template <> struct hash<nonstd::monostate> {
+  std::size_t operator()(nonstd::monostate) const variant_noexcept { return 42; }
+};
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+          class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+struct hash<nonstd::variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>> {
+  std::size_t operator()(nonstd::variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
+                                         T14, T15> const &v) const variant_noexcept {
+    namespace nvd = nonstd::variants::detail;
+
+    switch (v.index()) {
+    case 0:
+      return nvd::hash(0) ^ nvd::hash(get<0>(v));
+    case 1:
+      return nvd::hash(1) ^ nvd::hash(get<1>(v));
+    case 2:
+      return nvd::hash(2) ^ nvd::hash(get<2>(v));
+    case 3:
+      return nvd::hash(3) ^ nvd::hash(get<3>(v));
+    case 4:
+      return nvd::hash(4) ^ nvd::hash(get<4>(v));
+    case 5:
+      return nvd::hash(5) ^ nvd::hash(get<5>(v));
+    case 6:
+      return nvd::hash(6) ^ nvd::hash(get<6>(v));
+    case 7:
+      return nvd::hash(7) ^ nvd::hash(get<7>(v));
+    case 8:
+      return nvd::hash(8) ^ nvd::hash(get<8>(v));
+    case 9:
+      return nvd::hash(9) ^ nvd::hash(get<9>(v));
+    case 10:
+      return nvd::hash(10) ^ nvd::hash(get<10>(v));
+    case 11:
+      return nvd::hash(11) ^ nvd::hash(get<11>(v));
+    case 12:
+      return nvd::hash(12) ^ nvd::hash(get<12>(v));
+    case 13:
+      return nvd::hash(13) ^ nvd::hash(get<13>(v));
+    case 14:
+      return nvd::hash(14) ^ nvd::hash(get<14>(v));
+    case 15:
+      return nvd::hash(15) ^ nvd::hash(get<15>(v));
+
+    default:
+      return 0;
+    }
+  }
+};
+
+} // namespace std
+
+#endif // variant_CPP11_OR_GREATER
+
+#if variant_BETWEEN(variant_COMPILER_MSVC_VER, 1300, 1900)
+#pragma warning(pop)
+#endif
+
+#endif // variant_USES_STD_VARIANT
+
+#endif // NONSTD_VARIANT_LITE_HPP
+//
+// Copyright (c) 2014-2018 Martin Moene
+//
+// https://github.com/martinmoene/optional-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_OPTIONAL_LITE_HPP
+#define NONSTD_OPTIONAL_LITE_HPP
+
+#define optional_lite_MAJOR 3
+#define optional_lite_MINOR 2
+#define optional_lite_PATCH 0
+
+#define optional_lite_VERSION                                                                      \
+  optional_STRINGIFY(optional_lite_MAJOR) "." optional_STRINGIFY(                                  \
+      optional_lite_MINOR) "." optional_STRINGIFY(optional_lite_PATCH)
+
+#define optional_STRINGIFY(x) optional_STRINGIFY_(x)
+#define optional_STRINGIFY_(x) #x
+
+// optional-lite configuration:
+
+#define optional_OPTIONAL_DEFAULT 0
+#define optional_OPTIONAL_NONSTD 1
+#define optional_OPTIONAL_STD 2
+
+#if !defined(optional_CONFIG_SELECT_OPTIONAL)
+#define optional_CONFIG_SELECT_OPTIONAL                                                            \
+  (optional_HAVE_STD_OPTIONAL ? optional_OPTIONAL_STD : optional_OPTIONAL_NONSTD)
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef optional_CONFIG_NO_EXCEPTIONS
+#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#define optional_CONFIG_NO_EXCEPTIONS 0
+#else
+#define optional_CONFIG_NO_EXCEPTIONS 1
+#endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef optional_CPLUSPLUS
+#if defined(_MSVC_LANG) && !defined(__clang__)
+#define optional_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
+#else
+#define optional_CPLUSPLUS __cplusplus
+#endif
+#endif
+
+#define optional_CPP98_OR_GREATER (optional_CPLUSPLUS >= 199711L)
+#define optional_CPP11_OR_GREATER (optional_CPLUSPLUS >= 201103L)
+#define optional_CPP11_OR_GREATER_ (optional_CPLUSPLUS >= 201103L)
+#define optional_CPP14_OR_GREATER (optional_CPLUSPLUS >= 201402L)
+#define optional_CPP17_OR_GREATER (optional_CPLUSPLUS >= 201703L)
+#define optional_CPP20_OR_GREATER (optional_CPLUSPLUS >= 202000L)
+
+// C++ language version (represent 98 as 3):
+
+#define optional_CPLUSPLUS_V                                                                       \
+  (optional_CPLUSPLUS / 100 - (optional_CPLUSPLUS > 200000 ? 2000 : 1994))
+
+// Use C++17 std::optional if available and requested:
+
+#if optional_CPP17_OR_GREATER && defined(__has_include)
+#if __has_include(<optional> )
+#define optional_HAVE_STD_OPTIONAL 1
+#else
+#define optional_HAVE_STD_OPTIONAL 0
+#endif
+#else
+#define optional_HAVE_STD_OPTIONAL 0
+#endif
+
+#define optional_USES_STD_OPTIONAL                                                                 \
+  ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_STD) ||                                   \
+   ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_DEFAULT) && optional_HAVE_STD_OPTIONAL))
+
+//
+// in_place: code duplicated in any-lite, expected-lite, optional-lite, value-ptr-lite,
+// variant-lite:
+//
+
+#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES
+#define nonstd_lite_HAVE_IN_PLACE_TYPES 1
+
+// C++17 std::in_place in <utility>:
+
+#if optional_CPP17_OR_GREATER
+
+#include <utility>
+
+namespace nonstd {
+
+using std::in_place;
+using std::in_place_index;
+using std::in_place_index_t;
+using std::in_place_t;
+using std::in_place_type;
+using std::in_place_type_t;
+
+#define nonstd_lite_in_place_t(T) std::in_place_t
+#define nonstd_lite_in_place_type_t(T) std::in_place_type_t<T>
+#define nonstd_lite_in_place_index_t(K) std::in_place_index_t<K>
+
+#define nonstd_lite_in_place(T)                                                                    \
+  std::in_place_t {}
+#define nonstd_lite_in_place_type(T)                                                               \
+  std::in_place_type_t<T> {}
+#define nonstd_lite_in_place_index(K)                                                              \
+  std::in_place_index_t<K> {}
+
+} // namespace nonstd
+
+#else // optional_CPP17_OR_GREATER
+
+#include <cstddef>
+
+namespace nonstd {
+namespace detail {
+
+template <class T> struct in_place_type_tag {};
+
+template <std::size_t K> struct in_place_index_tag {};
+
+} // namespace detail
+
+struct in_place_t {};
+
+template <class T>
+inline in_place_t
+in_place(detail::in_place_type_tag<T> /*unused*/ = detail::in_place_type_tag<T>()) {
+  return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t
+in_place(detail::in_place_index_tag<K> /*unused*/ = detail::in_place_index_tag<K>()) {
+  return in_place_t();
+}
+
+template <class T>
+inline in_place_t
+in_place_type(detail::in_place_type_tag<T> /*unused*/ = detail::in_place_type_tag<T>()) {
+  return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t
+in_place_index(detail::in_place_index_tag<K> /*unused*/ = detail::in_place_index_tag<K>()) {
+  return in_place_t();
+}
+
+// mimic templated typedef:
+
+#define nonstd_lite_in_place_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_type_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_index_t(K)                                                            \
+  nonstd::in_place_t (&)(nonstd::detail::in_place_index_tag<K>)
+
+#define nonstd_lite_in_place(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_type(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_index(K) nonstd::in_place_index<K>
+
+} // namespace nonstd
+
+#endif // optional_CPP17_OR_GREATER
+#endif // nonstd_lite_HAVE_IN_PLACE_TYPES
+
+//
+// Using std::optional:
+//
+
+#if optional_USES_STD_OPTIONAL
+
+#include <optional>
+
+namespace nonstd {
+
+using std::bad_optional_access;
+using std::hash;
+using std::optional;
+
+using std::nullopt;
+using std::nullopt_t;
+
+using std::operator==;
+using std::operator!=;
+using std::operator<;
+using std::operator<=;
+using std::operator>;
+using std::operator>=;
+using std::make_optional;
+using std::swap;
+} // namespace nonstd
+
+#else // optional_USES_STD_OPTIONAL
+
+#include <cassert>
+#include <utility>
+
+// optional-lite alignment configuration:
+
+#ifndef optional_CONFIG_MAX_ALIGN_HACK
+#define optional_CONFIG_MAX_ALIGN_HACK 0
+#endif
+
+#ifndef optional_CONFIG_ALIGN_AS
+// no default, used in #if defined()
+#endif
+
+#ifndef optional_CONFIG_ALIGN_AS_FALLBACK
+#define optional_CONFIG_ALIGN_AS_FALLBACK double
+#endif
+
+// Compiler warning suppression:
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundef"
+#elif defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wundef"
+#elif defined(_MSC_VER)
+#pragma warning(push)
+#endif
+
+// half-open range [lo..hi):
+#define optional_BETWEEN(v, lo, hi) ((lo) <= (v) && (v) < (hi))
+
+// Compiler versions:
+//
+// MSVC++  6.0  _MSC_VER == 1200  optional_COMPILER_MSVC_VERSION ==  60  (Visual Studio 6.0)
+// MSVC++  7.0  _MSC_VER == 1300  optional_COMPILER_MSVC_VERSION ==  70  (Visual Studio .NET 2002)
+// MSVC++  7.1  _MSC_VER == 1310  optional_COMPILER_MSVC_VERSION ==  71  (Visual Studio .NET 2003)
+// MSVC++  8.0  _MSC_VER == 1400  optional_COMPILER_MSVC_VERSION ==  80  (Visual Studio 2005)
+// MSVC++  9.0  _MSC_VER == 1500  optional_COMPILER_MSVC_VERSION ==  90  (Visual Studio 2008)
+// MSVC++ 10.0  _MSC_VER == 1600  optional_COMPILER_MSVC_VERSION == 100  (Visual Studio 2010)
+// MSVC++ 11.0  _MSC_VER == 1700  optional_COMPILER_MSVC_VERSION == 110  (Visual Studio 2012)
+// MSVC++ 12.0  _MSC_VER == 1800  optional_COMPILER_MSVC_VERSION == 120  (Visual Studio 2013)
+// MSVC++ 14.0  _MSC_VER == 1900  optional_COMPILER_MSVC_VERSION == 140  (Visual Studio 2015)
+// MSVC++ 14.1  _MSC_VER >= 1910  optional_COMPILER_MSVC_VERSION == 141  (Visual Studio 2017)
+// MSVC++ 14.2  _MSC_VER >= 1920  optional_COMPILER_MSVC_VERSION == 142  (Visual Studio 2019)
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define optional_COMPILER_MSVC_VER (_MSC_VER)
+#define optional_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * (5 + (_MSC_VER < 1900)))
+#else
+#define optional_COMPILER_MSVC_VER 0
+#define optional_COMPILER_MSVC_VERSION 0
+#endif
+
+#define optional_COMPILER_VERSION(major, minor, patch) (10 * (10 * (major) + (minor)) + (patch))
+
+#if defined(__GNUC__) && !defined(__clang__)
+#define optional_COMPILER_GNUC_VERSION                                                             \
+  optional_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+#define optional_COMPILER_GNUC_VERSION 0
+#endif
+
+#if defined(__clang__)
+#define optional_COMPILER_CLANG_VERSION                                                            \
+  optional_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+#define optional_COMPILER_CLANG_VERSION 0
+#endif
+
+#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 140)
+#pragma warning(disable : 4345) // initialization behavior changed
+#endif
+
+#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 150)
+#pragma warning(disable : 4814) // in C++14 'constexpr' will not imply 'const'
+#endif
+
+// Presence of language and library features:
+
+#define optional_HAVE(FEATURE) (optional_HAVE_##FEATURE)
+
+#ifdef _HAS_CPP0X
+#define optional_HAS_CPP0X _HAS_CPP0X
+#else
+#define optional_HAS_CPP0X 0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for optional-lite:
+
+#if optional_COMPILER_MSVC_VER >= 1900
+#undef optional_CPP11_OR_GREATER
+#define optional_CPP11_OR_GREATER 1
+#endif
+
+#define optional_CPP11_90 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1500)
+#define optional_CPP11_100 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1600)
+#define optional_CPP11_110 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1700)
+#define optional_CPP11_120 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1800)
+#define optional_CPP11_140 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1900)
+#define optional_CPP11_141 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1910)
+
+#define optional_CPP11_140_490                                                                     \
+  ((optional_CPP11_OR_GREATER_ && optional_COMPILER_GNUC_VERSION >= 490) ||                        \
+   (optional_COMPILER_MSVC_VER >= 1910))
+
+#define optional_CPP14_000 (optional_CPP14_OR_GREATER)
+#define optional_CPP17_000 (optional_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+#define optional_HAVE_CONSTEXPR_11 optional_CPP11_140
+#define optional_HAVE_IS_DEFAULT optional_CPP11_140
+#define optional_HAVE_NOEXCEPT optional_CPP11_140
+#define optional_HAVE_NULLPTR optional_CPP11_100
+#define optional_HAVE_REF_QUALIFIER optional_CPP11_140_490
+#define optional_HAVE_INITIALIZER_LIST optional_CPP11_140
+
+// Presence of C++14 language features:
+
+#define optional_HAVE_CONSTEXPR_14 optional_CPP14_000
+
+// Presence of C++17 language features:
+
+#define optional_HAVE_NODISCARD optional_CPP17_000
+
+// Presence of C++ library features:
+
+#define optional_HAVE_CONDITIONAL optional_CPP11_120
+#define optional_HAVE_REMOVE_CV optional_CPP11_120
+#define optional_HAVE_TYPE_TRAITS optional_CPP11_90
+
+#define optional_HAVE_TR1_TYPE_TRAITS (!!optional_COMPILER_GNUC_VERSION)
+#define optional_HAVE_TR1_ADD_POINTER (!!optional_COMPILER_GNUC_VERSION)
+
+// C++ feature usage:
+
+#if optional_HAVE(CONSTEXPR_11)
+#define optional_constexpr constexpr
+#else
+#define optional_constexpr /*constexpr*/
+#endif
+
+#if optional_HAVE(IS_DEFAULT)
+#define optional_is_default = default;
+#else
+#define optional_is_default                                                                        \
+  {}
+#endif
+
+#if optional_HAVE(CONSTEXPR_14)
+#define optional_constexpr14 constexpr
+#else
+#define optional_constexpr14 /*constexpr*/
+#endif
+
+#if optional_HAVE(NODISCARD)
+#define optional_nodiscard [[nodiscard]]
+#else
+#define optional_nodiscard /*[[nodiscard]]*/
+#endif
+
+#if optional_HAVE(NOEXCEPT)
+#define optional_noexcept noexcept
+#else
+#define optional_noexcept /*noexcept*/
+#endif
+
+#if optional_HAVE(NULLPTR)
+#define optional_nullptr nullptr
+#else
+#define optional_nullptr NULL
+#endif
+
+#if optional_HAVE(REF_QUALIFIER)
+// NOLINTNEXTLINE( bugprone-macro-parentheses )
+#define optional_ref_qual &
+#define optional_refref_qual &&
+#else
+#define optional_ref_qual    /*&*/
+#define optional_refref_qual /*&&*/
+#endif
+
+// additional includes:
+
+#if optional_CONFIG_NO_EXCEPTIONS
+// already included: <cassert>
+#else
+#include <stdexcept>
+#endif
+
+#if optional_CPP11_OR_GREATER
+#include <functional>
+#endif
+
+#if optional_HAVE(INITIALIZER_LIST)
+#include <initializer_list>
+#endif
+
+#if optional_HAVE(TYPE_TRAITS)
+#include <type_traits>
+#elif optional_HAVE(TR1_TYPE_TRAITS)
+#include <tr1/type_traits>
+#endif
+
+// Method enabling
+
+#if optional_CPP11_OR_GREATER
+
+#define optional_REQUIRES_0(...)                                                                   \
+  template <bool B = (__VA_ARGS__), typename std::enable_if<B, int>::type = 0>
+
+#define optional_REQUIRES_T(...) , typename std::enable_if<(__VA_ARGS__), int>::type = 0
+
+#define optional_REQUIRES_R(R, ...) typename std::enable_if<(__VA_ARGS__), R>::type
+
+#define optional_REQUIRES_A(...) , typename std::enable_if<(__VA_ARGS__), void *>::type = nullptr
+
+#endif
+
+//
+// optional:
+//
+
+namespace nonstd {
+namespace optional_lite {
+
+namespace std11 {
+
+#if optional_CPP11_OR_GREATER
+using std::move;
+#else
+template <typename T> T &move(T &t) { return t; }
+#endif
+
+#if optional_HAVE(CONDITIONAL)
+using std::conditional;
+#else
+template <bool B, typename T, typename F> struct conditional { typedef T type; };
+template <typename T, typename F> struct conditional<false, T, F> { typedef F type; };
+#endif // optional_HAVE_CONDITIONAL
+
+// gcc < 5:
+#if optional_CPP11_OR_GREATER
+#if optional_BETWEEN(optional_COMPILER_GNUC_VERSION, 1, 500)
+template <typename T> struct is_trivially_copy_constructible : std::true_type {};
+template <typename T> struct is_trivially_move_constructible : std::true_type {};
+#else
+using std::is_trivially_copy_constructible;
+using std::is_trivially_move_constructible;
+#endif
+#endif
+} // namespace std11
+
+#if optional_CPP11_OR_GREATER
+
+/// type traits C++17:
+
+namespace std17 {
+
+#if optional_CPP17_OR_GREATER
+
+using std::is_nothrow_swappable;
+using std::is_swappable;
+
+#elif optional_CPP11_OR_GREATER
+
+namespace detail {
+
+using std::swap;
+
+struct is_swappable {
+  template <typename T, typename = decltype(swap(std::declval<T &>(), std::declval<T &>()))>
+  static std::true_type test(int /*unused*/);
+
+  template <typename> static std::false_type test(...);
+};
+
+struct is_nothrow_swappable {
+  // wrap noexcept(expr) in separate function as work-around for VC140 (VS2015):
+
+  template <typename T> static constexpr bool satisfies() {
+    return noexcept(swap(std::declval<T &>(), std::declval<T &>()));
+  }
+
+  template <typename T>
+  static auto test(int /*unused*/) -> std::integral_constant<bool, satisfies<T>()> {}
+
+  template <typename> static auto test(...) -> std::false_type;
+};
+
+} // namespace detail
+
+// is [nothow] swappable:
+
+template <typename T> struct is_swappable : decltype(detail::is_swappable::test<T>(0)) {};
+
+template <typename T>
+struct is_nothrow_swappable : decltype(detail::is_nothrow_swappable::test<T>(0)) {};
+
+#endif // optional_CPP17_OR_GREATER
+
+} // namespace std17
+
+/// type traits C++20:
+
+namespace std20 {
+
+template <typename T> struct remove_cvref {
+  typedef typename std::remove_cv<typename std::remove_reference<T>::type>::type type;
+};
+
+} // namespace std20
+
+#endif // optional_CPP11_OR_GREATER
+
+/// class optional
+
+template <typename T> class optional;
+
+namespace detail {
+
+// C++11 emulation:
+
+struct nulltype {};
+
+template <typename Head, typename Tail> struct typelist {
+  typedef Head head;
+  typedef Tail tail;
+};
+
+#if optional_CONFIG_MAX_ALIGN_HACK
+
+// Max align, use most restricted type for alignment:
+
+#define optional_UNIQUE(name) optional_UNIQUE2(name, __LINE__)
+#define optional_UNIQUE2(name, line) optional_UNIQUE3(name, line)
+#define optional_UNIQUE3(name, line) name##line
+
+#define optional_ALIGN_TYPE(type)                                                                  \
+  type optional_UNIQUE(_t);                                                                        \
+  struct_t<type> optional_UNIQUE(_st)
+
+template <typename T> struct struct_t { T _; };
+
+union max_align_t {
+  optional_ALIGN_TYPE(char);
+  optional_ALIGN_TYPE(short int);
+  optional_ALIGN_TYPE(int);
+  optional_ALIGN_TYPE(long int);
+  optional_ALIGN_TYPE(float);
+  optional_ALIGN_TYPE(double);
+  optional_ALIGN_TYPE(long double);
+  optional_ALIGN_TYPE(char *);
+  optional_ALIGN_TYPE(short int *);
+  optional_ALIGN_TYPE(int *);
+  optional_ALIGN_TYPE(long int *);
+  optional_ALIGN_TYPE(float *);
+  optional_ALIGN_TYPE(double *);
+  optional_ALIGN_TYPE(long double *);
+  optional_ALIGN_TYPE(void *);
+
+#ifdef HAVE_LONG_LONG
+  optional_ALIGN_TYPE(long long);
+#endif
+
+  struct Unknown;
+
+  Unknown (*optional_UNIQUE(_))(Unknown);
+  Unknown *Unknown::*optional_UNIQUE(_);
+  Unknown (Unknown::*optional_UNIQUE(_))(Unknown);
+
+  struct_t<Unknown (*)(Unknown)> optional_UNIQUE(_);
+  struct_t<Unknown * Unknown::*> optional_UNIQUE(_);
+  struct_t<Unknown (Unknown::*)(Unknown)> optional_UNIQUE(_);
+};
+
+#undef optional_UNIQUE
+#undef optional_UNIQUE2
+#undef optional_UNIQUE3
+
+#undef optional_ALIGN_TYPE
+
+#elif defined(optional_CONFIG_ALIGN_AS) // optional_CONFIG_MAX_ALIGN_HACK
+
+// Use user-specified type for alignment:
+
+#define optional_ALIGN_AS(unused) optional_CONFIG_ALIGN_AS
+
+#else // optional_CONFIG_MAX_ALIGN_HACK
+
+// Determine POD type to use for alignment:
+
+#define optional_ALIGN_AS(to_align)                                                                \
+  typename type_of_size<alignment_types, alignment_of<to_align>::value>::type
+
+template <typename T> struct alignment_of;
+
+template <typename T> struct alignment_of_hack {
+  char c;
+  T t;
+  alignment_of_hack();
+};
+
+template <size_t A, size_t S> struct alignment_logic {
+  enum { value = A < S ? A : S };
+};
+
+template <typename T> struct alignment_of {
+  enum { value = alignment_logic<sizeof(alignment_of_hack<T>) - sizeof(T), sizeof(T)>::value };
+};
+
+template <typename List, size_t N> struct type_of_size {
+  typedef
+      typename std11::conditional<N == sizeof(typename List::head), typename List::head,
+                                  typename type_of_size<typename List::tail, N>::type>::type type;
+};
+
+template <size_t N> struct type_of_size<nulltype, N> {
+  typedef optional_CONFIG_ALIGN_AS_FALLBACK type;
+};
+
+template <typename T> struct struct_t { T _; };
+
+#define optional_ALIGN_TYPE(type) typelist < type, typelist < struct_t<type>
+
+struct Unknown;
+
+typedef optional_ALIGN_TYPE(char), optional_ALIGN_TYPE(short), optional_ALIGN_TYPE(int),
+    optional_ALIGN_TYPE(long), optional_ALIGN_TYPE(float), optional_ALIGN_TYPE(double),
+    optional_ALIGN_TYPE(long double),
+
+    optional_ALIGN_TYPE(char *), optional_ALIGN_TYPE(short *), optional_ALIGN_TYPE(int *),
+    optional_ALIGN_TYPE(long *), optional_ALIGN_TYPE(float *), optional_ALIGN_TYPE(double *),
+    optional_ALIGN_TYPE(long double *),
+
+    optional_ALIGN_TYPE(Unknown (*)(Unknown)), optional_ALIGN_TYPE(Unknown *Unknown::*),
+    optional_ALIGN_TYPE(Unknown (Unknown::*)(Unknown)),
+
+    nulltype >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> alignment_types;
+
+#undef optional_ALIGN_TYPE
+
+#endif // optional_CONFIG_MAX_ALIGN_HACK
+
+/// C++03 constructed union to hold value.
+
+template <typename T> union storage_t {
+  // private:
+  //    template< typename > friend class optional;
+
+  typedef T value_type;
+
+  storage_t() optional_is_default
+
+      explicit storage_t(value_type const &v) {
+    construct_value(v);
+  }
+
+  void construct_value(value_type const &v) { ::new (value_ptr()) value_type(v); }
+
+#if optional_CPP11_OR_GREATER
+
+  explicit storage_t(value_type &&v) { construct_value(std::move(v)); }
+
+  void construct_value(value_type &&v) { ::new (value_ptr()) value_type(std::move(v)); }
+
+  template <class... Args> void emplace(Args &&... args) {
+    ::new (value_ptr()) value_type(std::forward<Args>(args)...);
+  }
+
+  template <class U, class... Args> void emplace(std::initializer_list<U> il, Args &&... args) {
+    ::new (value_ptr()) value_type(il, std::forward<Args>(args)...);
+  }
+
+#endif
+
+  void destruct_value() { value_ptr()->~T(); }
+
+  optional_nodiscard value_type const *value_ptr() const { return as<value_type>(); }
+
+  value_type *value_ptr() { return as<value_type>(); }
+
+  optional_nodiscard value_type const &value() const optional_ref_qual { return *value_ptr(); }
+
+  value_type &value() optional_ref_qual { return *value_ptr(); }
+
+#if optional_HAVE(REF_QUALIFIER)
+
+  optional_nodiscard value_type const &&value() const optional_refref_qual {
+    return std::move(value());
+  }
+
+  value_type &&value() optional_refref_qual { return std::move(value()); }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+
+  using aligned_storage_t =
+      typename std::aligned_storage<sizeof(value_type), alignof(value_type)>::type;
+  aligned_storage_t data;
+
+#elif optional_CONFIG_MAX_ALIGN_HACK
+
+  typedef struct {
+    unsigned char data[sizeof(value_type)];
+  } aligned_storage_t;
+
+  max_align_t hack;
+  aligned_storage_t data;
+
+#else
+  typedef optional_ALIGN_AS(value_type) align_as_type;
+
+  typedef struct {
+    align_as_type data[1 + (sizeof(value_type) - 1) / sizeof(align_as_type)];
+  } aligned_storage_t;
+  aligned_storage_t data;
+
+#undef optional_ALIGN_AS
+
+#endif // optional_CONFIG_MAX_ALIGN_HACK
+
+  optional_nodiscard void *ptr() optional_noexcept { return &data; }
+
+  optional_nodiscard void const *ptr() const optional_noexcept { return &data; }
+
+  template <typename U> optional_nodiscard U *as() { return reinterpret_cast<U *>(ptr()); }
+
+  template <typename U> optional_nodiscard U const *as() const {
+    return reinterpret_cast<U const *>(ptr());
+  }
+};
+
+} // namespace detail
+
+/// disengaged state tag
+
+struct nullopt_t {
+  struct init {};
+  explicit optional_constexpr nullopt_t(init /*unused*/) optional_noexcept {}
+};
+
+#if optional_HAVE(CONSTEXPR_11)
+constexpr nullopt_t nullopt{nullopt_t::init{}};
+#else
+// extra parenthesis to prevent the most vexing parse:
+const nullopt_t nullopt((nullopt_t::init()));
+#endif
+
+/// optional access error
+
+#if !optional_CONFIG_NO_EXCEPTIONS
+
+class bad_optional_access : public std::logic_error {
+public:
+  explicit bad_optional_access() : logic_error("bad optional access") {}
+};
+
+#endif // optional_CONFIG_NO_EXCEPTIONS
+
+/// optional
+
+template <typename T> class optional {
+private:
+  template <typename> friend class optional;
+
+  typedef void (optional::*safe_bool)() const;
+
+public:
+  typedef T value_type;
+
+  // x.x.3.1, constructors
+
+  // 1a - default construct
+  optional_constexpr optional() optional_noexcept : has_value_(false), contained() {}
+
+  // 1b - construct explicitly empty
+  // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+  optional_constexpr optional(nullopt_t /*unused*/) optional_noexcept : has_value_(false),
+                                                                        contained() {}
+
+  // 2 - copy-construct
+#if optional_CPP11_OR_GREATER
+  // template< typename U = T
+  //     optional_REQUIRES_T(
+  //         std::is_copy_constructible<U>::value
+  //         || std11::is_trivially_copy_constructible<U>::value
+  //     )
+  // >
+#endif
+  optional_constexpr14 optional(optional const &other) : has_value_(other.has_value()) {
+    if (other.has_value()) {
+      contained.construct_value(other.contained.value());
+    }
+  }
+
+#if optional_CPP11_OR_GREATER
+
+  // 3 (C++11) - move-construct from optional
+  template <typename U = T optional_REQUIRES_T(std::is_move_constructible<U>::value ||
+                                               std11::is_trivially_move_constructible<U>::value)>
+  optional_constexpr14 optional(optional &&other)
+      // NOLINTNEXTLINE( performance-noexcept-move-constructor )
+      noexcept(std::is_nothrow_move_constructible<T>::value)
+      : has_value_(other.has_value()) {
+    if (other.has_value()) {
+      contained.construct_value(std::move(other.contained.value()));
+    }
+  }
+
+  // 4a (C++11) - explicit converting copy-construct from optional
+  template <typename U optional_REQUIRES_T(std::is_constructible<T, U const &>::value &&
+                                           !std::is_constructible<T, optional<U> &>::value &&
+                                           !std::is_constructible<T, optional<U> &&>::value &&
+                                           !std::is_constructible<T, optional<U> const &>::value &&
+                                           !std::is_constructible<T, optional<U> const &&>::value &&
+                                           !std::is_convertible<optional<U> &, T>::value &&
+                                           !std::is_convertible<optional<U> &&, T>::value &&
+                                           !std::is_convertible<optional<U> const &, T>::value &&
+                                           !std::is_convertible<optional<U> const &&, T>::value &&
+                                           !std::is_convertible<U const &, T>::value /*=> explicit
+                                                                                      */
+                                           )>
+  explicit optional(optional<U> const &other) : has_value_(other.has_value()) {
+    if (other.has_value()) {
+      contained.construct_value(T{other.contained.value()});
+    }
+  }
+#endif // optional_CPP11_OR_GREATER
+
+  // 4b (C++98 and later) - non-explicit converting copy-construct from optional
+  template <typename U
+#if optional_CPP11_OR_GREATER
+                optional_REQUIRES_T(std::is_constructible<T, U const &>::value &&
+                                    !std::is_constructible<T, optional<U> &>::value &&
+                                    !std::is_constructible<T, optional<U> &&>::value &&
+                                    !std::is_constructible<T, optional<U> const &>::value &&
+                                    !std::is_constructible<T, optional<U> const &&>::value &&
+                                    !std::is_convertible<optional<U> &, T>::value &&
+                                    !std::is_convertible<optional<U> &&, T>::value &&
+                                    !std::is_convertible<optional<U> const &, T>::value &&
+                                    !std::is_convertible<optional<U> const &&, T>::value &&
+                                    std::is_convertible<U const &, T>::value /*=> non-explicit */
+                                    )
+#endif // optional_CPP11_OR_GREATER
+            >
+  // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+  /*non-explicit*/ optional(optional<U> const &other) : has_value_(other.has_value()) {
+    if (other.has_value()) {
+      contained.construct_value(other.contained.value());
+    }
+  }
+
+#if optional_CPP11_OR_GREATER
+
+  // 5a (C++11) - explicit converting move-construct from optional
+  template <typename U optional_REQUIRES_T(std::is_constructible<T, U &&>::value &&
+                                           !std::is_constructible<T, optional<U> &>::value &&
+                                           !std::is_constructible<T, optional<U> &&>::value &&
+                                           !std::is_constructible<T, optional<U> const &>::value &&
+                                           !std::is_constructible<T, optional<U> const &&>::value &&
+                                           !std::is_convertible<optional<U> &, T>::value &&
+                                           !std::is_convertible<optional<U> &&, T>::value &&
+                                           !std::is_convertible<optional<U> const &, T>::value &&
+                                           !std::is_convertible<optional<U> const &&, T>::value &&
+                                           !std::is_convertible<U &&, T>::value /*=> explicit */
+                                           )>
+  explicit optional(optional<U> &&other) : has_value_(other.has_value()) {
+    if (other.has_value()) {
+      contained.construct_value(T{std::move(other.contained.value())});
+    }
+  }
+
+  // 5a (C++11) - non-explicit converting move-construct from optional
+  template <typename U optional_REQUIRES_T(std::is_constructible<T, U &&>::value &&
+                                           !std::is_constructible<T, optional<U> &>::value &&
+                                           !std::is_constructible<T, optional<U> &&>::value &&
+                                           !std::is_constructible<T, optional<U> const &>::value &&
+                                           !std::is_constructible<T, optional<U> const &&>::value &&
+                                           !std::is_convertible<optional<U> &, T>::value &&
+                                           !std::is_convertible<optional<U> &&, T>::value &&
+                                           !std::is_convertible<optional<U> const &, T>::value &&
+                                           !std::is_convertible<optional<U> const &&, T>::value &&
+                                           std::is_convertible<U &&, T>::value /*=> non-explicit */
+                                           )>
+  // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+  /*non-explicit*/ optional(optional<U> &&other) : has_value_(other.has_value()) {
+    if (other.has_value()) {
+      contained.construct_value(std::move(other.contained.value()));
+    }
+  }
+
+  // 6 (C++11) - in-place construct
+  template <typename... Args optional_REQUIRES_T(std::is_constructible<T, Args &&...>::value)>
+  optional_constexpr explicit optional(nonstd_lite_in_place_t(T), Args &&... args)
+      : has_value_(true), contained(T(std::forward<Args>(args)...)) {}
+
+  // 7 (C++11) - in-place construct,  initializer-list
+  template <typename U,
+            typename... Args optional_REQUIRES_T(
+                std::is_constructible<T, std::initializer_list<U> &, Args &&...>::value)>
+  optional_constexpr explicit optional(nonstd_lite_in_place_t(T), std::initializer_list<U> il,
+                                       Args &&... args)
+      : has_value_(true), contained(T(il, std::forward<Args>(args)...)) {}
+
+  // 8a (C++11) - explicit move construct from value
+  template <
+      typename U = T optional_REQUIRES_T(
+          std::is_constructible<T, U &&>::value &&
+          !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value &&
+          !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value &&
+          !std::is_convertible<U &&, T>::value /*=> explicit */
+          )>
+  optional_constexpr explicit optional(U &&value)
+      : has_value_(true), contained(T{std::forward<U>(value)}) {}
+
+  // 8b (C++11) - non-explicit move construct from value
+  template <
+      typename U = T optional_REQUIRES_T(
+          std::is_constructible<T, U &&>::value &&
+          !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value &&
+          !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value &&
+          std::is_convertible<U &&, T>::value /*=> non-explicit */
+          )>
+  // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+  optional_constexpr /*non-explicit*/ optional(U &&value)
+      : has_value_(true), contained(std::forward<U>(value)) {}
+
+#else // optional_CPP11_OR_GREATER
+
+  // 8 (C++98)
+  optional(value_type const &value) : has_value_(true), contained(value) {}
+
+#endif // optional_CPP11_OR_GREATER
+
+  // x.x.3.2, destructor
+
+  ~optional() {
+    if (has_value()) {
+      contained.destruct_value();
+    }
+  }
+
+  // x.x.3.3, assignment
+
+  // 1 (C++98and later) -  assign explicitly empty
+  optional &operator=(nullopt_t /*unused*/) optional_noexcept {
+    reset();
+    return *this;
+  }
+
+  // 2 (C++98and later) - copy-assign from optional
+#if optional_CPP11_OR_GREATER
+  // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+  // misc-unconventional-assign-operator )
+  optional_REQUIRES_R(optional &, true
+                      //      std::is_copy_constructible<T>::value
+                      //      && std::is_copy_assignable<T>::value
+                      )
+  operator=(optional const &other) noexcept(
+      std::is_nothrow_move_assignable<T>::value &&std::is_nothrow_move_constructible<T>::value)
+#else
+  optional &operator=(optional const &other)
+#endif
+  {
+    if ((has_value() == true) && (other.has_value() == false)) {
+      reset();
+    } else if ((has_value() == false) && (other.has_value() == true)) {
+      initialize(*other);
+    } else if ((has_value() == true) && (other.has_value() == true)) {
+      contained.value() = *other;
+    }
+    return *this;
+  }
+
+#if optional_CPP11_OR_GREATER
+
+  // 3 (C++11) - move-assign from optional
+  // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+  // misc-unconventional-assign-operator )
+  optional_REQUIRES_R(optional &, true
+                      //      std::is_move_constructible<T>::value
+                      //      && std::is_move_assignable<T>::value
+                      )
+  operator=(optional &&other) noexcept {
+    if ((has_value() == true) && (other.has_value() == false)) {
+      reset();
+    } else if ((has_value() == false) && (other.has_value() == true)) {
+      initialize(std::move(*other));
+    } else if ((has_value() == true) && (other.has_value() == true)) {
+      contained.value() = std::move(*other);
+    }
+    return *this;
+  }
+
+  // 4 (C++11) - move-assign from value
+  template <typename U = T>
+  // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+  // misc-unconventional-assign-operator )
+  optional_REQUIRES_R(
+      optional &,
+      std::is_constructible<T, U>::value &&std::is_assignable<T &, U>::value &&
+          !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value &&
+          !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value &&
+          !(std::is_scalar<T>::value && std::is_same<T, typename std::decay<U>::type>::value))
+  operator=(U &&value) {
+    if (has_value()) {
+      contained.value() = std::forward<U>(value);
+    } else {
+      initialize(T(std::forward<U>(value)));
+    }
+    return *this;
+  }
+
+#else // optional_CPP11_OR_GREATER
+
+  // 4 (C++98) - copy-assign from value
+  template <typename U /*= T*/> optional &operator=(U const &value) {
+    if (has_value())
+      contained.value() = value;
+    else
+      initialize(T(value));
+    return *this;
+  }
+
+#endif // optional_CPP11_OR_GREATER
+
+  // 5 (C++98 and later) - converting copy-assign from optional
+  template <typename U>
+#if optional_CPP11_OR_GREATER
+  // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+  // misc-unconventional-assign-operator )
+  optional_REQUIRES_R(
+      optional &,
+      std::is_constructible<T, U const &>::value &&std::is_assignable<T &, U const &>::value &&
+          !std::is_constructible<T, optional<U> &>::value &&
+          !std::is_constructible<T, optional<U> &&>::value &&
+          !std::is_constructible<T, optional<U> const &>::value &&
+          !std::is_constructible<T, optional<U> const &&>::value &&
+          !std::is_convertible<optional<U> &, T>::value &&
+          !std::is_convertible<optional<U> &&, T>::value &&
+          !std::is_convertible<optional<U> const &, T>::value &&
+          !std::is_convertible<optional<U> const &&, T>::value &&
+          !std::is_assignable<T &, optional<U> &>::value &&
+          !std::is_assignable<T &, optional<U> &&>::value &&
+          !std::is_assignable<T &, optional<U> const &>::value &&
+          !std::is_assignable<T &, optional<U> const &&>::value)
+#else
+  optional &
+#endif // optional_CPP11_OR_GREATER
+  operator=(optional<U> const &other) {
+    return *this = optional(other);
+  }
+
+#if optional_CPP11_OR_GREATER
+
+  // 6 (C++11) -  converting move-assign from optional
+  template <typename U>
+  // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+  // misc-unconventional-assign-operator )
+  optional_REQUIRES_R(optional &,
+                      std::is_constructible<T, U>::value &&std::is_assignable<T &, U>::value &&
+                          !std::is_constructible<T, optional<U> &>::value &&
+                          !std::is_constructible<T, optional<U> &&>::value &&
+                          !std::is_constructible<T, optional<U> const &>::value &&
+                          !std::is_constructible<T, optional<U> const &&>::value &&
+                          !std::is_convertible<optional<U> &, T>::value &&
+                          !std::is_convertible<optional<U> &&, T>::value &&
+                          !std::is_convertible<optional<U> const &, T>::value &&
+                          !std::is_convertible<optional<U> const &&, T>::value &&
+                          !std::is_assignable<T &, optional<U> &>::value &&
+                          !std::is_assignable<T &, optional<U> &&>::value &&
+                          !std::is_assignable<T &, optional<U> const &>::value &&
+                          !std::is_assignable<T &, optional<U> const &&>::value)
+  operator=(optional<U> &&other) {
+    return *this = optional(std::move(other));
+  }
+
+  // 7 (C++11) - emplace
+  template <typename... Args optional_REQUIRES_T(std::is_constructible<T, Args &&...>::value)>
+  T &emplace(Args &&... args) {
+    *this = nullopt;
+    contained.emplace(std::forward<Args>(args)...);
+    has_value_ = true;
+    return contained.value();
+  }
+
+  // 8 (C++11) - emplace, initializer-list
+  template <typename U,
+            typename... Args optional_REQUIRES_T(
+                std::is_constructible<T, std::initializer_list<U> &, Args &&...>::value)>
+  T &emplace(std::initializer_list<U> il, Args &&... args) {
+    *this = nullopt;
+    contained.emplace(il, std::forward<Args>(args)...);
+    has_value_ = true;
+    return contained.value();
+  }
+
+#endif // optional_CPP11_OR_GREATER
+
+  // x.x.3.4, swap
+
+  void swap(optional &other)
+#if optional_CPP11_OR_GREATER
+      noexcept(std::is_nothrow_move_constructible<T>::value &&std17::is_nothrow_swappable<T>::value)
+#endif
+  {
+    using std::swap;
+    if ((has_value() == true) && (other.has_value() == true)) {
+      swap(**this, *other);
+    } else if ((has_value() == false) && (other.has_value() == true)) {
+      initialize(std11::move(*other));
+      other.reset();
+    } else if ((has_value() == true) && (other.has_value() == false)) {
+      other.initialize(std11::move(**this));
+      reset();
+    }
+  }
+
+  // x.x.3.5, observers
+
+  optional_constexpr value_type const *operator->() const {
+    return assert(has_value()), contained.value_ptr();
+  }
+
+  optional_constexpr14 value_type *operator->() {
+    return assert(has_value()), contained.value_ptr();
+  }
+
+  optional_constexpr value_type const &operator*() const optional_ref_qual {
+    return assert(has_value()), contained.value();
+  }
+
+  optional_constexpr14 value_type &operator*() optional_ref_qual {
+    return assert(has_value()), contained.value();
+  }
+
+#if optional_HAVE(REF_QUALIFIER)
+
+  optional_constexpr value_type const &&operator*() const optional_refref_qual {
+    return std::move(**this);
+  }
+
+  optional_constexpr14 value_type &&operator*() optional_refref_qual { return std::move(**this); }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+  optional_constexpr explicit operator bool() const optional_noexcept { return has_value(); }
+#else
+  optional_constexpr operator safe_bool() const optional_noexcept {
+    return has_value() ? &optional::this_type_does_not_support_comparisons : 0;
+  }
+#endif
+
+  // NOLINTNEXTLINE( modernize-use-nodiscard )
+  /*optional_nodiscard*/ optional_constexpr bool has_value() const optional_noexcept {
+    return has_value_;
+  }
+
+  // NOLINTNEXTLINE( modernize-use-nodiscard )
+  /*optional_nodiscard*/ optional_constexpr14 value_type const &value() const optional_ref_qual {
+#if optional_CONFIG_NO_EXCEPTIONS
+    assert(has_value());
+#else
+    if (!has_value()) {
+      throw bad_optional_access();
+    }
+#endif
+    return contained.value();
+  }
+
+  optional_constexpr14 value_type &value() optional_ref_qual {
+#if optional_CONFIG_NO_EXCEPTIONS
+    assert(has_value());
+#else
+    if (!has_value()) {
+      throw bad_optional_access();
+    }
+#endif
+    return contained.value();
+  }
+
+#if optional_HAVE(REF_QUALIFIER) &&                                                                \
+    (!optional_COMPILER_GNUC_VERSION || optional_COMPILER_GNUC_VERSION >= 490)
+
+  // NOLINTNEXTLINE( modernize-use-nodiscard )
+  /*optional_nodiscard*/ optional_constexpr value_type const &&value() const optional_refref_qual {
+    return std::move(value());
+  }
+
+  optional_constexpr14 value_type &&value() optional_refref_qual { return std::move(value()); }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+
+  template <typename U> optional_constexpr value_type value_or(U &&v) const optional_ref_qual {
+    return has_value() ? contained.value() : static_cast<T>(std::forward<U>(v));
+  }
+
+  template <typename U> optional_constexpr14 value_type value_or(U &&v) optional_refref_qual {
+    return has_value() ? std::move(contained.value()) : static_cast<T>(std::forward<U>(v));
+  }
+
+#else
+
+  template <typename U> optional_constexpr value_type value_or(U const &v) const {
+    return has_value() ? contained.value() : static_cast<value_type>(v);
+  }
+
+#endif // optional_CPP11_OR_GREATER
+
+  // x.x.3.6, modifiers
+
+  void reset() optional_noexcept {
+    if (has_value()) {
+      contained.destruct_value();
+    }
+
+    has_value_ = false;
+  }
+
+private:
+  void this_type_does_not_support_comparisons() const {}
+
+  template <typename V> void initialize(V const &value) {
+    assert(!has_value());
+    contained.construct_value(value);
+    has_value_ = true;
+  }
+
+#if optional_CPP11_OR_GREATER
+  template <typename V> void initialize(V &&value) {
+    assert(!has_value());
+    contained.construct_value(std::move(value));
+    has_value_ = true;
+  }
+
+#endif
+
+private:
+  bool has_value_;
+  detail::storage_t<value_type> contained;
+};
+
+// Relational operators
+
+template <typename T, typename U>
+inline optional_constexpr bool operator==(optional<T> const &x, optional<U> const &y) {
+  return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator!=(optional<T> const &x, optional<U> const &y) {
+  return !(x == y);
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<(optional<T> const &x, optional<U> const &y) {
+  return (!y) ? false : (!x) ? true : *x < *y;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>(optional<T> const &x, optional<U> const &y) {
+  return (y < x);
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<=(optional<T> const &x, optional<U> const &y) {
+  return !(y < x);
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>=(optional<T> const &x, optional<U> const &y) {
+  return !(x < y);
+}
+
+// Comparison with nullopt
+
+template <typename T>
+inline optional_constexpr bool operator==(optional<T> const &x,
+                                          nullopt_t /*unused*/) optional_noexcept {
+  return (!x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator==(nullopt_t /*unused*/,
+                                          optional<T> const &x) optional_noexcept {
+  return (!x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator!=(optional<T> const &x,
+                                          nullopt_t /*unused*/) optional_noexcept {
+  return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator!=(nullopt_t /*unused*/,
+                                          optional<T> const &x) optional_noexcept {
+  return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator<(optional<T> const & /*unused*/,
+                                         nullopt_t /*unused*/) optional_noexcept {
+  return false;
+}
+
+template <typename T>
+inline optional_constexpr bool operator<(nullopt_t /*unused*/,
+                                         optional<T> const &x) optional_noexcept {
+  return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator<=(optional<T> const &x,
+                                          nullopt_t /*unused*/) optional_noexcept {
+  return (!x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator<=(nullopt_t /*unused*/,
+                                          optional<T> const & /*unused*/) optional_noexcept {
+  return true;
+}
+
+template <typename T>
+inline optional_constexpr bool operator>(optional<T> const &x,
+                                         nullopt_t /*unused*/) optional_noexcept {
+  return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator>(nullopt_t /*unused*/,
+                                         optional<T> const & /*unused*/) optional_noexcept {
+  return false;
+}
+
+template <typename T>
+inline optional_constexpr bool operator>=(optional<T> const & /*unused*/,
+                                          nullopt_t /*unused*/) optional_noexcept {
+  return true;
+}
+
+template <typename T>
+inline optional_constexpr bool operator>=(nullopt_t /*unused*/,
+                                          optional<T> const &x) optional_noexcept {
+  return (!x);
+}
+
+// Comparison with T
+
+template <typename T, typename U>
+inline optional_constexpr bool operator==(optional<T> const &x, U const &v) {
+  return bool(x) ? *x == v : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator==(U const &v, optional<T> const &x) {
+  return bool(x) ? v == *x : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator!=(optional<T> const &x, U const &v) {
+  return bool(x) ? *x != v : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator!=(U const &v, optional<T> const &x) {
+  return bool(x) ? v != *x : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<(optional<T> const &x, U const &v) {
+  return bool(x) ? *x < v : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<(U const &v, optional<T> const &x) {
+  return bool(x) ? v < *x : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<=(optional<T> const &x, U const &v) {
+  return bool(x) ? *x <= v : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<=(U const &v, optional<T> const &x) {
+  return bool(x) ? v <= *x : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>(optional<T> const &x, U const &v) {
+  return bool(x) ? *x > v : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>(U const &v, optional<T> const &x) {
+  return bool(x) ? v > *x : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>=(optional<T> const &x, U const &v) {
+  return bool(x) ? *x >= v : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>=(U const &v, optional<T> const &x) {
+  return bool(x) ? v >= *x : true;
+}
+
+// Specialized algorithms
+
+template <
+    typename T
+#if optional_CPP11_OR_GREATER
+        optional_REQUIRES_T(std::is_move_constructible<T>::value &&std17::is_swappable<T>::value)
+#endif
+    >
+void swap(optional<T> &x, optional<T> &y)
+#if optional_CPP11_OR_GREATER
+    noexcept(noexcept(x.swap(y)))
+#endif
+{
+  x.swap(y);
+}
+
+#if optional_CPP11_OR_GREATER
+
+template <typename T>
+optional_constexpr optional<typename std::decay<T>::type> make_optional(T &&value) {
+  return optional<typename std::decay<T>::type>(std::forward<T>(value));
+}
+
+template <typename T, typename... Args>
+optional_constexpr optional<T> make_optional(Args &&... args) {
+  return optional<T>(nonstd_lite_in_place(T), std::forward<Args>(args)...);
+}
+
+template <typename T, typename U, typename... Args>
+optional_constexpr optional<T> make_optional(std::initializer_list<U> il, Args &&... args) {
+  return optional<T>(nonstd_lite_in_place(T), il, std::forward<Args>(args)...);
+}
+
+#else
+
+template <typename T> optional<T> make_optional(T const &value) { return optional<T>(value); }
+
+#endif // optional_CPP11_OR_GREATER
+
+} // namespace optional_lite
+
+using optional_lite::nullopt;
+using optional_lite::nullopt_t;
+using optional_lite::optional;
+
+#if !optional_CONFIG_NO_EXCEPTIONS
+using optional_lite::bad_optional_access;
+#endif
+
+using optional_lite::make_optional;
+
+} // namespace nonstd
+
+#if optional_CPP11_OR_GREATER
+
+// specialize the std::hash algorithm:
+
+namespace std {
+
+template <class T> struct hash<nonstd::optional<T>> {
+public:
+  std::size_t operator()(nonstd::optional<T> const &v) const optional_noexcept {
+    return bool(v) ? std::hash<T>{}(*v) : 0;
+  }
+};
+
+} // namespace std
+
+#endif // optional_CPP11_OR_GREATER
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#elif defined(__GNUC__)
+#pragma GCC diagnostic pop
+#elif defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+#endif // optional_USES_STD_OPTIONAL
+
+#endif // NONSTD_OPTIONAL_LITE_HPP
+// Copyright 2017-2020 by Martin Moene
+//
+// string-view lite, a C++17-like string_view for C++98 and later.
+// For more information see https://github.com/martinmoene/string-view-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_SV_LITE_H_INCLUDED
+#define NONSTD_SV_LITE_H_INCLUDED
+
+#define string_view_lite_MAJOR  1
+#define string_view_lite_MINOR  6
+#define string_view_lite_PATCH  0
+
+#define string_view_lite_VERSION  nssv_STRINGIFY(string_view_lite_MAJOR) "." nssv_STRINGIFY(string_view_lite_MINOR) "." nssv_STRINGIFY(string_view_lite_PATCH)
+
+#define nssv_STRINGIFY(  x )  nssv_STRINGIFY_( x )
+#define nssv_STRINGIFY_( x )  #x
+
+// string-view lite configuration:
+
+#define nssv_STRING_VIEW_DEFAULT  0
+#define nssv_STRING_VIEW_NONSTD   1
+#define nssv_STRING_VIEW_STD      2
+
+// tweak header support:
+
+#ifdef __has_include
+# if __has_include(<nonstd/string_view.tweak.hpp>)
+#  include <nonstd/string_view.tweak.hpp>
+# endif
+#define nssv_HAVE_TWEAK_HEADER  1
+#else
+#define nssv_HAVE_TWEAK_HEADER  0
+//# pragma message("string_view.hpp: Note: Tweak header not supported.")
+#endif
+
+// string_view selection and configuration:
+
+#if !defined( nssv_CONFIG_SELECT_STRING_VIEW )
+# define nssv_CONFIG_SELECT_STRING_VIEW  ( nssv_HAVE_STD_STRING_VIEW ? nssv_STRING_VIEW_STD : nssv_STRING_VIEW_NONSTD )
+#endif
+
+#ifndef  nssv_CONFIG_STD_SV_OPERATOR
+# define nssv_CONFIG_STD_SV_OPERATOR  0
+#endif
+
+#ifndef  nssv_CONFIG_USR_SV_OPERATOR
+# define nssv_CONFIG_USR_SV_OPERATOR  1
+#endif
+
+#ifdef   nssv_CONFIG_CONVERSION_STD_STRING
+# define nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS   nssv_CONFIG_CONVERSION_STD_STRING
+# define nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS  nssv_CONFIG_CONVERSION_STD_STRING
+#endif
+
+#ifndef  nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS
+# define nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS  1
+#endif
+
+#ifndef  nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+# define nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS  1
+#endif
+
+#ifndef  nssv_CONFIG_NO_STREAM_INSERTION
+# define nssv_CONFIG_NO_STREAM_INSERTION  0
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef nssv_CONFIG_NO_EXCEPTIONS
+# if defined(_MSC_VER)
+#  include <cstddef>    // for _HAS_EXCEPTIONS
+# endif
+# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS)
+#  define nssv_CONFIG_NO_EXCEPTIONS  0
+# else
+#  define nssv_CONFIG_NO_EXCEPTIONS  1
+# endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef   nssv_CPLUSPLUS
+# if defined(_MSVC_LANG ) && !defined(__clang__)
+#  define nssv_CPLUSPLUS  (_MSC_VER == 1900 ? 201103L : _MSVC_LANG )
+# else
+#  define nssv_CPLUSPLUS  __cplusplus
+# endif
+#endif
+
+#define nssv_CPP98_OR_GREATER  ( nssv_CPLUSPLUS >= 199711L )
+#define nssv_CPP11_OR_GREATER  ( nssv_CPLUSPLUS >= 201103L )
+#define nssv_CPP11_OR_GREATER_ ( nssv_CPLUSPLUS >= 201103L )
+#define nssv_CPP14_OR_GREATER  ( nssv_CPLUSPLUS >= 201402L )
+#define nssv_CPP17_OR_GREATER  ( nssv_CPLUSPLUS >= 201703L )
+#define nssv_CPP20_OR_GREATER  ( nssv_CPLUSPLUS >= 202000L )
+
+// use C++17 std::string_view if available and requested:
+
+#if nssv_CPP17_OR_GREATER && defined(__has_include )
+# if __has_include( <string_view> )
+#  define nssv_HAVE_STD_STRING_VIEW  1
+# else
+#  define nssv_HAVE_STD_STRING_VIEW  0
+# endif
+#else
+# define  nssv_HAVE_STD_STRING_VIEW  0
+#endif
+
+#define  nssv_USES_STD_STRING_VIEW  ( (nssv_CONFIG_SELECT_STRING_VIEW == nssv_STRING_VIEW_STD) || ((nssv_CONFIG_SELECT_STRING_VIEW == nssv_STRING_VIEW_DEFAULT) && nssv_HAVE_STD_STRING_VIEW) )
+
+#define nssv_HAVE_STARTS_WITH ( nssv_CPP20_OR_GREATER || !nssv_USES_STD_STRING_VIEW )
+#define nssv_HAVE_ENDS_WITH     nssv_HAVE_STARTS_WITH
+
+//
+// Use C++17 std::string_view:
+//
+
+#if nssv_USES_STD_STRING_VIEW
+
+#include <string_view>
+
+// Extensions for std::string:
+
+#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+namespace nonstd {
+
+template< class CharT, class Traits, class Allocator = std::allocator<CharT> >
+std::basic_string<CharT, Traits, Allocator>
+to_string( std::basic_string_view<CharT, Traits> v, Allocator const & a = Allocator() )
+{
+    return std::basic_string<CharT,Traits, Allocator>( v.begin(), v.end(), a );
+}
+
+template< class CharT, class Traits, class Allocator >
+std::basic_string_view<CharT, Traits>
+to_string_view( std::basic_string<CharT, Traits, Allocator> const & s )
+{
+    return std::basic_string_view<CharT, Traits>( s.data(), s.size() );
+}
+
+// Literal operators sv and _sv:
+
+#if nssv_CONFIG_STD_SV_OPERATOR
+
+using namespace std::literals::string_view_literals;
+
+#endif
+
+#if nssv_CONFIG_USR_SV_OPERATOR
+
+inline namespace literals {
+inline namespace string_view_literals {
+
+
+constexpr std::string_view operator "" _sv( const char* str, size_t len ) noexcept  // (1)
+{
+    return std::string_view{ str, len };
+}
+
+constexpr std::u16string_view operator "" _sv( const char16_t* str, size_t len ) noexcept  // (2)
+{
+    return std::u16string_view{ str, len };
+}
+
+constexpr std::u32string_view operator "" _sv( const char32_t* str, size_t len ) noexcept  // (3)
+{
+    return std::u32string_view{ str, len };
+}
+
+constexpr std::wstring_view operator "" _sv( const wchar_t* str, size_t len ) noexcept  // (4)
+{
+    return std::wstring_view{ str, len };
+}
+
+}} // namespace literals::string_view_literals
+
+#endif // nssv_CONFIG_USR_SV_OPERATOR
+
+} // namespace nonstd
+
+#endif // nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+namespace nonstd {
+
+using std::string_view;
+using std::wstring_view;
+using std::u16string_view;
+using std::u32string_view;
+using std::basic_string_view;
+
+// literal "sv" and "_sv", see above
+
+using std::operator==;
+using std::operator!=;
+using std::operator<;
+using std::operator<=;
+using std::operator>;
+using std::operator>=;
+
+using std::operator<<;
+
+} // namespace nonstd
+
+#else // nssv_HAVE_STD_STRING_VIEW
+
+//
+// Before C++17: use string_view lite:
+//
+
+// Compiler versions:
+//
+// MSVC++  6.0  _MSC_VER == 1200  nssv_COMPILER_MSVC_VERSION ==  60  (Visual Studio 6.0)
+// MSVC++  7.0  _MSC_VER == 1300  nssv_COMPILER_MSVC_VERSION ==  70  (Visual Studio .NET 2002)
+// MSVC++  7.1  _MSC_VER == 1310  nssv_COMPILER_MSVC_VERSION ==  71  (Visual Studio .NET 2003)
+// MSVC++  8.0  _MSC_VER == 1400  nssv_COMPILER_MSVC_VERSION ==  80  (Visual Studio 2005)
+// MSVC++  9.0  _MSC_VER == 1500  nssv_COMPILER_MSVC_VERSION ==  90  (Visual Studio 2008)
+// MSVC++ 10.0  _MSC_VER == 1600  nssv_COMPILER_MSVC_VERSION == 100  (Visual Studio 2010)
+// MSVC++ 11.0  _MSC_VER == 1700  nssv_COMPILER_MSVC_VERSION == 110  (Visual Studio 2012)
+// MSVC++ 12.0  _MSC_VER == 1800  nssv_COMPILER_MSVC_VERSION == 120  (Visual Studio 2013)
+// MSVC++ 14.0  _MSC_VER == 1900  nssv_COMPILER_MSVC_VERSION == 140  (Visual Studio 2015)
+// MSVC++ 14.1  _MSC_VER >= 1910  nssv_COMPILER_MSVC_VERSION == 141  (Visual Studio 2017)
+// MSVC++ 14.2  _MSC_VER >= 1920  nssv_COMPILER_MSVC_VERSION == 142  (Visual Studio 2019)
+
+#if defined(_MSC_VER ) && !defined(__clang__)
+# define nssv_COMPILER_MSVC_VER      (_MSC_VER )
+# define nssv_COMPILER_MSVC_VERSION  (_MSC_VER / 10 - 10 * ( 5 + (_MSC_VER < 1900 ) ) )
+#else
+# define nssv_COMPILER_MSVC_VER      0
+# define nssv_COMPILER_MSVC_VERSION  0
+#endif
+
+#define nssv_COMPILER_VERSION( major, minor, patch )  ( 10 * ( 10 * (major) + (minor) ) + (patch) )
+
+#if defined( __apple_build_version__ )
+# define nssv_COMPILER_APPLECLANG_VERSION  nssv_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+# define nssv_COMPILER_CLANG_VERSION       0
+#elif defined( __clang__ )
+# define nssv_COMPILER_APPLECLANG_VERSION  0
+# define nssv_COMPILER_CLANG_VERSION       nssv_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+# define nssv_COMPILER_APPLECLANG_VERSION  0
+# define nssv_COMPILER_CLANG_VERSION       0
+#endif
+
+#if defined(__GNUC__) && !defined(__clang__)
+# define nssv_COMPILER_GNUC_VERSION  nssv_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+# define nssv_COMPILER_GNUC_VERSION  0
+#endif
+
+// half-open range [lo..hi):
+#define nssv_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) )
+
+// Presence of language and library features:
+
+#ifdef _HAS_CPP0X
+# define nssv_HAS_CPP0X  _HAS_CPP0X
+#else
+# define nssv_HAS_CPP0X  0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for variant-lite:
+
+#if nssv_COMPILER_MSVC_VER >= 1900
+# undef  nssv_CPP11_OR_GREATER
+# define nssv_CPP11_OR_GREATER  1
+#endif
+
+#define nssv_CPP11_90   (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1500)
+#define nssv_CPP11_100  (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1600)
+#define nssv_CPP11_110  (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1700)
+#define nssv_CPP11_120  (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1800)
+#define nssv_CPP11_140  (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1900)
+#define nssv_CPP11_141  (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1910)
+
+#define nssv_CPP14_000  (nssv_CPP14_OR_GREATER)
+#define nssv_CPP17_000  (nssv_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+#define nssv_HAVE_CONSTEXPR_11          nssv_CPP11_140
+#define nssv_HAVE_EXPLICIT_CONVERSION   nssv_CPP11_140
+#define nssv_HAVE_INLINE_NAMESPACE      nssv_CPP11_140
+#define nssv_HAVE_NOEXCEPT              nssv_CPP11_140
+#define nssv_HAVE_NULLPTR               nssv_CPP11_100
+#define nssv_HAVE_REF_QUALIFIER         nssv_CPP11_140
+#define nssv_HAVE_UNICODE_LITERALS      nssv_CPP11_140
+#define nssv_HAVE_USER_DEFINED_LITERALS nssv_CPP11_140
+#define nssv_HAVE_WCHAR16_T             nssv_CPP11_100
+#define nssv_HAVE_WCHAR32_T             nssv_CPP11_100
+
+#if ! ( ( nssv_CPP11_OR_GREATER && nssv_COMPILER_CLANG_VERSION ) || nssv_BETWEEN( nssv_COMPILER_CLANG_VERSION, 300, 400 ) )
+# define nssv_HAVE_STD_DEFINED_LITERALS  nssv_CPP11_140
+#else
+# define nssv_HAVE_STD_DEFINED_LITERALS  0
+#endif
+
+// Presence of C++14 language features:
+
+#define nssv_HAVE_CONSTEXPR_14          nssv_CPP14_000
+
+// Presence of C++17 language features:
+
+#define nssv_HAVE_NODISCARD             nssv_CPP17_000
+
+// Presence of C++ library features:
+
+#define nssv_HAVE_STD_HASH              nssv_CPP11_120
+
+// Presence of compiler intrinsics:
+
+// Providing char-type specializations for compare() and length() that
+// use compiler intrinsics can improve compile- and run-time performance.
+//
+// The challenge is in using the right combinations of builtin availability
+// and its constexpr-ness.
+//
+// | compiler | __builtin_memcmp (constexpr) | memcmp  (constexpr) |
+// |----------|------------------------------|---------------------|
+// | clang    | 4.0              (>= 4.0   ) | any     (?        ) |
+// | clang-a  | 9.0              (>= 9.0   ) | any     (?        ) |
+// | gcc      | any              (constexpr) | any     (?        ) |
+// | msvc     | >= 14.2 C++17    (>= 14.2  ) | any     (?        ) |
+
+#define nssv_HAVE_BUILTIN_VER     ( (nssv_CPP17_000 && nssv_COMPILER_MSVC_VERSION >= 142) || nssv_COMPILER_GNUC_VERSION > 0 || nssv_COMPILER_CLANG_VERSION >= 400 || nssv_COMPILER_APPLECLANG_VERSION >= 900 )
+#define nssv_HAVE_BUILTIN_CE      (  nssv_HAVE_BUILTIN_VER )
+
+#define nssv_HAVE_BUILTIN_MEMCMP  ( (nssv_HAVE_CONSTEXPR_14 && nssv_HAVE_BUILTIN_CE) || !nssv_HAVE_CONSTEXPR_14 )
+#define nssv_HAVE_BUILTIN_STRLEN  ( (nssv_HAVE_CONSTEXPR_11 && nssv_HAVE_BUILTIN_CE) || !nssv_HAVE_CONSTEXPR_11 )
+
+#ifdef __has_builtin
+# define nssv_HAVE_BUILTIN( x )  __has_builtin( x )
+#else
+# define nssv_HAVE_BUILTIN( x )  0
+#endif
+
+#if nssv_HAVE_BUILTIN(__builtin_memcmp) || nssv_HAVE_BUILTIN_VER
+# define nssv_BUILTIN_MEMCMP  __builtin_memcmp
+#else
+# define nssv_BUILTIN_MEMCMP  memcmp
+#endif
+
+#if nssv_HAVE_BUILTIN(__builtin_strlen) || nssv_HAVE_BUILTIN_VER
+# define nssv_BUILTIN_STRLEN  __builtin_strlen
+#else
+# define nssv_BUILTIN_STRLEN  strlen
+#endif
+
+// C++ feature usage:
+
+#if nssv_HAVE_CONSTEXPR_11
+# define nssv_constexpr  constexpr
+#else
+# define nssv_constexpr  /*constexpr*/
+#endif
+
+#if  nssv_HAVE_CONSTEXPR_14
+# define nssv_constexpr14  constexpr
+#else
+# define nssv_constexpr14  /*constexpr*/
+#endif
+
+#if nssv_HAVE_EXPLICIT_CONVERSION
+# define nssv_explicit  explicit
+#else
+# define nssv_explicit  /*explicit*/
+#endif
+
+#if nssv_HAVE_INLINE_NAMESPACE
+# define nssv_inline_ns  inline
+#else
+# define nssv_inline_ns  /*inline*/
+#endif
+
+#if nssv_HAVE_NOEXCEPT
+# define nssv_noexcept  noexcept
+#else
+# define nssv_noexcept  /*noexcept*/
+#endif
+
+//#if nssv_HAVE_REF_QUALIFIER
+//# define nssv_ref_qual  &
+//# define nssv_refref_qual  &&
+//#else
+//# define nssv_ref_qual  /*&*/
+//# define nssv_refref_qual  /*&&*/
+//#endif
+
+#if nssv_HAVE_NULLPTR
+# define nssv_nullptr  nullptr
+#else
+# define nssv_nullptr  NULL
+#endif
+
+#if nssv_HAVE_NODISCARD
+# define nssv_nodiscard  [[nodiscard]]
+#else
+# define nssv_nodiscard  /*[[nodiscard]]*/
+#endif
+
+// Additional includes:
+
+#include <algorithm>
+#include <cassert>
+#include <iterator>
+#include <limits>
+#include <string>   // std::char_traits<>
+
+#if ! nssv_CONFIG_NO_STREAM_INSERTION
+# include <ostream>
+#endif
+
+#if ! nssv_CONFIG_NO_EXCEPTIONS
+# include <stdexcept>
+#endif
+
+#if nssv_CPP11_OR_GREATER
+# include <type_traits>
+#endif
+
+// Clang, GNUC, MSVC warning suppression macros:
+
+#if defined(__clang__)
+# pragma clang diagnostic ignored "-Wreserved-user-defined-literal"
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wuser-defined-literals"
+#elif defined(__GNUC__)
+# pragma  GCC  diagnostic push
+# pragma  GCC  diagnostic ignored "-Wliteral-suffix"
+#endif // __clang__
+
+#if nssv_COMPILER_MSVC_VERSION >= 140
+# define nssv_SUPPRESS_MSGSL_WARNING(expr)        [[gsl::suppress(expr)]]
+# define nssv_SUPPRESS_MSVC_WARNING(code, descr)  __pragma(warning(suppress: code) )
+# define nssv_DISABLE_MSVC_WARNINGS(codes)        __pragma(warning(push))  __pragma(warning(disable: codes))
+#else
+# define nssv_SUPPRESS_MSGSL_WARNING(expr)
+# define nssv_SUPPRESS_MSVC_WARNING(code, descr)
+# define nssv_DISABLE_MSVC_WARNINGS(codes)
+#endif
+
+#if defined(__clang__)
+# define nssv_RESTORE_WARNINGS()  _Pragma("clang diagnostic pop")
+#elif defined(__GNUC__)
+# define nssv_RESTORE_WARNINGS()  _Pragma("GCC diagnostic pop")
+#elif nssv_COMPILER_MSVC_VERSION >= 140
+# define nssv_RESTORE_WARNINGS()  __pragma(warning(pop ))
+#else
+# define nssv_RESTORE_WARNINGS()
+#endif
+
+// Suppress the following MSVC (GSL) warnings:
+// - C4455, non-gsl   : 'operator ""sv': literal suffix identifiers that do not
+//                      start with an underscore are reserved
+// - C26472, gsl::t.1 : don't use a static_cast for arithmetic conversions;
+//                      use brace initialization, gsl::narrow_cast or gsl::narow
+// - C26481: gsl::b.1 : don't use pointer arithmetic. Use span instead
+
+nssv_DISABLE_MSVC_WARNINGS( 4455 26481 26472 )
+//nssv_DISABLE_CLANG_WARNINGS( "-Wuser-defined-literals" )
+//nssv_DISABLE_GNUC_WARNINGS( -Wliteral-suffix )
+
+namespace nonstd { namespace sv_lite {
+
+namespace detail {
+
+// support constexpr comparison in C++14;
+// for C++17 and later, use provided traits:
+
+template< typename CharT >
+inline nssv_constexpr14 int compare( CharT const * s1, CharT const * s2, std::size_t count )
+{
+    while ( count-- != 0 )
+    {
+        if ( *s1 < *s2 ) return -1;
+        if ( *s1 > *s2 ) return +1;
+        ++s1; ++s2;
+    }
+    return 0;
+}
+
+#if nssv_HAVE_BUILTIN_MEMCMP
+
+// specialization of compare() for char, see also generic compare() above:
+
+inline nssv_constexpr14 int compare( char const * s1, char const * s2, std::size_t count )
+{
+    return nssv_BUILTIN_MEMCMP( s1, s2, count );
+}
+
+#endif
+
+#if nssv_HAVE_BUILTIN_STRLEN
+
+// specialization of length() for char, see also generic length() further below:
+
+inline nssv_constexpr std::size_t length( char const * s )
+{
+    return nssv_BUILTIN_STRLEN( s );
+}
+
+#endif
+
+#if defined(__OPTIMIZE__)
+
+// gcc, clang provide __OPTIMIZE__
+// Expect tail call optimization to make length() non-recursive:
+
+template< typename CharT >
+inline nssv_constexpr std::size_t length( CharT * s, std::size_t result = 0 )
+{
+    return *s == '\0' ? result : length( s + 1, result + 1 );
+}
+
+#else // OPTIMIZE
+
+// non-recursive:
+
+template< typename CharT >
+inline nssv_constexpr14 std::size_t length( CharT * s )
+{
+    std::size_t result = 0;
+    while ( *s++ != '\0' )
+    {
+       ++result;
+    }
+    return result;
+}
+
+#endif // OPTIMIZE
+
+} // namespace detail
+
+template
+<
+    class CharT,
+    class Traits = std::char_traits<CharT>
+>
+class basic_string_view;
+
+//
+// basic_string_view:
+//
+
+template
+<
+    class CharT,
+    class Traits /* = std::char_traits<CharT> */
+>
+class basic_string_view
+{
+public:
+    // Member types:
+
+    typedef Traits traits_type;
+    typedef CharT  value_type;
+
+    typedef CharT       * pointer;
+    typedef CharT const * const_pointer;
+    typedef CharT       & reference;
+    typedef CharT const & const_reference;
+
+    typedef const_pointer iterator;
+    typedef const_pointer const_iterator;
+    typedef std::reverse_iterator< const_iterator > reverse_iterator;
+    typedef    std::reverse_iterator< const_iterator > const_reverse_iterator;
+
+    typedef std::size_t     size_type;
+    typedef std::ptrdiff_t  difference_type;
+
+    // 24.4.2.1 Construction and assignment:
+
+    nssv_constexpr basic_string_view() nssv_noexcept
+        : data_( nssv_nullptr )
+        , size_( 0 )
+    {}
+
+#if nssv_CPP11_OR_GREATER
+    nssv_constexpr basic_string_view( basic_string_view const & other ) nssv_noexcept = default;
+#else
+    nssv_constexpr basic_string_view( basic_string_view const & other ) nssv_noexcept
+        : data_( other.data_)
+        , size_( other.size_)
+    {}
+#endif
+
+    nssv_constexpr basic_string_view( CharT const * s, size_type count ) nssv_noexcept // non-standard noexcept
+        : data_( s )
+        , size_( count )
+    {}
+
+    nssv_constexpr basic_string_view( CharT const * s) nssv_noexcept // non-standard noexcept
+        : data_( s )
+#if nssv_CPP17_OR_GREATER
+        , size_( Traits::length(s) )
+#elif nssv_CPP11_OR_GREATER
+        , size_( detail::length(s) )
+#else
+        , size_( Traits::length(s) )
+#endif
+    {}
+
+    // Assignment:
+
+#if nssv_CPP11_OR_GREATER
+    nssv_constexpr14 basic_string_view & operator=( basic_string_view const & other ) nssv_noexcept = default;
+#else
+    nssv_constexpr14 basic_string_view & operator=( basic_string_view const & other ) nssv_noexcept
+    {
+        data_ = other.data_;
+        size_ = other.size_;
+        return *this;
+    }
+#endif
+
+    // 24.4.2.2 Iterator support:
+
+    nssv_constexpr const_iterator begin()  const nssv_noexcept { return data_;         }
+    nssv_constexpr const_iterator end()    const nssv_noexcept { return data_ + size_; }
+
+    nssv_constexpr const_iterator cbegin() const nssv_noexcept { return begin(); }
+    nssv_constexpr const_iterator cend()   const nssv_noexcept { return end();   }
+
+    nssv_constexpr const_reverse_iterator rbegin()  const nssv_noexcept { return const_reverse_iterator( end() );   }
+    nssv_constexpr const_reverse_iterator rend()    const nssv_noexcept { return const_reverse_iterator( begin() ); }
+
+    nssv_constexpr const_reverse_iterator crbegin() const nssv_noexcept { return rbegin(); }
+    nssv_constexpr const_reverse_iterator crend()   const nssv_noexcept { return rend();   }
+
+    // 24.4.2.3 Capacity:
+
+    nssv_constexpr size_type size()     const nssv_noexcept { return size_; }
+    nssv_constexpr size_type length()   const nssv_noexcept { return size_; }
+    nssv_constexpr size_type max_size() const nssv_noexcept { return (std::numeric_limits< size_type >::max)(); }
+
+    // since C++20
+    nssv_nodiscard nssv_constexpr bool empty() const nssv_noexcept
+    {
+        return 0 == size_;
+    }
+
+    // 24.4.2.4 Element access:
+
+    nssv_constexpr const_reference operator[]( size_type pos ) const
+    {
+        return data_at( pos );
+    }
+
+    nssv_constexpr14 const_reference at( size_type pos ) const
+    {
+#if nssv_CONFIG_NO_EXCEPTIONS
+        assert( pos < size() );
+#else
+        if ( pos >= size() )
+        {
+            throw std::out_of_range("nonstd::string_view::at()");
+        }
+#endif
+        return data_at( pos );
+    }
+
+    nssv_constexpr const_reference front() const { return data_at( 0 );          }
+    nssv_constexpr const_reference back()  const { return data_at( size() - 1 ); }
+
+    nssv_constexpr const_pointer   data()  const nssv_noexcept { return data_; }
+
+    // 24.4.2.5 Modifiers:
+
+    nssv_constexpr14 void remove_prefix( size_type n )
+    {
+        assert( n <= size() );
+        data_ += n;
+        size_ -= n;
+    }
+
+    nssv_constexpr14 void remove_suffix( size_type n )
+    {
+        assert( n <= size() );
+        size_ -= n;
+    }
+
+    nssv_constexpr14 void swap( basic_string_view & other ) nssv_noexcept
+    {
+        const basic_string_view tmp(other);
+        other = *this;
+        *this = tmp;
+    }
+
+    // 24.4.2.6 String operations:
+
+    size_type copy( CharT * dest, size_type n, size_type pos = 0 ) const
+    {
+#if nssv_CONFIG_NO_EXCEPTIONS
+        assert( pos <= size() );
+#else
+        if ( pos > size() )
+        {
+            throw std::out_of_range("nonstd::string_view::copy()");
+        }
+#endif
+        const size_type rlen = (std::min)( n, size() - pos );
+
+        (void) Traits::copy( dest, data() + pos, rlen );
+
+        return rlen;
+    }
+
+    nssv_constexpr14 basic_string_view substr( size_type pos = 0, size_type n = npos ) const
+    {
+#if nssv_CONFIG_NO_EXCEPTIONS
+        assert( pos <= size() );
+#else
+        if ( pos > size() )
+        {
+            throw std::out_of_range("nonstd::string_view::substr()");
+        }
+#endif
+        return basic_string_view( data() + pos, (std::min)( n, size() - pos ) );
+    }
+
+    // compare(), 6x:
+
+    nssv_constexpr14 int compare( basic_string_view other ) const nssv_noexcept // (1)
+    {
+#if nssv_CPP17_OR_GREATER
+        if ( const int result = Traits::compare( data(), other.data(), (std::min)( size(), other.size() ) ) )
+#else
+        if ( const int result = detail::compare( data(), other.data(), (std::min)( size(), other.size() ) ) )
+#endif
+        {
+            return result;
+        }
+
+        return size() == other.size() ? 0 : size() < other.size() ? -1 : 1;
+    }
+
+    nssv_constexpr int compare( size_type pos1, size_type n1, basic_string_view other ) const // (2)
+    {
+        return substr( pos1, n1 ).compare( other );
+    }
+
+    nssv_constexpr int compare( size_type pos1, size_type n1, basic_string_view other, size_type pos2, size_type n2 ) const // (3)
+    {
+        return substr( pos1, n1 ).compare( other.substr( pos2, n2 ) );
+    }
+
+    nssv_constexpr int compare( CharT const * s ) const // (4)
+    {
+        return compare( basic_string_view( s ) );
+    }
+
+    nssv_constexpr int compare( size_type pos1, size_type n1, CharT const * s ) const // (5)
+    {
+        return substr( pos1, n1 ).compare( basic_string_view( s ) );
+    }
+
+    nssv_constexpr int compare( size_type pos1, size_type n1, CharT const * s, size_type n2 ) const // (6)
+    {
+        return substr( pos1, n1 ).compare( basic_string_view( s, n2 ) );
+    }
+
+    // 24.4.2.7 Searching:
+
+    // starts_with(), 3x, since C++20:
+
+    nssv_constexpr bool starts_with( basic_string_view v ) const nssv_noexcept  // (1)
+    {
+        return size() >= v.size() && compare( 0, v.size(), v ) == 0;
+    }
+
+    nssv_constexpr bool starts_with( CharT c ) const nssv_noexcept  // (2)
+    {
+        return starts_with( basic_string_view( &c, 1 ) );
+    }
+
+    nssv_constexpr bool starts_with( CharT const * s ) const  // (3)
+    {
+        return starts_with( basic_string_view( s ) );
+    }
+
+    // ends_with(), 3x, since C++20:
+
+    nssv_constexpr bool ends_with( basic_string_view v ) const nssv_noexcept  // (1)
+    {
+        return size() >= v.size() && compare( size() - v.size(), npos, v ) == 0;
+    }
+
+    nssv_constexpr bool ends_with( CharT c ) const nssv_noexcept  // (2)
+    {
+        return ends_with( basic_string_view( &c, 1 ) );
+    }
+
+    nssv_constexpr bool ends_with( CharT const * s ) const  // (3)
+    {
+        return ends_with( basic_string_view( s ) );
+    }
+
+    // find(), 4x:
+
+    nssv_constexpr14 size_type find( basic_string_view v, size_type pos = 0 ) const nssv_noexcept  // (1)
+    {
+        return assert( v.size() == 0 || v.data() != nssv_nullptr )
+            , pos >= size()
+            ? npos
+            : to_pos( std::search( cbegin() + pos, cend(), v.cbegin(), v.cend(), Traits::eq ) );
+    }
+
+    nssv_constexpr14 size_type find( CharT c, size_type pos = 0 ) const nssv_noexcept  // (2)
+    {
+        return find( basic_string_view( &c, 1 ), pos );
+    }
+
+    nssv_constexpr14 size_type find( CharT const * s, size_type pos, size_type n ) const  // (3)
+    {
+        return find( basic_string_view( s, n ), pos );
+    }
+
+    nssv_constexpr14 size_type find( CharT const * s, size_type pos = 0 ) const  // (4)
+    {
+        return find( basic_string_view( s ), pos );
+    }
+
+    // rfind(), 4x:
+
+    nssv_constexpr14 size_type rfind( basic_string_view v, size_type pos = npos ) const nssv_noexcept  // (1)
+    {
+        if ( size() < v.size() )
+        {
+            return npos;
+        }
+
+        if ( v.empty() )
+        {
+            return (std::min)( size(), pos );
+        }
+
+        const_iterator last   = cbegin() + (std::min)( size() - v.size(), pos ) + v.size();
+        const_iterator result = std::find_end( cbegin(), last, v.cbegin(), v.cend(), Traits::eq );
+
+        return result != last ? size_type( result - cbegin() ) : npos;
+    }
+
+    nssv_constexpr14 size_type rfind( CharT c, size_type pos = npos ) const nssv_noexcept  // (2)
+    {
+        return rfind( basic_string_view( &c, 1 ), pos );
+    }
+
+    nssv_constexpr14 size_type rfind( CharT const * s, size_type pos, size_type n ) const  // (3)
+    {
+        return rfind( basic_string_view( s, n ), pos );
+    }
+
+    nssv_constexpr14 size_type rfind( CharT const * s, size_type pos = npos ) const  // (4)
+    {
+        return rfind( basic_string_view( s ), pos );
+    }
+
+    // find_first_of(), 4x:
+
+    nssv_constexpr size_type find_first_of( basic_string_view v, size_type pos = 0 ) const nssv_noexcept  // (1)
+    {
+        return pos >= size()
+            ? npos
+            : to_pos( std::find_first_of( cbegin() + pos, cend(), v.cbegin(), v.cend(), Traits::eq ) );
+    }
+
+    nssv_constexpr size_type find_first_of( CharT c, size_type pos = 0 ) const nssv_noexcept  // (2)
+    {
+        return find_first_of( basic_string_view( &c, 1 ), pos );
+    }
+
+    nssv_constexpr size_type find_first_of( CharT const * s, size_type pos, size_type n ) const  // (3)
+    {
+        return find_first_of( basic_string_view( s, n ), pos );
+    }
+
+    nssv_constexpr size_type find_first_of(  CharT const * s, size_type pos = 0 ) const  // (4)
+    {
+        return find_first_of( basic_string_view( s ), pos );
+    }
+
+    // find_last_of(), 4x:
+
+    nssv_constexpr size_type find_last_of( basic_string_view v, size_type pos = npos ) const nssv_noexcept  // (1)
+    {
+        return empty()
+            ? npos
+            : pos >= size()
+            ? find_last_of( v, size() - 1 )
+            : to_pos( std::find_first_of( const_reverse_iterator( cbegin() + pos + 1 ), crend(), v.cbegin(), v.cend(), Traits::eq ) );
+    }
+
+    nssv_constexpr size_type find_last_of( CharT c, size_type pos = npos ) const nssv_noexcept  // (2)
+    {
+        return find_last_of( basic_string_view( &c, 1 ), pos );
+    }
+
+    nssv_constexpr size_type find_last_of( CharT const * s, size_type pos, size_type count ) const  // (3)
+    {
+        return find_last_of( basic_string_view( s, count ), pos );
+    }
+
+    nssv_constexpr size_type find_last_of( CharT const * s, size_type pos = npos ) const  // (4)
+    {
+        return find_last_of( basic_string_view( s ), pos );
+    }
+
+    // find_first_not_of(), 4x:
+
+    nssv_constexpr size_type find_first_not_of( basic_string_view v, size_type pos = 0 ) const nssv_noexcept  // (1)
+    {
+        return pos >= size()
+            ? npos
+            : to_pos( std::find_if( cbegin() + pos, cend(), not_in_view( v ) ) );
+    }
+
+    nssv_constexpr size_type find_first_not_of( CharT c, size_type pos = 0 ) const nssv_noexcept  // (2)
+    {
+        return find_first_not_of( basic_string_view( &c, 1 ), pos );
+    }
+
+    nssv_constexpr size_type find_first_not_of( CharT const * s, size_type pos, size_type count ) const  // (3)
+    {
+        return find_first_not_of( basic_string_view( s, count ), pos );
+    }
+
+    nssv_constexpr size_type find_first_not_of( CharT const * s, size_type pos = 0 ) const  // (4)
+    {
+        return find_first_not_of( basic_string_view( s ), pos );
+    }
+
+    // find_last_not_of(), 4x:
+
+    nssv_constexpr size_type find_last_not_of( basic_string_view v, size_type pos = npos ) const nssv_noexcept  // (1)
+    {
+        return empty()
+            ? npos
+            : pos >= size()
+            ? find_last_not_of( v, size() - 1 )
+            : to_pos( std::find_if( const_reverse_iterator( cbegin() + pos + 1 ), crend(), not_in_view( v ) ) );
+    }
+
+    nssv_constexpr size_type find_last_not_of( CharT c, size_type pos = npos ) const nssv_noexcept  // (2)
+    {
+        return find_last_not_of( basic_string_view( &c, 1 ), pos );
+    }
+
+    nssv_constexpr size_type find_last_not_of( CharT const * s, size_type pos, size_type count ) const  // (3)
+    {
+        return find_last_not_of( basic_string_view( s, count ), pos );
+    }
+
+    nssv_constexpr size_type find_last_not_of( CharT const * s, size_type pos = npos ) const  // (4)
+    {
+        return find_last_not_of( basic_string_view( s ), pos );
+    }
+
+    // Constants:
+
+#if nssv_CPP17_OR_GREATER
+    static nssv_constexpr size_type npos = size_type(-1);
+#elif nssv_CPP11_OR_GREATER
+    enum : size_type { npos = size_type(-1) };
+#else
+    enum { npos = size_type(-1) };
+#endif
+
+private:
+    struct not_in_view
+    {
+        const basic_string_view v;
+
+        nssv_constexpr explicit not_in_view( basic_string_view v_ ) : v( v_ ) {}
+
+        nssv_constexpr bool operator()( CharT c ) const
+        {
+            return npos == v.find_first_of( c );
+        }
+    };
+
+    nssv_constexpr size_type to_pos( const_iterator it ) const
+    {
+        return it == cend() ? npos : size_type( it - cbegin() );
+    }
+
+    nssv_constexpr size_type to_pos( const_reverse_iterator it ) const
+    {
+        return it == crend() ? npos : size_type( crend() - it - 1 );
+    }
+
+    nssv_constexpr const_reference data_at( size_type pos ) const
+    {
+#if nssv_BETWEEN( nssv_COMPILER_GNUC_VERSION, 1, 500 )
+        return data_[pos];
+#else
+        return assert( pos < size() ), data_[pos];
+#endif
+    }
+
+private:
+    const_pointer data_;
+    size_type     size_;
+
+public:
+#if nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS
+
+    template< class Allocator >
+    basic_string_view( std::basic_string<CharT, Traits, Allocator> const & s ) nssv_noexcept
+        : data_( s.data() )
+        , size_( s.size() )
+    {}
+
+#if nssv_HAVE_EXPLICIT_CONVERSION
+
+    template< class Allocator >
+    explicit operator std::basic_string<CharT, Traits, Allocator>() const
+    {
+        return to_string( Allocator() );
+    }
+
+#endif // nssv_HAVE_EXPLICIT_CONVERSION
+
+#if nssv_CPP11_OR_GREATER
+
+    template< class Allocator = std::allocator<CharT> >
+    std::basic_string<CharT, Traits, Allocator>
+    to_string( Allocator const & a = Allocator() ) const
+    {
+        return std::basic_string<CharT, Traits, Allocator>( begin(), end(), a );
+    }
+
+#else
+
+    std::basic_string<CharT, Traits>
+    to_string() const
+    {
+        return std::basic_string<CharT, Traits>( begin(), end() );
+    }
+
+    template< class Allocator >
+    std::basic_string<CharT, Traits, Allocator>
+    to_string( Allocator const & a ) const
+    {
+        return std::basic_string<CharT, Traits, Allocator>( begin(), end(), a );
+    }
+
+#endif // nssv_CPP11_OR_GREATER
+
+#endif // nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS
+};
+
+//
+// Non-member functions:
+//
+
+// 24.4.3 Non-member comparison functions:
+// lexicographically compare two string views (function template):
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator== (
+    basic_string_view <CharT, Traits> lhs,
+    basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator!= (
+    basic_string_view <CharT, Traits> lhs,
+    basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator< (
+    basic_string_view <CharT, Traits> lhs,
+    basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator<= (
+    basic_string_view <CharT, Traits> lhs,
+    basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator> (
+    basic_string_view <CharT, Traits> lhs,
+    basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator>= (
+    basic_string_view <CharT, Traits> lhs,
+    basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+// Let S be basic_string_view<CharT, Traits>, and sv be an instance of S.
+// Implementations shall provide sufficient additional overloads marked
+// constexpr and noexcept so that an object t with an implicit conversion
+// to S can be compared according to Table 67.
+
+#if ! nssv_CPP11_OR_GREATER || nssv_BETWEEN( nssv_COMPILER_MSVC_VERSION, 100, 141 )
+
+// accommodate for older compilers:
+
+// ==
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+    basic_string_view<CharT, Traits> lhs,
+    CharT const * rhs ) nssv_noexcept
+{ return lhs.size() == detail::length( rhs ) && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+    CharT const * lhs,
+    basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return detail::length( lhs ) == rhs.size() && rhs.compare( lhs ) == 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+    basic_string_view<CharT, Traits> lhs,
+    std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+    std::basic_string<CharT, Traits> rhs,
+    basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+// !=
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+    basic_string_view<CharT, Traits> lhs,
+    CharT const * rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+    CharT const * lhs,
+    basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+    basic_string_view<CharT, Traits> lhs,
+    std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+    std::basic_string<CharT, Traits> rhs,
+    basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+// <
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+    basic_string_view<CharT, Traits> lhs,
+    CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+    CharT const * lhs,
+    basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) > 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+    basic_string_view<CharT, Traits> lhs,
+    std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+    std::basic_string<CharT, Traits> rhs,
+    basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) > 0; }
+
+// <=
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+    basic_string_view<CharT, Traits> lhs,
+    CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+    CharT const * lhs,
+    basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) >= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+    basic_string_view<CharT, Traits> lhs,
+    std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+    std::basic_string<CharT, Traits> rhs,
+    basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) >= 0; }
+
+// >
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+    basic_string_view<CharT, Traits> lhs,
+    CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+    CharT const * lhs,
+    basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) < 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+    basic_string_view<CharT, Traits> lhs,
+    std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+    std::basic_string<CharT, Traits> rhs,
+    basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) < 0; }
+
+// >=
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+    basic_string_view<CharT, Traits> lhs,
+    CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+    CharT const * lhs,
+    basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) <= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+    basic_string_view<CharT, Traits> lhs,
+    std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+    std::basic_string<CharT, Traits> rhs,
+    basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) <= 0; }
+
+#else // newer compilers:
+
+#define nssv_BASIC_STRING_VIEW_I(T,U)  typename std::decay< basic_string_view<T,U> >::type
+
+#if defined(_MSC_VER)       // issue 40
+# define nssv_MSVC_ORDER(x)  , int=x
+#else
+# define nssv_MSVC_ORDER(x)  /*, int=x*/
+#endif
+
+// ==
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator==(
+         basic_string_view  <CharT, Traits> lhs,
+    nssv_BASIC_STRING_VIEW_I(CharT, Traits) rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator==(
+    nssv_BASIC_STRING_VIEW_I(CharT, Traits) lhs,
+         basic_string_view  <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+// !=
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator!= (
+         basic_string_view  < CharT, Traits > lhs,
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator!= (
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+         basic_string_view  < CharT, Traits > rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+// <
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator< (
+         basic_string_view  < CharT, Traits > lhs,
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator< (
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+         basic_string_view  < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+// <=
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator<= (
+         basic_string_view  < CharT, Traits > lhs,
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator<= (
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+         basic_string_view  < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+// >
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator> (
+         basic_string_view  < CharT, Traits > lhs,
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator> (
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+         basic_string_view  < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+// >=
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator>= (
+         basic_string_view  < CharT, Traits > lhs,
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+template< class CharT, class Traits  nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator>= (
+    nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+         basic_string_view  < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+#undef nssv_MSVC_ORDER
+#undef nssv_BASIC_STRING_VIEW_I
+
+#endif // compiler-dependent approach to comparisons
+
+// 24.4.4 Inserters and extractors:
+
+#if ! nssv_CONFIG_NO_STREAM_INSERTION
+
+namespace detail {
+
+template< class Stream >
+void write_padding( Stream & os, std::streamsize n )
+{
+    for ( std::streamsize i = 0; i < n; ++i )
+        os.rdbuf()->sputc( os.fill() );
+}
+
+template< class Stream, class View >
+Stream & write_to_stream( Stream & os, View const & sv )
+{
+    typename Stream::sentry sentry( os );
+
+    if ( !os )
+        return os;
+
+    const std::streamsize length = static_cast<std::streamsize>( sv.length() );
+
+    // Whether, and how, to pad:
+    const bool      pad = ( length < os.width() );
+    const bool left_pad = pad && ( os.flags() & std::ios_base::adjustfield ) == std::ios_base::right;
+
+    if ( left_pad )
+        write_padding( os, os.width() - length );
+
+    // Write span characters:
+    os.rdbuf()->sputn( sv.begin(), length );
+
+    if ( pad && !left_pad )
+        write_padding( os, os.width() - length );
+
+    // Reset output stream width:
+    os.width( 0 );
+
+    return os;
+}
+
+} // namespace detail
+
+template< class CharT, class Traits >
+std::basic_ostream<CharT, Traits> &
+operator<<(
+    std::basic_ostream<CharT, Traits>& os,
+    basic_string_view <CharT, Traits> sv )
+{
+    return detail::write_to_stream( os, sv );
+}
+
+#endif // nssv_CONFIG_NO_STREAM_INSERTION
+
+// Several typedefs for common character types are provided:
+
+typedef basic_string_view<char>      string_view;
+typedef basic_string_view<wchar_t>   wstring_view;
+#if nssv_HAVE_WCHAR16_T
+typedef basic_string_view<char16_t>  u16string_view;
+typedef basic_string_view<char32_t>  u32string_view;
+#endif
+
+}} // namespace nonstd::sv_lite
+
+//
+// 24.4.6 Suffix for basic_string_view literals:
+//
+
+#if nssv_HAVE_USER_DEFINED_LITERALS
+
+namespace nonstd {
+nssv_inline_ns namespace literals {
+nssv_inline_ns namespace string_view_literals {
+
+#if nssv_CONFIG_STD_SV_OPERATOR && nssv_HAVE_STD_DEFINED_LITERALS
+
+nssv_constexpr nonstd::sv_lite::string_view operator "" sv( const char* str, size_t len ) nssv_noexcept  // (1)
+{
+    return nonstd::sv_lite::string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u16string_view operator "" sv( const char16_t* str, size_t len ) nssv_noexcept  // (2)
+{
+    return nonstd::sv_lite::u16string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u32string_view operator "" sv( const char32_t* str, size_t len ) nssv_noexcept  // (3)
+{
+    return nonstd::sv_lite::u32string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::wstring_view operator "" sv( const wchar_t* str, size_t len ) nssv_noexcept  // (4)
+{
+    return nonstd::sv_lite::wstring_view{ str, len };
+}
+
+#endif // nssv_CONFIG_STD_SV_OPERATOR && nssv_HAVE_STD_DEFINED_LITERALS
+
+#if nssv_CONFIG_USR_SV_OPERATOR
+
+nssv_constexpr nonstd::sv_lite::string_view operator "" _sv( const char* str, size_t len ) nssv_noexcept  // (1)
+{
+    return nonstd::sv_lite::string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u16string_view operator "" _sv( const char16_t* str, size_t len ) nssv_noexcept  // (2)
+{
+    return nonstd::sv_lite::u16string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u32string_view operator "" _sv( const char32_t* str, size_t len ) nssv_noexcept  // (3)
+{
+    return nonstd::sv_lite::u32string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::wstring_view operator "" _sv( const wchar_t* str, size_t len ) nssv_noexcept  // (4)
+{
+    return nonstd::sv_lite::wstring_view{ str, len };
+}
+
+#endif // nssv_CONFIG_USR_SV_OPERATOR
+
+}}} // namespace nonstd::literals::string_view_literals
+
+#endif
+
+//
+// Extensions for std::string:
+//
+
+#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+namespace nonstd {
+namespace sv_lite {
+
+// Exclude MSVC 14 (19.00): it yields ambiguous to_string():
+
+#if nssv_CPP11_OR_GREATER && nssv_COMPILER_MSVC_VERSION != 140
+
+template< class CharT, class Traits, class Allocator = std::allocator<CharT> >
+std::basic_string<CharT, Traits, Allocator>
+to_string( basic_string_view<CharT, Traits> v, Allocator const & a = Allocator() )
+{
+    return std::basic_string<CharT,Traits, Allocator>( v.begin(), v.end(), a );
+}
+
+#else
+
+template< class CharT, class Traits >
+std::basic_string<CharT, Traits>
+to_string( basic_string_view<CharT, Traits> v )
+{
+    return std::basic_string<CharT, Traits>( v.begin(), v.end() );
+}
+
+template< class CharT, class Traits, class Allocator >
+std::basic_string<CharT, Traits, Allocator>
+to_string( basic_string_view<CharT, Traits> v, Allocator const & a )
+{
+    return std::basic_string<CharT, Traits, Allocator>( v.begin(), v.end(), a );
+}
+
+#endif // nssv_CPP11_OR_GREATER
+
+template< class CharT, class Traits, class Allocator >
+basic_string_view<CharT, Traits>
+to_string_view( std::basic_string<CharT, Traits, Allocator> const & s )
+{
+    return basic_string_view<CharT, Traits>( s.data(), s.size() );
+}
+
+}} // namespace nonstd::sv_lite
+
+#endif // nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+//
+// make types and algorithms available in namespace nonstd:
+//
+
+namespace nonstd {
+
+using sv_lite::basic_string_view;
+using sv_lite::string_view;
+using sv_lite::wstring_view;
+
+#if nssv_HAVE_WCHAR16_T
+using sv_lite::u16string_view;
+#endif
+#if nssv_HAVE_WCHAR32_T
+using sv_lite::u32string_view;
+#endif
+
+// literal "sv"
+
+using sv_lite::operator==;
+using sv_lite::operator!=;
+using sv_lite::operator<;
+using sv_lite::operator<=;
+using sv_lite::operator>;
+using sv_lite::operator>=;
+
+#if ! nssv_CONFIG_NO_STREAM_INSERTION
+using sv_lite::operator<<;
+#endif
+
+#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+using sv_lite::to_string;
+using sv_lite::to_string_view;
+#endif
+
+} // namespace nonstd
+
+// 24.4.5 Hash support (C++11):
+
+// Note: The hash value of a string view object is equal to the hash value of
+// the corresponding string object.
+
+#if nssv_HAVE_STD_HASH
+
+#include <functional>
+
+namespace std {
+
+template<>
+struct hash< nonstd::string_view >
+{
+public:
+    std::size_t operator()( nonstd::string_view v ) const nssv_noexcept
+    {
+        return std::hash<std::string>()( std::string( v.data(), v.size() ) );
+    }
+};
+
+template<>
+struct hash< nonstd::wstring_view >
+{
+public:
+    std::size_t operator()( nonstd::wstring_view v ) const nssv_noexcept
+    {
+        return std::hash<std::wstring>()( std::wstring( v.data(), v.size() ) );
+    }
+};
+
+template<>
+struct hash< nonstd::u16string_view >
+{
+public:
+    std::size_t operator()( nonstd::u16string_view v ) const nssv_noexcept
+    {
+        return std::hash<std::u16string>()( std::u16string( v.data(), v.size() ) );
+    }
+};
+
+template<>
+struct hash< nonstd::u32string_view >
+{
+public:
+    std::size_t operator()( nonstd::u32string_view v ) const nssv_noexcept
+    {
+        return std::hash<std::u32string>()( std::u32string( v.data(), v.size() ) );
+    }
+};
+
+} // namespace std
+
+#endif // nssv_HAVE_STD_HASH
+
+nssv_RESTORE_WARNINGS()
+
+#endif // nssv_HAVE_STD_STRING_VIEW
+#endif // NONSTD_SV_LITE_H_INCLUDED
+//!
+//! termcolor
+//! ~~~~~~~~~
+//!
+//! termcolor is a header-only c++ library for printing colored messages
+//! to the terminal. Written just for fun with a help of the Force.
+//!
+//! :copyright: (c) 2013 by Ihor Kalnytskyi
+//! :license: BSD, see LICENSE for details
+//!
+
+#ifndef TERMCOLOR_HPP_
+#define TERMCOLOR_HPP_
+
+// the following snippet of code detects the current OS and
+// defines the appropriate macro that is used to wrap some
+// platform specific things
+#if defined(_WIN32) || defined(_WIN64)
+#define TERMCOLOR_OS_WINDOWS
+#elif defined(__APPLE__)
+#define TERMCOLOR_OS_MACOS
+#elif defined(__unix__) || defined(__unix)
+#define TERMCOLOR_OS_LINUX
+#else
+#error unsupported platform
+#endif
+
+// This headers provides the `isatty()`/`fileno()` functions,
+// which are used for testing whether a standart stream refers
+// to the terminal. As for Windows, we also need WinApi funcs
+// for changing colors attributes of the terminal.
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+#include <unistd.h>
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#include <io.h>
+#include <windows.h>
+#endif
+
+#include <cstdio>
+#include <iostream>
+
+namespace termcolor {
+// Forward declaration of the `_internal` namespace.
+// All comments are below.
+namespace _internal {
+// An index to be used to access a private storage of I/O streams. See
+// colorize / nocolorize I/O manipulators for details.
+static int colorize_index = std::ios_base::xalloc();
+
+inline FILE *get_standard_stream(const std::ostream &stream);
+inline bool is_colorized(std::ostream &stream);
+inline bool is_atty(const std::ostream &stream);
+
+#if defined(TERMCOLOR_OS_WINDOWS)
+inline void win_change_attributes(std::ostream &stream, int foreground, int background = -1);
+#endif
+} // namespace _internal
+
+inline std::ostream &colorize(std::ostream &stream) {
+  stream.iword(_internal::colorize_index) = 1L;
+  return stream;
+}
+
+inline std::ostream &nocolorize(std::ostream &stream) {
+  stream.iword(_internal::colorize_index) = 0L;
+  return stream;
+}
+
+inline std::ostream &reset(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[00m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, -1, -1);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &bold(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[1m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &dark(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[2m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &italic(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[3m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &underline(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[4m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &blink(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[5m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &reverse(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[7m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &concealed(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[8m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &crossed(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[9m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &grey(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[30m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream,
+                                     0 // grey (black)
+    );
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &red(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[31m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, FOREGROUND_RED);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &green(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[32m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, FOREGROUND_GREEN);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &yellow(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[33m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, FOREGROUND_GREEN | FOREGROUND_RED);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &blue(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[34m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, FOREGROUND_BLUE);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &magenta(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[35m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_RED);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &cyan(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[36m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_GREEN);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &white(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[37m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &on_grey(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[40m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, -1,
+                                     0 // grey (black)
+    );
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &on_red(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[41m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, -1, BACKGROUND_RED);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &on_green(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[42m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &on_yellow(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[43m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN | BACKGROUND_RED);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &on_blue(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[44m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, -1, BACKGROUND_BLUE);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &on_magenta(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[45m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, -1, BACKGROUND_BLUE | BACKGROUND_RED);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &on_cyan(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[46m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN | BACKGROUND_BLUE);
+#endif
+  }
+  return stream;
+}
+
+inline std::ostream &on_white(std::ostream &stream) {
+  if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+    stream << "\033[47m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+    _internal::win_change_attributes(stream, -1,
+                                     BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_RED);
+#endif
+  }
+
+  return stream;
+}
+
+//! Since C++ hasn't a way to hide something in the header from
+//! the outer access, I have to introduce this namespace which
+//! is used for internal purpose and should't be access from
+//! the user code.
+namespace _internal {
+//! Since C++ hasn't a true way to extract stream handler
+//! from the a given `std::ostream` object, I have to write
+//! this kind of hack.
+inline FILE *get_standard_stream(const std::ostream &stream) {
+  if (&stream == &std::cout)
+    return stdout;
+  else if ((&stream == &std::cerr) || (&stream == &std::clog))
+    return stderr;
+
+  return 0;
+}
+
+// Say whether a given stream should be colorized or not. It's always
+// true for ATTY streams and may be true for streams marked with
+// colorize flag.
+inline bool is_colorized(std::ostream &stream) {
+  return is_atty(stream) || static_cast<bool>(stream.iword(colorize_index));
+}
+
+//! Test whether a given `std::ostream` object refers to
+//! a terminal.
+inline bool is_atty(const std::ostream &stream) {
+  FILE *std_stream = get_standard_stream(stream);
+
+  // Unfortunately, fileno() ends with segmentation fault
+  // if invalid file descriptor is passed. So we need to
+  // handle this case gracefully and assume it's not a tty
+  // if standard stream is not detected, and 0 is returned.
+  if (!std_stream)
+    return false;
+
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+  return ::isatty(fileno(std_stream));
+#elif defined(TERMCOLOR_OS_WINDOWS)
+  return ::_isatty(_fileno(std_stream));
+#endif
+}
+
+#if defined(TERMCOLOR_OS_WINDOWS)
+//! Change Windows Terminal colors attribute. If some
+//! parameter is `-1` then attribute won't changed.
+inline void win_change_attributes(std::ostream &stream, int foreground, int background) {
+  // yeah, i know.. it's ugly, it's windows.
+  static WORD defaultAttributes = 0;
+
+  // Windows doesn't have ANSI escape sequences and so we use special
+  // API to change Terminal output color. That means we can't
+  // manipulate colors by means of "std::stringstream" and hence
+  // should do nothing in this case.
+  if (!_internal::is_atty(stream))
+    return;
+
+  // get terminal handle
+  HANDLE hTerminal = INVALID_HANDLE_VALUE;
+  if (&stream == &std::cout)
+    hTerminal = GetStdHandle(STD_OUTPUT_HANDLE);
+  else if (&stream == &std::cerr)
+    hTerminal = GetStdHandle(STD_ERROR_HANDLE);
+
+  // save default terminal attributes if it unsaved
+  if (!defaultAttributes) {
+    CONSOLE_SCREEN_BUFFER_INFO info;
+    if (!GetConsoleScreenBufferInfo(hTerminal, &info))
+      return;
+    defaultAttributes = info.wAttributes;
+  }
+
+  // restore all default settings
+  if (foreground == -1 && background == -1) {
+    SetConsoleTextAttribute(hTerminal, defaultAttributes);
+    return;
+  }
+
+  // get current settings
+  CONSOLE_SCREEN_BUFFER_INFO info;
+  if (!GetConsoleScreenBufferInfo(hTerminal, &info))
+    return;
+
+  if (foreground != -1) {
+    info.wAttributes &= ~(info.wAttributes & 0x0F);
+    info.wAttributes |= static_cast<WORD>(foreground);
+  }
+
+  if (background != -1) {
+    info.wAttributes &= ~(info.wAttributes & 0xF0);
+    info.wAttributes |= static_cast<WORD>(background);
+  }
+
+  SetConsoleTextAttribute(hTerminal, info.wAttributes);
+}
+#endif // TERMCOLOR_OS_WINDOWS
+
+} // namespace _internal
+
+} // namespace termcolor
+
+#undef TERMCOLOR_OS_WINDOWS
+#undef TERMCOLOR_OS_MACOS
+#undef TERMCOLOR_OS_LINUX
+
+#endif // TERMCOLOR_HPP_
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <cstdint>
+#include <string>
+
+#include <clocale>
+#include <locale>
+
+#include <cstdlib>
+// #include <tabulate/termcolor.hpp>
+#include <wchar.h>
+
+namespace tabulate {
+
+#if defined(__unix__) || defined(__unix) || defined(__APPLE__)
+inline int get_wcswidth(const std::string &string, const std::string &locale,
+                        size_t max_column_width) {
+  if (string.size() == 0)
+    return 0;
+
+  // The behavior of wcswidth() depends on the LC_CTYPE category of the current locale.
+  // Set the current locale based on cell properties before computing width
+  auto old_locale = std::locale::global(std::locale(locale));
+
+  // Convert from narrow std::string to wide string
+  wchar_t *wide_string = new wchar_t[string.size()];
+  std::mbstowcs(wide_string, string.c_str(), string.size());
+
+  // Compute display width of wide string
+  int result = wcswidth(wide_string, max_column_width);
+  delete[] wide_string;
+
+  // Restore old locale
+  std::locale::global(old_locale);
+
+  return result;
+}
+#endif
+
+inline size_t get_sequence_length(const std::string &text, const std::string &locale,
+                                  bool is_multi_byte_character_support_enabled) {
+  if (!is_multi_byte_character_support_enabled)
+    return text.length();
+
+#if defined(_WIN32) || defined(_WIN64)
+  return (text.length() - std::count_if(text.begin(), text.end(),
+                                        [](char c) -> bool { return (c & 0xC0) == 0x80; }));
+#elif defined(__unix__) || defined(__unix) || defined(__APPLE__)
+  auto result = get_wcswidth(text, locale, text.size());
+  if (result >= 0)
+    return result;
+  else
+    return (text.length() - std::count_if(text.begin(), text.end(),
+                                          [](char c) -> bool { return (c & 0xC0) == 0x80; }));
+#endif
+}
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/termcolor.hpp>
+
+namespace tabulate {
+
+enum class Color { none, grey, red, green, yellow, blue, magenta, cyan, white };
+}
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+
+namespace tabulate {
+
+enum class FontAlign { left, right, center };
+}
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+
+namespace tabulate {
+
+enum class FontStyle { bold, dark, italic, underline, blink, reverse, concealed, crossed };
+}
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <iostream>
+#include <memory>
+#include <string>
+// #include <tabulate/format.hpp>
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#include <algorithm>
+#include <cctype>
+#include <cstddef>
+#include <sstream>
+#include <string>
+// #include <tabulate/color.hpp>
+// #include <tabulate/font_align.hpp>
+// #include <tabulate/font_style.hpp>
+// #include <tabulate/utf8.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <vector>
+
+namespace tabulate {
+
+class Format {
+public:
+  Format &width(size_t value) {
+    width_ = value;
+    return *this;
+  }
+
+  Format &height(size_t value) {
+    height_ = value;
+    return *this;
+  }
+
+  Format &padding(size_t value) {
+    padding_left_ = value;
+    padding_right_ = value;
+    padding_top_ = value;
+    padding_bottom_ = value;
+    return *this;
+  }
+
+  Format &padding_left(size_t value) {
+    padding_left_ = value;
+    return *this;
+  }
+
+  Format &padding_right(size_t value) {
+    padding_right_ = value;
+    return *this;
+  }
+
+  Format &padding_top(size_t value) {
+    padding_top_ = value;
+    return *this;
+  }
+
+  Format &padding_bottom(size_t value) {
+    padding_bottom_ = value;
+    return *this;
+  }
+
+  Format &border(const std::string &value) {
+    border_left_ = value;
+    border_right_ = value;
+    border_top_ = value;
+    border_bottom_ = value;
+    return *this;
+  }
+
+  Format &border_color(Color value) {
+    border_left_color_ = value;
+    border_right_color_ = value;
+    border_top_color_ = value;
+    border_bottom_color_ = value;
+    return *this;
+  }
+
+  Format &border_background_color(Color value) {
+    border_left_background_color_ = value;
+    border_right_background_color_ = value;
+    border_top_background_color_ = value;
+    border_bottom_background_color_ = value;
+    return *this;
+  }
+
+  Format &border_left(const std::string &value) {
+    border_left_ = value;
+    return *this;
+  }
+
+  Format &border_left_color(Color value) {
+    border_left_color_ = value;
+    return *this;
+  }
+
+  Format &border_left_background_color(Color value) {
+    border_left_background_color_ = value;
+    return *this;
+  }
+
+  Format &border_right(const std::string &value) {
+    border_right_ = value;
+    return *this;
+  }
+
+  Format &border_right_color(Color value) {
+    border_right_color_ = value;
+    return *this;
+  }
+
+  Format &border_right_background_color(Color value) {
+    border_right_background_color_ = value;
+    return *this;
+  }
+
+  Format &border_top(const std::string &value) {
+    border_top_ = value;
+    return *this;
+  }
+
+  Format &border_top_color(Color value) {
+    border_top_color_ = value;
+    return *this;
+  }
+
+  Format &border_top_background_color(Color value) {
+    border_top_background_color_ = value;
+    return *this;
+  }
+
+  Format &border_bottom(const std::string &value) {
+    border_bottom_ = value;
+    return *this;
+  }
+
+  Format &border_bottom_color(Color value) {
+    border_bottom_color_ = value;
+    return *this;
+  }
+
+  Format &border_bottom_background_color(Color value) {
+    border_bottom_background_color_ = value;
+    return *this;
+  }
+
+  Format &show_border() {
+    show_border_top_ = true;
+    show_border_bottom_ = true;
+    show_border_left_ = true;
+    show_border_right_ = true;
+    return *this;
+  }
+
+  Format &hide_border() {
+    show_border_top_ = false;
+    show_border_bottom_ = false;
+    show_border_left_ = false;
+    show_border_right_ = false;
+    return *this;
+  }
+
+  Format &show_border_top() {
+    show_border_top_ = true;
+    return *this;
+  }
+
+  Format &hide_border_top() {
+    show_border_top_ = false;
+    return *this;
+  }
+
+  Format &show_border_bottom() {
+    show_border_bottom_ = true;
+    return *this;
+  }
+
+  Format &hide_border_bottom() {
+    show_border_bottom_ = false;
+    return *this;
+  }
+
+  Format &show_border_left() {
+    show_border_left_ = true;
+    return *this;
+  }
+
+  Format &hide_border_left() {
+    show_border_left_ = false;
+    return *this;
+  }
+
+  Format &show_border_right() {
+    show_border_right_ = true;
+    return *this;
+  }
+
+  Format &hide_border_right() {
+    show_border_right_ = false;
+    return *this;
+  }
+
+  Format &corner(const std::string &value) {
+    corner_top_left_ = value;
+    corner_top_right_ = value;
+    corner_bottom_left_ = value;
+    corner_bottom_right_ = value;
+    return *this;
+  }
+
+  Format &corner_color(Color value) {
+    corner_top_left_color_ = value;
+    corner_top_right_color_ = value;
+    corner_bottom_left_color_ = value;
+    corner_bottom_right_color_ = value;
+    return *this;
+  }
+
+  Format &corner_background_color(Color value) {
+    corner_top_left_background_color_ = value;
+    corner_top_right_background_color_ = value;
+    corner_bottom_left_background_color_ = value;
+    corner_bottom_right_background_color_ = value;
+    return *this;
+  }
+
+  Format &corner_top_left(const std::string &value) {
+    corner_top_left_ = value;
+    return *this;
+  }
+
+  Format &corner_top_left_color(Color value) {
+    corner_top_left_color_ = value;
+    return *this;
+  }
+
+  Format &corner_top_left_background_color(Color value) {
+    corner_top_left_background_color_ = value;
+    return *this;
+  }
+
+  Format &corner_top_right(const std::string &value) {
+    corner_top_right_ = value;
+    return *this;
+  }
+
+  Format &corner_top_right_color(Color value) {
+    corner_top_right_color_ = value;
+    return *this;
+  }
+
+  Format &corner_top_right_background_color(Color value) {
+    corner_top_right_background_color_ = value;
+    return *this;
+  }
+
+  Format &corner_bottom_left(const std::string &value) {
+    corner_bottom_left_ = value;
+    return *this;
+  }
+
+  Format &corner_bottom_left_color(Color value) {
+    corner_bottom_left_color_ = value;
+    return *this;
+  }
+
+  Format &corner_bottom_left_background_color(Color value) {
+    corner_bottom_left_background_color_ = value;
+    return *this;
+  }
+
+  Format &corner_bottom_right(const std::string &value) {
+    corner_bottom_right_ = value;
+    return *this;
+  }
+
+  Format &corner_bottom_right_color(Color value) {
+    corner_bottom_right_color_ = value;
+    return *this;
+  }
+
+  Format &corner_bottom_right_background_color(Color value) {
+    corner_bottom_right_background_color_ = value;
+    return *this;
+  }
+
+  Format &column_separator(const std::string &value) {
+    column_separator_ = value;
+    return *this;
+  }
+
+  Format &column_separator_color(Color value) {
+    column_separator_color_ = value;
+    return *this;
+  }
+
+  Format &column_separator_background_color(Color value) {
+    column_separator_background_color_ = value;
+    return *this;
+  }
+
+  Format &font_align(FontAlign value) {
+    font_align_ = value;
+    return *this;
+  }
+
+  Format &font_style(const std::vector<FontStyle> &style) {
+    if (font_style_.has_value()) {
+      for (auto &s : style)
+        font_style_->push_back(s);
+    } else {
+      font_style_ = style;
+    }
+    return *this;
+  }
+
+  Format &font_color(Color value) {
+    font_color_ = value;
+    return *this;
+  }
+
+  Format &font_background_color(Color value) {
+    font_background_color_ = value;
+    return *this;
+  }
+
+  Format &color(Color value) {
+    font_color(value);
+    border_color(value);
+    corner_color(value);
+    return *this;
+  }
+
+  Format &background_color(Color value) {
+    font_background_color(value);
+    border_background_color(value);
+    corner_background_color(value);
+    return *this;
+  }
+
+  Format &multi_byte_characters(bool value) {
+    multi_byte_characters_ = value;
+    return *this;
+  }
+
+  Format &locale(const std::string &value) {
+    locale_ = value;
+    return *this;
+  }
+
+  // Apply word wrap
+  // Given an input string and a line length, this will insert \n
+  // in strategic places in input string and apply word wrapping
+  static std::string word_wrap(const std::string &str, size_t width, const std::string &locale,
+                               bool is_multi_byte_character_support_enabled) {
+    std::vector<std::string> words = explode_string(str, {" ", "-", "\t"});
+    size_t current_line_length = 0;
+    std::string result;
+
+    for (size_t i = 0; i < words.size(); ++i) {
+      std::string word = words[i];
+      // If adding the new word to the current line would be too long,
+      // then put it on a new line (and split it up if it's too long).
+      if (current_line_length +
+              get_sequence_length(word, locale, is_multi_byte_character_support_enabled) >
+          width) {
+        // Only move down to a new line if we have text on the current line.
+        // Avoids situation where wrapped whitespace causes emptylines in text.
+        if (current_line_length > 0) {
+          result += '\n';
+          current_line_length = 0;
+        }
+
+        // If the current word is too long to fit on a line even on it's own then
+        // split the word up.
+        while (get_sequence_length(word, locale, is_multi_byte_character_support_enabled) > width) {
+          result += word.substr(0, width - 1) + "-";
+          word = word.substr(width - 1);
+          result += '\n';
+        }
+
+        // Remove leading whitespace from the word so the new line starts flush to the left.
+        word = trim_left(word);
+      }
+      result += word;
+      current_line_length +=
+          get_sequence_length(word, locale, is_multi_byte_character_support_enabled);
+    }
+    return result;
+  }
+
+  static std::vector<std::string> split_lines(const std::string &text, const std::string &delimiter,
+                                              const std::string &locale,
+                                              bool is_multi_byte_character_support_enabled) {
+    std::vector<std::string> result{};
+    std::string input = text;
+    size_t pos = 0;
+    std::string token;
+    while ((pos = input.find(delimiter)) != std::string::npos) {
+      token = input.substr(0, pos);
+      result.push_back(token);
+      input.erase(0, pos + delimiter.length());
+    }
+    if (get_sequence_length(input, locale, is_multi_byte_character_support_enabled))
+      result.push_back(input);
+    return result;
+  };
+
+  // Merge two formats
+  // first has higher precedence
+  // e.g., first = cell-level formatting and
+  // second = row-level formatting
+  // Result has attributes of both with cell-level
+  // formatting taking precedence
+  static Format merge(Format first, Format second) {
+    Format result;
+
+    // Width and height
+    if (first.width_.has_value())
+      result.width_ = first.width_;
+    else
+      result.width_ = second.width_;
+
+    if (first.height_.has_value())
+      result.height_ = first.height_;
+    else
+      result.height_ = second.height_;
+
+    // Font styling
+    if (first.font_align_.has_value())
+      result.font_align_ = first.font_align_;
+    else
+      result.font_align_ = second.font_align_;
+
+    if (first.font_style_.has_value()) {
+      // Merge font styles using std::set_union
+      std::vector<FontStyle> merged_font_style(first.font_style_->size() +
+                                               second.font_style_->size());
+#if defined(_WIN32) || defined(_WIN64)
+      // Fixes error in Windows - Sequence not ordered
+      std::sort(first.font_style_->begin(), first.font_style_->end());
+      std::sort(second.font_style_->begin(), second.font_style_->end());
+#endif
+      std::set_union(first.font_style_->begin(), first.font_style_->end(),
+                     second.font_style_->begin(), second.font_style_->end(),
+                     merged_font_style.begin());
+      result.font_style_ = merged_font_style;
+    } else
+      result.font_style_ = second.font_style_;
+
+    if (first.font_color_.has_value())
+      result.font_color_ = first.font_color_;
+    else
+      result.font_color_ = second.font_color_;
+
+    if (first.font_background_color_.has_value())
+      result.font_background_color_ = first.font_background_color_;
+    else
+      result.font_background_color_ = second.font_background_color_;
+
+    // Padding
+    if (first.padding_left_.has_value())
+      result.padding_left_ = first.padding_left_;
+    else
+      result.padding_left_ = second.padding_left_;
+
+    if (first.padding_top_.has_value())
+      result.padding_top_ = first.padding_top_;
+    else
+      result.padding_top_ = second.padding_top_;
+
+    if (first.padding_right_.has_value())
+      result.padding_right_ = first.padding_right_;
+    else
+      result.padding_right_ = second.padding_right_;
+
+    if (first.padding_bottom_.has_value())
+      result.padding_bottom_ = first.padding_bottom_;
+    else
+      result.padding_bottom_ = second.padding_bottom_;
+
+    // Border
+    if (first.border_left_.has_value())
+      result.border_left_ = first.border_left_;
+    else
+      result.border_left_ = second.border_left_;
+
+    if (first.border_left_color_.has_value())
+      result.border_left_color_ = first.border_left_color_;
+    else
+      result.border_left_color_ = second.border_left_color_;
+
+    if (first.border_left_background_color_.has_value())
+      result.border_left_background_color_ = first.border_left_background_color_;
+    else
+      result.border_left_background_color_ = second.border_left_background_color_;
+
+    if (first.border_top_.has_value())
+      result.border_top_ = first.border_top_;
+    else
+      result.border_top_ = second.border_top_;
+
+    if (first.border_top_color_.has_value())
+      result.border_top_color_ = first.border_top_color_;
+    else
+      result.border_top_color_ = second.border_top_color_;
+
+    if (first.border_top_background_color_.has_value())
+      result.border_top_background_color_ = first.border_top_background_color_;
+    else
+      result.border_top_background_color_ = second.border_top_background_color_;
+
+    if (first.border_bottom_.has_value())
+      result.border_bottom_ = first.border_bottom_;
+    else
+      result.border_bottom_ = second.border_bottom_;
+
+    if (first.border_bottom_color_.has_value())
+      result.border_bottom_color_ = first.border_bottom_color_;
+    else
+      result.border_bottom_color_ = second.border_bottom_color_;
+
+    if (first.border_bottom_background_color_.has_value())
+      result.border_bottom_background_color_ = first.border_bottom_background_color_;
+    else
+      result.border_bottom_background_color_ = second.border_bottom_background_color_;
+
+    if (first.border_right_.has_value())
+      result.border_right_ = first.border_right_;
+    else
+      result.border_right_ = second.border_right_;
+
+    if (first.border_right_color_.has_value())
+      result.border_right_color_ = first.border_right_color_;
+    else
+      result.border_right_color_ = second.border_right_color_;
+
+    if (first.border_right_background_color_.has_value())
+      result.border_right_background_color_ = first.border_right_background_color_;
+    else
+      result.border_right_background_color_ = second.border_right_background_color_;
+
+    if (first.show_border_top_.has_value())
+      result.show_border_top_ = first.show_border_top_;
+    else
+      result.show_border_top_ = second.show_border_top_;
+
+    if (first.show_border_bottom_.has_value())
+      result.show_border_bottom_ = first.show_border_bottom_;
+    else
+      result.show_border_bottom_ = second.show_border_bottom_;
+
+    if (first.show_border_left_.has_value())
+      result.show_border_left_ = first.show_border_left_;
+    else
+      result.show_border_left_ = second.show_border_left_;
+
+    if (first.show_border_right_.has_value())
+      result.show_border_right_ = first.show_border_right_;
+    else
+      result.show_border_right_ = second.show_border_right_;
+
+    // Corner
+    if (first.corner_top_left_.has_value())
+      result.corner_top_left_ = first.corner_top_left_;
+    else
+      result.corner_top_left_ = second.corner_top_left_;
+
+    if (first.corner_top_left_color_.has_value())
+      result.corner_top_left_color_ = first.corner_top_left_color_;
+    else
+      result.corner_top_left_color_ = second.corner_top_left_color_;
+
+    if (first.corner_top_left_background_color_.has_value())
+      result.corner_top_left_background_color_ = first.corner_top_left_background_color_;
+    else
+      result.corner_top_left_background_color_ = second.corner_top_left_background_color_;
+
+    if (first.corner_top_right_.has_value())
+      result.corner_top_right_ = first.corner_top_right_;
+    else
+      result.corner_top_right_ = second.corner_top_right_;
+
+    if (first.corner_top_right_color_.has_value())
+      result.corner_top_right_color_ = first.corner_top_right_color_;
+    else
+      result.corner_top_right_color_ = second.corner_top_right_color_;
+
+    if (first.corner_top_right_background_color_.has_value())
+      result.corner_top_right_background_color_ = first.corner_top_right_background_color_;
+    else
+      result.corner_top_right_background_color_ = second.corner_top_right_background_color_;
+
+    if (first.corner_bottom_left_.has_value())
+      result.corner_bottom_left_ = first.corner_bottom_left_;
+    else
+      result.corner_bottom_left_ = second.corner_bottom_left_;
+
+    if (first.corner_bottom_left_color_.has_value())
+      result.corner_bottom_left_color_ = first.corner_bottom_left_color_;
+    else
+      result.corner_bottom_left_color_ = second.corner_bottom_left_color_;
+
+    if (first.corner_bottom_left_background_color_.has_value())
+      result.corner_bottom_left_background_color_ = first.corner_bottom_left_background_color_;
+    else
+      result.corner_bottom_left_background_color_ = second.corner_bottom_left_background_color_;
+
+    if (first.corner_bottom_right_.has_value())
+      result.corner_bottom_right_ = first.corner_bottom_right_;
+    else
+      result.corner_bottom_right_ = second.corner_bottom_right_;
+
+    if (first.corner_bottom_right_color_.has_value())
+      result.corner_bottom_right_color_ = first.corner_bottom_right_color_;
+    else
+      result.corner_bottom_right_color_ = second.corner_bottom_right_color_;
+
+    if (first.corner_bottom_right_background_color_.has_value())
+      result.corner_bottom_right_background_color_ = first.corner_bottom_right_background_color_;
+    else
+      result.corner_bottom_right_background_color_ = second.corner_bottom_right_background_color_;
+
+    // Column separator
+    if (first.column_separator_.has_value())
+      result.column_separator_ = first.column_separator_;
+    else
+      result.column_separator_ = second.column_separator_;
+
+    if (first.column_separator_color_.has_value())
+      result.column_separator_color_ = first.column_separator_color_;
+    else
+      result.column_separator_color_ = second.column_separator_color_;
+
+    if (first.column_separator_background_color_.has_value())
+      result.column_separator_background_color_ = first.column_separator_background_color_;
+    else
+      result.column_separator_background_color_ = second.column_separator_background_color_;
+
+    // Internationlization
+    if (first.multi_byte_characters_.has_value())
+      result.multi_byte_characters_ = first.multi_byte_characters_;
+    else
+      result.multi_byte_characters_ = second.multi_byte_characters_;
+
+    if (first.locale_.has_value())
+      result.locale_ = first.locale_;
+    else
+      result.locale_ = second.locale_;
+
+    return result;
+  }
+
+private:
+  friend class Cell;
+  friend class Row;
+  friend class Column;
+  friend class TableInternal;
+  friend class Printer;
+  friend class MarkdownExporter;
+  friend class LatexExporter;
+  friend class AsciiDocExporter;
+
+  void set_defaults() {
+    // NOTE: width and height are not set here
+    font_align_ = FontAlign::left;
+    font_style_ = std::vector<FontStyle>{};
+    font_color_ = font_background_color_ = Color::none;
+    padding_left_ = padding_right_ = 1;
+    padding_top_ = padding_bottom_ = 0;
+    border_top_ = border_bottom_ = "-";
+    border_left_ = border_right_ = "|";
+    show_border_left_ = show_border_right_ = show_border_top_ = show_border_bottom_ = true;
+    border_top_color_ = border_top_background_color_ = border_bottom_color_ =
+        border_bottom_background_color_ = border_left_color_ = border_left_background_color_ =
+            border_right_color_ = border_right_background_color_ = Color::none;
+    corner_top_left_ = corner_top_right_ = corner_bottom_left_ = corner_bottom_right_ = "+";
+    corner_top_left_color_ = corner_top_left_background_color_ = corner_top_right_color_ =
+        corner_top_right_background_color_ = corner_bottom_left_color_ =
+            corner_bottom_left_background_color_ = corner_bottom_right_color_ =
+                corner_bottom_right_background_color_ = Color::none;
+    column_separator_ = "|";
+    column_separator_color_ = column_separator_background_color_ = Color::none;
+    multi_byte_characters_ = false;
+    locale_ = "";
+  }
+
+  // Helper methods for word wrapping:
+
+  // trim white spaces from the left end of an input string
+  static std::string trim_left(const std::string &input_string) {
+    std::string result = input_string;
+    result.erase(result.begin(), std::find_if(result.begin(), result.end(),
+                                              [](int ch) { return !std::isspace(ch); }));
+    return result;
+  }
+
+  // trim white spaces from right end of an input string
+  static std::string trim_right(const std::string &input_string) {
+    std::string result = input_string;
+    result.erase(
+        std::find_if(result.rbegin(), result.rend(), [](int ch) { return !std::isspace(ch); })
+            .base(),
+        result.end());
+    return result;
+  }
+
+  // trim white spaces from either end of an input string
+  static std::string trim(const std::string &input_string) {
+    return trim_left(trim_right(input_string));
+  }
+
+  static size_t index_of_any(const std::string &input, size_t start_index,
+                             const std::vector<std::string> &split_characters) {
+    std::vector<size_t> indices{};
+    for (auto &c : split_characters) {
+      auto index = input.find(c, start_index);
+      if (index != std::string::npos)
+        indices.push_back(index);
+    }
+    if (indices.size() > 0)
+      return *std::min_element(indices.begin(), indices.end());
+    else
+      return std::string::npos;
+  }
+
+  static std::vector<std::string> explode_string(const std::string &input,
+                                                 const std::vector<std::string> &split_characters) {
+    std::vector<std::string> result{};
+    size_t start_index{0};
+    while (true) {
+      auto index = index_of_any(input, start_index, split_characters);
+
+      if (index == std::string::npos) {
+        result.push_back(input.substr(start_index));
+        return result;
+      }
+
+      std::string word = input.substr(start_index, index - start_index);
+      char next_character = input.substr(index, 1)[0];
+      // Unlike whitespace, dashes and the like should stick to the word occurring before it.
+      if (isspace(next_character)) {
+        result.push_back(word);
+        result.push_back(std::string(1, next_character));
+      } else {
+        result.push_back(word + next_character);
+      }
+      start_index = index + 1;
+    }
+
+    return result;
+  }
+
+  // Element width and height
+  optional<size_t> width_{};
+  optional<size_t> height_{};
+
+  // Font styling
+  optional<FontAlign> font_align_{};
+  optional<std::vector<FontStyle>> font_style_{};
+  optional<Color> font_color_{};
+  optional<Color> font_background_color_{};
+
+  // Element padding
+  optional<size_t> padding_left_{};
+  optional<size_t> padding_top_{};
+  optional<size_t> padding_right_{};
+  optional<size_t> padding_bottom_{};
+
+  // Element border
+  optional<bool> show_border_top_{};
+  optional<std::string> border_top_{};
+  optional<Color> border_top_color_{};
+  optional<Color> border_top_background_color_{};
+
+  optional<bool> show_border_bottom_{};
+  optional<std::string> border_bottom_{};
+  optional<Color> border_bottom_color_{};
+  optional<Color> border_bottom_background_color_{};
+
+  optional<bool> show_border_left_{};
+  optional<std::string> border_left_{};
+  optional<Color> border_left_color_{};
+  optional<Color> border_left_background_color_{};
+
+  optional<bool> show_border_right_{};
+  optional<std::string> border_right_{};
+  optional<Color> border_right_color_{};
+  optional<Color> border_right_background_color_{};
+
+  // Element corner
+  optional<std::string> corner_top_left_{};
+  optional<Color> corner_top_left_color_{};
+  optional<Color> corner_top_left_background_color_{};
+
+  optional<std::string> corner_top_right_{};
+  optional<Color> corner_top_right_color_{};
+  optional<Color> corner_top_right_background_color_{};
+
+  optional<std::string> corner_bottom_left_{};
+  optional<Color> corner_bottom_left_color_{};
+  optional<Color> corner_bottom_left_background_color_{};
+
+  optional<std::string> corner_bottom_right_{};
+  optional<Color> corner_bottom_right_color_{};
+  optional<Color> corner_bottom_right_background_color_{};
+
+  // Element column separator
+  optional<std::string> column_separator_{};
+  optional<Color> column_separator_color_{};
+  optional<Color> column_separator_background_color_{};
+
+  // Internationalization
+  optional<bool> multi_byte_characters_{};
+  optional<std::string> locale_{};
+};
+
+} // namespace tabulate
+
+// #include <tabulate/utf8.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <vector>
+
+namespace tabulate {
+
+class Cell {
+public:
+  explicit Cell(std::shared_ptr<class Row> parent) : parent_(parent) {}
+
+  void set_text(const std::string &text) { data_ = text; }
+
+  const std::string &get_text() { return data_; }
+
+  size_t size() {
+    return get_sequence_length(data_, locale(), is_multi_byte_character_support_enabled());
+  }
+
+  std::string locale() { return *format().locale_; }
+
+  Format &format();
+
+  bool is_multi_byte_character_support_enabled();
+
+private:
+  std::string data_;
+  std::weak_ptr<class Row> parent_;
+  optional<Format> format_;
+};
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <iostream>
+#include <memory>
+#include <string>
+// #include <tabulate/cell.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <vector>
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+
+namespace tabulate {
+
+class Row {
+public:
+  explicit Row(std::shared_ptr<class TableInternal> parent) : parent_(parent) {}
+
+  void add_cell(std::shared_ptr<Cell> cell) { cells_.push_back(cell); }
+
+  Cell &operator[](size_t index) { return cell(index); }
+
+  Cell &cell(size_t index) { return *(cells_[index]); }
+
+  std::vector<std::shared_ptr<Cell>> cells() const { return cells_; }
+
+  size_t size() const { return cells_.size(); }
+
+  Format &format();
+
+  class CellIterator {
+  public:
+    explicit CellIterator(std::vector<std::shared_ptr<Cell>>::iterator ptr) : ptr(ptr) {}
+
+    CellIterator operator++() {
+      ++ptr;
+      return *this;
+    }
+    bool operator!=(const CellIterator &other) const { return ptr != other.ptr; }
+    Cell &operator*() { return **ptr; }
+
+  private:
+    std::vector<std::shared_ptr<Cell>>::iterator ptr;
+  };
+
+  auto begin() -> CellIterator { return CellIterator(cells_.begin()); }
+  auto end() -> CellIterator { return CellIterator(cells_.end()); }
+
+private:
+  friend class Printer;
+
+  // Returns the row height as configured
+  // For each cell in the row, check the cell.format.height
+  // property and return the largest configured row height
+  // This is used to ensure that all cells in a row are
+  // aligned when printing the column
+  size_t get_configured_height() {
+    size_t result{0};
+    for (size_t i = 0; i < size(); ++i) {
+      auto cell = cells_[i];
+      auto format = cell->format();
+      if (format.height_.has_value())
+        result = std::max(result, *format.height_);
+    }
+    return result;
+  }
+
+  // Computes the height of the row based on cell contents
+  // and configured cell padding
+  // For each cell, compute:
+  //   padding_top + (cell_contents / column height) + padding_bottom
+  // and return the largest value
+  //
+  // This is useful when no cell.format.height is configured
+  // Call get_configured_height()
+  // - If this returns 0, then use get_computed_height()
+  size_t get_computed_height(const std::vector<size_t> &column_widths) {
+    size_t result{0};
+    for (size_t i = 0; i < size(); ++i) {
+      result = std::max(result, get_cell_height(i, column_widths[i]));
+    }
+    return result;
+  }
+
+  // Returns padding_top + cell_contents / column_height + padding_bottom
+  // for a given cell in the column
+  // e.g.,
+  // column width = 5
+  // cell_contents = "I love tabulate" (size/length = 15)
+  // padding top and padding bottom are 1
+  // then, cell height = 1 + (15 / 5) + 1 = 1 + 3 + 1 = 5
+  // The cell will look like this:
+  //
+  // .....
+  // I lov
+  // e tab
+  // ulate
+  // .....
+  size_t get_cell_height(size_t cell_index, size_t column_width) {
+    size_t result{0};
+    Cell &cell = *(cells_[cell_index]);
+    auto format = cell.format();
+    auto text = cell.get_text();
+
+    auto padding_left = *format.padding_left_;
+    auto padding_right = *format.padding_right_;
+
+    result += *format.padding_top_;
+
+    if (column_width > (padding_left + padding_right)) {
+      column_width -= (padding_left + padding_right);
+    }
+
+    // Check if input text has embedded newline characters
+    auto newlines_in_text = std::count(text.begin(), text.end(), '\n');
+    std::string word_wrapped_text;
+    if (newlines_in_text == 0) {
+      // No new lines in input
+      // Apply automatic word wrapping and compute row height
+      word_wrapped_text = Format::word_wrap(text, column_width, cell.locale(),
+                                            cell.is_multi_byte_character_support_enabled());
+    } else {
+      // There are embedded '\n' characters
+      // Respect these characters
+      word_wrapped_text = text;
+    }
+
+    auto newlines_in_wrapped_text =
+        std::count(word_wrapped_text.begin(), word_wrapped_text.end(), '\n');
+    auto estimated_row_height = newlines_in_wrapped_text;
+
+    if (!word_wrapped_text.empty() &&
+        word_wrapped_text[word_wrapped_text.size() - 1] != '\n') // text doesn't end with a newline
+      estimated_row_height += 1;
+
+    result += estimated_row_height;
+
+    result += *format.padding_bottom_;
+
+    return result;
+  }
+
+  std::vector<std::shared_ptr<Cell>> cells_;
+  std::weak_ptr<class TableInternal> parent_;
+  optional<Format> format_;
+};
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+
+namespace tabulate {
+
+class ColumnFormat : public Format {
+public:
+  explicit ColumnFormat(class Column &column) : column_(column) {}
+
+  ColumnFormat &width(size_t value);
+  ColumnFormat &height(size_t value);
+
+  // Padding
+  ColumnFormat &padding(size_t value);
+  ColumnFormat &padding_left(size_t value);
+  ColumnFormat &padding_right(size_t value);
+  ColumnFormat &padding_top(size_t value);
+  ColumnFormat &padding_bottom(size_t value);
+
+  // Border
+  ColumnFormat &border(const std::string &value);
+  ColumnFormat &border_color(Color value);
+  ColumnFormat &border_background_color(Color value);
+  ColumnFormat &border_left(const std::string &value);
+  ColumnFormat &border_left_color(Color value);
+  ColumnFormat &border_left_background_color(Color value);
+  ColumnFormat &border_right(const std::string &value);
+  ColumnFormat &border_right_color(Color value);
+  ColumnFormat &border_right_background_color(Color value);
+  ColumnFormat &border_top(const std::string &value);
+  ColumnFormat &border_top_color(Color value);
+  ColumnFormat &border_top_background_color(Color value);
+  ColumnFormat &border_bottom(const std::string &value);
+  ColumnFormat &border_bottom_color(Color value);
+  ColumnFormat &border_bottom_background_color(Color value);
+
+  // Corner
+  ColumnFormat &corner(const std::string &value);
+  ColumnFormat &corner_color(Color value);
+  ColumnFormat &corner_background_color(Color value);
+
+  // Column separator
+  ColumnFormat &column_separator(const std::string &value);
+  ColumnFormat &column_separator_color(Color value);
+  ColumnFormat &column_separator_background_color(Color value);
+
+  // Font styling
+  ColumnFormat &font_align(FontAlign value);
+  ColumnFormat &font_style(const std::vector<FontStyle> &style);
+  ColumnFormat &font_color(Color value);
+  ColumnFormat &font_background_color(Color value);
+  ColumnFormat &color(Color value);
+  ColumnFormat &background_color(Color value);
+
+  // Locale
+  ColumnFormat &multi_byte_characters(bool value);
+  ColumnFormat &locale(const std::string &value);
+
+private:
+  std::reference_wrapper<class Column> column_;
+};
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <functional>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <string>
+// #include <tabulate/cell.hpp>
+// #include <tabulate/column_format.hpp>
+#include <vector>
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+
+namespace tabulate {
+
+class Column {
+public:
+  explicit Column(std::shared_ptr<class TableInternal> parent) : parent_(parent) {}
+
+  void add_cell(Cell &cell) { cells_.push_back(cell); }
+
+  Cell &operator[](size_t index) { return cells_[index]; }
+
+  std::vector<std::reference_wrapper<Cell>> cells() const { return cells_; }
+
+  size_t size() const { return cells_.size(); }
+
+  ColumnFormat format() { return ColumnFormat(*this); }
+
+  class CellIterator {
+  public:
+    explicit CellIterator(std::vector<std::reference_wrapper<Cell>>::iterator ptr) : ptr(ptr) {}
+
+    CellIterator operator++() {
+      ++ptr;
+      return *this;
+    }
+    bool operator!=(const CellIterator &other) const { return ptr != other.ptr; }
+    Cell &operator*() { return *ptr; }
+
+  private:
+    std::vector<std::reference_wrapper<Cell>>::iterator ptr;
+  };
+
+  auto begin() -> CellIterator { return CellIterator(cells_.begin()); }
+  auto end() -> CellIterator { return CellIterator(cells_.end()); }
+
+private:
+  friend class ColumnFormat;
+  friend class Printer;
+
+  // Returns the column width as configured
+  // For each cell in the column, check the cell.format.width
+  // property and return the largest configured column width
+  // This is used to ensure that all cells in a column are
+  // aligned when printing the column
+  size_t get_configured_width() {
+    size_t result{0};
+    for (size_t i = 0; i < size(); ++i) {
+      auto cell = cells_[i];
+      auto format = cell.get().format();
+      if (format.width_.has_value())
+        result = std::max(result, *format.width_);
+    }
+    return result;
+  }
+
+  // Computes the width of the column based on cell contents
+  // and configured cell padding
+  // For each cell, compute padding_left + cell_contents + padding_right
+  // and return the largest value
+  //
+  // This is useful when no cell.format.width is configured
+  // Call get_configured_width()
+  // - If this returns 0, then use get_computed_width()
+  size_t get_computed_width() {
+    size_t result{0};
+    for (size_t i = 0; i < size(); ++i) {
+      result = std::max(result, get_cell_width(i));
+    }
+    return result;
+  }
+
+  // Returns padding_left + cell_contents.size() + padding_right
+  // for a given cell in the column
+  size_t get_cell_width(size_t cell_index) {
+    size_t result{0};
+    Cell &cell = cells_[cell_index].get();
+    auto format = cell.format();
+    if (format.padding_left_.has_value())
+      result += *format.padding_left_;
+
+    // Check if input text has newlines
+    auto text = cell.get_text();
+    auto split_lines = Format::split_lines(text, "\n", cell.locale(),
+                                           cell.is_multi_byte_character_support_enabled());
+
+    // If there are no newlines in input, set column_width = text.size()
+    if (split_lines.size() == 1) {
+      result += cell.size();
+    } else {
+      // There are newlines in input
+      // Find widest substring in input and use this as column_width
+      size_t widest_sub_string_size{0};
+      for (auto &line : split_lines)
+        if (get_sequence_length(line, cell.locale(),
+                                cell.is_multi_byte_character_support_enabled()) >
+            widest_sub_string_size)
+          widest_sub_string_size = get_sequence_length(
+              line, cell.locale(), cell.is_multi_byte_character_support_enabled());
+      result += widest_sub_string_size;
+    }
+
+    if (format.padding_right_.has_value())
+      result += *format.padding_right_;
+
+    return result;
+  }
+
+  std::vector<std::reference_wrapper<Cell>> cells_;
+  std::weak_ptr<class TableInternal> parent_;
+};
+
+inline ColumnFormat &ColumnFormat::width(size_t value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().width(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::height(size_t value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().height(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding(size_t value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().padding(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_left(size_t value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().padding_left(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_right(size_t value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().padding_right(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_top(size_t value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().padding_top(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_bottom(size_t value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().padding_bottom(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border(const std::string &value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_background_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_background_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_left(const std::string &value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_left(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_left_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_left_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_left_background_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_left_background_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_right(const std::string &value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_right(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_right_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_right_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_right_background_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_right_background_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_top(const std::string &value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_top(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_top_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_top_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_top_background_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_top_background_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_bottom(const std::string &value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_bottom(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_bottom_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_bottom_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_bottom_background_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().border_bottom_background_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::corner(const std::string &value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().corner(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::corner_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().corner_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::corner_background_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().corner_background_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::column_separator(const std::string &value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().column_separator(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::column_separator_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().column_separator_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::column_separator_background_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().column_separator_background_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_align(FontAlign value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().font_align(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_style(const std::vector<FontStyle> &style) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().font_style(style);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().font_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_background_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().font_background_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::background_color(Color value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().background_color(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::multi_byte_characters(bool value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().multi_byte_characters(value);
+  return *this;
+}
+
+inline ColumnFormat &ColumnFormat::locale(const std::string &value) {
+  for (auto &cell : column_.get().cells_)
+    cell.get().format().locale(value);
+  return *this;
+}
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/color.hpp>
+// #include <tabulate/font_style.hpp>
+#include <utility>
+#include <vector>
+
+namespace tabulate {
+
+class Printer {
+public:
+  static std::pair<std::vector<size_t>, std::vector<size_t>>
+  compute_cell_dimensions(TableInternal &table);
+
+  static void print_table(std::ostream &stream, TableInternal &table);
+
+  static void print_row_in_cell(std::ostream &stream, TableInternal &table,
+                                const std::pair<size_t, size_t> &index,
+                                const std::pair<size_t, size_t> &dimension, size_t num_columns,
+                                size_t row_index);
+
+  static bool print_cell_border_top(std::ostream &stream, TableInternal &table,
+                                    const std::pair<size_t, size_t> &index,
+                                    const std::pair<size_t, size_t> &dimension, size_t num_columns);
+  static bool print_cell_border_bottom(std::ostream &stream, TableInternal &table,
+                                       const std::pair<size_t, size_t> &index,
+                                       const std::pair<size_t, size_t> &dimension,
+                                       size_t num_columns);
+
+  static void apply_element_style(std::ostream &stream, Color foreground_color,
+                                  Color background_color,
+                                  const std::vector<FontStyle> &font_style) {
+    apply_foreground_color(stream, foreground_color);
+    apply_background_color(stream, background_color);
+    for (auto &style : font_style)
+      apply_font_style(stream, style);
+  }
+
+  static void reset_element_style(std::ostream &stream) { stream << termcolor::reset; }
+
+private:
+  static void print_content_left_aligned(std::ostream &stream, const std::string &cell_content,
+                                         const Format &format, size_t text_with_padding_size,
+                                         size_t column_width) {
+
+    // Apply font style
+    apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+                        *format.font_style_);
+    stream << cell_content;
+    // Only apply font_style to the font
+    // Not the padding. So calling apply_element_style with font_style = {}
+    reset_element_style(stream);
+    apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+
+    if (text_with_padding_size < column_width) {
+      for (size_t j = 0; j < (column_width - text_with_padding_size); ++j) {
+        stream << " ";
+      }
+    }
+  }
+
+  static void print_content_center_aligned(std::ostream &stream, const std::string &cell_content,
+                                           const Format &format, size_t text_with_padding_size,
+                                           size_t column_width) {
+    auto num_spaces = column_width - text_with_padding_size;
+    if (num_spaces % 2 == 0) {
+      // Even spacing on either side
+      for (size_t j = 0; j < num_spaces / 2; ++j)
+        stream << " ";
+
+      // Apply font style
+      apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+                          *format.font_style_);
+      stream << cell_content;
+      // Only apply font_style to the font
+      // Not the padding. So calling apply_element_style with font_style = {}
+      reset_element_style(stream);
+      apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+
+      for (size_t j = 0; j < num_spaces / 2; ++j)
+        stream << " ";
+    } else {
+      auto num_spaces_before = num_spaces / 2 + 1;
+      for (size_t j = 0; j < num_spaces_before; ++j)
+        stream << " ";
+
+      // Apply font style
+      apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+                          *format.font_style_);
+      stream << cell_content;
+      // Only apply font_style to the font
+      // Not the padding. So calling apply_element_style with font_style = {}
+      reset_element_style(stream);
+      apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+
+      for (size_t j = 0; j < num_spaces - num_spaces_before; ++j)
+        stream << " ";
+    }
+  }
+
+  static void print_content_right_aligned(std::ostream &stream, const std::string &cell_content,
+                                          const Format &format, size_t text_with_padding_size,
+                                          size_t column_width) {
+    if (text_with_padding_size < column_width) {
+      for (size_t j = 0; j < (column_width - text_with_padding_size); ++j) {
+        stream << " ";
+      }
+    }
+
+    // Apply font style
+    apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+                        *format.font_style_);
+    stream << cell_content;
+    // Only apply font_style to the font
+    // Not the padding. So calling apply_element_style with font_style = {}
+    reset_element_style(stream);
+    apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+  }
+
+  static void apply_font_style(std::ostream &stream, FontStyle style) {
+    switch (style) {
+    case FontStyle::bold:
+      stream << termcolor::bold;
+      break;
+    case FontStyle::dark:
+      stream << termcolor::dark;
+      break;
+    case FontStyle::italic:
+      stream << termcolor::italic;
+      break;
+    case FontStyle::underline:
+      stream << termcolor::underline;
+      break;
+    case FontStyle::blink:
+      stream << termcolor::blink;
+      break;
+    case FontStyle::reverse:
+      stream << termcolor::reverse;
+      break;
+    case FontStyle::concealed:
+      stream << termcolor::concealed;
+      break;
+    case FontStyle::crossed:
+      stream << termcolor::crossed;
+      break;
+    default:
+      break;
+    }
+  }
+
+  static void apply_foreground_color(std::ostream &stream, Color foreground_color) {
+    switch (foreground_color) {
+    case Color::grey:
+      stream << termcolor::grey;
+      break;
+    case Color::red:
+      stream << termcolor::red;
+      break;
+    case Color::green:
+      stream << termcolor::green;
+      break;
+    case Color::yellow:
+      stream << termcolor::yellow;
+      break;
+    case Color::blue:
+      stream << termcolor::blue;
+      break;
+    case Color::magenta:
+      stream << termcolor::magenta;
+      break;
+    case Color::cyan:
+      stream << termcolor::cyan;
+      break;
+    case Color::white:
+      stream << termcolor::white;
+      break;
+    case Color::none:
+    default:
+      break;
+    }
+  }
+
+  static void apply_background_color(std::ostream &stream, Color background_color) {
+    switch (background_color) {
+    case Color::grey:
+      stream << termcolor::on_grey;
+      break;
+    case Color::red:
+      stream << termcolor::on_red;
+      break;
+    case Color::green:
+      stream << termcolor::on_green;
+      break;
+    case Color::yellow:
+      stream << termcolor::on_yellow;
+      break;
+    case Color::blue:
+      stream << termcolor::on_blue;
+      break;
+    case Color::magenta:
+      stream << termcolor::on_magenta;
+      break;
+    case Color::cyan:
+      stream << termcolor::on_cyan;
+      break;
+    case Color::white:
+      stream << termcolor::on_white;
+      break;
+    case Color::none:
+    default:
+      break;
+    }
+  }
+};
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <iostream>
+#include <string>
+// #include <tabulate/column.hpp>
+// #include <tabulate/font_style.hpp>
+// #include <tabulate/printer.hpp>
+// #include <tabulate/row.hpp>
+// #include <tabulate/termcolor.hpp>
+#include <vector>
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+
+namespace tabulate {
+
+class TableInternal : public std::enable_shared_from_this<TableInternal> {
+public:
+  static std::shared_ptr<TableInternal> create() {
+    auto result = std::shared_ptr<TableInternal>(new TableInternal());
+    result->format_.set_defaults();
+    return result;
+  }
+
+  void add_row(const std::vector<std::string> &cells) {
+    auto row = std::make_shared<Row>(shared_from_this());
+    for (auto &c : cells) {
+      auto cell = std::make_shared<Cell>(row);
+      cell->set_text(c);
+      row->add_cell(cell);
+    }
+    rows_.push_back(row);
+  }
+
+  Row &operator[](size_t index) { return *(rows_[index]); }
+
+  const Row &operator[](size_t index) const { return *(rows_[index]); }
+
+  Column column(size_t index) {
+    Column column(shared_from_this());
+    for (size_t i = 0; i < rows_.size(); ++i) {
+      auto row = rows_[i];
+      auto &cell = row->cell(index);
+      column.add_cell(cell);
+    }
+    return column;
+  }
+
+  size_t size() const { return rows_.size(); }
+
+  std::pair<size_t, size_t> shape() {
+    std::pair<size_t, size_t> result{0, 0};
+    std::stringstream stream;
+    print(stream);
+    auto buffer = stream.str();
+    auto lines = Format::split_lines(buffer, "\n", "", true);
+    if (lines.size()) {
+      result = {get_sequence_length(lines[0], "", true), lines.size()};
+    }
+    return result;
+  }
+
+  Format &format() { return format_; }
+
+  void print(std::ostream &stream) { Printer::print_table(stream, *this); }
+
+  size_t estimate_num_columns() const {
+    size_t result{0};
+    if (size()) {
+      auto first_row = operator[](size_t(0));
+      result = first_row.size();
+    }
+    return result;
+  }
+
+private:
+  friend class Table;
+  friend class MarkdownExporter;
+
+  TableInternal() {}
+  TableInternal &operator=(const TableInternal &);
+  TableInternal(const TableInternal &);
+
+  std::vector<std::shared_ptr<Row>> rows_;
+  Format format_;
+};
+
+inline Format &Cell::format() {
+  std::shared_ptr<Row> parent = parent_.lock();
+  if (!format_.has_value()) {   // no cell format
+    format_ = parent->format(); // Use parent row format
+  } else {
+    // Cell has formatting
+    // Merge cell formatting with parent row formatting
+    format_ = Format::merge(*format_, parent->format());
+  }
+  return *format_;
+}
+
+inline bool Cell::is_multi_byte_character_support_enabled() {
+  return (*format().multi_byte_characters_);
+}
+
+inline Format &Row::format() {
+  std::shared_ptr<TableInternal> parent = parent_.lock();
+  if (!format_.has_value()) {   // no row format
+    format_ = parent->format(); // Use parent table format
+  } else {
+    // Row has formatting rules
+    // Merge with parent table format
+    format_ = Format::merge(*format_, parent->format());
+  }
+  return *format_;
+}
+
+inline std::pair<std::vector<size_t>, std::vector<size_t>>
+Printer::compute_cell_dimensions(TableInternal &table) {
+  std::pair<std::vector<size_t>, std::vector<size_t>> result;
+  size_t num_rows = table.size();
+  size_t num_columns = table.estimate_num_columns();
+
+  std::vector<size_t> row_heights, column_widths{};
+
+  for (size_t i = 0; i < num_columns; ++i) {
+    Column column = table.column(i);
+    size_t configured_width = column.get_configured_width();
+    size_t computed_width = column.get_computed_width();
+    if (configured_width != 0)
+      column_widths.push_back(configured_width);
+    else
+      column_widths.push_back(computed_width);
+  }
+
+  for (size_t i = 0; i < num_rows; ++i) {
+    Row row = table[i];
+    size_t configured_height = row.get_configured_height();
+    size_t computed_height = row.get_computed_height(column_widths);
+
+    // NOTE: Unlike column width, row height is calculated as the max
+    // b/w configured height and computed height
+    // which means that .width() has higher precedence than .height()
+    // when both are configured by the user
+    //
+    // TODO: Maybe this can be configured?
+    // If such a configuration is exposed, i.e., prefer height over width
+    // then the logic will be reversed, i.e.,
+    // column_widths.push_back(std::max(configured_width, computed_width))
+    // and
+    // row_height = configured_height if != 0 else computed_height
+
+    row_heights.push_back(std::max(configured_height, computed_height));
+  }
+
+  result.first = row_heights;
+  result.second = column_widths;
+
+  return result;
+}
+
+inline void Printer::print_table(std::ostream &stream, TableInternal &table) {
+  size_t num_rows = table.size();
+  size_t num_columns = table.estimate_num_columns();
+  auto dimensions = compute_cell_dimensions(table);
+  auto row_heights = dimensions.first;
+  auto column_widths = dimensions.second;
+
+  // For each row,
+  for (size_t i = 0; i < num_rows; ++i) {
+
+    // Print top border
+    bool border_top_printed{true};
+    for (size_t j = 0; j < num_columns; ++j) {
+      border_top_printed &= print_cell_border_top(stream, table, {i, j},
+                                                  {row_heights[i], column_widths[j]}, num_columns);
+    }
+    if (border_top_printed)
+      stream << termcolor::reset << "\n";
+
+    // Print row contents with word wrapping
+    for (size_t k = 0; k < row_heights[i]; ++k) {
+      for (size_t j = 0; j < num_columns; ++j) {
+        print_row_in_cell(stream, table, {i, j}, {row_heights[i], column_widths[j]}, num_columns,
+                          k);
+      }
+      if (k + 1 < row_heights[i])
+        stream << termcolor::reset << "\n";
+    }
+
+    if (i + 1 == num_rows) {
+
+      // Check if there is bottom border to print:
+      auto bottom_border_needed{true};
+      for (size_t j = 0; j < num_columns; ++j) {
+        auto cell = table[i][j];
+        auto format = cell.format();
+        auto corner = *format.corner_bottom_left_;
+        auto border_bottom = *format.border_bottom_;
+        if (corner == "" && border_bottom == "") {
+          bottom_border_needed = false;
+          break;
+        }
+      }
+
+      if (bottom_border_needed)
+        stream << termcolor::reset << "\n";
+      // Print bottom border for table
+      for (size_t j = 0; j < num_columns; ++j) {
+        print_cell_border_bottom(stream, table, {i, j}, {row_heights[i], column_widths[j]},
+                                 num_columns);
+      }
+    }
+    if (i + 1 < num_rows)
+      stream << termcolor::reset << "\n"; // Don't add newline after last row
+  }
+}
+
+inline void Printer::print_row_in_cell(std::ostream &stream, TableInternal &table,
+                                       const std::pair<size_t, size_t> &index,
+                                       const std::pair<size_t, size_t> &dimension,
+                                       size_t num_columns, size_t row_index) {
+  auto column_width = dimension.second;
+  auto cell = table[index.first][index.second];
+  auto locale = cell.locale();
+  auto is_multi_byte_character_support_enabled = cell.is_multi_byte_character_support_enabled();
+  auto old_locale = std::locale::global(std::locale(locale));
+  auto format = cell.format();
+  auto text = cell.get_text();
+  auto word_wrapped_text =
+      Format::word_wrap(text, column_width, locale, is_multi_byte_character_support_enabled);
+  auto text_height = std::count(word_wrapped_text.begin(), word_wrapped_text.end(), '\n') + 1;
+  auto padding_top = *format.padding_top_;
+
+  if (*format.show_border_left_) {
+    apply_element_style(stream, *format.border_left_color_, *format.border_left_background_color_,
+                        {});
+    stream << *format.border_left_;
+    reset_element_style(stream);
+  }
+
+  apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+  if (row_index < padding_top) {
+    // Padding top
+    stream << std::string(column_width, ' ');
+  } else if (row_index >= padding_top && (row_index <= (padding_top + text_height))) {
+    // // Row contents
+
+    // Retrieve padding left and right
+    // (column_width - padding_left - padding_right) is the amount of space
+    // available for cell text - Use this to word wrap cell contents
+    auto padding_left = *format.padding_left_;
+    auto padding_right = *format.padding_right_;
+
+    // Check if input text has embedded \n that are to be respected
+    auto newlines_in_input = Format::split_lines(text, "\n", cell.locale(),
+                                                 cell.is_multi_byte_character_support_enabled())
+                                 .size() -
+                             1;
+    std::string word_wrapped_text;
+
+    // If there are no embedded \n characters, then apply word wrap
+    if (newlines_in_input == 0) {
+      // Apply word wrapping to input text
+      // Then display one word-wrapped line at a time within cell
+      if (column_width > (padding_left + padding_right))
+        word_wrapped_text =
+            Format::word_wrap(text, column_width - padding_left - padding_right, cell.locale(),
+                              cell.is_multi_byte_character_support_enabled());
+      else {
+        // Configured column width cannot be lower than (padding_left + padding_right)
+        // This is a bad configuration
+        // E.g., the user is trying to force the column width to be 5
+        // when padding_left and padding_right are each configured to 3
+        // (padding_left + padding_right) = 6 > column_width
+      }
+    } else {
+      word_wrapped_text = text; // repect the embedded '\n' characters
+    }
+
+    auto lines = Format::split_lines(word_wrapped_text, "\n", cell.locale(),
+                                     cell.is_multi_byte_character_support_enabled());
+
+    if (row_index - padding_top < lines.size()) {
+      auto line = lines[row_index - padding_top];
+
+      // Print left padding characters
+      stream << std::string(padding_left, ' ');
+
+      // Print word-wrapped line
+      line = Format::trim(line);
+      auto line_with_padding_size =
+          get_sequence_length(line, cell.locale(), cell.is_multi_byte_character_support_enabled()) +
+          padding_left + padding_right;
+      switch (*format.font_align_) {
+      case FontAlign::left:
+        print_content_left_aligned(stream, line, format, line_with_padding_size, column_width);
+        break;
+      case FontAlign::center:
+        print_content_center_aligned(stream, line, format, line_with_padding_size, column_width);
+        break;
+      case FontAlign::right:
+        print_content_right_aligned(stream, line, format, line_with_padding_size, column_width);
+        break;
+      }
+
+      // Print right padding characters
+      stream << std::string(padding_right, ' ');
+    } else
+      stream << std::string(column_width, ' ');
+
+  } else {
+    // Padding bottom
+    stream << std::string(column_width, ' ');
+  }
+
+  reset_element_style(stream);
+
+  if (index.second + 1 == num_columns) {
+    // Print right border after last column
+    if (*format.show_border_right_) {
+      apply_element_style(stream, *format.border_right_color_,
+                          *format.border_right_background_color_, {});
+      stream << *format.border_right_;
+      reset_element_style(stream);
+    }
+  }
+  std::locale::global(old_locale);
+}
+
+inline bool Printer::print_cell_border_top(std::ostream &stream, TableInternal &table,
+                                           const std::pair<size_t, size_t> &index,
+                                           const std::pair<size_t, size_t> &dimension,
+                                           size_t num_columns) {
+  auto cell = table[index.first][index.second];
+  auto locale = cell.locale();
+  auto old_locale = std::locale::global(std::locale(locale));
+  auto format = cell.format();
+  auto column_width = dimension.second;
+
+  auto corner = *format.corner_top_left_;
+  auto corner_color = *format.corner_top_left_color_;
+  auto corner_background_color = *format.corner_top_left_background_color_;
+  auto border_top = *format.border_top_;
+
+  if ((corner == "" && border_top == "") || !*format.show_border_top_)
+    return false;
+
+  apply_element_style(stream, corner_color, corner_background_color, {});
+  stream << corner;
+  reset_element_style(stream);
+
+  for (size_t i = 0; i < column_width; ++i) {
+    apply_element_style(stream, *format.border_top_color_, *format.border_top_background_color_,
+                        {});
+    stream << border_top;
+    reset_element_style(stream);
+  }
+
+  if (index.second + 1 == num_columns) {
+    // Print corner after last column
+    corner = *format.corner_top_right_;
+    corner_color = *format.corner_top_right_color_;
+    corner_background_color = *format.corner_top_right_background_color_;
+
+    apply_element_style(stream, corner_color, corner_background_color, {});
+    stream << corner;
+    reset_element_style(stream);
+  }
+  std::locale::global(old_locale);
+  return true;
+}
+
+inline bool Printer::print_cell_border_bottom(std::ostream &stream, TableInternal &table,
+                                              const std::pair<size_t, size_t> &index,
+                                              const std::pair<size_t, size_t> &dimension,
+                                              size_t num_columns) {
+  auto cell = table[index.first][index.second];
+  auto locale = cell.locale();
+  auto old_locale = std::locale::global(std::locale(locale));
+  auto format = cell.format();
+  auto column_width = dimension.second;
+
+  auto corner = *format.corner_bottom_left_;
+  auto corner_color = *format.corner_bottom_left_color_;
+  auto corner_background_color = *format.corner_bottom_left_background_color_;
+  auto border_bottom = *format.border_bottom_;
+
+  if ((corner == "" && border_bottom == "") || !*format.show_border_bottom_)
+    return false;
+
+  apply_element_style(stream, corner_color, corner_background_color, {});
+  stream << corner;
+  reset_element_style(stream);
+
+  for (size_t i = 0; i < column_width; ++i) {
+    apply_element_style(stream, *format.border_bottom_color_,
+                        *format.border_bottom_background_color_, {});
+    stream << border_bottom;
+    reset_element_style(stream);
+  }
+
+  if (index.second + 1 == num_columns) {
+    // Print corner after last column
+    corner = *format.corner_bottom_right_;
+    corner_color = *format.corner_bottom_right_color_;
+    corner_background_color = *format.corner_bottom_right_background_color_;
+
+    apply_element_style(stream, corner_color, corner_background_color, {});
+    stream << corner;
+    reset_element_style(stream);
+  }
+  std::locale::global(old_locale);
+  return true;
+}
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/table_internal.hpp>
+
+#if __cplusplus >= 201703L
+#include <string_view>
+#include <variant>
+using std::get_if;
+using std::holds_alternative;
+using std::variant;
+using std::visit;
+using std::string_view;
+#else
+// #include <tabulate/string_view_lite.hpp>
+// #include <tabulate/variant_lite.hpp>
+using nonstd::get_if;
+using nonstd::holds_alternative;
+using nonstd::variant;
+using nonstd::visit;
+using nonstd::string_view;
+#endif
+
+#include <utility>
+
+namespace tabulate {
+
+class Table {
+public:
+  Table() : table_(TableInternal::create()) {}
+
+  using Row_t = std::vector<variant<std::string, const char *, string_view, Table>>;
+
+  Table &add_row(const Row_t &cells) {
+
+    if (rows_ == 0) {
+      // This is the first row added
+      // cells.size() is the number of columns
+      cols_ = cells.size();
+    }
+
+    std::vector<std::string> cell_strings;
+    if (cells.size() < cols_) {
+      cell_strings.resize(cols_);
+      std::fill(cell_strings.begin(), cell_strings.end(), "");
+    } else {
+      cell_strings.resize(cells.size());
+      std::fill(cell_strings.begin(), cell_strings.end(), "");
+    }
+
+    for (size_t i = 0; i < cells.size(); ++i) {
+      auto cell = cells[i];
+      if (holds_alternative<std::string>(cell)) {
+        cell_strings[i] = *get_if<std::string>(&cell);
+      } else if (holds_alternative<const char *>(cell)) {
+        cell_strings[i] = *get_if<const char *>(&cell);
+      }  else if (holds_alternative<string_view>(cell)) {
+        cell_strings[i] = std::string{*get_if<string_view>(&cell)};
+      } else {
+        auto table = *get_if<Table>(&cell);
+        std::stringstream stream;
+        table.print(stream);
+        cell_strings[i] = stream.str();
+      }
+    }
+
+    table_->add_row(cell_strings);
+    rows_ += 1;
+    return *this;
+  }
+
+  Row &operator[](size_t index) { return row(index); }
+
+  Row &row(size_t index) { return (*table_)[index]; }
+
+  Column column(size_t index) { return table_->column(index); }
+
+  Format &format() { return table_->format(); }
+
+  void print(std::ostream &stream) { table_->print(stream); }
+
+  std::string str() {
+    std::stringstream stream;
+    print(stream);
+    return stream.str();
+  }
+
+  std::pair<size_t, size_t> shape() { return table_->shape(); }
+
+  class RowIterator {
+  public:
+    explicit RowIterator(std::vector<std::shared_ptr<Row>>::iterator ptr) : ptr(ptr) {}
+
+    RowIterator operator++() {
+      ++ptr;
+      return *this;
+    }
+    bool operator!=(const RowIterator &other) const { return ptr != other.ptr; }
+    Row &operator*() { return **ptr; }
+
+  private:
+    std::vector<std::shared_ptr<Row>>::iterator ptr;
+  };
+
+  auto begin() -> RowIterator { return RowIterator(table_->rows_.begin()); }
+  auto end() -> RowIterator { return RowIterator(table_->rows_.end()); }
+
+private:
+  friend class MarkdownExporter;
+  friend class LatexExporter;
+  friend class AsciiDocExporter;
+
+  friend std::ostream &operator<<(std::ostream &stream, const Table &table);
+  size_t rows_{0};
+  size_t cols_{0};
+  std::shared_ptr<TableInternal> table_;
+};
+
+inline std::ostream &operator<<(std::ostream &stream, const Table &table) {
+  const_cast<Table &>(table).print(stream);
+  return stream;
+}
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <string>
+// #include <tabulate/table.hpp>
+
+namespace tabulate {
+
+class Exporter {
+public:
+  virtual std::string dump(Table &table) = 0;
+  virtual ~Exporter() {}
+};
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/exporter.hpp>
+
+namespace tabulate {
+
+class MarkdownExporter : public Exporter {
+public:
+  std::string dump(Table &table) override {
+    std::string result{""};
+    apply_markdown_format(table);
+    result = table.str();
+    restore_table_format(table);
+    return result;
+  }
+
+  virtual ~MarkdownExporter() {}
+
+private:
+  void add_alignment_header_row(Table &table) {
+    auto &rows = table.table_->rows_;
+
+    if (rows.size() >= 1) {
+      auto alignment_row = std::make_shared<Row>(table.table_->shared_from_this());
+
+      // Create alignment header cells
+      std::vector<std::string> alignment_cells{};
+      for (auto &cell : table[0]) {
+        auto format = cell.format();
+        if (format.font_align_.value() == FontAlign::left) {
+          alignment_cells.push_back(":----");
+        } else if (format.font_align_.value() == FontAlign::center) {
+          alignment_cells.push_back(":---:");
+        } else if (format.font_align_.value() == FontAlign::right) {
+          alignment_cells.push_back("----:");
+        }
+      }
+
+      // Add alignment header cells to alignment row
+      for (auto &c : alignment_cells) {
+        auto cell = std::make_shared<Cell>(alignment_row);
+        cell->format()
+            .hide_border_top()
+            .hide_border_bottom()
+            .border_left("|")
+            .border_right("|")
+            .column_separator("|")
+            .corner("|");
+        cell->set_text(c);
+        if (c == ":---:")
+          cell->format().font_align(FontAlign::center);
+        else if (c == "----:")
+          cell->format().font_align(FontAlign::right);
+        alignment_row->add_cell(cell);
+      }
+
+      // Insert alignment header row
+      if (rows.size() > 1)
+        rows.insert(rows.begin() + 1, alignment_row);
+      else
+        rows.push_back(alignment_row);
+    }
+  }
+
+  void remove_alignment_header_row(Table &table) {
+    auto &rows = table.table_->rows_;
+    table.table_->rows_.erase(rows.begin() + 1);
+  }
+
+  void apply_markdown_format(Table &table) {
+    // Apply markdown format to cells in each row
+    for (auto row : table) {
+      for (auto &cell : row) {
+        auto format = cell.format();
+        formats_.push_back(format);
+        cell.format()
+            .hide_border_top()
+            .hide_border_bottom()
+            .border_left("|")
+            .border_right("|")
+            .column_separator("|")
+            .corner("|");
+      }
+    }
+    // Add alignment header row at position 1
+    add_alignment_header_row(table);
+  }
+
+  void restore_table_format(Table &table) {
+    // Remove alignment header row at position 1
+    remove_alignment_header_row(table);
+
+    // Restore original formatting for each cell
+    size_t format_index{0};
+    for (auto row : table) {
+      for (auto &cell : row) {
+        cell.format() = formats_[format_index];
+        format_index += 1;
+      }
+    }
+  }
+
+  std::vector<Format> formats_;
+};
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/exporter.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+namespace tabulate {
+
+class LatexExporter : public Exporter {
+
+  static const char new_line = '\n';
+
+public:
+  class ExportOptions {
+  public:
+    ExportOptions &indentation(std::size_t value) {
+      indentation_ = value;
+      return *this;
+    }
+
+  private:
+    friend class LatexExporter;
+    optional<size_t> indentation_;
+  };
+
+  ExportOptions &configure() { return options_; }
+
+  std::string dump(Table &table) override {
+    std::string result{"\\begin{tabular}"};
+    result += new_line;
+
+    result += add_alignment_header(table);
+    result += new_line;
+    const auto rows = table.rows_;
+    // iterate content and put text into the table.
+    for (size_t i = 0; i < rows; i++) {
+      auto &row = table[i];
+      // apply row content indentation
+      if (options_.indentation_.has_value()) {
+        result += std::string(options_.indentation_.value(), ' ');
+      }
+
+      for (size_t j = 0; j < row.size(); j++) {
+
+        result += row[j].get_text();
+
+        // check column position, need "\\" at the end of each row
+        if (j < row.size() - 1) {
+          result += " & ";
+        } else {
+          result += " \\\\";
+        }
+      }
+      result += new_line;
+    }
+
+    result += "\\end{tabular}";
+    return result;
+  }
+
+  virtual ~LatexExporter() {}
+
+private:
+  std::string add_alignment_header(Table &table) {
+    std::string result{"{"};
+
+    for (auto &cell : table[0]) {
+      auto format = cell.format();
+      if (format.font_align_.value() == FontAlign::left) {
+        result += 'l';
+      } else if (format.font_align_.value() == FontAlign::center) {
+        result += 'c';
+      } else if (format.font_align_.value() == FontAlign::right) {
+        result += 'r';
+      }
+    }
+
+    result += "}";
+    return result;
+  }
+  ExportOptions options_;
+};
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <optional>
+#include <sstream>
+#include <string>
+// #include <tabulate/exporter.hpp>
+
+namespace tabulate {
+
+class AsciiDocExporter : public Exporter {
+
+  static const char new_line = '\n';
+
+public:
+  std::string dump(Table &table) override {
+    std::stringstream ss;
+    ss << add_alignment_header(table);
+    ss << new_line;
+
+    const auto rows = table.rows_;
+    // iterate content and put text into the table.
+    for (size_t row_index = 0; row_index < rows; row_index++) {
+      auto &row = table[row_index];
+
+      for (size_t cell_index = 0; cell_index < row.size(); cell_index++) {
+        ss << "|";
+        ss << add_formatted_cell(row[cell_index]);
+      }
+      ss << new_line;
+      if (row_index == 0) {
+        ss << new_line;
+      }
+    }
+
+    ss << "|===";
+    return ss.str();
+  }
+
+  virtual ~AsciiDocExporter() {}
+
+private:
+  std::string add_formatted_cell(Cell &cell) const {
+    std::stringstream ss;
+    auto format = cell.format();
+    std::string cell_string = cell.get_text();
+
+    auto font_style = format.font_style_.value();
+
+    bool format_bold = false;
+    bool format_italic = false;
+    std::for_each(font_style.begin(), font_style.end(), [&](FontStyle &style) {
+      if (style == FontStyle::bold) {
+        format_bold = true;
+      } else if (style == FontStyle::italic) {
+        format_italic = true;
+      }
+    });
+
+    if (format_bold) {
+      ss << '*';
+    }
+    if (format_italic) {
+      ss << '_';
+    }
+
+    ss << cell_string;
+    if (format_italic) {
+      ss << '_';
+    }
+    if (format_bold) {
+      ss << '*';
+    }
+    return ss.str();
+  }
+
+  std::string add_alignment_header(Table &table) {
+    std::stringstream ss;
+    ss << (R"([cols=")");
+
+    size_t column_count = table[0].size();
+    size_t column_index = 0;
+    for (auto &cell : table[0]) {
+      auto format = cell.format();
+
+      if (format.font_align_.value() == FontAlign::left) {
+        ss << '<';
+      } else if (format.font_align_.value() == FontAlign::center) {
+        ss << '^';
+      } else if (format.font_align_.value() == FontAlign::right) {
+        ss << '>';
+      }
+
+      ++column_index;
+      if (column_index != column_count) {
+        ss << ",";
+      }
+    }
+
+    ss << R"("])";
+    ss << new_line;
+    ss << "|===";
+
+    return ss.str();
+  }
+};
+
+} // namespace tabulate
+
+/*
+  __        ___.         .__          __
+_/  |______ \_ |__  __ __|  | _____ _/  |_  ____
+\   __\__  \ | __ \|  |  \  | \__  \\   __\/ __ \
+ |  |  / __ \| \_\ \  |  /  |__/ __ \|  | \  ___/
+ |__| (____  /___  /____/|____(____  /__|  \___  >
+           \/    \/                \/          \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby  granted, free of charge, to any  person obtaining a copy
+of this software and associated  documentation files (the "Software"), to deal
+in the Software  without restriction, including without  limitation the rights
+to  use, copy,  modify, merge,  publish, distribute,  sublicense, and/or  sell
+copies  of  the Software,  and  to  permit persons  to  whom  the Software  is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE  IS PROVIDED "AS  IS", WITHOUT WARRANTY  OF ANY KIND,  EXPRESS OR
+IMPLIED,  INCLUDING BUT  NOT  LIMITED TO  THE  WARRANTIES OF  MERCHANTABILITY,
+FITNESS FOR  A PARTICULAR PURPOSE AND  NONINFRINGEMENT. IN NO EVENT  SHALL THE
+AUTHORS  OR COPYRIGHT  HOLDERS  BE  LIABLE FOR  ANY  CLAIM,  DAMAGES OR  OTHER
+LIABILITY, WHETHER IN AN ACTION OF  CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE  OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#ifndef TABULATE_EXPORT_HPP
+#define TABULATE_EXPORT_HPP
+
+// #ifdef _WIN32
+//     #ifdef TABULATE_STATIC_LIB
+//         #define TABULATE_API
+//     #else
+//         #ifdef TABULATE_EXPORTS
+//             #define TABULATE_API __declspec(dllexport)
+//         #else
+//             #define TABULATE_API __declspec(dllimport)
+//         #endif
+//     #endif
+// #else
+//     #define TABULATE_API
+// #endif
+
+// Project version
+#define TABULATE_VERSION_MAJOR 1
+#define TABULATE_VERSION_MINOR 4
+#define TABULATE_VERSION_PATCH 0
+
+// Composing the protocol version string from major, and minor
+#define TABULATE_CONCATENATE(A, B) TABULATE_CONCATENATE_IMPL(A, B)
+#define TABULATE_CONCATENATE_IMPL(A, B) A##B
+#define TABULATE_STRINGIFY(a) TABULATE_STRINGIFY_IMPL(a)
+#define TABULATE_STRINGIFY_IMPL(a) #a
+
+#endif
diff --git a/lib/tokenize.cc b/lib/tokenize.cc
new file mode 100644 (file)
index 0000000..96a8708
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+**  Copyright (C) 2017-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include <string>
+#include <iostream>
+
+#include "mu-tokenizer.hh"
+
+int
+main(int argc, char* argv[])
+{
+       std::string s;
+
+       for (auto i = 1; i < argc; ++i)
+               s += " " + std::string(argv[i]);
+
+       const auto tvec = Mu::tokenize(s);
+       for (const auto& t : tvec)
+               std::cout << t << std::endl;
+
+       return 0;
+}
diff --git a/lib/utils/Makefile.am b/lib/utils/Makefile.am
new file mode 100644 (file)
index 0000000..422b1de
--- /dev/null
@@ -0,0 +1,74 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+AM_CFLAGS=                                                     \
+       $(WARN_CFLAGS)                                          \
+       $(GLIB_CFLAGS)                                          \
+       $(ASAN_CFLAGS)                                          \
+       $(CODE_COVERAGE_CFLAGS)                                 \
+       -DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\"      \
+       -DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\"    \
+       -Wno-format-nonliteral                                  \
+       -Wno-switch-enum                                        \
+       -Wno-deprecated-declarations                            \
+       -Wno-inline                                             \
+       -I${top_srcdir}/lib
+
+AM_CPPFLAGS=                                                   \
+       $(CODE_COVERAGE_CPPFLAGS)
+
+AM_CXXFLAGS=                                                   \
+       $(WARN_CXXFLAGS)                                        \
+       $(GLIB_CFLAGS)                                          \
+       $(ASAN_CXXFLAGS)                                        \
+       $(CODE_COVERAGE_CFLAGS)                                 \
+       -I${top_srcdir}/lib
+
+AM_LDFLAGS=                                                    \
+       $(ASAN_LDFLAGS)
+
+noinst_LTLIBRARIES=                                            \
+       libmu-utils.la
+
+libmu_utils_la_SOURCES=                                                \
+       mu-async-queue.hh                                       \
+       mu-command-parser.cc                                    \
+       mu-command-parser.hh                                    \
+       mu-error.hh                                             \
+       mu-logger.cc                                            \
+       mu-logger.hh                                            \
+       mu-option.hh                                            \
+       mu-option.cc                                            \
+       mu-readline.cc                                          \
+       mu-readline.hh                                          \
+       mu-result.hh                                            \
+       mu-sexp.cc                                              \
+       mu-sexp.hh                                              \
+       mu-util.c                                               \
+       mu-util.h                                               \
+       mu-utils.cc                                             \
+       mu-utils.hh                                             \
+       mu-utils-format.hh                                      \
+       mu-xapian-utils.hh
+
+libmu_utils_la_LIBADD=                                         \
+       $(GLIB_LIBS)                                            \
+       $(READLINE_LIBS)                                        \
+       $(CODE_COVERAGE_LIBS)
+
+include $(top_srcdir)/aminclude_static.am
diff --git a/lib/utils/meson.build b/lib/utils/meson.build
new file mode 100644 (file)
index 0000000..6277396
--- /dev/null
@@ -0,0 +1,42 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+
+lib_mu_utils=static_library('mu-utils', [
+                 'mu-command-parser.cc',
+                 'mu-logger.cc',
+                  'mu-option.cc',
+                 'mu-readline.cc',
+                 'mu-sexp.cc',
+                  'mu-test-utils.cc',
+                 'mu-util.c',
+                 'mu-util.h',
+                 'mu-utils.cc'],
+                dependencies: [
+                  glib_dep,
+                   gio_dep,
+                  config_h_dep,
+                  readline_dep
+                ],
+                include_directories: include_directories(['.','..']),
+                install: false)
+
+lib_mu_utils_dep = declare_dependency(
+  link_with: lib_mu_utils,
+  include_directories: include_directories(['.', '..'])
+)
+
+subdir('tests')
diff --git a/lib/utils/mu-async-queue.hh b/lib/utils/mu-async-queue.hh
new file mode 100644 (file)
index 0000000..bc3e655
--- /dev/null
@@ -0,0 +1,199 @@
+/*
+** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef __MU_ASYNC_QUEUE_HH__
+#define __MU_ASYNC_QUEUE_HH__
+
+#include <deque>
+#include <mutex>
+#include <chrono>
+#include <condition_variable>
+
+namespace Mu {
+
+constexpr std::size_t UnlimitedAsyncQueueSize{0};
+
+template <typename ItemType,                             /**< the type of Item to queue */
+          std::size_t MaxSize = UnlimitedAsyncQueueSize, /**< maximum size for the queue */
+          typename Allocator  = std::allocator<ItemType>> /**< allocator the items */
+
+class AsyncQueue {
+      public:
+       using value_type      = ItemType;
+       using allocator_type  = Allocator;
+       using size_type       = std::size_t;
+       using reference       = value_type&;
+       using const_reference = const value_type&;
+       using pointer         = typename std::allocator_traits<allocator_type>::pointer;
+       using const_pointer   = typename std::allocator_traits<allocator_type>::const_pointer;
+
+       using Timeout = std::chrono::steady_clock::duration;
+
+#define LOCKED std::unique_lock<std::mutex> lock(m_);
+
+       /**
+        * Push an item to the end of the queue by moving it
+        *
+        * @param item the item to move to the end of the queue
+        * @param timeout and optional timeout
+        *
+        * @return true if the item was pushed; false otherwise.
+        */
+       bool push(const value_type& item, Timeout timeout = {})
+       {
+               return push(std::move(value_type(item)), timeout);
+       }
+
+       /**
+        * Push an item to the end of the queue by moving it
+        *
+        * @param item the item to move to the end of the queue
+        * @param timeout and optional timeout
+        *
+        * @return true if the item was pushed; false otherwise.
+        */
+       bool push(value_type&& item, Timeout timeout = {})
+       {
+               std::unique_lock lock{m_};
+
+               if (!unlimited()) {
+                       const auto rv = cv_full_.wait_for(lock, timeout, [&]() {
+                               return !full_unlocked();
+                       }) && !full_unlocked();
+                       if (!rv)
+                               return false;
+               }
+
+               q_.emplace_back(std::move(item));
+               cv_empty_.notify_one();
+
+               return true;
+       }
+
+       /**
+        * Pop an item from the queue
+        *
+        * @param receives the value if the function returns true
+        * @param timeout optional time to wait for an item to become available
+        *
+        * @return true if an item was popped (into val), false otherwise.
+        */
+       bool pop(value_type& val, Timeout timeout = {})
+       {
+               std::unique_lock lock{m_};
+
+               if (timeout != Timeout{}) {
+                       const auto rv = cv_empty_.wait_for(lock, timeout, [&]() {
+                               return !q_.empty();
+                       }) && !q_.empty();
+                       if (!rv)
+                               return false;
+
+               } else if (q_.empty())
+                       return false;
+
+               val = std::move(q_.front());
+               q_.pop_front();
+               cv_full_.notify_one();
+
+               return true;
+       }
+
+       /**
+        * Clear the queue
+        *
+        */
+       void clear()
+       {
+               std::unique_lock lock{m_};
+               q_.clear();
+               cv_full_.notify_one();
+       }
+
+       /**
+        * Size of the queue
+        *
+        *
+        * @return the size
+        */
+       size_type size() const
+       {
+               std::unique_lock lock{m_};
+               return q_.size();
+       }
+
+       /**
+        * Maximum size of the queue if specified through the template
+        * parameter; otherwise the (theoretical) max_size of the inner
+        * container.
+        *
+        * @return the maximum size
+        */
+       size_type max_size() const
+       {
+               if (unlimited())
+                       return q_.max_size();
+               else
+                       return MaxSize;
+       }
+
+       /**
+        * Is the queue empty?
+        *
+        * @return true or false
+        */
+       bool empty() const
+       {
+               std::unique_lock lock{m_};
+               return q_.empty();
+       }
+
+       /**
+        * Is the queue full? Returns false unless a maximum size was specified
+        * (as a template argument)
+        *
+        * @return true or false.
+        */
+       bool full() const
+       {
+               if (unlimited())
+                       return false;
+
+               std::unique_lock lock{m_};
+               return full_unlocked();
+       }
+
+       /**
+        * Is this queue (theoretically) unlimited in size?
+        *
+        * @return true or false
+        */
+       constexpr static bool unlimited() { return MaxSize == UnlimitedAsyncQueueSize; }
+
+private:
+       bool full_unlocked() const { return q_.size() >= max_size(); }
+
+       std::deque<ItemType, Allocator> q_;
+       mutable std::mutex              m_;
+       std::condition_variable         cv_full_, cv_empty_;
+};
+
+} // namespace Mu
+
+#endif /* __MU_ASYNC_QUEUE_HH__ */
diff --git a/lib/utils/mu-command-parser.cc b/lib/utils/mu-command-parser.cc
new file mode 100644 (file)
index 0000000..40be1e9
--- /dev/null
@@ -0,0 +1,204 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-command-parser.hh"
+#include "mu-error.hh"
+#include "mu-utils.hh"
+
+#include <iostream>
+#include <algorithm>
+
+using namespace Mu;
+using namespace Command;
+
+void
+Command::invoke(const Command::CommandMap& cmap, const Sexp& call)
+{
+       if (!call.is_call()) {
+               throw Mu::Error{Error::Code::Command,
+                               "expected call-sexpr but got %s",
+                               call.to_sexp_string().c_str()};
+       }
+
+       const auto& params{call.list()};
+       const auto  cmd_it = cmap.find(params.at(0).value());
+       if (cmd_it == cmap.end())
+               throw Mu::Error{Error::Code::Command,
+                               "unknown command in call %s",
+                               call.to_sexp_string().c_str()};
+
+       const auto& cinfo{cmd_it->second};
+
+       // all required parameters must be present
+       for (auto&& arg : cinfo.args) {
+               const auto& argname{arg.first};
+               const auto& arginfo{arg.second};
+
+               // calls used keyword-parameters, e.g.
+               //    (my-function :bar 1 :cuux "fnorb")
+               // so, we're looking for the odd-numbered parameters.
+               const auto param_it = [&]() -> Sexp::Seq::const_iterator {
+                       for (size_t i = 1; i < params.size(); i += 2)
+                               if (params.at(i).is_symbol() && params.at(i).value() == argname)
+                                       return params.begin() + i + 1;
+
+                       return params.end();
+               }();
+
+               // it's an error when a required parameter is missing.
+               if (param_it == params.end()) {
+                       if (arginfo.required)
+                               throw Mu::Error{Error::Code::Command,
+                                               "missing required parameter %s in call %s",
+                                               argname.c_str(),
+                                               call.to_sexp_string().c_str()};
+                       continue; // not required
+               }
+
+               // the types must match, but the 'nil' symbol is acceptable as
+               // "no value"
+               if (param_it->type() != arginfo.type && !(param_it->is_nil()))
+                       throw Mu::Error{Error::Code::Command,
+                                       "parameter %s expects type %s, but got %s in call %s",
+                                       argname.c_str(),
+                                       to_string(arginfo.type).c_str(),
+                                       to_string(param_it->type()).c_str(),
+                                       call.to_sexp_string().c_str()};
+       }
+
+       // all passed parameters must be known
+       for (size_t i = 1; i < params.size(); i += 2) {
+               if (std::none_of(cinfo.args.begin(), cinfo.args.end(), [&](auto&& arg) {
+                           return params.at(i).value() == arg.first;
+                   }))
+                       throw Mu::Error{Error::Code::Command,
+                                       "unknown parameter %s in call %s",
+                                       params.at(i).value().c_str(),
+                                       call.to_sexp_string().c_str()};
+       }
+
+       if (cinfo.handler)
+               cinfo.handler(params);
+}
+
+static Sexp::Seq::const_iterator
+find_param_node(const Parameters& params, const std::string& argname)
+{
+       if (params.empty())
+               throw Error(Error::Code::InvalidArgument, "params must not be empty");
+
+       if (argname.empty() || argname.at(0) != ':')
+               throw Error(Error::Code::InvalidArgument,
+                           "property key must start with ':' but got '%s')",
+                           argname.c_str());
+
+       for (size_t i = 1; i < params.size(); i += 2) {
+               if (i + 1 != params.size() && params.at(i).is_symbol() &&
+                   params.at(i).value() == argname)
+                       return params.begin() + i + 1;
+       }
+
+       return params.end();
+}
+
+static Error
+wrong_type(Sexp::Type expected, Sexp::Type got)
+{
+       return Error(Error::Code::InvalidArgument,
+                    "expected <%s> but got <%s>",
+                    to_string(expected).c_str(),
+                    to_string(got).c_str());
+}
+
+Option<std::string>
+Command::get_string(const Parameters& params, const std::string& argname)
+{
+       const auto it = find_param_node(params, argname);
+       if (it == params.end() || it->is_nil())
+               return Nothing;
+       else if (!it->is_string())
+               throw wrong_type(Sexp::Type::String, it->type());
+       else
+               return it->value();
+}
+
+Option<std::string>
+Command::get_symbol(const Parameters& params, const std::string& argname)
+{
+       const auto it = find_param_node(params, argname);
+       if (it == params.end() || it->is_nil())
+               return Nothing;
+       else if (!it->is_symbol())
+               throw wrong_type(Sexp::Type::Symbol, it->type());
+       else
+               return it->value();
+}
+
+Option<int>
+Command::get_int(const Parameters& params, const std::string& argname)
+{
+       const auto it = find_param_node(params, argname);
+       if (it == params.end() || it->is_nil())
+               return Nothing;
+       else if (!it->is_number())
+               throw wrong_type(Sexp::Type::Number, it->type());
+       else
+               return ::atoi(it->value().c_str());
+}
+
+Option<unsigned>
+Command::get_unsigned(const Parameters& params, const std::string& argname)
+{
+       if (auto val = get_int(params, argname); val && *val >= 0)
+               return val;
+       else
+               return Nothing;
+}
+
+
+Option<bool>
+Command::get_bool(const Parameters& params, const std::string& argname)
+{
+       const auto it = find_param_node(params, argname);
+       if (it == params.end())
+               return Nothing;
+       else if (!it->is_symbol())
+               throw wrong_type(Sexp::Type::Symbol, it->type());
+       else
+               return it->is_nil() ? false : true;
+}
+
+std::vector<std::string>
+Command::get_string_vec(const Parameters& params, const std::string& argname)
+{
+       const auto it = find_param_node(params, argname);
+       if (it == params.end() || it->is_nil())
+               return {};
+       else if (!it->is_list())
+               throw wrong_type(Sexp::Type::List, it->type());
+
+       std::vector<std::string> vec;
+       for (const auto& n : it->list()) {
+               if (!n.is_string())
+                       throw wrong_type(Sexp::Type::String, n.type());
+               vec.emplace_back(n.value());
+       }
+
+       return vec;
+}
diff --git a/lib/utils/mu-command-parser.hh b/lib/utils/mu-command-parser.hh
new file mode 100644 (file)
index 0000000..751e5d8
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#ifndef MU_COMMAND_PARSER_HH__
+#define MU_COMMAND_PARSER_HH__
+
+#include <vector>
+#include <string>
+#include <ostream>
+#include <stdexcept>
+#include <unordered_map>
+#include <functional>
+#include <algorithm>
+
+#include "utils/mu-error.hh"
+#include "utils/mu-sexp.hh"
+#include "utils/mu-option.hh"
+
+namespace Mu {
+namespace Command {
+
+///
+/// Commands are s-expressions with the follow properties:
+
+/// 1) a command is a list with a command-name as its first argument
+/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some
+///    type (ie. 'keyword arguments')
+/// 3) each command is described by its CommandInfo structure, which defines the type
+/// 4) calls to the command must include all required parameters
+/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed
+///    for specify a non-required parameter to be absent; this is for convenience on the
+///    call side.
+
+/// Information about a function argument
+struct ArgInfo {
+       ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg)
+           : type{typearg}, required{requiredarg}, docstring{std::move(docarg)}
+       {
+       }
+       const Sexp::Type  type;      /**< Sexp::Type of the argument */
+       const bool        required;  /**< Is this argument required? */
+       const std::string docstring; /**< Documentation */
+};
+
+/// The arguments for a function, which maps their names to the information.
+using ArgMap = std::unordered_map<std::string, ArgInfo>;
+// The parameters to a Handler.
+using Parameters = Sexp::Seq;
+
+Option<int>            get_int(const Parameters& parms, const std::string& argname);
+Option<unsigned>       get_unsigned(const Parameters& parms, const std::string& argname);
+Option<bool>           get_bool(const Parameters& parms, const std::string& argname);
+Option<std::string>    get_string(const Parameters& parms, const std::string& argname);
+Option<std::string>    get_symbol(const Parameters& parms, const std::string& argname);
+
+std::vector<std::string> get_string_vec(const Parameters& params, const std::string& argname);
+
+/*
+ * backward compat
+ */
+static inline int
+get_int_or(const Parameters& parms, const std::string& arg, int alt = 0) {
+       return get_int(parms, arg).value_or(alt);
+}
+
+static inline bool
+get_bool_or(const Parameters& parms, const std::string& arg, bool alt = false) {
+       return get_bool(parms, arg).value_or(alt);
+}
+static inline std::string
+get_string_or(const Parameters& parms, const std::string& arg, const std::string& alt = ""){
+       return get_string(parms, arg).value_or(alt);
+}
+
+static inline std::string
+get_symbol_or(const Parameters& parms, const std::string& arg, const std::string& alt = "nil") {
+       return get_symbol(parms, arg).value_or(alt);
+}
+
+
+
+
+// A handler function
+using Handler = std::function<void(const Parameters&)>;
+
+/// Information about some command
+struct CommandInfo {
+       CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg)
+           : args{std::move(argmaparg)}, docstring{std::move(docarg)}, handler{
+                                                                           std::move(handlerarg)}
+       {
+       }
+       const ArgMap      args;
+       const std::string docstring;
+       const Handler     handler;
+
+       /**
+        * Get a sorted list of argument names, for display. Required args come
+        * first, then alphabetical.
+        *
+        * @return vec with the sorted names.
+        */
+       std::vector<std::string> sorted_argnames() const
+       { // sort args -- by required, then alphabetical.
+               std::vector<std::string> names;
+               for (auto&& arg : args)
+                       names.emplace_back(arg.first);
+               std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) {
+                       const auto& arg1{args.find(name1)->second};
+                       const auto& arg2{args.find(name2)->second};
+                       if (arg1.required != arg2.required)
+                               return arg1.required;
+                       else
+                               return name1 < name2;
+               });
+               return names;
+       }
+};
+/// All commands, mapping their name to information about them.
+using CommandMap = std::unordered_map<std::string, CommandInfo>;
+
+/**
+ * Validate that the call (a Sexp) specifies a valid call, then invoke it.
+ *
+ * A call uses keyword arguments, e.g. something like:
+ *    (foo :bar 1 :cuux "fnorb")
+ *
+ * On error, throw Error.
+ *
+ * @param cmap map of commands
+ * @param call node describing a call.
+ */
+void invoke(const Command::CommandMap& cmap, const Sexp& call);
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Command::ArgInfo& info)
+{
+       os << info.type << " (" << (info.required ? "required" : "optional") << ")";
+
+       return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Command::CommandInfo& info)
+{
+       for (auto&& arg : info.args)
+               os << "  " << arg.first << " " << arg.second << '\n'
+                  << "    " << arg.second.docstring << "\n";
+
+       return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Command::CommandMap& map)
+{
+       for (auto&& c : map)
+               os << c.first << '\n' << c.second;
+
+       return os;
+}
+
+} // namespace Command
+} // namespace Mu
+
+#endif /* MU_COMMAND_PARSER_HH__ */
diff --git a/lib/utils/mu-error.hh b/lib/utils/mu-error.hh
new file mode 100644 (file)
index 0000000..c67fc5a
--- /dev/null
@@ -0,0 +1,174 @@
+/*
+** Copyright (C) 2019-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_ERROR_HH__
+#define MU_ERROR_HH__
+
+#include <stdexcept>
+#include "mu-utils-format.hh"
+#include "mu-util.h"
+#include <glib.h>
+
+namespace Mu {
+
+struct Error final : public std::exception {
+
+       // 16 lower bits are for the error code the next 8 bits is for the return code
+       // upper byte is for flags
+
+       static constexpr uint32_t SoftError = 1 << 23;
+
+#define ERROR_ENUM(RV,CAT) \
+       static_cast<uint32_t>(__LINE__ | ((RV) << 15) | (CAT))
+
+       enum struct Code: uint32_t {
+               AccessDenied        = ERROR_ENUM(1,0),
+               AssertionFailure    = ERROR_ENUM(1,0),
+               Command             = ERROR_ENUM(1,0),
+               ContactNotFound     = ERROR_ENUM(2,SoftError),
+               Crypto              = ERROR_ENUM(1,0),
+               File                = ERROR_ENUM(1,0),
+               Index               = ERROR_ENUM(1,0),
+               Internal            = ERROR_ENUM(1,0),
+               InvalidArgument     = ERROR_ENUM(1,0),
+               Message             = ERROR_ENUM(1,0),
+               NoMatches           = ERROR_ENUM(4,SoftError),
+               NotFound            = ERROR_ENUM(1,0),
+               Parsing             = ERROR_ENUM(1,0),
+               Play                = ERROR_ENUM(1,0),
+               Query               = ERROR_ENUM(1,0),
+               SchemaMismatch      = ERROR_ENUM(1,0),
+               Store               = ERROR_ENUM(1,0),
+               StoreLock           = ERROR_ENUM(19,0),
+               UnverifiedSignature = ERROR_ENUM(1,0),
+               User                = ERROR_ENUM(1,0),
+               Xapian              = ERROR_ENUM(1,0),
+       };
+
+
+       /**
+        * Construct an error
+        *
+        * @param codearg error-code
+        * #param msgarg the error diecription
+        */
+       Error(Code codearg, const std::string& msgarg) : code_{codearg}, what_{msgarg} {}
+       Error(Code codearg, std::string&& msgarg) : code_{codearg}, what_{std::move(msgarg)} {}
+
+       /**
+        * Build an error from an error-code and a format string
+        *
+        * @param code error-code
+        * @param frm format string
+        * @param ... format parameters
+        *
+        * @return an Error object
+        */
+       __attribute__((format(printf, 3, 0))) Error(Code codearg, const char* frm, ...)
+           : code_{codearg}
+       {
+               va_list args;
+               va_start(args, frm);
+               what_ = vformat(frm, args);
+               va_end(args);
+       }
+
+       Error(Error&& rhs)      = default;
+       Error(const Error& rhs) = default;
+
+       /**
+        * Build an error from a GError an error-code and a format string
+        *
+        * @param code error-code
+        * @param gerr a GError or {}, which is consumed
+        * @param frm format string
+        * @param ... format parameters
+        *
+        * @return an Error object
+        */
+       __attribute__((format(printf, 4, 0)))
+       Error(Code codearg, GError** err, const char* frm, ...)
+           : code_{codearg}
+       {
+               va_list args;
+               va_start(args, frm);
+               what_ = vformat(frm, args);
+               va_end(args);
+
+               if (err && *err)
+                       what_ += format(": %s", (*err)->message);
+               else
+                       what_ += ": something went wrong";
+
+               g_clear_error(err);
+       }
+
+       /**
+        * DTOR
+        *
+        */
+       virtual ~Error() override = default;
+
+       /**
+        * Get the descriptive message.
+        *
+        * @return
+        */
+       virtual const char* what() const noexcept override { return what_.c_str(); }
+
+       /**
+        * Get the error-code for this error
+        *
+        * @return the error-code
+        */
+       Code code() const noexcept { return code_; }
+
+
+       /**
+        * Is this is a 'soft error'?
+        *
+        * @return true or false
+        */
+       constexpr bool is_soft_error() const {
+               return (static_cast<uint32_t>(code_) & SoftError) != 0;
+       }
+
+       constexpr uint8_t exit_code() const {
+               return ((static_cast<uint32_t>(code_) >> 15) & 0xff);
+       }
+
+
+       /**
+        * Fill a GError with the error information
+        *
+        * @param err GError** (or NULL)
+        */
+       void fill_g_error(GError **err) const noexcept{
+               g_set_error(err, MU_ERROR_DOMAIN, static_cast<int>(code_),
+                           "%s", what_.c_str());
+       }
+
+private:
+       const Code  code_;
+       std::string what_;
+};
+
+} // namespace Mu
+
+#endif /* MU_ERROR_HH__ */
diff --git a/lib/utils/mu-logger.cc b/lib/utils/mu-logger.cc
new file mode 100644 (file)
index 0000000..40ac4e0
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#define G_LOG_USE_STRUCTURED
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <iostream>
+#include <fstream>
+#include <cstring>
+
+#include "mu-logger.hh"
+
+using namespace Mu;
+
+static bool           MuLogInitialized = false;
+static Mu::LogOptions MuLogOptions;
+static std::ofstream  MuStream;
+static auto           MaxLogFileSize = 1000 * 1024;
+
+static std::string MuLogPath;
+
+static bool
+maybe_open_logfile()
+{
+       if (MuStream.is_open())
+               return true;
+
+       MuStream.open(MuLogPath, std::ios::out | std::ios::app);
+       if (!MuStream.is_open()) {
+               std::cerr << "opening " << MuLogPath << " failed:" << g_strerror(errno)
+                         << std::endl;
+               return false;
+       }
+
+       MuStream.sync_with_stdio(false);
+       return true;
+}
+
+static bool
+maybe_rotate_logfile()
+{
+       static unsigned n = 0;
+
+       if (n++ % 1000 != 0)
+               return true;
+
+       GStatBuf statbuf;
+       if (g_stat(MuLogPath.c_str(), &statbuf) == -1 || statbuf.st_size <= MaxLogFileSize)
+               return true;
+
+       const auto old = MuLogPath + ".old";
+       g_unlink(old.c_str()); // opportunistic
+
+       if (MuStream.is_open())
+               MuStream.close();
+
+       if (g_rename(MuLogPath.c_str(), old.c_str()) != 0)
+               std::cerr << "failed to rename " << MuLogPath << " -> " << old.c_str() << ": "
+                         << g_strerror(errno) << std::endl;
+
+       return maybe_open_logfile();
+}
+
+static GLogWriterOutput
+log_file(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data)
+{
+       if (!maybe_open_logfile())
+               return G_LOG_WRITER_UNHANDLED;
+
+       char   timebuf[22];
+       time_t now{::time(NULL)};
+       ::strftime(timebuf, sizeof(timebuf), "%F %T", ::localtime(&now));
+
+       char* msg = g_log_writer_format_fields(level, fields, n_fields, FALSE);
+       if (msg && msg[0] == '\n') // hmm... seems lines start with '\n'r
+               msg[0] = ' ';
+
+       MuStream << timebuf << ' ' << msg << std::endl;
+
+       g_free(msg);
+
+       return maybe_rotate_logfile() ? G_LOG_WRITER_HANDLED : G_LOG_WRITER_UNHANDLED;
+}
+
+static GLogWriterOutput
+log_stdouterr(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data)
+{
+       return g_log_writer_standard_streams(level, fields, n_fields, user_data);
+}
+
+static GLogWriterOutput
+log_journal(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data)
+{
+       return g_log_writer_journald(level, fields, n_fields, user_data);
+}
+
+void
+Mu::log_init(const std::string& path, Mu::LogOptions opts)
+{
+       if (MuLogInitialized) {
+               g_error("logging is already initialized");
+               return;
+       }
+
+       if (g_getenv("MU_LOG_STDOUTERR"))
+               opts |= LogOptions::StdOutErr;
+
+       MuLogOptions = opts;
+       MuLogPath    = path;
+
+       g_log_set_writer_func(
+           [](GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data) {
+                   // filter out debug-level messages?
+                   if (level == G_LOG_LEVEL_DEBUG &&
+                       (none_of(MuLogOptions & Mu::LogOptions::Debug)))
+                           return G_LOG_WRITER_HANDLED;
+
+                   // log criticals to stdout / err or if asked
+                   if (level == G_LOG_LEVEL_CRITICAL ||
+                       any_of(MuLogOptions & Mu::LogOptions::StdOutErr)) {
+                           log_stdouterr(level, fields, n_fields, user_data);
+                   }
+
+                   // log to the journal, or, if not available to a file.
+                   if (log_journal(level, fields, n_fields, user_data) != G_LOG_WRITER_HANDLED)
+                           return log_file(level, fields, n_fields, user_data);
+                   else
+                           return G_LOG_WRITER_HANDLED;
+           },
+           NULL,
+           NULL);
+
+       g_message("logging initialized; debug: %s, stdout/stderr: %s",
+                 any_of(log_get_options() & LogOptions::Debug) ? "yes" : "no",
+                 any_of(log_get_options() & LogOptions::StdOutErr) ? "yes" : "no");
+
+       MuLogInitialized = true;
+}
+
+void
+Mu::log_uninit()
+{
+       if (!MuLogInitialized)
+               return;
+
+       if (MuStream.is_open())
+               MuStream.close();
+
+       MuLogInitialized = false;
+}
+
+void
+Mu::log_set_options(Mu::LogOptions opts)
+{
+       MuLogOptions = opts;
+}
+
+Mu::LogOptions
+Mu::log_get_options()
+{
+       return MuLogOptions;
+}
diff --git a/lib/utils/mu-logger.hh b/lib/utils/mu-logger.hh
new file mode 100644 (file)
index 0000000..61b187b
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_LOGGER_HH__
+#define MU_LOGGER_HH__
+
+#include <string>
+#include "utils/mu-utils.hh"
+
+namespace Mu {
+
+/**
+ * Logging options
+ *
+ */
+enum struct LogOptions {
+       None      = 0,      /**< Nothing specific */
+       StdOutErr = 1 << 1, /**< Log to stdout/stderr */
+       Debug     = 1 << 2, /**< Include debug-level logs */
+};
+
+/**
+ * Initialize the logging system. Note that the path is only used if structured
+ * logging fails -- practically, it goes to the file if there's
+ * systemd/journald.
+ *
+ * if the environment variable MU_LOG_STDOUTERR is set, LogOptions::StdoutErr is
+ * implied.
+ *
+ * @param path path to the log file
+ * @param opts logging options
+ */
+void log_init(const std::string& path, LogOptions opts);
+
+/**
+ * Uninitialize the logging system
+ *
+ */
+void log_uninit();
+
+/**
+ * Change the logging options.
+ *
+ * @param opts options
+ */
+void log_set_options(LogOptions opts);
+
+/**
+ * Get the current log options
+ *
+ * @return the log options
+ */
+LogOptions log_get_options();
+
+} // namespace Mu
+MU_ENABLE_BITOPS(Mu::LogOptions);
+
+#endif /* MU_LOGGER_HH__ */
diff --git a/lib/utils/mu-option.cc b/lib/utils/mu-option.cc
new file mode 100644 (file)
index 0000000..a136b86
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-option.hh"
+#include <glib.h>
+
+using namespace Mu;
+
+Mu::Option<std::string>
+Mu::to_string_opt_gchar(gchar*&& str)
+{
+       auto res = to_string_opt(str);
+       g_free(str);
+
+       return res;
+}
diff --git a/lib/utils/mu-option.hh b/lib/utils/mu-option.hh
new file mode 100644 (file)
index 0000000..6af72e1
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Created on 2020-11-08 by Dirk-Jan C. Binnema <dbinnema@logitech.com>
+ *
+ * Copyright (c) 2020 Logitech, Inc.  All Rights Reserved
+ * This program is a trade secret of LOGITECH, and it is not to be reproduced,
+ * published, disclosed to others, copied, adapted, distributed or displayed
+ * without the prior authorization of LOGITECH.
+ *
+ * Licensee agrees to attach or embed this notice on all copies of the program,
+ * including partial copies or modified versions thereof.
+ *
+ */
+
+#ifndef MU_OPTION__
+#define MU_OPTION__
+
+#include "thirdparty/optional.hpp"
+#include <string>
+
+namespace Mu {
+
+/// Either a value of type T, or None
+template <typename T> using Option = tl::optional<T>;
+
+template <typename T>
+Option<T>
+Some(T&& t)
+{
+       return std::move(t);
+}
+constexpr auto Nothing = tl::nullopt; // 'None' is take already
+
+/**
+ * Maybe create a string from a const char pointer.
+ *
+ * @param str a char pointer or NULL
+ *
+ * @return option with either the string or nothing if str was NULL.
+ */
+Option<std::string>
+static inline to_string_opt(const char* str) {
+       if (str)
+               return std::string{str};
+       else
+               return Nothing;
+}
+
+/**
+ * Like maybe_string that takes a const char*, but additionally,
+ * g_free() the string.
+ *
+ * @param str char pointer or NULL (consumed)
+ *
+ * @return option with either the string or nothing if str was NULL.
+ */
+Option<std::string> to_string_opt_gchar(char*&& str);
+
+
+} // namespace Mu
+#endif /*MU_OPTION__*/
diff --git a/lib/utils/mu-readline.cc b/lib/utils/mu-readline.cc
new file mode 100644 (file)
index 0000000..2f9c72a
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-readline.hh"
+#include "config.h"
+
+#include <iostream>
+#include <unistd.h>
+#include <string>
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#ifdef HAVE_LIBREADLINE
+#if defined(HAVE_READLINE_READLINE_H)
+#include <readline/readline.h>
+#elif defined(HAVE_READLINE_H)
+#include <readline.h>
+#else  /* !defined(HAVE_READLINE_H) */
+extern char* readline();
+#endif /* !defined(HAVE_READLINE_H) */
+char* cmdline = NULL;
+#else  /* !defined(HAVE_READLINE_READLINE_H) */
+/* no readline */
+#endif /* HAVE_LIBREADLINE */
+
+#ifdef HAVE_READLINE_HISTORY
+#if defined(HAVE_READLINE_HISTORY_H)
+#include <readline/history.h>
+#elif defined(HAVE_HISTORY_H)
+#include <history.h>
+#else  /* !defined(HAVE_HISTORY_H) */
+extern void  add_history();
+extern int   write_history();
+extern int   read_history();
+#endif /* defined(HAVE_READLINE_HISTORY_H) */
+/* no history */
+#endif /* HAVE_READLINE_HISTORY */
+
+#if defined(HAVE_LIBREADLINE) && defined(HAVE_READLINE_HISTORY)
+#define HAVE_READLINE (1)
+#else
+#define HAVE_READLINE (0)
+#endif
+
+using namespace Mu;
+
+static bool        is_a_tty{};
+static std::string hist_path;
+static size_t      max_lines{};
+
+bool
+Mu::have_readline()
+{
+       return HAVE_READLINE != 0;
+}
+
+
+void
+Mu::setup_readline(const std::string& histpath, size_t maxlines)
+{
+       is_a_tty  = !!::isatty(::fileno(stdout));
+       hist_path = histpath;
+       max_lines = maxlines;
+
+#if HAVE_READLINE
+       rl_bind_key('\t', rl_insert); // default (filenames) is not useful
+       using_history();
+       read_history(hist_path.c_str());
+
+       if (max_lines > 0)
+               stifle_history(max_lines);
+#endif /*HAVE_READLINE*/
+}
+
+void
+Mu::shutdown_readline()
+{
+#if HAVE_READLINE
+       if (!is_a_tty)
+               return;
+
+       write_history(hist_path.c_str());
+       if (max_lines > 0)
+               history_truncate_file(hist_path.c_str(), max_lines);
+#endif /*HAVE_READLINE*/
+}
+
+std::string
+Mu::read_line(bool& do_quit)
+{
+#if HAVE_READLINE
+       if (is_a_tty) {
+               auto buf = readline(";; mu% ");
+               if (!buf) {
+                       do_quit = true;
+                       return {};
+               }
+               std::string line{buf};
+               ::free(buf);
+               return line;
+       }
+#endif /*HAVE_READLINE*/
+
+       std::string line;
+       std::cout << ";; mu> ";
+       if (!std::getline(std::cin, line))
+               do_quit = true;
+
+       return line;
+}
+
+void
+Mu::save_line(const std::string& line)
+{
+#if HAVE_READLINE
+       if (is_a_tty)
+               add_history(line.c_str());
+#endif /*HAVE_READLINE*/
+}
diff --git a/lib/utils/mu-readline.hh b/lib/utils/mu-readline.hh
new file mode 100644 (file)
index 0000000..ca0455f
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <string>
+
+namespace Mu {
+
+/**
+ * Setup readline when available and on tty.
+ *
+ * @param histpath path to the history file
+ * @param max_lines maximum number of history to save
+ */
+void setup_readline(const std::string& histpath, size_t max_lines);
+
+/**
+ * Shutdown readline
+ *
+ */
+void shutdown_readline();
+
+/**
+ * Read a command line
+ *
+ * @param do_quit recceives whether we should quit.
+ *
+ * @return the string read or empty
+ */
+std::string read_line(bool& do_quit);
+
+/**
+ * Save a line to history (or do nothing when readline is not active)
+ *
+ * @param line a line.
+ */
+void save_line(const std::string& line);
+
+
+/**
+ * Do we have the non-shim readline?
+ *
+ * @return true or failse
+ */
+bool have_readline();
+
+} // namespace Mu
diff --git a/lib/utils/mu-result.hh b/lib/utils/mu-result.hh
new file mode 100644 (file)
index 0000000..ee709f1
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+** Copyright (C) 2019-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_RESULT_HH__
+#define MU_RESULT_HH__
+
+#include "thirdparty/expected.hpp"
+#include "utils/mu-error.hh"
+
+namespace Mu {
+/**
+ * A little Rust-envy...a Result is _either_ some value of type T, _or_ a Mu::Error
+ */
+template <typename T> using Result = tl::expected<T, Error>;
+
+/**
+ * Ok() is not typically strictly needed (unlike Err), but imitates Rust's Ok
+ * and it helps the reader.
+ *
+ * @param t the value to return
+ *
+ * @return a success Result<T>
+ */
+template <typename T>
+class Result<T>::expected
+// note: "class", not "typename";
+// https://stackoverflow.com/questions/46412754/class-name-injection-and-constructors
+Ok(T&& t)
+{
+       return std::move(t);
+}
+
+/**
+ * Implementation of Ok() for void results.
+ *
+ * @return a success Result<void>
+ */
+static inline Result<void>
+Ok()
+{
+       return {};
+}
+
+/**
+ * Return an error
+ *
+ * @param err the error
+ *
+ * @return error
+ */
+static inline tl::unexpected<Error>
+Err(Error&& err)
+{
+       return tl::unexpected(std::move(err));
+}
+
+static inline tl::unexpected<Error>
+Err(const Error& err)
+{
+       return tl::unexpected(err);
+}
+
+template<typename T>
+static inline tl::unexpected<Error>
+Err(const Result<T>& res)
+{
+       return res.error();
+}
+
+
+
+template <typename T>
+static inline Result<void>
+Ok(const T& t)
+{
+       if (t)
+               return Ok();
+       else
+               return Err(t.error());
+}
+
+
+/*
+ * convenience
+ */
+
+static inline tl::unexpected<Error>
+Err(Error::Code errcode, std::string&& msg="")
+{
+       return Err(Error{errcode, std::move(msg)});
+}
+
+__attribute__((format(printf, 2, 0)))
+static inline tl::unexpected<Error>
+Err(Error::Code errcode, const char* frm, ...)
+{
+       va_list args;
+       va_start(args, frm);
+       auto str{vformat(frm, args)};
+       va_end(args);
+
+       return Err(errcode, std::move(str));
+}
+
+__attribute__((format(printf, 3, 0)))
+static inline tl::unexpected<Error>
+Err(Error::Code errcode, GError **err, const char* frm, ...)
+{
+       va_list args;
+       va_start(args, frm);
+       auto str{vformat(frm, args)};
+       va_end(args);
+
+       if (err && *err)
+               str += format(" (%s)", (*err)->message ? (*err)->message : "");
+       g_clear_error(err);
+
+       return Err(errcode, std::move(str));
+}
+
+
+
+/**
+ * Assert that some result has a value (for unit tests)
+ *
+ * @param R some result
+ */
+#define assert_valid_result(R) do {                    \
+       if(!R) {                                        \
+               g_printerr("%s:%u: error-result: %s\n", \
+                          __FILE__, __LINE__,          \
+                          (R).error().what());         \
+               g_assert_true(!!R);                     \
+       }                                               \
+} while(0)
+
+
+
+}// namespace Mu
+
+#endif /* MU_RESULT_HH__ */
diff --git a/lib/utils/mu-sexp.cc b/lib/utils/mu-sexp.cc
new file mode 100644 (file)
index 0000000..a8e1ff6
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-sexp.hh"
+#include "mu-utils.hh"
+
+#include <sstream>
+#include <array>
+
+using namespace Mu;
+
+__attribute__((format(printf, 2, 0))) static Mu::Error
+parsing_error(size_t pos, const char* frm, ...)
+{
+       va_list args;
+       va_start(args, frm);
+       auto msg = vformat(frm, args);
+       va_end(args);
+
+       if (pos == 0)
+               return Mu::Error(Error::Code::Parsing, "%s", msg.c_str());
+       else
+               return Mu::Error(Error::Code::Parsing, "%zu: %s", pos, msg.c_str());
+}
+static size_t
+skip_whitespace(const std::string& s, size_t pos)
+{
+       while (pos != s.size()) {
+               if (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n')
+                       ++pos;
+               else
+                       break;
+       }
+
+       return pos;
+}
+
+static Sexp parse(const std::string& expr, size_t& pos);
+
+static Sexp
+parse_list(const std::string& expr, size_t& pos)
+{
+       if (expr[pos] != '(') // sanity check.
+               throw parsing_error(pos, "expected: '(' but got '%c", expr[pos]);
+
+       Sexp::List list;
+
+       ++pos;
+       while (expr[pos] != ')' && pos != expr.size())
+               list.add(parse(expr, pos));
+
+       if (expr[pos] != ')')
+               throw parsing_error(pos, "expected: ')' but got '%c'", expr[pos]);
+       ++pos;
+       return Sexp::make_list(std::move(list));
+}
+
+// parse string
+static Sexp
+parse_string(const std::string& expr, size_t& pos)
+{
+       if (expr[pos] != '"') // sanity check.
+               throw parsing_error(pos, "expected: '\"'' but got '%c", expr[pos]);
+
+       bool        escape{};
+       std::string str;
+       for (++pos; pos != expr.size(); ++pos) {
+               auto kar = expr[pos];
+               if (escape && (kar == '"' || kar == '\\')) {
+                       str += kar;
+                       escape = false;
+                       continue;
+               }
+
+               if (kar == '"')
+                       break;
+               else if (kar == '\\')
+                       escape = true;
+               else
+                       str += kar;
+       }
+
+       if (escape || expr[pos] != '"')
+               throw parsing_error(pos, "unterminated string '%s'", str.c_str());
+
+       ++pos;
+       return Sexp::make_string(std::move(str));
+}
+
+static Sexp
+parse_integer(const std::string& expr, size_t& pos)
+{
+       if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check.
+               throw parsing_error(pos, "expected: <digit> but got '%c", expr[pos]);
+
+       std::string num; // negative number?
+       if (expr[pos] == '-') {
+               num = "-";
+               ++pos;
+       }
+
+       for (; isdigit(expr[pos]); ++pos)
+               num += expr[pos];
+
+       return Sexp::make_number(::atoi(num.c_str()));
+}
+
+static Sexp
+parse_symbol(const std::string& expr, size_t& pos)
+{
+       if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check.
+               throw parsing_error(pos, "expected: <alpha>|: but got '%c", expr[pos]);
+
+       std::string symbol(1, expr[pos]);
+       for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos)
+               symbol += expr[pos];
+
+       return Sexp::make_symbol(std::move(symbol));
+}
+
+static Sexp
+parse(const std::string& expr, size_t& pos)
+{
+       pos = skip_whitespace(expr, pos);
+
+       if (pos == expr.size())
+               throw parsing_error(pos, "expected: character '%c", expr[pos]);
+
+       const auto kar  = expr[pos];
+       const auto node = [&]() -> Sexp {
+               if (kar == '(')
+                       return parse_list(expr, pos);
+               else if (kar == '"')
+                       return parse_string(expr, pos);
+               else if (isdigit(kar) || kar == '-')
+                       return parse_integer(expr, pos);
+               else if (isalpha(kar) || kar == ':')
+                       return parse_symbol(expr, pos);
+               else
+                       throw parsing_error(pos, "unexpected character '%c", kar);
+       }();
+
+       pos = skip_whitespace(expr, pos);
+
+       return node;
+}
+
+Sexp
+Sexp::make_parse(const std::string& expr)
+{
+       size_t pos{};
+       auto   node{::parse(expr, pos)};
+
+       if (pos != expr.size())
+               throw parsing_error(pos, "trailing data starting with '%c'", expr[pos]);
+
+       return node;
+}
+
+std::string
+Sexp::to_sexp_string() const
+{
+       std::stringstream sstrm;
+
+       switch (type()) {
+       case Type::List: {
+               sstrm << '(';
+               bool first{true};
+               for (auto&& child : list()) {
+                       sstrm << (first ? "" : " ") << child.to_sexp_string();
+                       first = false;
+               }
+               sstrm << ')';
+
+               if (any_of(formatting_opts & FormattingOptions::SplitList))
+                       sstrm << '\n';
+               break;
+       }
+       case Type::String:
+               sstrm << quote(value());
+               break;
+       case Type::Raw:
+               sstrm << value();
+               break;
+       case Type::Number:
+       case Type::Symbol:
+       case Type::Empty:
+       default: sstrm << value();
+       }
+
+       return sstrm.str();
+}
+
+// LCOV_EXCL_START
+
+std::string
+Sexp::to_json_string() const
+{
+       std::stringstream sstrm;
+
+       switch (type()) {
+       case Type::List: {
+               // property-lists become JSON objects
+               if (is_prop_list()) {
+                       sstrm << "{";
+                       auto it{list().begin()};
+                       bool first{true};
+                       while (it != list().end()) {
+                               sstrm << (first ? "" : ",") << quote(it->value()) << ":";
+                               ++it;
+                               sstrm << it->to_json_string();
+                               ++it;
+                               first = false;
+                       }
+                       sstrm << "}";
+                       if (any_of(formatting_opts & FormattingOptions::SplitList))
+                       sstrm << '\n';
+               } else { // other lists become arrays.
+                       sstrm << '[';
+                       bool first{true};
+                       for (auto&& child : list()) {
+                               sstrm << (first ? "" : ", ") << child.to_json_string();
+                               first = false;
+                       }
+                       sstrm << ']';
+                       if (any_of(formatting_opts & FormattingOptions::SplitList))
+                               sstrm << '\n';
+               }
+               break;
+       }
+       case Type::String:
+               sstrm << quote(value());
+               break;
+       case Type::Raw: // FIXME: implement this.
+               break;
+
+       case Type::Symbol:
+               if (is_nil())
+                       sstrm << "false";
+               else if (is_t())
+                       sstrm << "true";
+               else
+                       sstrm << quote(value());
+               break;
+       case Type::Number:
+       case Type::Empty:
+       default: sstrm << value();
+       }
+
+       return sstrm.str();
+}
+
+// LCOV_EXCL_STOP
diff --git a/lib/utils/mu-sexp.hh b/lib/utils/mu-sexp.hh
new file mode 100644 (file)
index 0000000..f04b735
--- /dev/null
@@ -0,0 +1,447 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_SEXP_HH__
+#define MU_SEXP_HH__
+
+#include <string>
+#include <vector>
+#include <type_traits>
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-error.hh"
+
+namespace Mu {
+/// Simple s-expression parser & list that parses lists () and atoms (strings
+/// ("-quoted), (positive) integers ([0..9]+) and symbol starting with alpha or
+/// ':', then alphanum and '-')
+///
+/// (:foo (1234 "bar" nil) :quux (a b c))
+
+/// Parse node
+struct Sexp {
+       /// Node type
+       enum struct Type { Empty, List, String, Number, Symbol, Raw };
+
+       /**
+        * Default CTOR
+        */
+       Sexp() : type_{Type::Empty} {}
+
+       // Underlying data type for list; we'd like to use std::dequeu here,
+       // but that does not compile with libc++ (it does with libstdc++)
+       using Seq = std::vector<Sexp>;
+
+       /**
+        * Make a sexp out of an s-expression string.
+        *
+        * @param expr a string containing an s-expression
+        *
+        * @return the parsed s-expression, or throw Error.
+        */
+       static Sexp make_parse(const std::string& expr);
+
+       /**
+        * Make a node for a string/integer/symbol/list value
+        *
+        * @param val some value
+        * @param empty_is_nil turn empty string into a 'nil' symbol
+        *
+        * @return a node
+        */
+       static Sexp make_string(std::string&& val, bool empty_is_nil=false)
+       {
+               if (empty_is_nil && val.empty())
+                       return make_symbol("nil");
+               else
+                       return Sexp{Type::String, std::move(val)};
+       }
+       static Sexp make_string(const std::string& val, bool empty_is_nil=false)
+       {
+               if (empty_is_nil && val.empty())
+                       return make_symbol("nil");
+               else
+                       return Sexp{Type::String, std::string(val)};
+       }
+
+       static Sexp make_number(int val) { return Sexp{Type::Number, format("%d", val)}; }
+       static Sexp make_symbol(std::string&& val) {
+               if (val.empty())
+                       throw Error(Error::Code::InvalidArgument,
+                                   "symbol must be non-empty");
+               return Sexp{Type::Symbol, std::move(val)};
+       }
+       static Sexp make_symbol_sv(std::string_view val) {
+               return make_symbol(std::string{val});
+       }
+
+       /**
+        * Add a raw string sexp.
+        *
+        * @param val value
+        *
+        * @return A sexp
+        */
+       static Sexp make_raw(std::string&& val) {
+               return Sexp{Type::Raw, std::string{val}};
+       }
+       static Sexp make_raw(const std::string& val) {
+               return make_raw(std::string{val});
+       }
+
+
+       /**
+        *
+        *
+        * The value of this node; invalid for list nodes.
+        *
+        * @return
+        */
+       const std::string& value() const {
+               if (is_list())
+                       throw Error(Error::Code::InvalidArgument, "no value for list");
+               if (is_empty())
+                       throw Error{Error::Code::InvalidArgument, "no value for empty"};
+               return value_;
+       }
+
+       /**
+        * The underlying container of this list node; only valid for lists
+        *
+        * @return
+        */
+       const Seq& list() const {
+               if (!is_list())
+                       throw Error(Error::Code::InvalidArgument, "not a list");
+               return seq_;
+       }
+
+       /**
+        * Convert a Sexp to its S-expression string representation
+        *
+        * @return the string representation
+        */
+       std::string to_sexp_string() const;
+
+       /**
+        * Convert a Sexp::Node to its JSON string representation
+        *
+        * @return the string representation
+        */
+       std::string to_json_string() const;
+
+       /**
+        * Return the type of this Node.
+        *
+        * @return the type
+        */
+       Type type() const { return type_; }
+
+       ///
+       /// Helper struct to build mutable lists.
+       ///
+       struct List {
+               List () = default;
+               List (const Seq& seq): seq_{seq} {}
+
+               /**
+                * Add a sexp to the list
+                *
+                * @param sexp a sexp
+                * @param args rest arguments
+                *
+                * @return a ref to this List (for chaining)
+                */
+               List& add() { return *this; }
+               List& add(Sexp&& sexp)
+               {
+                       seq_.emplace_back(std::move(sexp));
+                       return *this;
+               }
+               template <typename... Args> List& add(Sexp&& sexp, Args... args)
+               {
+                       seq_.emplace_back(std::move(sexp));
+                       seq_.emplace_back(std::forward<Args>(args)...);
+                       return *this;
+               }
+
+               /**
+                * Add a property (i.e., :key sexp ) to the list. Remove any
+                * prop with the same name
+                *
+                * @param name a property-name. Must start with ':', length > 1
+                * @param sexp a sexp
+                * @param args rest arguments
+                *
+                * @return a ref to this List (for chaining)
+                */
+               List& add_prop(std::string&& name, Sexp&& sexp) {
+                       remove_prop(name);
+                       if (!is_prop_name(name))
+                               throw Error{Error::Code::InvalidArgument,
+                                           "invalid property name ('%s')",
+                                           name.c_str()};
+                       seq_.emplace_back(make_symbol(std::move(name)));
+                       seq_.emplace_back(std::move(sexp));
+                       return *this;
+               }
+               template <typename... Args>
+               List& add_prop(std::string&& name, Sexp&& sexp, Args... args) {
+                       remove_prop(name);
+                       add_prop(std::move(name), std::move(sexp));
+                       add_prop(std::forward<Args>(args)...);
+                       return *this;
+               }
+
+               void remove_prop(const std::string& name) {
+                       if (!is_prop_name(name))
+                               throw Error{Error::Code::InvalidArgument,
+                                       "invalid property name ('%s')", name.c_str()};
+                       auto it = std::find_if(seq_.begin(), seq_.end(), [&](auto&& elm) {
+                               return elm.type() == Sexp::Type::Symbol &&
+                                       elm.value() == name;
+                       });
+                       if (it != seq_.cend() && it + 1 != seq_.cend()) {
+                               /* erase propname and value.*/
+                               seq_.erase(it, it + 2);
+                       }
+               }
+
+               /**
+                * Remove all elements from the list.
+                */
+               void clear() { seq_.clear(); }
+
+               /**
+                * Get the number of elements in the list
+                *
+                * @return number
+                */
+               size_t size() const { return seq_.size(); }
+
+               /**
+                * Is the list empty?
+                *
+                * @return true or false
+                */
+               size_t empty() const { return seq_.empty(); }
+
+       private:
+               friend struct Sexp;
+               Seq seq_;
+       };
+
+       /**
+        * Construct a list sexp from a List
+        *
+        * @param list a list-list
+        * @param sexp  a Sexp
+        * @param args rest arguments
+        *
+        * @return a sexp.
+        */
+       static Sexp make_list(List&& list) { return Sexp{Type::List, std::move(list.seq_)}; }
+       template <typename... Args> static Sexp make_list(Sexp&& sexp, Args... args)
+       {
+               List lst;
+               lst.add(std::move(sexp)).add(std::forward<Args>(args)...);
+               return make_list(std::move(lst));
+       }
+
+       /**
+        * Construct a property list sexp from a List
+        *
+        * @param name the property name; must start wtth ':'
+        * @param sexp  a Sexp
+        * @param args rest arguments (property list)
+        *
+        * @return a sexp.
+        */
+       template <typename... Args>
+       static Sexp make_prop_list(std::string&& name, Sexp&& sexp, Args... args)
+       {
+               List list;
+               list.add_prop(std::move(name), std::move(sexp), std::forward<Args>(args)...);
+               return make_list(std::move(list));
+       }
+
+       /**
+        * Construct a properrty list sexp from a List
+        *
+        * @param funcname function name for the call
+        * @param name the property name; must start wtth ':'
+        * @param sexp  a Sexp
+        * @param args rest arguments (property list)
+        *
+        * @return a sexp.
+        */
+       template <typename... Args>
+       static Sexp make_call(std::string&& funcname, std::string&& name, Sexp&& sexp, Args... args)
+       {
+               List list;
+               list.add(make_symbol(std::move(funcname)));
+               list.add_prop(std::move(name), std::move(sexp), std::forward<Args>(args)...);
+               return make_list(std::move(list));
+       }
+
+       /// Some type helpers
+       bool is_list() const { return type() == Type::List; }
+       bool is_string() const { return type() == Type::String; }
+       bool is_number() const { return type() == Type::Number; }
+       bool is_symbol() const { return type() == Type::Symbol; }
+       bool is_empty() const { return type() == Type::Empty; }
+
+       operator bool() const { return !is_empty(); }
+
+       static constexpr auto SymbolNil{"nil"};
+       static constexpr auto SymbolT{"t"};
+       bool                  is_nil() const { return is_symbol() && value() == SymbolNil; }
+       bool                  is_t() const { return is_symbol() && value() == SymbolT; }
+
+       /**
+        * Is this a prop-list? A prop list is a list sexp with alternating
+        * property / sexp
+        *
+        * @return
+        */
+       bool is_prop_list() const
+       {
+               if (!is_list() || list().size() % 2 != 0)
+                       return false;
+               else
+                       return is_prop_list(list().begin(), list().end());
+       }
+
+       /**
+        * Is this a call? A call is a list sexp with a symbol (function name),
+        * followed by a prop list
+        *
+        * @return
+        */
+       bool is_call() const
+       {
+               if (!is_list() || list().size() % 2 != 1 || !list().at(0).is_symbol())
+                       return false;
+               else
+                       return is_prop_list(list().begin() + 1, list().end());
+       }
+
+       enum struct FormattingOptions {
+               Default   = 0,  /**< Nothing in particular */
+               SplitList = 1 << 0,     /**< Insert newline after list item */
+       };
+
+       FormattingOptions formatting_opts{}; /**< Formatting option for the
+                                             * string output */
+
+private:
+       Sexp(Type typearg, std::string&& valuearg) : type_{typearg}, value_{std::move(valuearg)} {
+               if (is_list())
+                       throw Error{Error::Code::InvalidArgument, "cannot be a list type"};
+               if (is_empty())
+                       throw Error{Error::Code::InvalidArgument, "cannot be an empty type"};
+       }
+       Sexp(Type typearg, Seq&& seq) : type_{Type::List}, seq_{std::move(seq)} {
+               if (!is_list())
+                       throw Error{Error::Code::InvalidArgument, "must be a list type"};
+               if (is_empty())
+                       throw Error{Error::Code::InvalidArgument, "cannot be an empty type"};
+       }
+       /**
+        * Is the sexp a valid property name?
+        *
+        * @param sexp a Sexp.
+        *
+        * @return true or false.
+        */
+       static bool is_prop_name(const std::string& str)
+       {
+               return str.size() > 1 && str.at(0) == ':';
+       }
+       static bool is_prop_name(const Sexp& sexp)
+       {
+               return sexp.is_symbol() && is_prop_name(sexp.value());
+       }
+
+       static bool is_prop_list(Seq::const_iterator b, Seq::const_iterator e)
+       {
+               while (b != e) {
+                       const Sexp& s{*b};
+                       if (!is_prop_name(s))
+                               return false;
+                       if (++b == e)
+                               return false;
+                       ++b;
+               }
+               return b == e;
+       }
+
+       Type        type_;  /**<  Type of node */
+       std::string value_; /**< String value of node (only for
+                              * non-Type::Lst)*/
+       Seq seq_;           /**< Children of node (only for
+                                  * Type::Lst) */
+};
+
+static inline std::ostream&
+operator<<(std::ostream& os, Sexp::Type id)
+{
+       switch (id) {
+       case Sexp::Type::List:
+               os << "list";
+               break;
+       case Sexp::Type::String:
+               os << "string";
+               break;
+       case Sexp::Type::Number:
+               os << "number";
+               break;
+       case Sexp::Type::Symbol:
+               os << "symbol";
+               break;
+       case Sexp::Type::Raw:
+               os << "raw";
+               break;
+       case Sexp::Type::Empty:
+               os << "empty";
+               break;
+       default: throw std::runtime_error("unknown node type");
+       }
+
+       return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Sexp& sexp)
+{
+       os << sexp.to_sexp_string();
+       return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Sexp::List& sexp)
+{
+       os << Sexp::make_list(Sexp::List(sexp));
+       return os;
+}
+MU_ENABLE_BITOPS(Sexp::FormattingOptions);
+
+} // namespace Mu
+
+#endif /* MU_SEXP_HH__ */
diff --git a/lib/utils/mu-test-utils.cc b/lib/utils/mu-test-utils.cc
new file mode 100644 (file)
index 0000000..8e049b2
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include <langinfo.h>
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "utils/mu-error.hh"
+
+
+using namespace Mu;
+
+char*
+Mu::test_mu_common_get_random_tmpdir()
+{
+       char* dir;
+       int   res;
+
+       dir = g_strdup_printf("%s%cmu-test-%d%ctest-%x",
+                             g_get_tmp_dir(),
+                             G_DIR_SEPARATOR,
+                             getuid(),
+                             G_DIR_SEPARATOR,
+                             (int)random() * getpid() * (int)time(NULL));
+
+       res = g_mkdir_with_parents(dir, 0700);
+       g_assert(res != -1);
+
+       return dir;
+}
+
+const char*
+Mu::set_tz(const char* tz)
+{
+       static const char* oldtz;
+
+       oldtz = getenv("TZ");
+       if (tz)
+               setenv("TZ", tz, 1);
+       else
+               unsetenv("TZ");
+
+       tzset();
+       return oldtz;
+}
+
+bool
+Mu::set_en_us_utf8_locale()
+{
+       setenv("LC_ALL", "en_US.UTF-8", 1);
+       setlocale(LC_ALL, "en_US.UTF-8");
+
+       if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0) {
+               g_print("Note: Unit tests require the en_US.utf8 locale. "
+                       "Ignoring test cases.\n");
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static void
+black_hole(void)
+{
+       return; /* do nothing */
+}
+
+void
+Mu::mu_test_init(int *argc, char ***argv)
+{
+       g_test_init(argc, argv, NULL);
+
+       if (!g_test_verbose())
+               g_log_set_handler(
+                       NULL,
+                       (GLogLevelFlags)(G_LOG_LEVEL_MASK |
+                                        G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+                       (GLogFunc)black_hole, NULL);
+}
+
+
+
+void
+Mu::allow_warnings()
+{
+       g_test_log_set_fatal_handler(
+           [](const char*, GLogLevelFlags, const char*, gpointer) { return FALSE; },
+           {});
+}
+
+
+
+Mu::TempDir::TempDir(bool autodelete): autodelete_{autodelete}
+{
+       GError *err{};
+       gchar *tmpdir = g_dir_make_tmp("mu-tmp-XXXXXX", &err);
+       if (!tmpdir)
+               throw Mu::Error(Error::Code::File, &err,
+                               "failed to create temporary directory");
+
+       path_ = tmpdir;
+       g_free(tmpdir);
+
+       g_debug("created '%s'", path_.c_str());
+}
+
+Mu::TempDir::~TempDir()
+{
+       if (::access(path_.c_str(), F_OK) != 0)
+               return; /* nothing to do */
+
+       if (!autodelete_) {
+               g_debug("_not_ deleting %s", path_.c_str());
+               return;
+       }
+
+       /* ugly */
+       GError *err{};
+       const auto cmd{format("/bin/rm -rf '%s'", path_.c_str())};
+       if (!g_spawn_command_line_sync(cmd.c_str(), NULL, NULL, NULL, &err)) {
+               g_warning("error: %s\n", err ? err->message : "?");
+               g_clear_error(&err);
+       } else
+               g_debug("removed '%s'", path_.c_str());
+}
diff --git a/lib/utils/mu-test-utils.hh b/lib/utils/mu-test-utils.hh
new file mode 100644 (file)
index 0000000..c59b9d5
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_TEST_UTILS_HH__
+#define MU_TEST_UTILS_HH__
+
+#include <string>
+
+namespace Mu {
+
+/**
+ * get a dir name for a random temporary directory to do tests
+ *
+ * @return a random dir name, g_free when it's no longer needed
+ */
+char* test_mu_common_get_random_tmpdir();
+
+/**
+ * mu wrapper for g_test_init
+ *
+ * @param argc
+ * @param argv
+ */
+void mu_test_init(int *argc, char ***argv);
+
+/**
+ * set the timezone
+ *
+ * @param tz timezone
+ *
+ * @return the old timezone
+ */
+const char* set_tz(const char* tz);
+
+/**
+ * switch the locale to en_US.utf8, return TRUE if it succeeds
+ *
+ * @return true if the switch succeeds, false otherwise
+ */
+bool set_en_us_utf8_locale();
+
+/**
+ * For unit tests, assert two std::string's are equal.
+ *
+ * @param s1 string1
+ * @param s2 string2
+ */
+#define assert_equal(s1__,s2__) do {                                   \
+               std::string s1s__(s1__), s2s__(s2__);                   \
+               g_assert_cmpstr(s1s__.c_str(), ==, s2s__.c_str());      \
+       } while(0)
+
+
+#define assert_equal_seq(seq1__, seq2__) do {                          \
+               g_assert_cmpuint(seq1__.size(), ==, seq2__.size());     \
+               size_t n__{};                                           \
+               for (auto&& item__: seq1__) {                           \
+                       g_assert_true(item__ == seq2__.at(n__));        \
+                       ++n__;                                          \
+               }                                                       \
+               } while(0)
+
+#define assert_equal_seq_str(seq1__, seq2__) do {                      \
+               g_assert_cmpuint(seq1__.size(), ==, seq2__.size());     \
+               size_t n__{};                                           \
+               for (auto&& item__: seq1__) {                           \
+                       assert_equal(item__, seq2__.at(n__));           \
+                       ++n__;                                          \
+               }                                                       \
+               } while(0)
+
+/**
+ * For unit-tests, allow warnings in the current function.
+ *
+ */
+void allow_warnings();
+
+
+/**
+ * For unit-tests, a RAII tempdir.
+ *
+ */
+struct TempDir {
+       /**
+        * Construct a temporary directory
+        */
+       TempDir(bool autodelete=true);
+
+       /**
+        * DTOR; removes the temporary directory
+        *
+        *
+        * @return
+        */
+       ~TempDir();
+
+       /**
+        * Path to the temporary directory
+        *
+        * @return the path.
+        *
+        *
+        */
+       const std::string& path() {return path_; }
+private:
+       std::string path_;
+       const bool autodelete_;
+};
+
+} // namepace Mu
+
+
+#endif /* MU_TEST_UTILS_HH__ */
diff --git a/lib/utils/mu-util.c b/lib/utils/mu-util.c
new file mode 100644 (file)
index 0000000..a2e3bc6
--- /dev/null
@@ -0,0 +1,468 @@
+/*
+** Copyright (C) 2008-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 3 of the License, 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 <config.h>
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif /*_GNU_SOURCE*/
+
+#include "mu-util.h"
+#ifdef HAVE_WORDEXP_H
+#include <wordexp.h> /* for shell-style globbing */
+#endif /*HAVE_WORDEXP_H*/
+
+#include <stdlib.h>
+
+#include <string.h>
+#include <locale.h> /* for setlocale() */
+
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <glib-object.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+#include <errno.h>
+
+#include <langinfo.h>
+
+
+static char*
+do_wordexp (const char *path)
+{
+#ifdef HAVE_WORDEXP_H
+       wordexp_t wexp;
+       char *dir;
+
+       if (!path) {
+               /* g_debug ("%s: path is empty", __func__); */
+               return NULL;
+       }
+
+       if (wordexp (path, &wexp, 0) != 0) {
+               /* g_debug ("%s: expansion failed for %s", __func__, path); */
+               return NULL;
+       }
+
+       /* we just pick the first one */
+       dir = g_strdup (wexp.we_wordv[0]);
+
+       /* strangely, below seems to lead to a crash on MacOS (BSD);
+          so we have to allow for a tiny leak here on that
+          platform... maybe instead of __APPLE__ it should be
+          __BSD__?
+
+          Hmmm., cannot reproduce that crash anymore, so commenting
+          it out for now...
+          */
+/* #ifndef __APPLE__ */
+       wordfree (&wexp);
+/* #endif /\*__APPLE__*\/ */
+       return dir;
+
+# else /*!HAVE_WORDEXP_H*/
+/* E.g. OpenBSD does not have wordexp.h, so we ignore it */
+       return path ? g_strdup (path) : NULL;
+#endif /*HAVE_WORDEXP_H*/
+}
+
+
+/* note, the g_debugs are commented out because this function may be
+ * called before the log handler is installed. */
+char*
+mu_util_dir_expand (const char *path)
+{
+       char *dir;
+       char resolved[PATH_MAX + 1];
+
+       g_return_val_if_fail (path, NULL);
+
+       dir = do_wordexp (path);
+       if (!dir)
+               return NULL; /* error */
+
+       /* don't try realpath if the dir does not exist */
+       if (access (dir, F_OK) != 0)
+               return dir;
+
+       /* now resolve any symlinks, .. etc. */
+       if (realpath (dir, resolved) == NULL) {
+               /* g_debug ("%s: could not get realpath for '%s': %s", */
+               /*       __func__, dir, g_strerror(errno)); */
+               g_free (dir);
+               return NULL;
+       } else
+               g_free (dir);
+
+       return g_strdup (resolved);
+}
+
+GQuark
+mu_util_error_quark (void)
+{
+       static GQuark error_domain = 0;
+
+       if (G_UNLIKELY(error_domain == 0))
+               error_domain = g_quark_from_static_string
+                       ("mu-error-quark");
+
+       return error_domain;
+}
+
+gboolean
+mu_util_check_dir (const gchar* path, gboolean readable, gboolean writeable)
+{
+       int mode;
+       struct stat statbuf;
+
+       if (!path)
+               return FALSE;
+
+       mode = F_OK | (readable ? R_OK : 0) | (writeable ? W_OK : 0);
+
+       if (access (path, mode) != 0) {
+               /* g_debug ("Cannot access %s: %s", path, g_strerror (errno)); */
+               return FALSE;
+       }
+
+       if (stat (path, &statbuf) != 0) {
+               /* g_debug ("Cannot stat %s: %s", path, g_strerror (errno)); */
+               return FALSE;
+       }
+
+       return S_ISDIR(statbuf.st_mode) ? TRUE: FALSE;
+}
+
+
+gchar*
+mu_util_guess_maildir (void)
+{
+       const gchar *mdir1, *home;
+
+       /* first, try MAILDIR */
+       mdir1 = g_getenv ("MAILDIR");
+
+       if (mdir1 && mu_util_check_dir (mdir1, TRUE, FALSE))
+               return g_strdup (mdir1);
+
+       /* then, try <home>/Maildir */
+       home = g_get_home_dir();
+       if (home) {
+               char *mdir2;
+               mdir2 = g_strdup_printf ("%s%cMaildir",
+                       home, G_DIR_SEPARATOR);
+               if (mu_util_check_dir (mdir2, TRUE, FALSE))
+                       return mdir2;
+               g_free (mdir2);
+       }
+
+       /* nope; nothing found */
+       return NULL;
+}
+
+gboolean
+mu_util_create_dir_maybe (const gchar *path, mode_t mode, gboolean nowarn)
+{
+       struct stat statbuf;
+
+       g_return_val_if_fail (path, FALSE);
+
+       /* if it exists, it must be a readable dir */
+       if (stat (path, &statbuf) == 0) {
+               if ((!S_ISDIR(statbuf.st_mode)) ||
+                   (access (path, W_OK|R_OK) != 0)) {
+                       if (!nowarn)
+                               g_warning ("not a read-writable"
+                                          "directory: %s", path);
+                       return FALSE;
+               }
+       }
+
+       if (g_mkdir_with_parents (path, mode) != 0) {
+               if (!nowarn)
+                       g_warning ("failed to create %s: %s",
+                                  path, g_strerror(errno));
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+gboolean
+mu_util_supports (MuFeature feature)
+{
+       /* check for Guile support */
+#ifndef BUILD_GUILE
+       if (feature & MU_FEATURE_GUILE)
+               return FALSE;
+#endif /*BUILD_GUILE*/
+
+       /* check for Gnuplot */
+       if (feature & MU_FEATURE_GNUPLOT)
+               if (!mu_util_program_in_path ("gnuplot"))
+                       return FALSE;
+
+       return TRUE;
+}
+
+
+gboolean
+mu_util_program_in_path (const char *prog)
+{
+       gchar *path;
+
+       g_return_val_if_fail (prog, FALSE);
+
+       path = g_find_program_in_path (prog);
+       g_free (path);
+
+       return (path != NULL) ? TRUE : FALSE;
+}
+
+
+/*
+ * Set the child to a group leader to avoid being killed when the
+ * parent group is killed.
+ */
+static void
+maybe_setsid (G_GNUC_UNUSED gpointer user_data)
+{
+#if HAVE_SETSID
+       setsid();
+#endif /*HAVE_SETSID*/
+}
+
+gboolean
+mu_util_play (const char *path, GError **err)
+{
+       GFile *gf;
+       gboolean rv, is_native;
+       const gchar *argv[3];
+       const char *prog;
+
+       g_return_val_if_fail (path, FALSE);
+
+       gf = g_file_new_for_path(path);
+       is_native = g_file_is_native(gf);
+       g_object_unref(gf);
+
+       if (!is_native) {
+               mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_EXECUTE,
+                                    "'%s' is not a native file", path);
+               return FALSE;
+       }
+
+       prog = g_getenv ("MU_PLAY_PROGRAM");
+       if (!prog) {
+#ifdef __APPLE__
+               prog = "open";
+#else
+               prog = "xdg-open";
+#endif /*!__APPLE__*/
+       }
+
+       if (!mu_util_program_in_path (prog)) {
+               mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_EXECUTE,
+                                    "cannot find '%s' in path", prog);
+               return FALSE;
+       }
+
+       argv[0] = prog;
+       argv[1] = path;
+       argv[2] = NULL;
+
+       err = NULL;
+       rv = g_spawn_async (NULL, (gchar**)&argv, NULL,
+                           G_SPAWN_SEARCH_PATH, maybe_setsid,
+                           NULL, NULL, err);
+       return rv;
+}
+
+
+unsigned char
+mu_util_get_dtype (const char *path, gboolean use_lstat)
+{
+       int res;
+       struct stat statbuf;
+
+       g_return_val_if_fail (path, DT_UNKNOWN);
+
+       if (use_lstat)
+               res = lstat (path, &statbuf);
+       else
+               res = stat (path, &statbuf);
+
+       if (res != 0) {
+               g_warning ("%sstat failed on %s: %s",
+                          use_lstat ? "l" : "", path, g_strerror(errno));
+               return DT_UNKNOWN;
+       }
+
+       /* we only care about dirs, regular files and links */
+       if (S_ISREG (statbuf.st_mode))
+               return DT_REG;
+       else if (S_ISDIR (statbuf.st_mode))
+               return DT_DIR;
+       else if (S_ISLNK (statbuf.st_mode))
+               return DT_LNK;
+
+       return DT_UNKNOWN;
+}
+
+
+
+gboolean
+mu_util_locale_is_utf8 (void)
+{
+       const gchar *dummy;
+       static int is_utf8 = -1;
+
+       if (G_UNLIKELY(is_utf8 == -1))
+               is_utf8 = g_get_charset(&dummy) ? 1 : 0;
+
+       return is_utf8 ? TRUE : FALSE;
+}
+
+gboolean
+mu_util_fputs_encoded (const char *str, FILE *stream)
+{
+       int      rv;
+       char    *conv;
+
+       g_return_val_if_fail (stream, FALSE);
+
+       /* g_get_charset return TRUE when the locale is UTF8 */
+       if (mu_util_locale_is_utf8())
+               return fputs (str, stream) == EOF ? FALSE : TRUE;
+
+        /* charset is _not_ utf8, so we need to convert it */
+       conv = NULL;
+       if (g_utf8_validate (str, -1, NULL))
+               conv = g_locale_from_utf8 (str, -1, NULL, NULL, NULL);
+
+       /* conversion failed; this happens because is some cases GMime may gives
+        * us non-UTF-8 strings from e.g. wrongly encoded message-subjects; if
+        * so, we escape the string */
+       conv = conv ? conv : g_strescape (str, "\n\t");
+       rv   = conv ? fputs (conv, stream) : EOF;
+       g_free (conv);
+
+       return (rv == EOF) ? FALSE : TRUE;
+}
+
+
+
+gboolean
+mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...)
+{
+       va_list ap;
+       char *msg;
+
+       /* don't bother with NULL errors, or errors already set */
+       if (!err || *err)
+               return FALSE;
+
+       msg = NULL;
+       va_start (ap, frm);
+       g_vasprintf (&msg, frm, ap);
+       va_end (ap);
+
+       g_set_error (err, MU_ERROR_DOMAIN, errcode, "%s", msg);
+
+       g_free (msg);
+
+       return FALSE;
+}
+
+
+
+__attribute__((format(printf, 2, 0))) static gboolean
+print_args (FILE *stream, const char *frm, va_list args)
+{
+       gchar *str;
+       gboolean rv;
+
+       str = g_strdup_vprintf (frm, args);
+
+       rv = mu_util_fputs_encoded (str, stream);
+
+       g_free (str);
+
+       return rv;
+}
+
+
+gboolean
+mu_util_print_encoded (const char *frm, ...)
+{
+       va_list args;
+       gboolean rv;
+
+       g_return_val_if_fail (frm, FALSE);
+
+       va_start (args, frm);
+       rv = print_args (stdout, frm, args);
+       va_end (args);
+
+       return rv;
+}
+
+char*
+mu_str_summarize (const char* str, size_t max_lines)
+{
+       char *summary;
+       size_t nl_seen;
+       unsigned i,j;
+       gboolean last_was_blank;
+
+       g_return_val_if_fail (str, NULL);
+       g_return_val_if_fail (max_lines > 0, NULL);
+
+       /* len for summary <= original len */
+       summary = g_new (gchar, strlen(str) + 1);
+
+       /* copy the string up to max_lines lines, replace CR/LF/tab with
+        * single space */
+       for (i = j = 0, nl_seen = 0, last_was_blank = TRUE;
+            nl_seen < max_lines && str[i] != '\0'; ++i) {
+
+               if (str[i] == '\n' || str[i] == '\r' ||
+                   str[i] == '\t' || str[i] == ' ' ) {
+
+                       if (str[i] == '\n')
+                               ++nl_seen;
+
+                       /* no double-blanks or blank at end of str */
+                       if (!last_was_blank && str[i+1] != '\0')
+                               summary[j++] = ' ';
+
+                       last_was_blank = TRUE;
+               } else {
+
+                       summary[j++] = str[i];
+                       last_was_blank = FALSE;
+               }
+       }
+
+       summary[j] = '\0';
+       return summary;
+}
diff --git a/lib/utils/mu-util.h b/lib/utils/mu-util.h
new file mode 100644 (file)
index 0000000..3dedc0a
--- /dev/null
@@ -0,0 +1,349 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 3 of the License, 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_UTIL_H__
+#define __MU_UTIL_H__
+
+#include <glib.h>
+#include <stdio.h>
+#include <dirent.h>
+#include <sys/stat.h> /* for mode_t */
+
+/* hopefully, this should get us a sane PATH_MAX */
+#include <limits.h>
+/* not all systems provide PATH_MAX in limits.h */
+#ifndef PATH_MAX
+#include <sys/param.h>
+#ifndef PATH_MAX
+#define PATH_MAX MAXPATHLEN
+#endif /*!PATH_MAX*/
+#endif /*PATH_MAX*/
+
+G_BEGIN_DECLS
+
+/**
+ * get the expanded path; ie. perform shell expansion on the path. the
+ * path does not have to exist
+ *
+ * @param path path to expand
+ *
+ * @return the expanded path as a newly allocated string, or NULL in
+ * case of error
+ */
+char* mu_util_dir_expand(const char* path) G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * guess the maildir; first try $MAILDIR; if it is unset or
+ * non-existent, try ~/Maildir if both fail, return NULL
+ *
+ * @return full path of the guessed Maildir, or NULL; must be freed (gfree)
+ */
+char* mu_util_guess_maildir (void)
+       G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * if path exists, check that's a read/writeable dir; otherwise try to
+ * create it (with perms 0700)
+ *
+ * @param path path to the dir
+ * @param mode to set for the dir (as per chmod(1))
+ * @param nowarn, if TRUE, don't write warnings (if any) to stderr
+ *
+ * @return TRUE if a read/writeable directory `path' exists after
+ * leaving this function, FALSE otherwise
+ */
+gboolean mu_util_create_dir_maybe (const gchar *path, mode_t mode,
+                                  gboolean nowarn) G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * check whether path is a directory, and optionally, if it's readable
+ * and/or writeable
+ *
+ * @param path dir path
+ * @param readable check for readability
+ * @param writeable check for writability
+ *
+ * @return TRUE if dir exist and has the specified properties
+ */
+gboolean mu_util_check_dir (const gchar* path, gboolean readable,
+                           gboolean writeable)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * is the current locale utf-8 compatible?
+ *
+ * @return TRUE if it's utf8 compatible, FALSE otherwise
+ */
+gboolean mu_util_locale_is_utf8 (void) G_GNUC_CONST;
+
+/**
+ * get a 'summary' of the string, ie. the first /n/ lines of the
+ * strings, with all newlines removed, replaced by single spaces
+ *
+ * @param str the source string
+ * @param max_lines the maximum number of lines to include in the summary
+ *
+ * @return a newly allocated string with the summary. use g_free to free it.
+ */
+char* mu_str_summarize (const char* str, size_t max_lines)
+    G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * write a string (assumed to be in utf8-format) to a stream,
+ * converted to the current locale
+ *
+ * @param str a string
+ * @param stream a stream
+ *
+ * @return TRUE if printing worked, FALSE otherwise
+ */
+gboolean mu_util_fputs_encoded (const char *str, FILE *stream);
+
+/**
+ * print a formatted string (assumed to be in utf8-format) to stdout,
+ * converted to the current locale
+ *
+ * @param a standard printf() format string, followed by a parameter list
+ *
+ * @return TRUE if printing worked, FALSE otherwise
+ */
+gboolean mu_util_print_encoded (const char *frm, ...) G_GNUC_PRINTF(1,2);
+
+
+/**
+ * Try to 'play' (ie., open with it's associated program) a file. On MacOS, the
+ * the program 'open' is used for this; on other platforms 'xdg-open' to do the
+ * actual opening. In addition you can set it to another program by setting the
+ * MU_PLAY_PROGRAM environment variable
+ *
+ * This requires a 'native' file, see g_file_is_native()
+ *
+ * @param path full path of the file to open
+ * @param err receives error information, if any
+ *
+ * @return TRUE if it succeeded, FALSE otherwise
+ */
+gboolean mu_util_play (const char *path, GError **err);
+
+/**
+ * Check whether program prog exists in PATH
+ *
+ * @param prog a program (executable)
+ *
+ * @return TRUE if it exists and is executable, FALSE otherwise
+ */
+gboolean mu_util_program_in_path (const char *prog);
+
+
+enum _MuFeature {
+       MU_FEATURE_GUILE     = 1 << 0,  /* do we support Guile 2.0? */
+       MU_FEATURE_GNUPLOT   = 1 << 1,  /* do we have gnuplot installed? */
+};
+typedef enum _MuFeature MuFeature;
+
+/**
+ * Check whether mu supports some particular feature
+ *
+ * @param feature a feature (multiple features can be logical-or'd together)
+ *
+ * @return TRUE if the feature is supported, FALSE otherwise
+ */
+gboolean mu_util_supports (MuFeature feature);
+
+
+
+/**
+ * Get an error-query for mu, to be used in `g_set_error'. Recent
+ * version of Glib warn when using 0 for the error-domain in
+ * g_set_error.
+ *
+ *
+ * @return an error quark for mu
+ */
+GQuark mu_util_error_quark (void) G_GNUC_CONST;
+#define MU_ERROR_DOMAIN (mu_util_error_quark())
+
+
+/*
+ * for OSs with out support for direntry->d_type, like Solaris
+ */
+#ifndef DT_UNKNOWN
+enum {
+       DT_UNKNOWN = 0,
+#define DT_UNKNOWN     DT_UNKNOWN
+       DT_FIFO    = 1,
+#define DT_FIFO                DT_FIFO
+       DT_CHR     = 2,
+#define DT_CHR         DT_CHR
+       DT_DIR     = 4,
+#define DT_DIR         DT_DIR
+       DT_BLK     = 6,
+#define DT_BLK         DT_BLK
+       DT_REG     = 8,
+#define DT_REG         DT_REG
+       DT_LNK     = 10,
+#define DT_LNK         DT_LNK
+       DT_SOCK    = 12,
+#define DT_SOCK                DT_SOCK
+       DT_WHT     = 14
+#define DT_WHT         DT_WHT
+};
+#endif /*DT_UNKNOWN*/
+
+
+/**
+ * get the d_type (as in direntry->d_type) for the file at path, using either
+ * stat(3) or lstat(3)
+ *
+ * @param path full path
+ * @param use_lstat whether to use lstat (otherwise use stat)
+ *
+ * @return DT_REG, DT_DIR, DT_LNK, or DT_UNKNOWN (other values are not supported
+ * currently )
+ */
+unsigned char mu_util_get_dtype (const char *path, gboolean use_lstat);
+
+
+/**
+ * we need this when using Xapian::Document* from C
+ *
+ */
+typedef gpointer XapianDocument;
+
+/**
+ * we need this when using Xapian::Enquire* from C
+ *
+ */
+typedef gpointer XapianEnquire;
+
+
+/* print a warning for a GError, and free it */
+#define MU_HANDLE_G_ERROR(GE)                                                  \
+       do {                                                                    \
+               if (!(GE))                                                      \
+                       g_warning ("%s:%u: an error occurred in %s",            \
+                                  __FILE__, __LINE__, __func__);               \
+               else {                                                          \
+                       g_warning ("error %u: %s", (GE)->code, (GE)->message);  \
+                       g_error_free ((GE));                                    \
+               }                                                               \
+       } while (0)
+
+
+#define MU_G_ERROR_CODE(GE) ((GE)&&(*(GE))?(MuError)(*(GE))->code:MU_ERROR)
+
+
+enum _MuError {
+       /* no error at all! */
+       MU_OK                                 = 0,
+
+       /* generic error */
+       MU_ERROR                              = 1,
+       MU_ERROR_IN_PARAMETERS                = 2,
+       MU_ERROR_INTERNAL                     = 3,
+       MU_ERROR_NO_MATCHES                   = 4,
+
+       /* not really an error; for callbacks */
+       MU_IGNORE = 5,
+
+       MU_ERROR_SCRIPT_NOT_FOUND             = 8,
+
+       /* general xapian related error */
+       MU_ERROR_XAPIAN                       = 11,
+
+       /* (parsing) error in the query */
+       MU_ERROR_XAPIAN_QUERY                 = 13,
+
+       /* missing data for a document */
+       MU_ERROR_XAPIAN_MISSING_DATA         = 17,
+       /* can't get write lock */
+       MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK = 19,
+       /* could not write */
+       MU_ERROR_XAPIAN_STORE_FAILED         = 21,
+       /* could not remove */
+       MU_ERROR_XAPIAN_REMOVE_FAILED        = 22,
+       /* database was modified; reload */
+       MU_ERROR_XAPIAN_MODIFIED             = 23,
+       /* database was modified; reload */
+       MU_ERROR_XAPIAN_NEEDS_REINDEX        = 24,
+       /* database schema version doesn't match */
+       MU_ERROR_XAPIAN_SCHEMA_MISMATCH      = 25,
+       /* failed to open the database */
+       MU_ERROR_XAPIAN_CANNOT_OPEN          = 26,
+
+       /* GMime related errors */
+
+       /* gmime parsing related error */
+       MU_ERROR_GMIME                        = 30,
+
+       /* contacts related errors */
+       MU_ERROR_CONTACTS                     = 50,
+       MU_ERROR_CONTACTS_CANNOT_RETRIEVE     = 51,
+
+       /* crypto related errors */
+       MU_ERROR_CRYPTO                       = 60,
+
+
+       /* File errors */
+       /* generic file-related error */
+       MU_ERROR_FILE                         = 70,
+       MU_ERROR_FILE_INVALID_NAME            = 71,
+       MU_ERROR_FILE_CANNOT_LINK             = 72,
+       MU_ERROR_FILE_CANNOT_OPEN             = 73,
+       MU_ERROR_FILE_CANNOT_READ             = 74,
+       MU_ERROR_FILE_CANNOT_EXECUTE          = 75,
+       MU_ERROR_FILE_CANNOT_CREATE           = 76,
+       MU_ERROR_FILE_CANNOT_MKDIR            = 77,
+       MU_ERROR_FILE_STAT_FAILED             = 78,
+       MU_ERROR_FILE_READDIR_FAILED          = 79,
+       MU_ERROR_FILE_INVALID_SOURCE          = 80,
+       MU_ERROR_FILE_TARGET_EQUALS_SOURCE    = 81,
+       MU_ERROR_FILE_CANNOT_WRITE            = 82,
+       MU_ERROR_FILE_CANNOT_UNLINK           = 83,
+
+       /* not really an error, used in callbacks */
+       MU_STOP                               = 99
+};
+typedef enum _MuError MuError;
+
+
+/**
+ * set an error if it's not already set, and return FALSE
+ *
+ * @param err errptr, or NULL
+ * @param errcode error code
+ * @param frm printf-style format, followed by parameters
+ *
+ * @return FALSE
+ */
+gboolean mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...)
+       G_GNUC_PRINTF(3,4);
+
+#define MU_COLOR_RED           "\x1b[31m"
+#define MU_COLOR_GREEN         "\x1b[32m"
+#define MU_COLOR_YELLOW                "\x1b[33m"
+#define MU_COLOR_BLUE          "\x1b[34m"
+#define MU_COLOR_MAGENTA       "\x1b[35m"
+#define MU_COLOR_CYAN          "\x1b[36m"
+#define MU_COLOR_DEFAULT       "\x1b[0m"
+
+G_END_DECLS
+
+#endif /*__MU_UTIL_H__*/
diff --git a/lib/utils/mu-utils-format.hh b/lib/utils/mu-utils-format.hh
new file mode 100644 (file)
index 0000000..3ee0bcd
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_UTILS_FORMAT_HH__
+#define MU_UTILS_FORMAT_HH__
+
+#include <string>
+#include <cstdarg>
+
+namespace Mu {
+
+/**
+ * Quote & escape a string for " and \
+ *
+ * @param str a string
+ *
+ * @return quoted string
+ */
+std::string quote(const std::string& str);
+
+/**
+ * Format a string, printf style
+ *
+ * @param frm format string
+ * @param ... parameters
+ *
+ * @return a formatted string
+ */
+std::string format(const char* frm, ...) __attribute__((format(printf, 1, 2)));
+
+/**
+ * Format a string, printf style
+ *
+ * @param frm format string
+ * @param ... parameters
+ *
+ * @return a formatted string
+ */
+std::string vformat(const char* frm, va_list args) __attribute__((format(printf, 1, 0)));
+
+
+} // namepace Mu
+
+
+#endif /* MU_UTILS_FORMAT_HH__ */
diff --git a/lib/utils/mu-utils.cc b/lib/utils/mu-utils.cc
new file mode 100644 (file)
index 0000000..6a88788
--- /dev/null
@@ -0,0 +1,638 @@
+/*
+**  Copyright (C) 2017-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#ifndef _XOPEN_SOURCE
+#define _XOPEN_SOURCE
+#include <stdexcept>
+#endif /*_XOPEN_SOURCE*/
+
+#include <array>
+
+#include <time.h>
+
+#define GNU_SOURCE
+#include <stdio.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+#include <numeric>
+#include <functional>
+#include <cinttypes>
+#include <charconv>
+#include <limits>
+#include <regex>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "mu-utils.hh"
+#include "mu-utils-format.hh"
+#include "mu-util.h"
+#include "mu-error.hh"
+#include "mu-option.hh"
+
+using namespace Mu;
+
+namespace {
+
+static gunichar
+unichar_tolower(gunichar uc)
+{
+       if (!g_unichar_isalpha(uc))
+               return uc;
+
+       if (g_unichar_get_script(uc) != G_UNICODE_SCRIPT_LATIN)
+               return g_unichar_tolower(uc);
+
+       switch (uc) {
+       case 0x00e6:
+       case 0x00c6: return 'e'; /* æ */
+       case 0x00f8: return 'o'; /* ø */
+       case 0x0110:
+       case 0x0111:
+               return 'd'; /* đ */
+                           /* todo: many more */
+       default: return g_unichar_tolower(uc);
+       }
+}
+
+/**
+ * gx_utf8_flatten:
+ * @str: a UTF-8 string
+ * @len: the length of @str, or -1 if it is %NULL-terminated
+ *
+ * Flatten some UTF-8 string; that is, downcase it and remove any diacritics.
+ *
+ * Returns: (transfer full): a flattened string, free with g_free().
+ */
+static char*
+gx_utf8_flatten(const gchar* str, gssize len)
+{
+       GString* gstr;
+       char *   norm, *cur;
+
+       g_return_val_if_fail(str, NULL);
+
+       norm = g_utf8_normalize(str, len, G_NORMALIZE_ALL);
+       if (!norm)
+               return NULL;
+
+       gstr = g_string_sized_new(strlen(norm));
+
+       for (cur = norm; cur && *cur; cur = g_utf8_next_char(cur)) {
+               gunichar uc;
+
+               uc = g_utf8_get_char(cur);
+               if (g_unichar_combining_class(uc) != 0)
+                       continue;
+
+               g_string_append_unichar(gstr, unichar_tolower(uc));
+       }
+
+       g_free(norm);
+
+       return g_string_free(gstr, FALSE);
+}
+
+} // namespace
+
+std::string // gx_utf8_flatten
+Mu::utf8_flatten(const char* str)
+{
+       if (!str)
+               return {};
+
+       // the pure-ascii case
+       if (g_str_is_ascii(str)) {
+               auto    l = g_ascii_strdown(str, -1);
+               std::string s{l};
+               g_free(l);
+               return s;
+       }
+
+       // seems we need the big guns
+       char* flat = gx_utf8_flatten(str, -1);
+       if (!flat)
+               return {};
+
+       std::string s{flat};
+       g_free(flat);
+
+       return s;
+}
+
+
+/* turn \0-terminated buf into ascii (which is a utf8 subset); convert
+ *   any non-ascii into '.'
+ */
+static char*
+asciify_in_place (char *buf)
+{
+       char *c;
+
+       g_return_val_if_fail (buf, NULL);
+
+       for (c = buf; c && *c; ++c) {
+               if ((!isprint(*c) && !isspace (*c)) || !isascii(*c))
+                       *c = '.';
+       }
+
+       return buf;
+}
+
+static char*
+utf8ify (const char *buf)
+{
+       char *utf8;
+
+       g_return_val_if_fail (buf, NULL);
+
+       utf8 = g_strdup (buf);
+
+       if (!g_utf8_validate (buf, -1, NULL))
+               asciify_in_place (utf8);
+
+       return utf8;
+}
+
+
+std::string
+Mu::utf8_clean(const std::string& dirty)
+{
+       g_autoptr(GString) gstr = g_string_sized_new(dirty.length());
+       g_autofree char *cstr   = utf8ify(dirty.c_str());
+
+       for (auto cur = cstr; cur && *cur; cur = g_utf8_next_char(cur)) {
+               const gunichar uc = g_utf8_get_char(cur);
+               if (g_unichar_iscntrl(uc))
+                       g_string_append_c(gstr, ' ');
+               else
+                       g_string_append_unichar(gstr, uc);
+       }
+
+       return std::string{g_strstrip(gstr->str)};
+}
+
+std::string
+Mu::remove_ctrl(const std::string& str)
+{
+       char        prev{'\0'};
+       std::string result;
+       result.reserve(str.length());
+
+       for (auto&& c : str) {
+               if (::iscntrl(c) || c == ' ') {
+                       if (prev != ' ')
+                               result += prev = ' ';
+               } else
+                       result += prev = c;
+       }
+
+       return result;
+}
+
+std::vector<std::string>
+Mu::split(const std::string& str, const std::string& sepa)
+{
+       std::vector<std::string> vec;
+       size_t b = 0, e = 0;
+
+       /* special cases */
+       if (str.empty())
+               return vec;
+       else if (sepa.empty()) {
+               for (auto&& c: str)
+                       vec.emplace_back(1, c);
+               return vec;
+       }
+
+       while (true) {
+               if (e = str.find(sepa, b); e != std::string::npos) {
+                       vec.emplace_back(str.substr(b, e - b));
+                       b = e + sepa.length();
+               } else {
+                       vec.emplace_back(str.substr(b));
+                       break;
+               }
+       }
+
+       return vec;
+}
+
+std::vector<std::string>
+Mu::split(const std::string& str, char sepa)
+{
+       std::vector<std::string> vec;
+       size_t b = 0, e = 0;
+
+       /* special case */
+       if (str.empty())
+               return vec;
+
+       while (true) {
+               if (e = str.find(sepa, b); e != std::string::npos) {
+                       vec.emplace_back(str.substr(b, e - b));
+                       b = e + sizeof(sepa);
+               } else {
+                       vec.emplace_back(str.substr(b));
+                       break;
+               }
+       }
+
+       return vec;
+}
+
+std::vector<std::string>
+Mu::split(const std::string& str, const std::regex& sepa_rx)
+{
+       std::sregex_token_iterator it(str.begin(), str.end(), sepa_rx, -1);
+       std::sregex_token_iterator end;
+
+       return {it, end};
+}
+
+std::string
+Mu::join(const std::vector<std::string>& svec, const std::string& sepa)
+{
+       if (svec.empty())
+               return {};
+
+
+       /* calculate the overall size beforehand, to avoid re-allocations. */
+       size_t value_len =
+               std::accumulate(svec.cbegin(), svec.cend(), 0,
+                               [](size_t size, const std::string& s) {
+                                       return size + s.size();
+                               }) + (svec.size() - 1) * sepa.length();
+
+       std::string value;
+       value.reserve(value_len);
+
+       std::accumulate(svec.cbegin(), svec.cend(), std::ref(value),
+                       [&](std::string& s1, const std::string& s2)->std::string& {
+                               if (s1.empty())
+                                       s1 = s2;
+                               else {
+                                       s1.append(sepa);
+                                       s1.append(s2);
+                               }
+                               return s1;
+                       });
+
+       return value;
+}
+
+std::string
+Mu::quote(const std::string& str)
+{
+       std::string res{"\""};
+
+       for (auto&& k : str) {
+               switch (k) {
+               case '"': res += "\\\""; break;
+               case '\\': res += "\\\\"; break;
+               default: res += k;
+               }
+       }
+
+       return res + "\"";
+}
+
+std::string
+Mu::format(const char* frm, ...)
+{
+       va_list args;
+
+       va_start(args, frm);
+       auto str = vformat(frm, args);
+       va_end(args);
+
+       return str;
+}
+
+std::string
+Mu::vformat(const char* frm, va_list args)
+{
+       char*      s{};
+       const auto res = g_vasprintf(&s, frm, args);
+       if (res == -1) {
+               std::cerr << "string format failed" << std::endl;
+               return {};
+       }
+
+       std::string str{s};
+       g_free(s);
+
+       return str;
+}
+
+std::string
+Mu::time_to_string(const char *frm, time_t t, bool utc)
+{
+       g_return_val_if_fail(frm, "");
+
+       GDateTime* dt = std::invoke([&] {
+               if (utc)
+                       return g_date_time_new_from_unix_utc(t);
+               else
+                       return g_date_time_new_from_unix_local(t);
+       });
+
+       if (!dt) {
+               g_warning("time_t out of range: <%" G_GUINT64_FORMAT ">",
+                         static_cast<guint64>(t));
+               return {};
+       }
+
+       frm = frm ? frm : "%c";
+       auto datestr{to_string_opt_gchar(g_date_time_format(dt, frm))};
+       g_date_time_unref(dt);
+       if (!datestr)
+               g_warning("failed to format time with format '%s'", frm);
+
+       return datestr.value_or("");
+}
+
+static Option<int64_t>
+delta_ymwdhMs(const std::string& expr)
+{
+       char* endptr;
+       auto  num = strtol(expr.c_str(), &endptr, 10);
+       if (num <= 0 || num > 9999 || !endptr || !*endptr)
+               return Nothing;
+
+       int years, months, weeks, days, hours, minutes, seconds;
+       years = months = weeks = days = hours = minutes = seconds = 0;
+
+       switch (endptr[0]) {
+       case 's': seconds = num; break;
+       case 'M': minutes = num; break;
+       case 'h': hours = num; break;
+       case 'd': days = num; break;
+       case 'w': weeks = num; break;
+       case 'm': months = num; break;
+       case 'y': years = num; break;
+       default:
+               return Nothing;
+       }
+
+       GDateTime *then, *now = g_date_time_new_now_local();
+       if (weeks != 0)
+               then = g_date_time_add_weeks(now, -weeks);
+       else
+               then =
+                   g_date_time_add_full(now, -years, -months, -days, -hours, -minutes, -seconds);
+
+       auto  t = std::max<int64_t>(0, g_date_time_to_unix(then));
+
+       g_date_time_unref(then);
+       g_date_time_unref(now);
+
+       return t;
+}
+
+static Option<int64_t>
+special_date_time(const std::string& d, bool is_first)
+{
+       if (d == "now")
+               return ::time({});
+
+       if (d == "today") {
+               GDateTime *dt, *midnight;
+               dt = g_date_time_new_now_local();
+
+               if (!is_first) {
+                       GDateTime* tmp = dt;
+                       dt             = g_date_time_add_days(dt, 1);
+                       g_date_time_unref(tmp);
+               }
+
+               midnight = g_date_time_add_full(dt,
+                                               0,
+                                               0,
+                                               0,
+                                               -g_date_time_get_hour(dt),
+                                               -g_date_time_get_minute(dt),
+                                               -g_date_time_get_second(dt));
+               time_t t = MAX(0, (gint64)g_date_time_to_unix(midnight));
+               g_date_time_unref(dt);
+               g_date_time_unref(midnight);
+
+               return t;
+       }
+
+       return Nothing;
+}
+
+// if a date has a month day greater than the number of days in that month,
+// change it to a valid date point to the last second in that month
+static void
+fixup_month(struct tm* tbuf)
+{
+       decltype(tbuf->tm_mday) max_days;
+       const auto              month = tbuf->tm_mon + 1;
+       const auto              year  = tbuf->tm_year + 1900;
+
+       switch (month) {
+       case 2:
+               if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))
+                       max_days = 29;
+               else
+                       max_days = 28;
+               break;
+       case 4:
+       case 6:
+       case 9:
+       case 11:
+               max_days = 30;
+               break;
+       default:
+               max_days = 31;
+               break;
+       }
+
+       if (tbuf->tm_mday > max_days) {
+               tbuf->tm_mday = max_days;
+               tbuf->tm_hour = 23;
+               tbuf->tm_min  = 59;
+               tbuf->tm_sec  = 59;
+       }
+ }
+
+
+Option<int64_t>
+Mu::parse_date_time(const std::string& dstr, bool is_first)
+{
+       struct tm tbuf{};
+       GDateTime *dtime{};
+       int64_t t;
+
+       /* one-sided dates */
+       if (dstr.empty())
+               return is_first ? 0 : G_MAXINT64;
+       else if (dstr == "today" || dstr == "now")
+               return special_date_time(dstr, is_first);
+       else if (dstr.find_first_of("ymdwhMs") != std::string::npos)
+               return delta_ymwdhMs(dstr);
+
+       constexpr char UserDateMin[] = "19700101000000";
+       constexpr char UserDateMax[] = "29991231235959";
+
+       std::string date(is_first ? UserDateMin : UserDateMax);
+       std::copy_if(dstr.begin(), dstr.end(), date.begin(), [](auto c) { return isdigit(c); });
+
+       if (!::strptime(date.c_str(), "%Y%m%d%H%M%S", &tbuf) &&
+           !::strptime(date.c_str(), "%Y%m%d%H%M", &tbuf) &&
+           !::strptime(date.c_str(), "%Y%m%d%H", &tbuf) &&
+           !::strptime(date.c_str(), "%Y%m%d", &tbuf) &&
+           !::strptime(date.c_str(), "%Y%m", &tbuf) &&
+           !::strptime(date.c_str(), "%Y", &tbuf))
+               return Nothing;
+
+       fixup_month(&tbuf);
+       dtime = g_date_time_new_local(tbuf.tm_year + 1900,
+                                     tbuf.tm_mon + 1,
+                                     tbuf.tm_mday,
+                                     tbuf.tm_hour,
+                                     tbuf.tm_min,
+                                     tbuf.tm_sec);
+       t = g_date_time_to_unix(dtime);
+       g_date_time_unref(dtime);
+
+       return std::max<int64_t>(t, 0);
+}
+
+
+Option<int64_t>
+Mu::parse_size(const std::string& val, bool is_first)
+{
+       int64_t         size{-1};
+       std::string     str;
+       GRegex*         rx;
+       GMatchInfo*     minfo;
+
+       /* one-sided ranges */
+       if (val.empty())
+               return is_first ? 0 : std::numeric_limits<int64_t>::max();
+
+       rx = g_regex_new("^(\\d+)(b|k|kb|m|mb|g|gb)?$",
+                        G_REGEX_CASELESS, (GRegexMatchFlags)0, NULL);
+       minfo = NULL;
+       if (g_regex_match(rx, val.c_str(), (GRegexMatchFlags)0, &minfo)) {
+
+               char*  s;
+               s    = g_match_info_fetch(minfo, 1);
+               size = atoll(s);
+               g_free(s);
+
+               s = g_match_info_fetch(minfo, 2);
+               switch (s ? g_ascii_tolower(s[0]) : 0) {
+               case 'k': size *= 1024; break;
+               case 'm': size *= (1024 * 1024); break;
+               case 'g': size *= (1024 * 1024 * 1024); break;
+               default: break;
+               }
+
+               g_free(s);
+       }
+
+       g_regex_unref(rx);
+       g_match_info_unref(minfo);
+
+       if (size < 0)
+               return Nothing;
+       else
+               return size;
+
+}
+
+std::string
+Mu::to_lexnum(int64_t val)
+{
+       char buf[18]; /* 1 byte prefix + hex + \0 */
+       buf[0] = 'f' + ::snprintf(buf + 1, sizeof(buf) - 1, "%" PRIx64, val);
+       return buf;
+}
+
+int64_t
+Mu::from_lexnum(const std::string& str)
+{
+       int64_t val{};
+       std::from_chars(str.c_str() + 1, str.c_str() + str.size(), val, 16);
+
+       return val;
+}
+
+
+std::string
+Mu::canonicalize_filename(const std::string& path, const std::string& relative_to)
+{
+       auto str{to_string_opt_gchar(
+               g_canonicalize_filename(
+                       path.c_str(),
+                       relative_to.empty() ? nullptr : relative_to.c_str())).value()};
+
+       // remove trailing '/'... is this needed?
+       if (str[str.length()-1] == G_DIR_SEPARATOR)
+               str.erase(str.length() - 1);
+
+       return str;
+}
+
+
+bool
+Mu::locale_workaround() try
+{
+       // quite horrible... but some systems break otherwise with
+       //  https://github.com/djcb/mu/issues/2252
+
+       try {
+               std::locale::global(std::locale(""));
+       } catch (const std::runtime_error& re) {
+               g_setenv("LC_ALL", "C", 1);
+               std::locale::global(std::locale(""));
+       }
+
+       return true;
+
+} catch (...) {
+       return false;
+}
+
+bool
+Mu::timezone_available(const std::string& tz)
+{
+       const auto old_tz = g_getenv("TZ");
+
+       g_setenv("TZ", tz.c_str(), TRUE);
+
+       auto tzone = g_time_zone_new_local ();
+       bool have_tz = g_strcmp0(g_time_zone_get_identifier(tzone), tz.c_str()) == 0;
+       g_time_zone_unref (tzone);
+
+       if (old_tz)
+               g_setenv("TZ", old_tz, TRUE);
+       else
+               g_unsetenv("TZ");
+
+       return have_tz;
+}
diff --git a/lib/utils/mu-utils.hh b/lib/utils/mu-utils.hh
new file mode 100644 (file)
index 0000000..4a9f70d
--- /dev/null
@@ -0,0 +1,459 @@
+/*
+**  Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#ifndef __MU_UTILS_HH__
+#define __MU_UTILS_HH__
+
+#include <string>
+#include <string_view>
+#include <sstream>
+#include <vector>
+#include <chrono>
+#include <memory>
+#include <cstdarg>
+#include <glib.h>
+#include <ostream>
+#include <iostream>
+#include <type_traits>
+#include <algorithm>
+#include <numeric>
+#include <regex>
+
+#include "mu-utils-format.hh"
+#include "mu-option.hh"
+
+namespace Mu {
+
+using StringVec = std::vector<std::string>;
+
+/**
+ * Flatten a string -- downcase and fold diacritics etc.
+ *
+ * @param str a string
+ *
+ * @return a flattened string
+ */
+std::string utf8_flatten(const char* str);
+inline std::string
+utf8_flatten(const std::string& s)
+{
+       return utf8_flatten(s.c_str());
+}
+
+/**
+ * Replace all control characters with spaces, and remove leading and trailing space.
+ *
+ * @param dirty an unclean string
+ *
+ * @return a cleaned-up string.
+ */
+std::string utf8_clean(const std::string& dirty);
+
+/**
+ * Remove ctrl characters, replacing them with ' '; subsequent
+ * ctrl characters are replaced by a single ' '
+ *
+ * @param str a string
+ *
+ * @return the string without control characters
+ */
+std::string remove_ctrl(const std::string& str);
+
+/**
+ * Split a string in parts. As a special case, splitting an empty string
+ * yields an empty vector (not a vector with a single empty element)
+ *
+ * @param str a string
+ * @param sepa the separator
+ *
+ * @return the parts.
+ */
+std::vector<std::string> split(const std::string& str, const std::string& sepa);
+
+/**
+ * Split a string in parts. As a special case, splitting an empty string
+ * yields an empty vector (not a vector with a single empty element)
+ *
+ * @param str a string
+ * @param sepa the separator
+ *
+ * @return the parts.
+ */
+std::vector<std::string> split(const std::string& str, char sepa);
+
+/**
+ * Split a string in parts
+ *
+ * @param str a string
+ * @param sepa the separator regex
+ *
+ * @return the parts.
+ */
+std::vector<std::string> split(const std::string& str, const std::regex& sepa_rx);
+
+/**
+ * Join the strings in svec into a string, separated by sepa
+ *
+ * @param svec a string vector
+ * @param sepa separator
+ *
+ * @return string
+ */
+std::string join(const std::vector<std::string>& svec, const std::string& sepa);
+static inline std::string join(const std::vector<std::string>& svec, char sepa) {
+       return join(svec, std::string(1, sepa));
+}
+
+/**
+ * Parse a date string to the corresponding time_t
+ * *
+ * @param date the date expressed a YYYYMMDDHHMMSS or any n... of the first
+ * characters, using the local timezone.
+ * @param first whether to fill out incomplete dates to the start or the end;
+ * ie. either 1972 -> 197201010000 or 1972 -> 197212312359
+ *
+ * @return the corresponding time_t or Nothing if parsing failed.
+ */
+Option<int64_t> parse_date_time(const std::string& date, bool first);
+
+/**
+ * 64-bit incarnation of time_t expressed as a 10-digit string. Uses 64-bit for the time-value,
+ *  regardless of the size of time_t.
+ *
+ * @param t some time value
+ *
+ * @return
+ */
+std::string date_to_time_t_string(int64_t t);
+
+/**
+ * Get a string for a given time_t and format
+ * memory that must be freed after use.
+ *
+ * @param frm the format of the string (in strftime(3) format)
+ * @param t the time as time_t
+ * @param utc whether to display as UTC(if true) or local time
+ *
+ * @return a string representation of the time in UTF8-format, or empty in case
+ * of error.
+ */
+std::string time_to_string(const char *frm, time_t t, bool utc = false) G_GNUC_CONST;
+
+
+/**
+ * Hack to avoid locale crashes
+ *
+ * @return true if setting locale worked; false otherwise
+ */
+bool locale_workaround();
+
+
+/**
+ * Is the given timezone available? For tests
+ *
+ * @param tz a timezone, such as Europe/Helsinki
+ *
+ * @return true or false
+ */
+bool timezone_available(const std::string& tz);
+
+
+
+// https://stackoverflow.com/questions/19053351/how-do-i-use-a-custom-deleter-with-a-stdunique-ptr-member
+template <auto fn>
+struct deleter_from_fn {
+       template <typename T>
+       constexpr void operator()(T* arg) const {
+               fn(arg);
+       }
+};
+template <typename T, auto fn>
+using deletable_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;
+
+
+
+using Clock    = std::chrono::steady_clock;
+using Duration = Clock::duration;
+
+template <typename Unit>
+constexpr int64_t
+to_unit(Duration d)
+{
+       using namespace std::chrono;
+       return duration_cast<Unit>(d).count();
+}
+
+constexpr int64_t
+to_s(Duration d)
+{
+       return to_unit<std::chrono::seconds>(d);
+}
+constexpr int64_t
+to_ms(Duration d)
+{
+       return to_unit<std::chrono::milliseconds>(d);
+}
+constexpr int64_t
+to_us(Duration d)
+{
+       return to_unit<std::chrono::microseconds>(d);
+}
+
+struct StopWatch {
+       using Clock = std::chrono::steady_clock;
+       StopWatch(const std::string name) : start_{Clock::now()}, name_{name} {}
+       ~StopWatch()
+       {
+               const auto us{static_cast<double>(to_us(Clock::now() - start_))};
+               if (us > 2000000)
+                       g_debug("%s: finished after %0.1f s", name_.c_str(), us / 1000000);
+               else if (us > 2000)
+                       g_debug("%s: finished after %0.1f ms", name_.c_str(), us / 1000);
+               else
+                       g_debug("%s: finished after %g us", name_.c_str(), us);
+       }
+
+private:
+       Clock::time_point start_;
+       std::string       name_;
+};
+
+/**
+ * See g_canonicalize_filename
+ *
+ * @param filename
+ * @param relative_to
+ *
+ * @return
+ */
+std::string canonicalize_filename(const std::string& path, const std::string& relative_to);
+
+/**
+ * Convert a size string to a size in bytes
+ *
+ * @param sizestr the size string
+ * @param first
+ *
+ * @return the size or Nothing if parsing failed
+ */
+Option<int64_t> parse_size(const std::string& sizestr, bool first);
+
+/**
+ * Convert a size into a size in bytes string
+ *
+ * @param size the size
+ * @param first
+ *
+ * @return the size expressed as a string with the decimal number of bytes
+ */
+std::string size_to_string(int64_t size);
+
+/**
+ * Convert any ostreamable<< value to a string
+ *
+ * @param t the value
+ *
+ * @return a std::string
+ */
+template <typename T>
+static inline std::string
+to_string(const T& val)
+{
+       std::stringstream sstr;
+       sstr << val;
+
+       return sstr.str();
+}
+
+/**
+ * Consume a gchar and return a std::string
+ *
+ * @param str a gchar* (consumed/freed)
+ *
+ * @return a std::string, empty if gchar was {}
+ */
+static inline std::string
+to_string_gchar(gchar*&& str)
+{
+       std::string s(str?str:"");
+       g_free(str);
+       return s;
+}
+
+
+/*
+ * Lexicals Number are lexicographically sortable string representations
+ * of numbers. Start with 'g' + length of number in hex, followed by
+ * the ascii for the hex represntation. So,
+ *
+ * 0  -> 'g0'
+ * 1  -> 'g1'
+ * 10 -> 'ga'
+ * 16 -> 'h10'
+ *
+ * etc.
+ */
+std::string to_lexnum(int64_t val);
+int64_t from_lexnum(const std::string& str);
+
+/**
+ * Like std::find_if, but using sequence instead of a range.
+ *
+ * @param seq some std::find_if compatible sequence
+ * @param pred a predicate
+ *
+ * @return an iterator
+ */
+template<typename Sequence, typename UnaryPredicate>
+typename Sequence::const_iterator seq_find_if(const Sequence& seq, UnaryPredicate pred) {
+       return std::find_if(seq.cbegin(), seq.cend(), pred);
+}
+
+/**
+ * Is at least pred(element) true for at least one element of sequence
+ *
+ * @param seq sequence
+ * @param pred a predicate
+ *
+ * @return true or false
+ */
+template<typename Sequence, typename UnaryPredicate>
+bool seq_some(const Sequence& seq, UnaryPredicate pred) {
+       return seq_find_if(seq, pred) != seq.cend();
+}
+
+/**
+ * Create a sequence that has all element of seq for which pred is true
+ *
+ * @param seq sequence
+ * @param pred false
+ *
+ * @return sequence
+ */
+template<typename Sequence, typename UnaryPredicate>
+Sequence seq_filter(const Sequence& seq, UnaryPredicate pred) {
+       Sequence res;
+       std::copy_if(seq.begin(), seq.end(), std::back_inserter(res), pred);
+       return res;
+}
+
+/**
+ * Create a sequence that has all element of seq for which pred is false
+ *
+ * @param seq sequence
+ * @param pred false
+ *
+ * @return sequence
+ */
+template<typename Sequence, typename UnaryPredicate>
+Sequence seq_remove(const Sequence& seq, UnaryPredicate pred) {
+       Sequence res;
+       std::remove_copy_if(seq.begin(), seq.end(), std::back_inserter(res), pred);
+       return res;
+}
+
+template<typename Sequence, typename Compare>
+void seq_sort(Sequence& seq, Compare cmp) { std::sort(seq.begin(), seq.end(), cmp); }
+
+
+/**
+ * Like std::accumulate, but using a sequence instead of a range.
+ *
+ * @param seq some std::accumulate compatible sequence
+ * @param init the initial value
+ * @param op binary operation to calculate the next element
+ *
+ * @return the result value.
+ */
+template<typename Sequence, typename ResultType,  typename BinaryOp>
+ResultType seq_fold(const Sequence& seq, ResultType init, BinaryOp op) {
+       return std::accumulate(seq.cbegin(), seq.cend(), init, op);
+}
+
+template<typename Sequence, typename UnaryOp>
+void seq_for_each(const Sequence& seq, UnaryOp op) {
+       std::for_each(seq.cbegin(), seq.cend(), op);
+}
+
+
+/**
+ * Convert string view in something printable with %*s
+ */
+#define STR_V(sv__) static_cast<int>((sv__).size()), (sv__).data()
+
+struct MaybeAnsi {
+       explicit MaybeAnsi(bool use_color) : color_{use_color} {}
+
+       enum struct Color {
+               Black   = 30,
+               Red     = 31,
+               Green   = 32,
+               Yellow  = 33,
+               Blue    = 34,
+               Magenta = 35,
+               Cyan    = 36,
+               White   = 37,
+
+               BrightBlack   = 90,
+               BrightRed     = 91,
+               BrightGreen   = 92,
+               BrightYellow  = 93,
+               BrightBlue    = 94,
+               BrightMagenta = 95,
+               BrightCyan    = 96,
+               BrightWhite   = 97,
+       };
+
+       std::string fg(Color c) const { return ansi(c, true); }
+       std::string bg(Color c) const { return ansi(c, false); }
+
+       std::string reset() const { return color_ ? "\x1b[0m" : ""; }
+
+private:
+       std::string ansi(Color c, bool fg = true) const
+       {
+               return color_ ? format("\x1b[%dm", static_cast<int>(c) + (fg ? 0 : 10)) : "";
+       }
+
+       const bool color_;
+};
+
+/// Allow using enum structs as bitflags
+#define MU_TO_NUM(ET, ELM)  std::underlying_type_t<ET>(ELM)
+#define MU_TO_ENUM(ET, NUM) static_cast<ET>(NUM)
+#define MU_ENABLE_BITOPS(ET)                                                                    \
+       constexpr ET operator&(ET e1, ET e2)                                                    \
+       {                                                                                       \
+               return MU_TO_ENUM(ET, MU_TO_NUM(ET, e1) & MU_TO_NUM(ET, e2));                   \
+       }                                                                                       \
+       constexpr ET operator|(ET e1, ET e2)                                                    \
+       {                                                                                       \
+               return MU_TO_ENUM(ET, MU_TO_NUM(ET, e1) | MU_TO_NUM(ET, e2));                   \
+       }                                                                                       \
+       constexpr ET      operator~(ET e) { return MU_TO_ENUM(ET, ~(MU_TO_NUM(ET, e))); }       \
+       constexpr bool    any_of(ET e) { return MU_TO_NUM(ET, e) != 0; }                        \
+       constexpr bool    none_of(ET e) { return MU_TO_NUM(ET, e) == 0; }                       \
+       constexpr bool    one_of(ET e1, ET e2) { return (e1 & e2) == e2; }                      \
+       constexpr ET& operator&=(ET& e1, ET e2) { return e1 = e1 & e2; }                        \
+       constexpr ET& operator|=(ET& e1, ET e2) { return e1 = e1 | e2; }                        \
+       static_assert(1==1) // require a semicolon
+
+} // namespace Mu
+
+#endif /* __MU_UTILS_HH__ */
diff --git a/lib/utils/mu-xapian-utils.hh b/lib/utils/mu-xapian-utils.hh
new file mode 100644 (file)
index 0000000..12f3c2c
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_XAPIAN_UTILS_HH__
+#define MU_XAPIAN_UTILS_HH__
+
+#include <xapian.h>
+#include <glib.h>
+#include "mu-result.hh"
+
+namespace Mu {
+
+// LCOV_EXCL_START
+
+// avoid exception-handling boilerplate.
+template <typename Func>
+void
+xapian_try(Func&& func) noexcept
+try {
+       func();
+} catch (const Xapian::Error& xerr) {
+       g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str());
+} catch (const std::runtime_error& re) {
+       g_critical("%s: error: %s", __func__, re.what());
+} catch (const std::exception& e) {
+       g_critical("%s: caught exception: %s", __func__, e.what());
+} catch (...) {
+       g_critical("%s: caught exception", __func__);
+}
+
+template <typename Func, typename Default = std::invoke_result<Func>>
+auto
+xapian_try(Func&& func, Default&& def) noexcept -> std::decay_t<decltype(func())>
+try {
+       return func();
+} catch (const Xapian::Error& xerr) {
+       g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str());
+       return static_cast<Default>(def);
+} catch (const std::runtime_error& re) {
+       g_critical("%s: error: %s", __func__, re.what());
+       return static_cast<Default>(def);
+} catch (const std::exception& e) {
+       g_critical("%s: caught exception: %s", __func__, e.what());
+       return static_cast<Default>(def);
+} catch (...) {
+       g_critical("%s: caught exception", __func__);
+       return static_cast<Default>(def);
+}
+
+
+template <typename Func>
+auto
+xapian_try_result(Func&& func) noexcept -> std::decay_t<decltype(func())>
+try {
+       return func();
+} catch (const Xapian::Error& xerr) {
+       return Err(Error::Code::Xapian, "%s", xerr.get_error_string());
+} catch (const std::runtime_error& re) {
+       return Err(Error::Code::Internal, "runtime error: %s", re.what());
+} catch (const std::exception& e) {
+       return Err(Error::Code::Internal, "caught exception: %s", e.what());
+} catch (...) {
+       return Err(Error::Code::Internal, "caught exception");
+}
+
+// LCOV_EXCL_STOP
+
+} // namespace Mu
+
+#endif /* MU_ XAPIAN_UTILS_HH__ */
diff --git a/lib/utils/tests/meson.build b/lib/utils/tests/meson.build
new file mode 100644 (file)
index 0000000..85dc3c5
--- /dev/null
@@ -0,0 +1,45 @@
+## Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+
+################################################################################
+# tests
+#
+test('test-command-parser',
+     executable('test-command-parser',
+               'test-command-parser.cc',
+               install: false,
+               dependencies: [glib_dep, lib_mu_utils_dep]))
+test('test-mu-util',
+     executable('test-mu-util',
+               'test-mu-util.c',
+               install: false,
+               dependencies: [glib_dep,config_h_dep, lib_mu_utils_dep]))
+test('test-option',
+     executable('test-option',
+               'test-option.cc',
+               install: false,
+               dependencies: [glib_dep, lib_mu_utils_dep]))
+test('test-mu-utils',
+     executable('test-mu-utils',
+               'test-utils.cc',
+               install: false,
+               dependencies: [glib_dep, lib_mu_utils_dep]))
+test('test-sexp',
+     executable('test-sexp',
+               'test-sexp.cc',
+               install: false,
+               dependencies: [glib_dep, lib_mu_utils_dep] ))
diff --git a/lib/utils/tests/test-command-parser.cc b/lib/utils/tests/test-command-parser.cc
new file mode 100644 (file)
index 0000000..4156b03
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+
+#include "mu-command-parser.hh"
+#include "mu-utils.hh"
+#include "mu-test-utils.hh"
+
+using namespace Mu;
+
+static void
+test_param_getters()
+{
+       const auto sexp{Sexp::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")};
+
+       if (g_test_verbose())
+               std::cout << sexp << "\n";
+
+       g_assert_cmpint(Command::get_int_or(sexp.list(), ":bar"), ==, 123);
+       assert_equal(Command::get_string_or(sexp.list(), ":bra", "bla"), "bla");
+       assert_equal(Command::get_string_or(sexp.list(), ":cuux"), "456");
+
+       g_assert_true(Command::get_bool_or(sexp.list(), ":boo") == false);
+       g_assert_true(Command::get_bool_or(sexp.list(), ":bah") == true);
+}
+
+static bool
+call(const Command::CommandMap& cmap, const std::string& str)
+try {
+       const auto sexp{Sexp::make_parse(str)};
+       invoke(cmap, sexp);
+
+       return true;
+
+} catch (const Error& err) {
+       g_warning("%s", err.what());
+       return false;
+}
+
+static void
+test_command()
+{
+       using namespace Command;
+       allow_warnings();
+
+       CommandMap cmap;
+
+       cmap.emplace(
+           "my-command",
+           CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
+                              {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
+                       "My command,",
+                       {}});
+
+       g_assert_true(call(cmap, "(my-command :param1 \"hello\")"));
+       g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
+
+       g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
+}
+
+static void
+test_command2()
+{
+       using namespace Command;
+       allow_warnings();
+
+       CommandMap cmap;
+       cmap.emplace("bla",
+                    CommandInfo{ArgMap{
+                                    {":foo", ArgInfo{Sexp::Type::Number, false, "foo"}},
+                                    {":bar", ArgInfo{Sexp::Type::String, false, "bar"}},
+                                },
+                                "yeah",
+                                [&](const auto& params) {}});
+
+       g_assert_true(call(cmap, "(bla :foo nil)"));
+       g_assert_false(call(cmap, "(bla :foo nil :bla nil)"));
+}
+
+static void
+test_command_fail()
+{
+       using namespace Command;
+
+       allow_warnings();
+
+       CommandMap cmap;
+
+       cmap.emplace(
+           "my-command",
+           CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
+                              {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
+                       "My command,",
+                       {}});
+
+       g_assert_false(call(cmap, "(my-command)"));
+       g_assert_false(call(cmap, "(my-command2)"));
+       g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)"));
+       g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")"));
+}
+
+static void
+black_hole()
+{
+}
+
+int
+main(int argc, char* argv[]) try {
+
+       mu_test_init(&argc, &argv);
+
+       g_test_add_func("/utils/command-parser/param-getters", test_param_getters);
+       g_test_add_func("/utils/command-parser/command", test_command);
+       g_test_add_func("/utils/command-parser/command2", test_command2);
+       g_test_add_func("/utils/command-parser/command-fail", test_command_fail);
+
+       g_log_set_handler(
+           NULL,
+           (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+           (GLogFunc)black_hole,
+           NULL);
+
+       return g_test_run();
+
+} catch (const std::runtime_error& re) {
+       std::cerr << re.what() << "\n";
+       return 1;
+}
diff --git a/lib/utils/tests/test-mu-str.c b/lib/utils/tests/test-mu-str.c
new file mode 100644 (file)
index 0000000..f714d3f
--- /dev/null
@@ -0,0 +1,169 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <locale.h>
+
+#include "mu-str.h"
+
+static void
+assert_cmplst (GSList *lst, const char *items[])
+{
+       int i;
+
+       if (!lst)
+               g_assert (!items);
+
+       for (i = 0; lst; lst = g_slist_next(lst), ++i)
+               g_assert_cmpstr ((char*)lst->data,==,items[i]);
+
+       g_assert (items[i] == NULL);
+}
+
+
+static GSList*
+create_list (const char *items[])
+{
+       GSList *lst;
+
+       lst = NULL;
+       while (items && *items) {
+               lst = g_slist_prepend (lst, g_strdup(*items));
+               ++items;
+       }
+
+       return g_slist_reverse (lst);
+
+}
+
+static void
+test_mu_str_from_list (void)
+{
+       {
+               const char *strs[] = {"aap", "noot", "mies", NULL};
+               GSList *lst = create_list (strs);
+               gchar  *str = mu_str_from_list (lst, ',');
+               g_assert_cmpstr ("aap,noot,mies", ==, str);
+               mu_str_free_list (lst);
+               g_free (str);
+       }
+
+       {
+               const char *strs[] = {"aap", "no,ot", "mies", NULL};
+               GSList *lst = create_list (strs);
+               gchar  *str = mu_str_from_list (lst, ',');
+               g_assert_cmpstr ("aap,no,ot,mies", ==, str);
+               mu_str_free_list (lst);
+               g_free (str);
+       }
+
+       {
+               const char *strs[] = {NULL};
+               GSList *lst = create_list (strs);
+               gchar  *str = mu_str_from_list (lst,'@');
+               g_assert_cmpstr (NULL, ==, str);
+               mu_str_free_list (lst);
+               g_free (str);
+       }
+
+
+}
+
+
+static void
+test_mu_str_to_list (void)
+{
+       {
+               const char *items[]= {"foo", "bar ", "cuux", NULL};
+               GSList *lst = mu_str_to_list ("foo@bar @cuux",'@', FALSE);
+               assert_cmplst (lst, items);
+               mu_str_free_list (lst);
+       }
+
+       {
+               GSList *lst = mu_str_to_list (NULL,'x',FALSE);
+               g_assert (lst == NULL);
+               mu_str_free_list (lst);
+       }
+}
+
+static void
+test_mu_str_to_list_strip (void)
+{
+       const char *items[]= {"foo", "bar", "cuux", NULL};
+       GSList *lst = mu_str_to_list ("foo@bar @cuux",'@', TRUE);
+       assert_cmplst (lst, items);
+               mu_str_free_list (lst);
+}
+
+static void
+test_mu_str_remove_ctrl_in_place (void)
+{
+       unsigned u;
+       struct {
+               char *str;
+               const char *exp;
+       } strings [] = {
+               { g_strdup(""), ""},
+               { g_strdup("hello, world!"), "hello, world!" },
+               { g_strdup("hello,\tworld!"), "hello, world!" },
+               { g_strdup("hello,\n\nworld!"), "hello,  world!", },
+               { g_strdup("hello,\x1f\x1e\x1ew\nor\nld!"), "hello,w or ld!" },
+               { g_strdup("\x1ehello, world!\x1f"), "hello, world!" }
+       };
+
+       for (u = 0; u != G_N_ELEMENTS(strings); ++u) {
+               char *res;
+               res = mu_str_remove_ctrl_in_place (strings[u].str);
+               g_assert_cmpstr (res,==,strings[u].exp);
+               g_free (strings[u].str);
+       }
+}
+
+
+int
+main (int argc, char *argv[])
+{
+       setlocale (LC_ALL, "");
+
+       g_test_init (&argc, &argv, NULL);
+
+       g_test_add_func ("/mu-str/mu-str-from-list",
+                        test_mu_str_from_list);
+       g_test_add_func ("/mu-str/mu-str-to-list",
+                        test_mu_str_to_list);
+       g_test_add_func ("/mu-str/mu-str-to-list-strip",
+                        test_mu_str_to_list_strip);
+
+       g_test_add_func ("/mu-str/mu_str_remove_ctrl_in_place",
+                        test_mu_str_remove_ctrl_in_place);
+
+
+       return g_test_run ();
+}
diff --git a/lib/utils/tests/test-mu-util.c b/lib/utils/tests/test-mu-util.c
new file mode 100644 (file)
index 0000000..e453ea5
--- /dev/null
@@ -0,0 +1,286 @@
+/*
+** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <limits.h>
+
+#include "mu-util.h"
+
+static void
+test_mu_util_dir_expand_00(void)
+{
+#ifdef HAVE_WORDEXP_H
+       gchar *got, *expected;
+
+       got      = mu_util_dir_expand("~/IProbablyDoNotExist");
+       expected = g_strdup_printf("%s%cIProbablyDoNotExist",
+                                  getenv("HOME"), G_DIR_SEPARATOR);
+
+       g_assert_cmpstr(got, ==, expected);
+
+       g_free(got);
+       g_free(expected);
+#endif /*HAVE_WORDEXP_H*/
+}
+
+static void
+test_mu_util_dir_expand_01(void)
+{
+       /* XXXX: the testcase does not work when using some dir
+        * setups; (see issue #585), although the code should still
+        * work. Turn of the test for now */
+       return;
+
+#ifdef HAVE_WORDEXP_H
+       {
+               gchar *got, *expected;
+
+               got      = mu_util_dir_expand("~/Desktop");
+               expected = g_strdup_printf("%s%cDesktop",
+                                          getenv("HOME"), G_DIR_SEPARATOR);
+
+               g_assert_cmpstr(got, ==, expected);
+
+               g_free(got);
+               g_free(expected);
+       }
+#endif /*HAVE_WORDEXP_H*/
+}
+
+static void
+test_mu_util_guess_maildir_01(void)
+{
+       char*       got;
+       const char* expected;
+
+       /* skip the test if there's no /tmp */
+       if (access("/tmp", F_OK))
+               return;
+
+       g_setenv("MAILDIR", "/tmp", TRUE);
+
+       got      = mu_util_guess_maildir();
+       expected = "/tmp";
+
+       g_assert_cmpstr(got, ==, expected);
+       g_free(got);
+}
+
+static void
+test_mu_util_guess_maildir_02(void)
+{
+       char *got, *mdir;
+
+       g_unsetenv("MAILDIR");
+
+       mdir = g_strdup_printf("%s%cMaildir",
+                              getenv("HOME"), G_DIR_SEPARATOR);
+       got  = mu_util_guess_maildir();
+
+       if (access(mdir, F_OK) == 0)
+               g_assert_cmpstr(got, ==, mdir);
+       else
+               g_assert_cmpstr(got, ==, NULL);
+
+       g_free(got);
+       g_free(mdir);
+}
+
+static void
+test_mu_util_check_dir_01(void)
+{
+       if (g_access("/usr/bin", F_OK) == 0) {
+               g_assert_cmpuint(
+                   mu_util_check_dir("/usr/bin", TRUE, FALSE) == TRUE,
+                   ==,
+                   g_access("/usr/bin", R_OK) == 0);
+       }
+}
+
+static void
+test_mu_util_check_dir_02(void)
+{
+       if (g_access("/tmp", F_OK) == 0) {
+               g_assert_cmpuint(
+                   mu_util_check_dir("/tmp", FALSE, TRUE) == TRUE,
+                   ==,
+                   g_access("/tmp", W_OK) == 0);
+       }
+}
+
+static void
+test_mu_util_check_dir_03(void)
+{
+       if (g_access(".", F_OK) == 0) {
+               g_assert_cmpuint(
+                   mu_util_check_dir(".", TRUE, TRUE) == TRUE,
+                   ==,
+                   g_access(".", W_OK | R_OK) == 0);
+       }
+}
+
+static void
+test_mu_util_check_dir_04(void)
+{
+       /* not a dir, so it must be false */
+       g_assert_cmpuint(
+           mu_util_check_dir("test-util.c", TRUE, TRUE),
+           ==,
+           FALSE);
+}
+
+static void
+test_mu_util_get_dtype_with_lstat(void)
+{
+       g_assert_cmpuint(
+           mu_util_get_dtype(MU_TESTMAILDIR, TRUE), ==, DT_DIR);
+       g_assert_cmpuint(
+           mu_util_get_dtype(MU_TESTMAILDIR2, TRUE), ==, DT_DIR);
+       g_assert_cmpuint(
+           mu_util_get_dtype(MU_TESTMAILDIR2 "/Foo/cur/mail5", TRUE),
+           ==, DT_REG);
+}
+
+static void
+test_mu_util_supports(void)
+{
+       gboolean has_guile;
+       gchar*   path;
+
+#ifdef BUILD_GUILE
+       has_guile = TRUE;
+#else
+       has_guile = FALSE;
+#endif /*BUILD_GUILE*/
+
+       g_assert_cmpuint(mu_util_supports(MU_FEATURE_GUILE), ==, has_guile);
+
+       path = g_find_program_in_path("gnuplot");
+       g_free(path);
+
+       g_assert_cmpuint(mu_util_supports(MU_FEATURE_GNUPLOT), ==,
+                        path ? TRUE : FALSE);
+
+       g_assert_cmpuint(
+           mu_util_supports(MU_FEATURE_GNUPLOT | MU_FEATURE_GUILE),
+           ==,
+           has_guile && path ? TRUE : FALSE);
+}
+
+static void
+test_mu_util_program_in_path(void)
+{
+       g_assert_cmpuint(mu_util_program_in_path("ls"), ==, TRUE);
+}
+
+
+static void
+test_mu_util_summarize(void)
+{
+       const char *txt =
+               "Khiron was fortified and made the seat of a pargana during "
+               "the reign of Asaf-ud-Daula.\n\the headquarters had previously "
+               "been at Satanpur since its foundation and fortification by "
+               "the Bais raja Sathna.\n\nKhiron was also historically the seat "
+               "of a taluqdari estate belonging to a Janwar dynasty.\n"
+               "There were also several Kayasth qanungo families, "
+               "including many descended from Rai Sahib Rai, who had been "
+               "a chakladar under the Nawabs of Awadh.";
+
+       char *summ = mu_str_summarize(txt, 3);
+       g_assert_cmpstr(summ, ==,
+                       "Khiron was fortified and made the seat of a pargana "
+                       "during the reign of Asaf-ud-Daula. he headquarters had "
+                       "previously been at Satanpur since its foundation and "
+                       "fortification by the Bais raja Sathna. ");
+       g_free (summ);
+}
+
+
+static void
+test_mu_error(void)
+{
+       GQuark q;
+       GError *err;
+       gboolean res;
+
+       q = mu_util_error_quark();
+       g_assert_true(q != 0);
+
+
+       err = NULL;
+       res = mu_util_g_set_error(&err, MU_ERROR_IN_PARAMETERS,
+                                 "Hello, %s!", "World");
+
+       g_assert_false(res);
+       g_assert_cmpuint(err->domain, ==, q);
+       g_assert_cmpuint(err->code, ==, MU_ERROR_IN_PARAMETERS);
+       g_assert_cmpstr(err->message,==,"Hello, World!");
+
+       g_clear_error(&err);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       /* mu_util_dir_expand */
+       g_test_add_func("/mu-util/mu-util-dir-expand-00",
+                       test_mu_util_dir_expand_00);
+       g_test_add_func("/mu-util/mu-util-dir-expand-01",
+                       test_mu_util_dir_expand_01);
+
+       /* mu_util_guess_maildir */
+       g_test_add_func("/mu-util/mu-util-guess-maildir-01",
+                       test_mu_util_guess_maildir_01);
+       g_test_add_func("/mu-util/mu-util-guess-maildir-02",
+                       test_mu_util_guess_maildir_02);
+
+       /* mu_util_check_dir */
+       g_test_add_func("/mu-util/mu-util-check-dir-01",
+                       test_mu_util_check_dir_01);
+       g_test_add_func("/mu-util/mu-util-check-dir-02",
+                       test_mu_util_check_dir_02);
+       g_test_add_func("/mu-util/mu-util-check-dir-03",
+                       test_mu_util_check_dir_03);
+       g_test_add_func("/mu-util/mu-util-check-dir-04",
+                       test_mu_util_check_dir_04);
+
+       g_test_add_func("/mu-util/mu-util-get-dtype-with-lstat",
+                       test_mu_util_get_dtype_with_lstat);
+
+       g_test_add_func("/mu-util/mu-util-supports", test_mu_util_supports);
+       g_test_add_func("/mu-util/mu-util-program-in-path",
+                       test_mu_util_program_in_path);
+
+       g_test_add_func("/mu-util/summarize", test_mu_util_summarize);
+       g_test_add_func("/mu-util/error", test_mu_error);
+
+       return g_test_run();
+}
diff --git a/lib/utils/tests/test-option.cc b/lib/utils/tests/test-option.cc
new file mode 100644 (file)
index 0000000..3313afb
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include "mu-utils.hh"
+#include "mu-option.hh"
+
+using namespace Mu;
+
+static Option<int>
+get_opt_int(bool b)
+{
+       if (b)
+               return Some(123);
+       else
+               return Nothing;
+}
+
+static void
+test_option()
+{
+       {
+               const auto oi{get_opt_int(true)};
+               g_assert_true(!!oi);
+               g_assert_cmpint(oi.value(), ==, 123);
+       }
+
+       {
+               const auto oi{get_opt_int(false)};
+               g_assert_false(!!oi);
+               g_assert_false(oi.has_value());
+               g_assert_cmpint(oi.value_or(456), ==, 456);
+       }
+}
+
+int
+main(int argc, char* argv[])
+{
+       g_test_init(&argc, &argv, NULL);
+
+       g_test_add_func("/option/option", test_option);
+
+       return g_test_run();
+}
diff --git a/lib/utils/tests/test-sexp.cc b/lib/utils/tests/test-sexp.cc
new file mode 100644 (file)
index 0000000..3cb1c5a
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+
+#include "mu-command-parser.hh"
+#include "mu-utils.hh"
+#include "mu-test-utils.hh"
+
+using namespace Mu;
+
+static bool
+check_parse(const std::string& expr, const std::string& expected)
+{
+       try {
+               const auto parsed{to_string(Sexp::make_parse(expr))};
+               assert_equal(parsed, expected);
+               return true;
+
+       } catch (const Error& err) {
+               g_warning("caught exception parsing '%s': %s", expr.c_str(), err.what());
+               return false;
+       }
+}
+
+static void
+test_parser()
+{
+       check_parse(":foo-123", ":foo-123");
+       check_parse("foo", "foo");
+       check_parse(R"(12345)", "12345");
+       check_parse(R"(-12345)", "-12345");
+       check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")");
+
+       check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\"");
+
+       check_parse(R"("foo
+bar")",
+                   "\"foo\nbar\"");
+}
+
+static void
+test_list()
+{
+       const auto nstr{Sexp::make_string("foo")};
+       g_assert_true(nstr.value() == "foo");
+       g_assert_true(nstr.type() == Sexp::Type::String);
+       assert_equal(nstr.to_sexp_string(), "\"foo\"");
+
+       const auto nnum{Sexp::make_number(123)};
+       g_assert_true(nnum.value() == "123");
+       g_assert_true(nnum.type() == Sexp::Type::Number);
+       assert_equal(nnum.to_sexp_string(), "123");
+
+       const auto nsym{Sexp::make_symbol("blub")};
+       g_assert_true(nsym.value() == "blub");
+       g_assert_true(nsym.type() == Sexp::Type::Symbol);
+       assert_equal(nsym.to_sexp_string(), "blub");
+
+       Sexp::List list;
+       list.add(Sexp::make_string("foo"))
+           .add(Sexp::make_number(123))
+           .add(Sexp::make_symbol("blub"));
+
+       const auto nlst = Sexp::make_list(std::move(list));
+       g_assert_true(nlst.list().size() == 3);
+       g_assert_true(nlst.type() == Sexp::Type::List);
+       g_assert_true(nlst.list().at(1).value() == "123");
+
+       assert_equal(nlst.to_sexp_string(), "(\"foo\" 123 blub)");
+}
+
+static void
+test_prop_list()
+{
+       Sexp::List l1;
+       l1.add_prop(":foo", Sexp::make_string("bar"));
+       Sexp s2{Sexp::make_list(std::move(l1))};
+       assert_equal(s2.to_sexp_string(), "(:foo \"bar\")");
+       g_assert_true(s2.is_prop_list());
+
+       Sexp::List        l2;
+       const std::string x{"bar"};
+       l2.add_prop(":foo", Sexp::make_string(x));
+       l2.add_prop(":bar", Sexp::make_number(77));
+       Sexp::List l3;
+       l3.add_prop(":cuux", Sexp::make_list(std::move(l2)));
+       Sexp s3{Sexp::make_list(std::move(l3))};
+       assert_equal(s3.to_sexp_string(), "(:cuux (:foo \"bar\" :bar 77))");
+}
+
+static void
+test_props()
+{
+       auto sexp2 = Sexp::make_list(Sexp::make_string("foo"),
+                                    Sexp::make_number(123),
+                                    Sexp::make_symbol("blub"));
+
+       auto sexp = Sexp::make_prop_list(":foo",
+                                        Sexp::make_string("bär"),
+                                        ":cuux",
+                                        Sexp::make_number(123),
+                                        ":flub",
+                                        Sexp::make_symbol("fnord"),
+                                        ":boo",
+                                        std::move(sexp2));
+
+       assert_equal(sexp.to_sexp_string(),
+                    "(:foo \"b\303\244r\" :cuux 123 :flub fnord :boo (\"foo\" 123 blub))");
+}
+
+static void
+test_prop_list_remove()
+{
+       {
+               Sexp::List lst;
+               lst.add_prop(":foo", Sexp::make_string("123"))
+                       .add_prop(":bar", Sexp::make_number(123));
+
+               assert_equal(Sexp::make_list(std::move(lst)).to_sexp_string(),
+                            R"((:foo "123" :bar 123))");
+       }
+
+       {
+               Sexp::List lst;
+               lst.add_prop(":foo", Sexp::make_string("123"))
+                       .add_prop(":bar", Sexp::make_number(123));
+
+               assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(),
+                            R"((:foo "123" :bar 123))");
+
+               lst.remove_prop(":bar");
+
+               assert_equal(Sexp::make_list(Sexp::List{lst}).to_sexp_string(),
+                            R"((:foo "123"))");
+
+               lst.clear();
+               g_assert_cmpuint(lst.size(), ==, 0);
+       }
+
+       {
+               Sexp::List lst;
+               lst.add(Sexp::make_number(123));
+               Sexp s2{Sexp::make_list(std::move(lst))};
+               g_assert_false(s2.is_prop_list());
+       }
+}
+
+int
+main(int argc, char* argv[])
+try {
+       mu_test_init(&argc, &argv);
+
+       if (argc == 2) {
+               std::cout << Sexp::make_parse(argv[1]) << '\n';
+               return 0;
+       }
+
+       g_test_add_func("/utils/sexp/parser", test_parser);
+       g_test_add_func("/utils/sexp/list", test_list);
+       g_test_add_func("/utils/sexp/proplist", test_prop_list);
+       g_test_add_func("/utils/sexp/proplist-remove", test_prop_list_remove);
+       g_test_add_func("/utils/sexp/props", test_props);
+
+       return g_test_run();
+
+} catch (const std::runtime_error& re) {
+       std::cerr << re.what() << "\n";
+       return 1;
+}
diff --git a/lib/utils/tests/test-utils.cc b/lib/utils/tests/test-utils.cc
new file mode 100644 (file)
index 0000000..b69c705
--- /dev/null
@@ -0,0 +1,322 @@
+/*
+** Copyright (C) 2017-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+**  This library is free software; you can redistribute it and/or
+**  modify it under the terms of the GNU Lesser General Public License
+**  as published by the Free Software Foundation; either version 2.1
+**  of the License, or (at your option) any later version.
+**
+**  This library 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
+**  Lesser General Public License for more details.
+**
+**  You should have received a copy of the GNU Lesser General Public
+**  License along with this library; if not, write to the Free
+**  Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+**  02110-1301, USA.
+*/
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+#include <functional>
+#include <array>
+
+#include "mu-utils.hh"
+#include "mu-test-utils.hh"
+#include "mu-error.hh"
+
+using namespace Mu;
+
+
+struct Case {
+       const std::string expr;
+       bool              is_first{};
+       const std::string expected;
+};
+using CaseVec  = std::vector<Case>;
+using ProcFunc = std::function<std::string(std::string, bool)>;
+
+static void
+test_cases(const CaseVec& cases, ProcFunc proc)
+{
+       for (const auto& casus : cases) {
+               const auto res = proc(casus.expr, casus.is_first);
+               if (g_test_verbose()) {
+                       std::cout << "\n";
+                       std::cout << casus.expr << ' ' << casus.is_first << std::endl;
+                       std::cout << "exp: '" << casus.expected << "'" << std::endl;
+                       std::cout << "got: '" << res << "'" << std::endl;
+               }
+
+               g_assert_true(casus.expected == res);
+       }
+}
+
+static void
+test_date_basic()
+{
+       const auto hki = "Europe/Helsinki";
+
+       // ensure we have the needed TZ or skip the test.
+       if (!timezone_available(hki)) {
+               g_test_skip("timezone Europe/Helsinki not available");
+               return;
+       }
+
+       g_setenv("TZ", hki, TRUE);
+       constexpr std::array<std::tuple<const char*, bool/*is_first*/, int64_t>, 13> cases = {{
+                       {"2015-09-18T09:10:23", true, 1442556623},
+                       {"1972-12-14T09:10:23", true, 93165023},
+                       {"1854-11-18T17:10:23", true, 0},
+
+                       {"2000-02-31T09:10:23", true, 951861599},
+                       {"2000-02-29T23:59:59", true, 951861599},
+
+                       {"20220602", true, 1654117200},
+                       {"20220605", false, 1654462799},
+
+                       {"202206", true, 1654030800},
+                       {"202206", false, 1656622799},
+
+                       {"2016", true, 1451599200},
+                       {"2016", false, 1483221599},
+
+                       // {"fnorb", true,  -1},
+                       // {"fnorb", false, -1},
+                       {"", false, G_MAXINT64},
+                       {"", true, 0}
+               }};
+
+       for (auto& test: cases) {
+               if (g_test_verbose())
+                       g_debug("checking %s", std::get<0>(test));
+               g_assert_cmpuint(parse_date_time(std::get<0>(test),
+                                                std::get<1>(test)).value_or(-1),==,
+                                std::get<2>(test));
+       }
+}
+
+static void
+test_date_ymwdhMs(void)
+{
+       struct testcase {
+               std::string     expr;
+               int64_t         diff;
+               int             tolerance;
+       };
+
+       std::array<testcase, 7> cases = {{
+               {"7s", 7, 1},
+               {"3M", 3 * 60, 1},
+               {"3h", 3 * 60 * 60, 1},
+               {"21d", 21 * 24 * 60 * 60, 3600 + 1},
+               {"2w", 2 * 7 * 24 * 60 * 60, 3600 + 1},
+               {"2y", 2 * 365 * 24 * 60 * 60, 24 * 3600 + 1},
+               {"3m", 3 * 30 * 24 * 60 * 60, 3 * 24 * 3600 + 1}
+               }};
+
+       for (auto&& tcase: cases) {
+               const auto date = parse_date_time(tcase.expr, true);
+               g_assert_true(date);
+               const auto diff = ::time({}) - *date;
+               if (g_test_verbose())
+                       std::cerr << tcase.expr << ' ' << diff << ' ' << tcase.diff << '\n';
+
+               g_assert_true(tcase.diff - diff <= tcase.tolerance);
+       }
+
+       // note: perhaps it'd be nice if we'd detect this error;
+       // currently we're being rather tolerant
+       // g_assert_false(!!parse_date_time("25q", false));
+}
+
+static void
+test_parse_size()
+{
+       constexpr std::array<std::tuple<const char*, bool, int64_t>, 6> cases = {{
+                       { "456", false, 456 },
+                       { "", false, G_MAXINT64 },
+                       { "", true, 0 },
+                       { "2K", false, 2048 },
+                       { "2M", true, 2097152 },
+                       { "5G", true, 5368709120 }
+               }};
+       for(auto&& test: cases) {
+               g_assert_cmpint(parse_size(std::get<0>(test), std::get<1>(test))
+                               .value_or(-1), ==, std::get<2>(test));
+       }
+
+       g_assert_false(!!parse_size("-1", true));
+       g_assert_false(!!parse_size("scoobydoobydoo", false));
+}
+
+static void
+test_flatten()
+{
+       CaseVec cases = {
+           {"Менделе́ев", true, "менделеев"},
+           {"", false, ""},
+           {"Ångström", true, "angstrom"},
+       };
+
+       test_cases(cases, [](auto s, auto f) { return utf8_flatten(s); });
+}
+
+static void
+test_remove_ctrl()
+{
+       CaseVec cases = {
+           {"Foo\n\nbar", true, "Foo bar"},
+           {"", false, ""},
+           {"   ", false, " "},
+           {"Hello   World   ", false, "Hello World "},
+           {"Ångström", false, "Ångström"},
+       };
+
+       test_cases(cases, [](auto s, auto f) { return remove_ctrl(s); });
+}
+
+static void
+test_clean()
+{
+       CaseVec cases = {
+           {"\t a\t\nb ", true, "a  b"},
+           {"", false, ""},
+           {"Ångström", true, "Ångström"},
+       };
+
+       test_cases(cases, [](auto s, auto f) { return utf8_clean(s); });
+}
+
+static void
+test_format()
+{
+       g_assert_true(format("hello %s", "world") == "hello world");
+       g_assert_true(format("hello %s, %u", "world", 123) == "hello world, 123");
+}
+
+static void
+test_split()
+{
+       using svec = std::vector<std::string>;
+       auto assert_equal_svec=[](const svec& sv1, const svec& sv2) {
+               g_assert_cmpuint(sv1.size(),==,sv2.size());
+               for (auto i = 0U; i != sv1.size(); ++i)
+                       g_assert_cmpstr(sv1[i].c_str(),==,sv2[i].c_str());
+       };
+
+       // string sepa
+       assert_equal_svec(split("axbxc", "x"), {"a", "b", "c"});
+       assert_equal_svec(split("axbxcx", "x"), {"a", "b", "c", ""});
+       assert_equal_svec(split("", "boo"), {});
+       assert_equal_svec(split("ayybyyc", "yy"), {"a", "b", "c"});
+       assert_equal_svec(split("abc", ""), {"a", "b", "c"});
+       assert_equal_svec(split("", "boo"), {});
+
+       // char sepa
+       assert_equal_svec(split("axbxc", 'x'), {"a", "b", "c"});
+       assert_equal_svec(split("axbxcx", 'x'), {"a", "b", "c", ""});
+
+       // rx sexp
+       assert_equal_svec(split("axbyc", std::regex("[xy]")), {"a", "b", "c"});
+}
+
+static void
+test_join()
+{
+       assert_equal(join({"a", "b", "c"}, "x"), "axbxc");
+       assert_equal(join({"a", "b", "c"}, ""), "abc");
+       assert_equal(join({},"foo"), "");
+       assert_equal(join({"d", "e", "f"}, "foo"), "dfooefoof");
+}
+
+
+enum struct Bits { None = 0, Bit1 = 1 << 0, Bit2 = 1 << 1 };
+MU_ENABLE_BITOPS(Bits);
+
+static void
+test_define_bitmap()
+{
+       g_assert_cmpuint((guint)Bits::None, ==, (guint)0);
+       g_assert_cmpuint((guint)Bits::Bit1, ==, (guint)1);
+       g_assert_cmpuint((guint)Bits::Bit2, ==, (guint)2);
+
+       g_assert_cmpuint((guint)(Bits::Bit1 | Bits::Bit2), ==, (guint)3);
+       g_assert_cmpuint((guint)(Bits::Bit1 & Bits::Bit2), ==, (guint)0);
+
+       g_assert_cmpuint((guint)(Bits::Bit1 & (~Bits::Bit2)), ==, (guint)1);
+
+       {
+               Bits b{Bits::Bit1};
+               b |= Bits::Bit2;
+               g_assert_cmpuint((guint)b, ==, (guint)3);
+       }
+
+       {
+               Bits b{Bits::Bit1};
+               b &= Bits::Bit1;
+               g_assert_cmpuint((guint)b, ==, (guint)1);
+       }
+}
+
+static void
+test_to_from_lexnum()
+{
+       assert_equal(to_lexnum(0), "g0");
+       assert_equal(to_lexnum(100), "h64");
+       assert_equal(to_lexnum(12345), "j3039");
+
+       g_assert_cmpuint(from_lexnum(to_lexnum(0)), ==, 0);
+       g_assert_cmpuint(from_lexnum(to_lexnum(7777)), ==, 7777);
+       g_assert_cmpuint(from_lexnum(to_lexnum(9876543)), ==, 9876543);
+}
+
+static void
+test_locale_workaround()
+{
+       g_assert_true(locale_workaround());
+
+       g_setenv("LC_ALL", "BOO", 1);
+
+       g_assert_true(locale_workaround());
+}
+
+static void
+test_error()
+{
+       GError *err;
+       err = g_error_new(MU_ERROR_DOMAIN, 77, "Hello, %s", "world");
+       Error ex{Error::Code::Crypto, &err, "boo"};
+       g_assert_cmpstr(ex.what(), ==, "boo: Hello, world");
+
+       ex.fill_g_error(&err);
+       g_assert_cmpuint(err->code, ==, static_cast<unsigned>(Error::Code::Crypto));
+       g_clear_error(&err);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+       mu_test_init(&argc, &argv);
+
+       g_test_add_func("/utils/date-basic", test_date_basic);
+       g_test_add_func("/utils/date-ymwdhMs", test_date_ymwdhMs);
+       g_test_add_func("/utils/parse-size", test_parse_size);
+       g_test_add_func("/utils/flatten", test_flatten);
+       g_test_add_func("/utils/remove-ctrl", test_remove_ctrl);
+       g_test_add_func("/utils/clean", test_clean);
+       g_test_add_func("/utils/format", test_format);
+       g_test_add_func("/utils/split", test_split);
+       g_test_add_func("/utils/join", test_join);
+       g_test_add_func("/utils/define-bitmap", test_define_bitmap);
+       g_test_add_func("/utils/to-from-lexnum", test_to_from_lexnum);
+       g_test_add_func("/utils/locale-workaround", test_locale_workaround);
+       g_test_add_func("/utils/error", test_error);
+
+       return g_test_run();
+}
diff --git a/m4/Makefile.am b/m4/Makefile.am
new file mode 100644 (file)
index 0000000..c6a97e3
--- /dev/null
@@ -0,0 +1,47 @@
+## Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+EXTRA_DIST=                            \
+       ax_ac_append_to_file.m4         \
+       ax_ac_print_to_file.m4          \
+       ax_add_am_macro_static.m4       \
+       ax_am_macros_static.m4          \
+       ax_append_compile_flags.m4      \
+       ax_append_flag.m4               \
+       ax_append_link_flags.m4         \
+       ax_check_compile_flag.m4        \
+       ax_check_enable_debug.m4        \
+       ax_check_gnu_make.m4            \
+       ax_check_link_flag.m4           \
+       ax_code_coverage.m4             \
+       ax_compiler_flags.m4            \
+       ax_compiler_flags_cflags.m4     \
+       ax_compiler_flags_cxxflags.m4   \
+       ax_compiler_flags_gir.m4        \
+       ax_compiler_flags_ldflags.m4    \
+       ax_cxx_compile_stdcxx.m4        \
+       ax_cxx_compile_stdcxx_17.m4     \
+       ax_file_escapes.m4              \
+       ax_is_release.m4                \
+       ax_lib_readline.m4              \
+       ax_require_defined.m4           \
+       ax_valgrind_check.m4            \
+       guile.m4                        \
+       lib-ld.m4                       \
+       lib-link.m4                     \
+       lib-prefix.m4
diff --git a/m4/ax_ac_append_to_file.m4 b/m4/ax_ac_append_to_file.m4
new file mode 100644 (file)
index 0000000..242b3d5
--- /dev/null
@@ -0,0 +1,32 @@
+# ===========================================================================
+#   https://www.gnu.org/software/autoconf-archive/ax_ac_append_to_file.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_AC_APPEND_TO_FILE([FILE],[DATA])
+#
+# DESCRIPTION
+#
+#   Appends the specified data to the specified Autoconf is run. If you want
+#   to append to a file when configure is run use AX_APPEND_TO_FILE instead.
+#
+# LICENSE
+#
+#   Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+#   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. This file is offered as-is, without any
+#   warranty.
+
+#serial 10
+
+AC_DEFUN([AX_AC_APPEND_TO_FILE],[
+AC_REQUIRE([AX_FILE_ESCAPES])
+m4_esyscmd(
+AX_FILE_ESCAPES
+[
+printf "%s" "$2" >> "$1"
+])
+])
diff --git a/m4/ax_ac_print_to_file.m4 b/m4/ax_ac_print_to_file.m4
new file mode 100644 (file)
index 0000000..642dfc1
--- /dev/null
@@ -0,0 +1,32 @@
+# ===========================================================================
+#   https://www.gnu.org/software/autoconf-archive/ax_ac_print_to_file.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_AC_PRINT_TO_FILE([FILE],[DATA])
+#
+# DESCRIPTION
+#
+#   Writes the specified data to the specified file when Autoconf is run. If
+#   you want to print to a file when configure is run use AX_PRINT_TO_FILE
+#   instead.
+#
+# LICENSE
+#
+#   Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+#   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. This file is offered as-is, without any
+#   warranty.
+
+#serial 10
+
+AC_DEFUN([AX_AC_PRINT_TO_FILE],[
+m4_esyscmd(
+AC_REQUIRE([AX_FILE_ESCAPES])
+[
+printf "%s" "$2" > "$1"
+])
+])
diff --git a/m4/ax_add_am_macro_static.m4 b/m4/ax_add_am_macro_static.m4
new file mode 100644 (file)
index 0000000..6442d24
--- /dev/null
@@ -0,0 +1,28 @@
+# ===========================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_add_am_macro_static.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_ADD_AM_MACRO_STATIC([RULE])
+#
+# DESCRIPTION
+#
+#   Adds the specified rule to $AMINCLUDE.
+#
+# LICENSE
+#
+#   Copyright (c) 2009 Tom Howard <tomhoward@users.sf.net>
+#   Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+#   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. This file is offered as-is, without any
+#   warranty.
+
+#serial 8
+
+AC_DEFUN([AX_ADD_AM_MACRO_STATIC],[
+  AC_REQUIRE([AX_AM_MACROS_STATIC])
+  AX_AC_APPEND_TO_FILE(AMINCLUDE_STATIC,[$1])
+])
diff --git a/m4/ax_am_macros_static.m4 b/m4/ax_am_macros_static.m4
new file mode 100644 (file)
index 0000000..f4cee8c
--- /dev/null
@@ -0,0 +1,38 @@
+# ===========================================================================
+#   https://www.gnu.org/software/autoconf-archive/ax_am_macros_static.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_AM_MACROS_STATIC
+#
+# DESCRIPTION
+#
+#   Adds support for macros that create Automake rules. You must manually
+#   add the following line
+#
+#     include $(top_srcdir)/aminclude_static.am
+#
+#   to your Makefile.am files.
+#
+# LICENSE
+#
+#   Copyright (c) 2009 Tom Howard <tomhoward@users.sf.net>
+#   Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+#   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. This file is offered as-is, without any
+#   warranty.
+
+#serial 11
+
+AC_DEFUN([AMINCLUDE_STATIC],[aminclude_static.am])
+
+AC_DEFUN([AX_AM_MACROS_STATIC],
+[
+AX_AC_PRINT_TO_FILE(AMINCLUDE_STATIC,[
+# ]AMINCLUDE_STATIC[ generated automatically by Autoconf
+# from AX_AM_MACROS_STATIC on ]m4_esyscmd([LC_ALL=C date])[
+])
+])
diff --git a/m4/ax_append_compile_flags.m4 b/m4/ax_append_compile_flags.m4
new file mode 100644 (file)
index 0000000..9c85635
--- /dev/null
@@ -0,0 +1,46 @@
+# ============================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_append_compile_flags.html
+# ============================================================================
+#
+# SYNOPSIS
+#
+#   AX_APPEND_COMPILE_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS], [INPUT])
+#
+# DESCRIPTION
+#
+#   For every FLAG1, FLAG2 it is checked whether the compiler works with the
+#   flag.  If it does, the flag is added FLAGS-VARIABLE
+#
+#   If FLAGS-VARIABLE is not specified, the current language's flags (e.g.
+#   CFLAGS) is used.  During the check the flag is always added to the
+#   current language's flags.
+#
+#   If EXTRA-FLAGS is defined, it is added to the current language's default
+#   flags (e.g. CFLAGS) when the check is done.  The check is thus made with
+#   the flags: "CFLAGS EXTRA-FLAGS FLAG".  This can for example be used to
+#   force the compiler to issue an error when a bad flag is given.
+#
+#   INPUT gives an alternative input source to AC_COMPILE_IFELSE.
+#
+#   NOTE: This macro depends on the AX_APPEND_FLAG and
+#   AX_CHECK_COMPILE_FLAG. Please keep this macro in sync with
+#   AX_APPEND_LINK_FLAGS.
+#
+# LICENSE
+#
+#   Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 7
+
+AC_DEFUN([AX_APPEND_COMPILE_FLAGS],
+[AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG])
+AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+for flag in $1; do
+  AX_CHECK_COMPILE_FLAG([$flag], [AX_APPEND_FLAG([$flag], [$2])], [], [$3], [$4])
+done
+])dnl AX_APPEND_COMPILE_FLAGS
diff --git a/m4/ax_append_flag.m4 b/m4/ax_append_flag.m4
new file mode 100644 (file)
index 0000000..dd6d8b6
--- /dev/null
@@ -0,0 +1,50 @@
+# ===========================================================================
+#      https://www.gnu.org/software/autoconf-archive/ax_append_flag.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_APPEND_FLAG(FLAG, [FLAGS-VARIABLE])
+#
+# DESCRIPTION
+#
+#   FLAG is appended to the FLAGS-VARIABLE shell variable, with a space
+#   added in between.
+#
+#   If FLAGS-VARIABLE is not specified, the current language's flags (e.g.
+#   CFLAGS) is used.  FLAGS-VARIABLE is not changed if it already contains
+#   FLAG.  If FLAGS-VARIABLE is unset in the shell, it is set to exactly
+#   FLAG.
+#
+#   NOTE: Implementation based on AX_CFLAGS_GCC_OPTION.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Guido U. Draheim <guidod@gmx.de>
+#   Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 8
+
+AC_DEFUN([AX_APPEND_FLAG],
+[dnl
+AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_SET_IF
+AS_VAR_PUSHDEF([FLAGS], [m4_default($2,_AC_LANG_PREFIX[FLAGS])])
+AS_VAR_SET_IF(FLAGS,[
+  AS_CASE([" AS_VAR_GET(FLAGS) "],
+    [*" $1 "*], [AC_RUN_LOG([: FLAGS already contains $1])],
+    [
+     AS_VAR_APPEND(FLAGS,[" $1"])
+     AC_RUN_LOG([: FLAGS="$FLAGS"])
+    ])
+  ],
+  [
+  AS_VAR_SET(FLAGS,[$1])
+  AC_RUN_LOG([: FLAGS="$FLAGS"])
+  ])
+AS_VAR_POPDEF([FLAGS])dnl
+])dnl AX_APPEND_FLAG
diff --git a/m4/ax_append_link_flags.m4 b/m4/ax_append_link_flags.m4
new file mode 100644 (file)
index 0000000..99b9fa5
--- /dev/null
@@ -0,0 +1,44 @@
+# ===========================================================================
+#   https://www.gnu.org/software/autoconf-archive/ax_append_link_flags.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_APPEND_LINK_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS], [INPUT])
+#
+# DESCRIPTION
+#
+#   For every FLAG1, FLAG2 it is checked whether the linker works with the
+#   flag.  If it does, the flag is added FLAGS-VARIABLE
+#
+#   If FLAGS-VARIABLE is not specified, the linker's flags (LDFLAGS) is
+#   used. During the check the flag is always added to the linker's flags.
+#
+#   If EXTRA-FLAGS is defined, it is added to the linker's default flags
+#   when the check is done.  The check is thus made with the flags: "LDFLAGS
+#   EXTRA-FLAGS FLAG".  This can for example be used to force the linker to
+#   issue an error when a bad flag is given.
+#
+#   INPUT gives an alternative input source to AC_COMPILE_IFELSE.
+#
+#   NOTE: This macro depends on the AX_APPEND_FLAG and AX_CHECK_LINK_FLAG.
+#   Please keep this macro in sync with AX_APPEND_COMPILE_FLAGS.
+#
+# LICENSE
+#
+#   Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 7
+
+AC_DEFUN([AX_APPEND_LINK_FLAGS],
+[AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG])
+AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+for flag in $1; do
+  AX_CHECK_LINK_FLAG([$flag], [AX_APPEND_FLAG([$flag], [m4_default([$2], [LDFLAGS])])], [], [$3], [$4])
+done
+])dnl AX_APPEND_LINK_FLAGS
diff --git a/m4/ax_check_compile_flag.m4 b/m4/ax_check_compile_flag.m4
new file mode 100644 (file)
index 0000000..bd753b3
--- /dev/null
@@ -0,0 +1,53 @@
+# ===========================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_check_compile_flag.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_CHECK_COMPILE_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT])
+#
+# DESCRIPTION
+#
+#   Check whether the given FLAG works with the current language's compiler
+#   or gives an error.  (Warnings, however, are ignored)
+#
+#   ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on
+#   success/failure.
+#
+#   If EXTRA-FLAGS is defined, it is added to the current language's default
+#   flags (e.g. CFLAGS) when the check is done.  The check is thus made with
+#   the flags: "CFLAGS EXTRA-FLAGS FLAG".  This can for example be used to
+#   force the compiler to issue an error when a bad flag is given.
+#
+#   INPUT gives an alternative input source to AC_COMPILE_IFELSE.
+#
+#   NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this
+#   macro in sync with AX_CHECK_{PREPROC,LINK}_FLAG.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Guido U. Draheim <guidod@gmx.de>
+#   Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 6
+
+AC_DEFUN([AX_CHECK_COMPILE_FLAG],
+[AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF
+AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_[]_AC_LANG_ABBREV[]flags_$4_$1])dnl
+AC_CACHE_CHECK([whether _AC_LANG compiler accepts $1], CACHEVAR, [
+  ax_check_save_flags=$[]_AC_LANG_PREFIX[]FLAGS
+  _AC_LANG_PREFIX[]FLAGS="$[]_AC_LANG_PREFIX[]FLAGS $4 $1"
+  AC_COMPILE_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])],
+    [AS_VAR_SET(CACHEVAR,[yes])],
+    [AS_VAR_SET(CACHEVAR,[no])])
+  _AC_LANG_PREFIX[]FLAGS=$ax_check_save_flags])
+AS_VAR_IF(CACHEVAR,yes,
+  [m4_default([$2], :)],
+  [m4_default([$3], :)])
+AS_VAR_POPDEF([CACHEVAR])dnl
+])dnl AX_CHECK_COMPILE_FLAGS
diff --git a/m4/ax_check_enable_debug.m4 b/m4/ax_check_enable_debug.m4
new file mode 100644 (file)
index 0000000..7bc7710
--- /dev/null
@@ -0,0 +1,124 @@
+# ===========================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_check_enable_debug.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_CHECK_ENABLE_DEBUG([enable by default=yes/info/profile/no], [ENABLE DEBUG VARIABLES ...], [DISABLE DEBUG VARIABLES NDEBUG ...], [IS-RELEASE])
+#
+# DESCRIPTION
+#
+#   Check for the presence of an --enable-debug option to configure, with
+#   the specified default value used when the option is not present.  Return
+#   the value in the variable $ax_enable_debug.
+#
+#   Specifying 'yes' adds '-g -O0' to the compilation flags for all
+#   languages. Specifying 'info' adds '-g' to the compilation flags.
+#   Specifying 'profile' adds '-g -pg' to the compilation flags and '-pg' to
+#   the linking flags. Otherwise, nothing is added.
+#
+#   Define the variables listed in the second argument if debug is enabled,
+#   defaulting to no variables.  Defines the variables listed in the third
+#   argument if debug is disabled, defaulting to NDEBUG.  All lists of
+#   variables should be space-separated.
+#
+#   If debug is not enabled, ensure AC_PROG_* will not add debugging flags.
+#   Should be invoked prior to any AC_PROG_* compiler checks.
+#
+#   IS-RELEASE can be used to change the default to 'no' when making a
+#   release.  Set IS-RELEASE to 'yes' or 'no' as appropriate. By default, it
+#   uses the value of $ax_is_release, so if you are using the AX_IS_RELEASE
+#   macro, there is no need to pass this parameter.
+#
+#     AX_IS_RELEASE([git-directory])
+#     AX_CHECK_ENABLE_DEBUG()
+#
+# LICENSE
+#
+#   Copyright (c) 2011 Rhys Ulerich <rhys.ulerich@gmail.com>
+#   Copyright (c) 2014, 2015 Philip Withnall <philip@tecnocode.co.uk>
+#
+#   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.
+
+#serial 9
+
+AC_DEFUN([AX_CHECK_ENABLE_DEBUG],[
+    AC_BEFORE([$0],[AC_PROG_CC])dnl
+    AC_BEFORE([$0],[AC_PROG_CXX])dnl
+    AC_BEFORE([$0],[AC_PROG_F77])dnl
+    AC_BEFORE([$0],[AC_PROG_FC])dnl
+
+    AC_MSG_CHECKING(whether to enable debugging)
+
+    ax_enable_debug_default=m4_tolower(m4_normalize(ifelse([$1],,[no],[$1])))
+    ax_enable_debug_is_release=m4_tolower(m4_normalize(ifelse([$4],,
+                                                              [$ax_is_release],
+                                                              [$4])))
+
+    # If this is a release, override the default.
+    AS_IF([test "$ax_enable_debug_is_release" = "yes"],
+      [ax_enable_debug_default="no"])
+
+    m4_define(ax_enable_debug_vars,[m4_normalize(ifelse([$2],,,[$2]))])
+    m4_define(ax_disable_debug_vars,[m4_normalize(ifelse([$3],,[NDEBUG],[$3]))])
+
+    AC_ARG_ENABLE(debug,
+        [AS_HELP_STRING([--enable-debug=]@<:@yes/info/profile/no@:>@,[compile with debugging])],
+        [],enable_debug=$ax_enable_debug_default)
+
+    # empty mean debug yes
+    AS_IF([test "x$enable_debug" = "x"],
+      [enable_debug="yes"])
+
+    # case of debug
+    AS_CASE([$enable_debug],
+      [yes],[
+        AC_MSG_RESULT(yes)
+        CFLAGS="${CFLAGS} -g -O0"
+        CXXFLAGS="${CXXFLAGS} -g -O0"
+        FFLAGS="${FFLAGS} -g -O0"
+        FCFLAGS="${FCFLAGS} -g -O0"
+        OBJCFLAGS="${OBJCFLAGS} -g -O0"
+      ],
+      [info],[
+        AC_MSG_RESULT(info)
+        CFLAGS="${CFLAGS} -g"
+        CXXFLAGS="${CXXFLAGS} -g"
+        FFLAGS="${FFLAGS} -g"
+        FCFLAGS="${FCFLAGS} -g"
+        OBJCFLAGS="${OBJCFLAGS} -g"
+      ],
+      [profile],[
+        AC_MSG_RESULT(profile)
+        CFLAGS="${CFLAGS} -g -pg"
+        CXXFLAGS="${CXXFLAGS} -g -pg"
+        FFLAGS="${FFLAGS} -g -pg"
+        FCFLAGS="${FCFLAGS} -g -pg"
+        OBJCFLAGS="${OBJCFLAGS} -g -pg"
+        LDFLAGS="${LDFLAGS} -pg"
+      ],
+      [
+        AC_MSG_RESULT(no)
+        dnl Ensure AC_PROG_CC/CXX/F77/FC/OBJC will not enable debug flags
+        dnl by setting any unset environment flag variables
+        AS_IF([test "x${CFLAGS+set}" != "xset"],
+          [CFLAGS=""])
+        AS_IF([test "x${CXXFLAGS+set}" != "xset"],
+          [CXXFLAGS=""])
+        AS_IF([test "x${FFLAGS+set}" != "xset"],
+          [FFLAGS=""])
+        AS_IF([test "x${FCFLAGS+set}" != "xset"],
+          [FCFLAGS=""])
+        AS_IF([test "x${OBJCFLAGS+set}" != "xset"],
+          [OBJCFLAGS=""])
+      ])
+
+    dnl Define various variables if debugging is disabled.
+    dnl assert.h is a NOP if NDEBUG is defined, so define it by default.
+    AS_IF([test "x$enable_debug" = "xyes"],
+      [m4_map_args_w(ax_enable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is enabled])])],
+      [m4_map_args_w(ax_disable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is disabled])])])
+    ax_enable_debug=$enable_debug
+])
diff --git a/m4/ax_check_gnu_make.m4 b/m4/ax_check_gnu_make.m4
new file mode 100644 (file)
index 0000000..6811043
--- /dev/null
@@ -0,0 +1,95 @@
+# ===========================================================================
+#    https://www.gnu.org/software/autoconf-archive/ax_check_gnu_make.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_CHECK_GNU_MAKE([run-if-true],[run-if-false])
+#
+# DESCRIPTION
+#
+#   This macro searches for a GNU version of make. If a match is found:
+#
+#     * The makefile variable `ifGNUmake' is set to the empty string, otherwise
+#       it is set to "#". This is useful for including a special features in a
+#       Makefile, which cannot be handled by other versions of make.
+#     * The makefile variable `ifnGNUmake' is set to #, otherwise
+#       it is set to the empty string. This is useful for including a special
+#       features in a Makefile, which can be handled
+#       by other versions of make or to specify else like clause.
+#     * The variable `_cv_gnu_make_command` is set to the command to invoke
+#       GNU make if it exists, the empty string otherwise.
+#     * The variable `ax_cv_gnu_make_command` is set to the command to invoke
+#       GNU make by copying `_cv_gnu_make_command`, otherwise it is unset.
+#     * If GNU Make is found, its version is extracted from the output of
+#       `make --version` as the last field of a record of space-separated
+#       columns and saved into the variable `ax_check_gnu_make_version`.
+#     * Additionally if GNU Make is found, run shell code run-if-true
+#       else run shell code run-if-false.
+#
+#   Here is an example of its use:
+#
+#   Makefile.in might contain:
+#
+#     # A failsafe way of putting a dependency rule into a makefile
+#     $(DEPEND):
+#             $(CC) -MM $(srcdir)/*.c > $(DEPEND)
+#
+#     @ifGNUmake@ ifeq ($(DEPEND),$(wildcard $(DEPEND)))
+#     @ifGNUmake@ include $(DEPEND)
+#     @ifGNUmake@ else
+#     fallback code
+#     @ifGNUmake@ endif
+#
+#   Then configure.in would normally contain:
+#
+#     AX_CHECK_GNU_MAKE()
+#     AC_OUTPUT(Makefile)
+#
+#   Then perhaps to cause gnu make to override any other make, we could do
+#   something like this (note that GNU make always looks for GNUmakefile
+#   first):
+#
+#     if  ! test x$_cv_gnu_make_command = x ; then
+#             mv Makefile GNUmakefile
+#             echo .DEFAULT: > Makefile ;
+#             echo \  $_cv_gnu_make_command \$@ >> Makefile;
+#     fi
+#
+#   Then, if any (well almost any) other make is called, and GNU make also
+#   exists, then the other make wraps the GNU make.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 John Darrington <j.darrington@elvis.murdoch.edu.au>
+#   Copyright (c) 2015 Enrico M. Crisostomo <enrico.m.crisostomo@gmail.com>
+#
+#   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. This file is offered as-is, without any
+#   warranty.
+
+#serial 11
+
+AC_DEFUN([AX_CHECK_GNU_MAKE],dnl
+  [AC_PROG_AWK
+  AC_CACHE_CHECK([for GNU make],[_cv_gnu_make_command],[dnl
+    _cv_gnu_make_command="" ;
+dnl Search all the common names for GNU make
+    for a in "$MAKE" make gmake gnumake ; do
+      if test -z "$a" ; then continue ; fi ;
+      if "$a" --version 2> /dev/null | grep GNU 2>&1 > /dev/null ; then
+        _cv_gnu_make_command=$a ;
+        AX_CHECK_GNU_MAKE_HEADLINE=$("$a" --version 2> /dev/null | grep "GNU Make")
+        ax_check_gnu_make_version=$(echo ${AX_CHECK_GNU_MAKE_HEADLINE} | ${AWK} -F " " '{ print $(NF); }')
+        break ;
+      fi
+    done ;])
+dnl If there was a GNU version, then set @ifGNUmake@ to the empty string, '#' otherwise
+  AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifGNUmake], ["#"])],   [AS_VAR_SET([ifGNUmake], [""])])
+  AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifnGNUmake], [""])],   [AS_VAR_SET([ifGNUmake], ["#"])])
+  AS_VAR_IF([_cv_gnu_make_command], [""], [AS_UNSET(ax_cv_gnu_make_command)], [AS_VAR_SET([ax_cv_gnu_make_command], [${_cv_gnu_make_command}])])
+  AS_VAR_IF([_cv_gnu_make_command], [""],[$2],[$1])
+  AC_SUBST([ifGNUmake])
+  AC_SUBST([ifnGNUmake])
+])
diff --git a/m4/ax_check_link_flag.m4 b/m4/ax_check_link_flag.m4
new file mode 100644 (file)
index 0000000..03a30ce
--- /dev/null
@@ -0,0 +1,53 @@
+# ===========================================================================
+#    https://www.gnu.org/software/autoconf-archive/ax_check_link_flag.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_CHECK_LINK_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT])
+#
+# DESCRIPTION
+#
+#   Check whether the given FLAG works with the linker or gives an error.
+#   (Warnings, however, are ignored)
+#
+#   ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on
+#   success/failure.
+#
+#   If EXTRA-FLAGS is defined, it is added to the linker's default flags
+#   when the check is done.  The check is thus made with the flags: "LDFLAGS
+#   EXTRA-FLAGS FLAG".  This can for example be used to force the linker to
+#   issue an error when a bad flag is given.
+#
+#   INPUT gives an alternative input source to AC_LINK_IFELSE.
+#
+#   NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this
+#   macro in sync with AX_CHECK_{PREPROC,COMPILE}_FLAG.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Guido U. Draheim <guidod@gmx.de>
+#   Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 6
+
+AC_DEFUN([AX_CHECK_LINK_FLAG],
+[AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF
+AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_ldflags_$4_$1])dnl
+AC_CACHE_CHECK([whether the linker accepts $1], CACHEVAR, [
+  ax_check_save_flags=$LDFLAGS
+  LDFLAGS="$LDFLAGS $4 $1"
+  AC_LINK_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])],
+    [AS_VAR_SET(CACHEVAR,[yes])],
+    [AS_VAR_SET(CACHEVAR,[no])])
+  LDFLAGS=$ax_check_save_flags])
+AS_VAR_IF(CACHEVAR,yes,
+  [m4_default([$2], :)],
+  [m4_default([$3], :)])
+AS_VAR_POPDEF([CACHEVAR])dnl
+])dnl AX_CHECK_LINK_FLAGS
diff --git a/m4/ax_code_coverage.m4 b/m4/ax_code_coverage.m4
new file mode 100644 (file)
index 0000000..6d08319
--- /dev/null
@@ -0,0 +1,272 @@
+# ===========================================================================
+#     https://www.gnu.org/software/autoconf-archive/ax_code_coverage.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_CODE_COVERAGE()
+#
+# DESCRIPTION
+#
+#   Defines CODE_COVERAGE_CPPFLAGS, CODE_COVERAGE_CFLAGS,
+#   CODE_COVERAGE_CXXFLAGS and CODE_COVERAGE_LIBS which should be included
+#   in the CPPFLAGS, CFLAGS CXXFLAGS and LIBS/LIBADD variables of every
+#   build target (program or library) which should be built with code
+#   coverage support. Also add rules using AX_ADD_AM_MACRO_STATIC; and
+#   $enable_code_coverage which can be used in subsequent configure output.
+#   CODE_COVERAGE_ENABLED is defined and substituted, and corresponds to the
+#   value of the --enable-code-coverage option, which defaults to being
+#   disabled.
+#
+#   Test also for gcov program and create GCOV variable that could be
+#   substituted.
+#
+#   Note that all optimization flags in CFLAGS must be disabled when code
+#   coverage is enabled.
+#
+#   Usage example:
+#
+#   configure.ac:
+#
+#     AX_CODE_COVERAGE
+#
+#   Makefile.am:
+#
+#     include $(top_srcdir)/aminclude_static.am
+#
+#     my_program_LIBS = ... $(CODE_COVERAGE_LIBS) ...
+#     my_program_CPPFLAGS = ... $(CODE_COVERAGE_CPPFLAGS) ...
+#     my_program_CFLAGS = ... $(CODE_COVERAGE_CFLAGS) ...
+#     my_program_CXXFLAGS = ... $(CODE_COVERAGE_CXXFLAGS) ...
+#
+#     clean-local: code-coverage-clean
+#     distclean-local: code-coverage-dist-clean
+#
+#   This results in a "check-code-coverage" rule being added to any
+#   Makefile.am which do "include $(top_srcdir)/aminclude_static.am"
+#   (assuming the module has been configured with --enable-code-coverage).
+#   Running `make check-code-coverage` in that directory will run the
+#   module's test suite (`make check`) and build a code coverage report
+#   detailing the code which was touched, then print the URI for the report.
+#
+#   This code was derived from Makefile.decl in GLib, originally licensed
+#   under LGPLv2.1+.
+#
+# LICENSE
+#
+#   Copyright (c) 2012, 2016 Philip Withnall
+#   Copyright (c) 2012 Xan Lopez
+#   Copyright (c) 2012 Christian Persch
+#   Copyright (c) 2012 Paolo Borelli
+#   Copyright (c) 2012 Dan Winship
+#   Copyright (c) 2015,2018 Bastien ROUCARIES
+#
+#   This library is free software; you can redistribute it and/or modify it
+#   under the terms of the GNU Lesser General Public License as published by
+#   the Free Software Foundation; either version 2.1 of the License, or (at
+#   your option) any later version.
+#
+#   This library 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 Lesser
+#   General Public License for more details.
+#
+#   You should have received a copy of the GNU Lesser General Public License
+#   along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+#serial 33
+
+m4_define(_AX_CODE_COVERAGE_RULES,[
+AX_ADD_AM_MACRO_STATIC([
+# Code coverage
+#
+# Optional:
+#  - CODE_COVERAGE_DIRECTORY: Top-level directory for code coverage reporting.
+#    Multiple directories may be specified, separated by whitespace.
+#    (Default: \$(top_builddir))
+#  - CODE_COVERAGE_OUTPUT_FILE: Filename and path for the .info file generated
+#    by lcov for code coverage. (Default:
+#    \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info)
+#  - CODE_COVERAGE_OUTPUT_DIRECTORY: Directory for generated code coverage
+#    reports to be created. (Default:
+#    \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage)
+#  - CODE_COVERAGE_BRANCH_COVERAGE: Set to 1 to enforce branch coverage,
+#    set to 0 to disable it and leave empty to stay with the default.
+#    (Default: empty)
+#  - CODE_COVERAGE_LCOV_SHOPTS_DEFAULT: Extra options shared between both lcov
+#    instances. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE)
+#  - CODE_COVERAGE_LCOV_SHOPTS: Extra options to shared between both lcov
+#    instances. (Default: $CODE_COVERAGE_LCOV_SHOPTS_DEFAULT)
+#  - CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH: --gcov-tool pathtogcov
+#  - CODE_COVERAGE_LCOV_OPTIONS_DEFAULT: Extra options to pass to the
+#    collecting lcov instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH)
+#  - CODE_COVERAGE_LCOV_OPTIONS: Extra options to pass to the collecting lcov
+#    instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_DEFAULT)
+#  - CODE_COVERAGE_LCOV_RMOPTS_DEFAULT: Extra options to pass to the filtering
+#    lcov instance. (Default: empty)
+#  - CODE_COVERAGE_LCOV_RMOPTS: Extra options to pass to the filtering lcov
+#    instance. (Default: $CODE_COVERAGE_LCOV_RMOPTS_DEFAULT)
+#  - CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT: Extra options to pass to the
+#    genhtml instance. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE)
+#  - CODE_COVERAGE_GENHTML_OPTIONS: Extra options to pass to the genhtml
+#    instance. (Default: $CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT)
+#  - CODE_COVERAGE_IGNORE_PATTERN: Extra glob pattern of files to ignore
+#
+# The generated report will be titled using the \$(PACKAGE_NAME) and
+# \$(PACKAGE_VERSION). In order to add the current git hash to the title,
+# use the git-version-gen script, available online.
+# Optional variables
+# run only on top dir
+if CODE_COVERAGE_ENABLED
+ ifeq (\$(abs_builddir), \$(abs_top_builddir))
+CODE_COVERAGE_DIRECTORY ?= \$(top_builddir)
+CODE_COVERAGE_OUTPUT_FILE ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info
+CODE_COVERAGE_OUTPUT_DIRECTORY ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage
+
+CODE_COVERAGE_BRANCH_COVERAGE ?=
+CODE_COVERAGE_LCOV_SHOPTS_DEFAULT ?= \$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\
+--rc lcov_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE))
+CODE_COVERAGE_LCOV_SHOPTS ?= \$(CODE_COVERAGE_LCOV_SHOPTS_DEFAULT)
+CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH ?= --gcov-tool \"\$(GCOV)\"
+CODE_COVERAGE_LCOV_OPTIONS_DEFAULT ?= \$(CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH)
+CODE_COVERAGE_LCOV_OPTIONS ?= \$(CODE_COVERAGE_LCOV_OPTIONS_DEFAULT)
+CODE_COVERAGE_LCOV_RMOPTS_DEFAULT ?=
+CODE_COVERAGE_LCOV_RMOPTS ?= \$(CODE_COVERAGE_LCOV_RMOPTS_DEFAULT)
+CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT ?=\
+\$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\
+--rc genhtml_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE))
+CODE_COVERAGE_GENHTML_OPTIONS ?= \$(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT)
+CODE_COVERAGE_IGNORE_PATTERN ?=
+
+GITIGNOREFILES = \$(GITIGNOREFILES) \$(CODE_COVERAGE_OUTPUT_FILE) \$(CODE_COVERAGE_OUTPUT_DIRECTORY)
+code_coverage_v_lcov_cap = \$(code_coverage_v_lcov_cap_\$(V))
+code_coverage_v_lcov_cap_ = \$(code_coverage_v_lcov_cap_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_v_lcov_cap_0 = @echo \"  LCOV   --capture\" \$(CODE_COVERAGE_OUTPUT_FILE);
+code_coverage_v_lcov_ign = \$(code_coverage_v_lcov_ign_\$(V))
+code_coverage_v_lcov_ign_ = \$(code_coverage_v_lcov_ign_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_v_lcov_ign_0 = @echo \"  LCOV   --remove /tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN);
+code_coverage_v_genhtml = \$(code_coverage_v_genhtml_\$(V))
+code_coverage_v_genhtml_ = \$(code_coverage_v_genhtml_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_v_genhtml_0 = @echo \"  GEN   \" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\";
+code_coverage_quiet = \$(code_coverage_quiet_\$(V))
+code_coverage_quiet_ = \$(code_coverage_quiet_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_quiet_0 = --quiet
+
+# sanitizes the test-name: replaces with underscores: dashes and dots
+code_coverage_sanitize = \$(subst -,_,\$(subst .,_,\$(1)))
+
+# Use recursive makes in order to ignore errors during check
+check-code-coverage:
+       -\$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) -k check
+       \$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) code-coverage-capture
+
+# Capture code coverage data
+code-coverage-capture: code-coverage-capture-hook
+       \$(code_coverage_v_lcov_cap)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --capture --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" --test-name \"\$(call code_coverage_sanitize,\$(PACKAGE_NAME)-\$(PACKAGE_VERSION))\" --no-checksum --compat-libtool \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_OPTIONS)
+       \$(code_coverage_v_lcov_ign)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --remove \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"/tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN) --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_RMOPTS)
+       -@rm -f \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\"
+       \$(code_coverage_v_genhtml)LANG=C \$(GENHTML) \$(code_coverage_quiet) \$(addprefix --prefix ,\$(CODE_COVERAGE_DIRECTORY)) --output-directory \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\" --title \"\$(PACKAGE_NAME)-\$(PACKAGE_VERSION) Code Coverage\" --legend --show-details \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_GENHTML_OPTIONS)
+       @echo \"file://\$(abs_builddir)/\$(CODE_COVERAGE_OUTPUT_DIRECTORY)/index.html\"
+
+code-coverage-clean:
+       -\$(LCOV) --directory \$(top_builddir) -z
+       -rm -rf \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\"
+       -find . \\( -name \"*.gcda\" -o -name \"*.gcno\" -o -name \"*.gcov\" \\) -delete
+
+code-coverage-dist-clean:
+
+A][M_DISTCHECK_CONFIGURE_FLAGS := \$(A][M_DISTCHECK_CONFIGURE_FLAGS) --disable-code-coverage
+ else # ifneq (\$(abs_builddir), \$(abs_top_builddir))
+check-code-coverage:
+
+code-coverage-capture: code-coverage-capture-hook
+
+code-coverage-clean:
+
+code-coverage-dist-clean:
+ endif # ifeq (\$(abs_builddir), \$(abs_top_builddir))
+else #! CODE_COVERAGE_ENABLED
+# Use recursive makes in order to ignore errors during check
+check-code-coverage:
+       @echo \"Need to reconfigure with --enable-code-coverage\"
+# Capture code coverage data
+code-coverage-capture: code-coverage-capture-hook
+       @echo \"Need to reconfigure with --enable-code-coverage\"
+
+code-coverage-clean:
+
+code-coverage-dist-clean:
+
+endif #CODE_COVERAGE_ENABLED
+# Hook rule executed before code-coverage-capture, overridable by the user
+code-coverage-capture-hook:
+
+.PHONY: check-code-coverage code-coverage-capture code-coverage-dist-clean code-coverage-clean code-coverage-capture-hook
+])
+])
+
+AC_DEFUN([_AX_CODE_COVERAGE_ENABLED],[
+       AX_CHECK_GNU_MAKE([],[AC_MSG_ERROR([not using GNU make that is needed for coverage])])
+       AC_REQUIRE([AX_ADD_AM_MACRO_STATIC])
+       # check for gcov
+       AC_CHECK_TOOL([GCOV],
+                 [$_AX_CODE_COVERAGE_GCOV_PROG_WITH],
+                 [:])
+       AS_IF([test "X$GCOV" = "X:"],
+             [AC_MSG_ERROR([gcov is needed to do coverage])])
+       AC_SUBST([GCOV])
+
+       dnl Check if gcc is being used
+       AS_IF([ test "$GCC" = "no" ], [
+               AC_MSG_ERROR([not compiling with gcc, which is required for gcov code coverage])
+             ])
+
+       AC_CHECK_PROG([LCOV], [lcov], [lcov])
+       AC_CHECK_PROG([GENHTML], [genhtml], [genhtml])
+
+       AS_IF([ test x"$LCOV" = x ], [
+               AC_MSG_ERROR([To enable code coverage reporting you must have lcov installed])
+             ])
+
+       AS_IF([ test x"$GENHTML" = x ], [
+               AC_MSG_ERROR([Could not find genhtml from the lcov package])
+       ])
+
+       dnl Build the code coverage flags
+       dnl Define CODE_COVERAGE_LDFLAGS for backwards compatibility
+       CODE_COVERAGE_CPPFLAGS="-DNDEBUG"
+       CODE_COVERAGE_CFLAGS="-O0 -g -fprofile-arcs -ftest-coverage"
+       CODE_COVERAGE_CXXFLAGS="-O0 -g -fprofile-arcs -ftest-coverage"
+       CODE_COVERAGE_LIBS="-lgcov"
+
+       AC_SUBST([CODE_COVERAGE_CPPFLAGS])
+       AC_SUBST([CODE_COVERAGE_CFLAGS])
+       AC_SUBST([CODE_COVERAGE_CXXFLAGS])
+       AC_SUBST([CODE_COVERAGE_LIBS])
+])
+
+AC_DEFUN([AX_CODE_COVERAGE],[
+       dnl Check for --enable-code-coverage
+
+       # allow to override gcov location
+       AC_ARG_WITH([gcov],
+         [AS_HELP_STRING([--with-gcov[=GCOV]], [use given GCOV for coverage (GCOV=gcov).])],
+         [_AX_CODE_COVERAGE_GCOV_PROG_WITH=$with_gcov],
+         [_AX_CODE_COVERAGE_GCOV_PROG_WITH=gcov])
+
+       AC_MSG_CHECKING([whether to build with code coverage support])
+       AC_ARG_ENABLE([code-coverage],
+         AS_HELP_STRING([--enable-code-coverage],
+         [Whether to enable code coverage support]),,
+         enable_code_coverage=no)
+
+       AM_CONDITIONAL([CODE_COVERAGE_ENABLED], [test "x$enable_code_coverage" = xyes])
+       AC_SUBST([CODE_COVERAGE_ENABLED], [$enable_code_coverage])
+       AC_MSG_RESULT($enable_code_coverage)
+
+       AS_IF([ test "x$enable_code_coverage" = xyes ], [
+               _AX_CODE_COVERAGE_ENABLED
+             ])
+
+       _AX_CODE_COVERAGE_RULES
+])
diff --git a/m4/ax_compiler_flags.m4 b/m4/ax_compiler_flags.m4
new file mode 100644 (file)
index 0000000..ddb0456
--- /dev/null
@@ -0,0 +1,158 @@
+# ===========================================================================
+#    https://www.gnu.org/software/autoconf-archive/ax_compiler_flags.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_COMPILER_FLAGS([CFLAGS-VARIABLE], [LDFLAGS-VARIABLE], [IS-RELEASE], [EXTRA-BASE-CFLAGS], [EXTRA-YES-CFLAGS], [UNUSED], [UNUSED], [UNUSED], [EXTRA-BASE-LDFLAGS], [EXTRA-YES-LDFLAGS], [UNUSED], [UNUSED], [UNUSED])
+#
+# DESCRIPTION
+#
+#   Check for the presence of an --enable-compile-warnings option to
+#   configure, defaulting to "error" in normal operation, or "yes" if
+#   IS-RELEASE is equal to "yes".  Return the value in the variable
+#   $ax_enable_compile_warnings.
+#
+#   Depending on the value of --enable-compile-warnings, different compiler
+#   warnings are checked to see if they work with the current compiler and,
+#   if so, are appended to CFLAGS-VARIABLE and LDFLAGS-VARIABLE.  This
+#   allows a consistent set of baseline compiler warnings to be used across
+#   a code base, irrespective of any warnings enabled locally by individual
+#   developers.  By standardising the warnings used by all developers of a
+#   project, the project can commit to a zero-warnings policy, using -Werror
+#   to prevent compilation if new warnings are introduced.  This makes
+#   catching bugs which are flagged by warnings a lot easier.
+#
+#   By providing a consistent --enable-compile-warnings argument across all
+#   projects using this macro, continuous integration systems can easily be
+#   configured the same for all projects.  Automated systems or build
+#   systems aimed at beginners may want to pass the --disable-Werror
+#   argument to unconditionally prevent warnings being fatal.
+#
+#   --enable-compile-warnings can take the values:
+#
+#    * no:      Base compiler warnings only; not even -Wall.
+#    * yes:     The above, plus a broad range of useful warnings.
+#    * error:   The above, plus -Werror so that all warnings are fatal.
+#               Use --disable-Werror to override this and disable fatal
+#               warnings.
+#
+#   The set of base and enabled flags can be augmented using the
+#   EXTRA-*-CFLAGS and EXTRA-*-LDFLAGS variables, which are tested and
+#   appended to the output variable if --enable-compile-warnings is not
+#   "no". Flags should not be disabled using these arguments, as the entire
+#   point of AX_COMPILER_FLAGS is to enforce a consistent set of useful
+#   compiler warnings on code, using warnings which have been chosen for low
+#   false positive rates.  If a compiler emits false positives for a
+#   warning, a #pragma should be used in the code to disable the warning
+#   locally. See:
+#
+#     https://gcc.gnu.org/onlinedocs/gcc-4.9.2/gcc/Diagnostic-Pragmas.html#Diagnostic-Pragmas
+#
+#   The EXTRA-* variables should only be used to supply extra warning flags,
+#   and not general purpose compiler flags, as they are controlled by
+#   configure options such as --disable-Werror.
+#
+#   IS-RELEASE can be used to disable -Werror when making a release, which
+#   is useful for those hairy moments when you just want to get the release
+#   done as quickly as possible.  Set it to "yes" to disable -Werror. By
+#   default, it uses the value of $ax_is_release, so if you are using the
+#   AX_IS_RELEASE macro, there is no need to pass this parameter. For
+#   example:
+#
+#     AX_IS_RELEASE([git-directory])
+#     AX_COMPILER_FLAGS()
+#
+#   CFLAGS-VARIABLE defaults to WARN_CFLAGS, and LDFLAGS-VARIABLE defaults
+#   to WARN_LDFLAGS.  Both variables are AC_SUBST-ed by this macro, but must
+#   be manually added to the CFLAGS and LDFLAGS variables for each target in
+#   the code base.
+#
+#   If C++ language support is enabled with AC_PROG_CXX, which must occur
+#   before this macro in configure.ac, warning flags for the C++ compiler
+#   are AC_SUBST-ed as WARN_CXXFLAGS, and must be manually added to the
+#   CXXFLAGS variables for each target in the code base.  EXTRA-*-CFLAGS can
+#   be used to augment the base and enabled flags.
+#
+#   Warning flags for g-ir-scanner (from GObject Introspection) are
+#   AC_SUBST-ed as WARN_SCANNERFLAGS.  This variable must be manually added
+#   to the SCANNERFLAGS variable for each GIR target in the code base.  If
+#   extra g-ir-scanner flags need to be enabled, the AX_COMPILER_FLAGS_GIR
+#   macro must be invoked manually.
+#
+#   AX_COMPILER_FLAGS may add support for other tools in future, in addition
+#   to the compiler and linker.  No extra EXTRA-* variables will be added
+#   for those tools, and all extra support will still use the single
+#   --enable-compile-warnings configure option.  For finer grained control
+#   over the flags for individual tools, use AX_COMPILER_FLAGS_CFLAGS,
+#   AX_COMPILER_FLAGS_LDFLAGS and AX_COMPILER_FLAGS_* for new tools.
+#
+#   The UNUSED variables date from a previous version of this macro, and are
+#   automatically appended to the preceding non-UNUSED variable. They should
+#   be left empty in new uses of the macro.
+#
+# LICENSE
+#
+#   Copyright (c) 2014, 2015 Philip Withnall <philip@tecnocode.co.uk>
+#   Copyright (c) 2015 David King <amigadave@amigadave.com>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 14
+
+# _AX_COMPILER_FLAGS_LANG([LANGNAME])
+m4_defun([_AX_COMPILER_FLAGS_LANG],
+[m4_ifdef([_AX_COMPILER_FLAGS_LANG_]$1[_enabled], [],
+          [m4_define([_AX_COMPILER_FLAGS_LANG_]$1[_enabled], [])dnl
+           AX_REQUIRE_DEFINED([AX_COMPILER_FLAGS_]$1[FLAGS])])dnl
+])
+
+AC_DEFUN([AX_COMPILER_FLAGS],[
+    # C support is enabled by default.
+    _AX_COMPILER_FLAGS_LANG([C])
+    # Only enable C++ support if AC_PROG_CXX is called. The redefinition of
+    # AC_PROG_CXX is so that a fatal error is emitted if this macro is called
+    # before AC_PROG_CXX, which would otherwise cause no C++ warnings to be
+    # checked.
+    AC_PROVIDE_IFELSE([AC_PROG_CXX],
+                      [_AX_COMPILER_FLAGS_LANG([CXX])],
+                      [m4_define([AC_PROG_CXX], defn([AC_PROG_CXX])[_AX_COMPILER_FLAGS_LANG([CXX])])])
+    AX_REQUIRE_DEFINED([AX_COMPILER_FLAGS_LDFLAGS])
+
+    # Default value for IS-RELEASE is $ax_is_release
+    ax_compiler_flags_is_release=m4_tolower(m4_normalize(ifelse([$3],,
+                                                                [$ax_is_release],
+                                                                [$3])))
+
+    AC_ARG_ENABLE([compile-warnings],
+                  AS_HELP_STRING([--enable-compile-warnings=@<:@no/yes/error@:>@],
+                                 [Enable compiler warnings and errors]),,
+                  [AS_IF([test "$ax_compiler_flags_is_release" = "yes"],
+                         [enable_compile_warnings="yes"],
+                         [enable_compile_warnings="error"])])
+    AC_ARG_ENABLE([Werror],
+                  AS_HELP_STRING([--disable-Werror],
+                                 [Unconditionally make all compiler warnings non-fatal]),,
+                  [enable_Werror=maybe])
+
+    # Return the user's chosen warning level
+    AS_IF([test "$enable_Werror" = "no" -a \
+                "$enable_compile_warnings" = "error"],[
+        enable_compile_warnings="yes"
+    ])
+
+    ax_enable_compile_warnings=$enable_compile_warnings
+
+    AX_COMPILER_FLAGS_CFLAGS([$1],[$ax_compiler_flags_is_release],
+                             [$4],[$5 $6 $7 $8])
+    m4_ifdef([_AX_COMPILER_FLAGS_LANG_CXX_enabled],
+             [AX_COMPILER_FLAGS_CXXFLAGS([WARN_CXXFLAGS],
+                                         [$ax_compiler_flags_is_release],
+                                         [$4],[$5 $6 $7 $8])])
+    AX_COMPILER_FLAGS_LDFLAGS([$2],[$ax_compiler_flags_is_release],
+                              [$9],[$10 $11 $12 $13])
+    AX_COMPILER_FLAGS_GIR([WARN_SCANNERFLAGS],[$ax_compiler_flags_is_release])
+])dnl AX_COMPILER_FLAGS
diff --git a/m4/ax_compiler_flags_cflags.m4 b/m4/ax_compiler_flags_cflags.m4
new file mode 100644 (file)
index 0000000..916f918
--- /dev/null
@@ -0,0 +1,161 @@
+# =============================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_cflags.html
+# =============================================================================
+#
+# SYNOPSIS
+#
+#   AX_COMPILER_FLAGS_CFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS])
+#
+# DESCRIPTION
+#
+#   Add warning flags for the C compiler to VARIABLE, which defaults to
+#   WARN_CFLAGS.  VARIABLE is AC_SUBST-ed by this macro, but must be
+#   manually added to the CFLAGS variable for each target in the code base.
+#
+#   This macro depends on the environment set up by AX_COMPILER_FLAGS.
+#   Specifically, it uses the value of $ax_enable_compile_warnings to decide
+#   which flags to enable.
+#
+# LICENSE
+#
+#   Copyright (c) 2014, 2015 Philip Withnall <philip@tecnocode.co.uk>
+#   Copyright (c) 2017, 2018 Reini Urban <rurban@cpan.org>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 17
+
+AC_DEFUN([AX_COMPILER_FLAGS_CFLAGS],[
+    AC_REQUIRE([AC_PROG_SED])
+    AX_REQUIRE_DEFINED([AX_APPEND_COMPILE_FLAGS])
+    AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+    AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG])
+
+    # Variable names
+    m4_define([ax_warn_cflags_variable],
+              [m4_normalize(ifelse([$1],,[WARN_CFLAGS],[$1]))])
+
+    AC_LANG_PUSH([C])
+
+    AC_COMPILE_IFELSE([AC_LANG_PROGRAM([
+      [#ifndef __cplusplus
+       #error "no C++"
+       #endif]])],
+      [ax_compiler_cxx=yes;],
+      [ax_compiler_cxx=no;])
+
+    # Always pass -Werror=unknown-warning-option to get Clang to fail on bad
+    # flags, otherwise they are always appended to the warn_cflags variable, and
+    # Clang warns on them for every compilation unit.
+    # If this is passed to GCC, it will explode, so the flag must be enabled
+    # conditionally.
+    AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[
+        ax_compiler_flags_test="-Werror=unknown-warning-option"
+    ],[
+        ax_compiler_flags_test=""
+    ])
+
+    # Check that -Wno-suggest-attribute=format is supported
+    AX_CHECK_COMPILE_FLAG([-Wno-suggest-attribute=format],[
+        ax_compiler_no_suggest_attribute_flags="-Wno-suggest-attribute=format"
+    ],[
+        ax_compiler_no_suggest_attribute_flags=""
+    ])
+
+    # Base flags
+    AX_APPEND_COMPILE_FLAGS([ dnl
+        -fno-strict-aliasing dnl
+        $3 dnl
+    ],ax_warn_cflags_variable,[$ax_compiler_flags_test])
+
+    AS_IF([test "$ax_enable_compile_warnings" != "no"],[
+        if test "$ax_compiler_cxx" = "no" ; then
+            # C-only flags. Warn in C++
+            AX_APPEND_COMPILE_FLAGS([ dnl
+                -Wnested-externs dnl
+                -Wmissing-prototypes dnl
+                -Wstrict-prototypes dnl
+                -Wdeclaration-after-statement dnl
+                -Wimplicit-function-declaration dnl
+                -Wold-style-definition dnl
+                -Wjump-misses-init dnl
+            ],ax_warn_cflags_variable,[$ax_compiler_flags_test])
+        fi
+
+        # "yes" flags
+        AX_APPEND_COMPILE_FLAGS([ dnl
+            -Wall dnl
+            -Wextra dnl
+            -Wundef dnl
+            -Wwrite-strings dnl
+            -Wpointer-arith dnl
+            -Wmissing-declarations dnl
+            -Wredundant-decls dnl
+            -Wno-unused-parameter dnl
+            -Wno-missing-field-initializers dnl
+            -Wformat=2 dnl
+            -Wcast-align dnl
+            -Wformat-nonliteral dnl
+            -Wformat-security dnl
+            -Wsign-compare dnl
+            -Wstrict-aliasing dnl
+            -Wshadow dnl
+            -Winline dnl
+            -Wpacked dnl
+            -Wmissing-format-attribute dnl
+            -Wmissing-noreturn dnl
+            -Winit-self dnl
+            -Wredundant-decls dnl
+            -Wmissing-include-dirs dnl
+            -Wunused-but-set-variable dnl
+            -Warray-bounds dnl
+            -Wreturn-type dnl
+            -Wswitch-enum dnl
+            -Wswitch-default dnl
+            -Wduplicated-cond dnl
+            -Wduplicated-branches dnl
+            -Wlogical-op dnl
+            -Wrestrict dnl
+            -Wnull-dereference dnl
+            -Wdouble-promotion dnl
+            $4 dnl
+            $5 dnl
+            $6 dnl
+            $7 dnl
+        ],ax_warn_cflags_variable,[$ax_compiler_flags_test])
+    ])
+    AS_IF([test "$ax_enable_compile_warnings" = "error"],[
+        # "error" flags; -Werror has to be appended unconditionally because
+        # it's not possible to test for
+        #
+        # suggest-attribute=format is disabled because it gives too many false
+        # positives
+        AX_APPEND_FLAG([-Werror],ax_warn_cflags_variable)
+
+        AX_APPEND_COMPILE_FLAGS([ dnl
+            [$ax_compiler_no_suggest_attribute_flags] dnl
+        ],ax_warn_cflags_variable,[$ax_compiler_flags_test])
+    ])
+
+    # In the flags below, when disabling specific flags, always add *both*
+    # -Wno-foo and -Wno-error=foo. This fixes the situation where (for example)
+    # we enable -Werror, disable a flag, and a build bot passes CFLAGS=-Wall,
+    # which effectively turns that flag back on again as an error.
+    for flag in $ax_warn_cflags_variable; do
+        AS_CASE([$flag],
+                [-Wno-*=*],[],
+                [-Wno-*],[
+                    AX_APPEND_COMPILE_FLAGS([-Wno-error=$(AS_ECHO([$flag]) | $SED 's/^-Wno-//')],
+                                            ax_warn_cflags_variable,
+                                            [$ax_compiler_flags_test])
+                ])
+    done
+
+    AC_LANG_POP([C])
+
+    # Substitute the variables
+    AC_SUBST(ax_warn_cflags_variable)
+])dnl AX_COMPILER_FLAGS
diff --git a/m4/ax_compiler_flags_cxxflags.m4 b/m4/ax_compiler_flags_cxxflags.m4
new file mode 100644 (file)
index 0000000..3067d9b
--- /dev/null
@@ -0,0 +1,136 @@
+# ===============================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_cxxflags.html
+# ===============================================================================
+#
+# SYNOPSIS
+#
+#   AX_COMPILER_FLAGS_CXXFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS])
+#
+# DESCRIPTION
+#
+#   Add warning flags for the C++ compiler to VARIABLE, which defaults to
+#   WARN_CXXFLAGS.  VARIABLE is AC_SUBST-ed by this macro, but must be
+#   manually added to the CXXFLAGS variable for each target in the code
+#   base.
+#
+#   This macro depends on the environment set up by AX_COMPILER_FLAGS.
+#   Specifically, it uses the value of $ax_enable_compile_warnings to decide
+#   which flags to enable.
+#
+# LICENSE
+#
+#   Copyright (c) 2015 David King <amigadave@amigadave.com>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 10
+
+AC_DEFUN([AX_COMPILER_FLAGS_CXXFLAGS],[
+    AC_REQUIRE([AC_PROG_SED])
+    AX_REQUIRE_DEFINED([AX_APPEND_COMPILE_FLAGS])
+    AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+    AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG])
+
+    # Variable names
+    m4_define([ax_warn_cxxflags_variable],
+              [m4_normalize(ifelse([$1],,[WARN_CXXFLAGS],[$1]))])
+
+    AC_LANG_PUSH([C++])
+
+    # Always pass -Werror=unknown-warning-option to get Clang to fail on bad
+    # flags, otherwise they are always appended to the warn_cxxflags variable,
+    # and Clang warns on them for every compilation unit.
+    # If this is passed to GCC, it will explode, so the flag must be enabled
+    # conditionally.
+    AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[
+        ax_compiler_flags_test="-Werror=unknown-warning-option"
+    ],[
+        ax_compiler_flags_test=""
+    ])
+
+    # Check that -Wno-suggest-attribute=format is supported
+    AX_CHECK_COMPILE_FLAG([-Wno-suggest-attribute=format],[
+        ax_compiler_no_suggest_attribute_flags="-Wno-suggest-attribute=format"
+    ],[
+        ax_compiler_no_suggest_attribute_flags=""
+    ])
+
+    # Base flags
+    AX_APPEND_COMPILE_FLAGS([ dnl
+        -fno-strict-aliasing dnl
+        $3 dnl
+    ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test])
+
+    AS_IF([test "$ax_enable_compile_warnings" != "no"],[
+        # "yes" flags
+        AX_APPEND_COMPILE_FLAGS([ dnl
+            -Wall dnl
+            -Wextra dnl
+            -Wundef dnl
+            -Wwrite-strings dnl
+            -Wpointer-arith dnl
+            -Wmissing-declarations dnl
+            -Wredundant-decls dnl
+            -Wno-unused-parameter dnl
+            -Wno-missing-field-initializers dnl
+            -Wformat=2 dnl
+            -Wcast-align dnl
+            -Wformat-nonliteral dnl
+            -Wformat-security dnl
+            -Wsign-compare dnl
+            -Wstrict-aliasing dnl
+            -Wshadow dnl
+            -Winline dnl
+            -Wpacked dnl
+            -Wmissing-format-attribute dnl
+            -Wmissing-noreturn dnl
+            -Winit-self dnl
+            -Wredundant-decls dnl
+            -Wmissing-include-dirs dnl
+            -Wunused-but-set-variable dnl
+            -Warray-bounds dnl
+            -Wreturn-type dnl
+            -Wno-overloaded-virtual dnl
+            -Wswitch-enum dnl
+            -Wswitch-default dnl
+            $4 dnl
+            $5 dnl
+            $6 dnl
+            $7 dnl
+        ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test])
+    ])
+    AS_IF([test "$ax_enable_compile_warnings" = "error"],[
+        # "error" flags; -Werror has to be appended unconditionally because
+        # it's not possible to test for
+        #
+        # suggest-attribute=format is disabled because it gives too many false
+        # positives
+        AX_APPEND_FLAG([-Werror],ax_warn_cxxflags_variable)
+
+        AX_APPEND_COMPILE_FLAGS([ dnl
+            [$ax_compiler_no_suggest_attribute_flags] dnl
+        ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test])
+    ])
+
+    # In the flags below, when disabling specific flags, always add *both*
+    # -Wno-foo and -Wno-error=foo. This fixes the situation where (for example)
+    # we enable -Werror, disable a flag, and a build bot passes CXXFLAGS=-Wall,
+    # which effectively turns that flag back on again as an error.
+    for flag in $ax_warn_cxxflags_variable; do
+        AS_CASE([$flag],
+                [-Wno-*=*],[],
+                [-Wno-*],[
+                    AX_APPEND_COMPILE_FLAGS([-Wno-error=$(AS_ECHO([$flag]) | $SED 's/^-Wno-//')],
+                                            ax_warn_cxxflags_variable,
+                                            [$ax_compiler_flags_test])
+                ])
+    done
+
+    AC_LANG_POP([C++])
+
+    # Substitute the variables
+    AC_SUBST(ax_warn_cxxflags_variable)
+])dnl AX_COMPILER_FLAGS_CXXFLAGS
diff --git a/m4/ax_compiler_flags_gir.m4 b/m4/ax_compiler_flags_gir.m4
new file mode 100644 (file)
index 0000000..5b4924a
--- /dev/null
@@ -0,0 +1,60 @@
+# ===========================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_gir.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_COMPILER_FLAGS_GIR([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS])
+#
+# DESCRIPTION
+#
+#   Add warning flags for the g-ir-scanner (from GObject Introspection) to
+#   VARIABLE, which defaults to WARN_SCANNERFLAGS.  VARIABLE is AC_SUBST-ed
+#   by this macro, but must be manually added to the SCANNERFLAGS variable
+#   for each GIR target in the code base.
+#
+#   This macro depends on the environment set up by AX_COMPILER_FLAGS.
+#   Specifically, it uses the value of $ax_enable_compile_warnings to decide
+#   which flags to enable.
+#
+# LICENSE
+#
+#   Copyright (c) 2015 Philip Withnall <philip@tecnocode.co.uk>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 6
+
+AC_DEFUN([AX_COMPILER_FLAGS_GIR],[
+    AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+
+    # Variable names
+    m4_define([ax_warn_scannerflags_variable],
+              [m4_normalize(ifelse([$1],,[WARN_SCANNERFLAGS],[$1]))])
+
+    # Base flags
+    AX_APPEND_FLAG([$3],ax_warn_scannerflags_variable)
+
+    AS_IF([test "$ax_enable_compile_warnings" != "no"],[
+        # "yes" flags
+        AX_APPEND_FLAG([ dnl
+            --warn-all dnl
+            $4 dnl
+            $5 dnl
+            $6 dnl
+            $7 dnl
+        ],ax_warn_scannerflags_variable)
+    ])
+    AS_IF([test "$ax_enable_compile_warnings" = "error"],[
+        # "error" flags
+        AX_APPEND_FLAG([ dnl
+            --warn-error dnl
+        ],ax_warn_scannerflags_variable)
+    ])
+
+    # Substitute the variables
+    AC_SUBST(ax_warn_scannerflags_variable)
+])dnl AX_COMPILER_FLAGS
diff --git a/m4/ax_compiler_flags_ldflags.m4 b/m4/ax_compiler_flags_ldflags.m4
new file mode 100644 (file)
index 0000000..976d119
--- /dev/null
@@ -0,0 +1,111 @@
+# ==============================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_ldflags.html
+# ==============================================================================
+#
+# SYNOPSIS
+#
+#   AX_COMPILER_FLAGS_LDFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS])
+#
+# DESCRIPTION
+#
+#   Add warning flags for the linker to VARIABLE, which defaults to
+#   WARN_LDFLAGS.  VARIABLE is AC_SUBST-ed by this macro, but must be
+#   manually added to the LDFLAGS variable for each target in the code base.
+#
+#   This macro depends on the environment set up by AX_COMPILER_FLAGS.
+#   Specifically, it uses the value of $ax_enable_compile_warnings to decide
+#   which flags to enable.
+#
+# LICENSE
+#
+#   Copyright (c) 2014, 2015 Philip Withnall <philip@tecnocode.co.uk>
+#   Copyright (c) 2017, 2018 Reini Urban <rurban@cpan.org>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 9
+
+AC_DEFUN([AX_COMPILER_FLAGS_LDFLAGS],[
+    AX_REQUIRE_DEFINED([AX_APPEND_LINK_FLAGS])
+    AX_REQUIRE_DEFINED([AX_APPEND_FLAG])
+    AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG])
+    AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG])
+
+    # Variable names
+    m4_define([ax_warn_ldflags_variable],
+              [m4_normalize(ifelse([$1],,[WARN_LDFLAGS],[$1]))])
+
+    # Always pass -Werror=unknown-warning-option to get Clang to fail on bad
+    # flags, otherwise they are always appended to the warn_ldflags variable,
+    # and Clang warns on them for every compilation unit.
+    # If this is passed to GCC, it will explode, so the flag must be enabled
+    # conditionally.
+    AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[
+        ax_compiler_flags_test="-Werror=unknown-warning-option"
+    ],[
+        ax_compiler_flags_test=""
+    ])
+
+    AX_CHECK_LINK_FLAG([-Wl,--as-needed], [
+        AX_APPEND_LINK_FLAGS([-Wl,--as-needed],
+          [AM_LDFLAGS],[$ax_compiler_flags_test])
+    ])
+    AX_CHECK_LINK_FLAG([-Wl,-z,relro], [
+        AX_APPEND_LINK_FLAGS([-Wl,-z,relro],
+          [AM_LDFLAGS],[$ax_compiler_flags_test])
+    ])
+    AX_CHECK_LINK_FLAG([-Wl,-z,now], [
+        AX_APPEND_LINK_FLAGS([-Wl,-z,now],
+          [AM_LDFLAGS],[$ax_compiler_flags_test])
+    ])
+    AX_CHECK_LINK_FLAG([-Wl,-z,noexecstack], [
+        AX_APPEND_LINK_FLAGS([-Wl,-z,noexecstack],
+          [AM_LDFLAGS],[$ax_compiler_flags_test])
+    ])
+    # textonly, retpolineplt not yet
+
+    # macOS and cygwin linker do not have --as-needed
+    AX_CHECK_LINK_FLAG([-Wl,--no-as-needed], [
+        ax_compiler_flags_as_needed_option="-Wl,--no-as-needed"
+    ], [
+        ax_compiler_flags_as_needed_option=""
+    ])
+
+    # macOS linker speaks with a different accent
+    ax_compiler_flags_fatal_warnings_option=""
+    AX_CHECK_LINK_FLAG([-Wl,--fatal-warnings], [
+        ax_compiler_flags_fatal_warnings_option="-Wl,--fatal-warnings"
+    ])
+    AX_CHECK_LINK_FLAG([-Wl,-fatal_warnings], [
+        ax_compiler_flags_fatal_warnings_option="-Wl,-fatal_warnings"
+    ])
+
+    # Base flags
+    AX_APPEND_LINK_FLAGS([ dnl
+        $ax_compiler_flags_as_needed_option dnl
+        $3 dnl
+    ],ax_warn_ldflags_variable,[$ax_compiler_flags_test])
+
+    AS_IF([test "$ax_enable_compile_warnings" != "no"],[
+        # "yes" flags
+        AX_APPEND_LINK_FLAGS([$4 $5 $6 $7],
+                                ax_warn_ldflags_variable,
+                                [$ax_compiler_flags_test])
+    ])
+    AS_IF([test "$ax_enable_compile_warnings" = "error"],[
+        # "error" flags; -Werror has to be appended unconditionally because
+        # it's not possible to test for
+        #
+        # suggest-attribute=format is disabled because it gives too many false
+        # positives
+        AX_APPEND_LINK_FLAGS([ dnl
+            $ax_compiler_flags_fatal_warnings_option dnl
+        ],ax_warn_ldflags_variable,[$ax_compiler_flags_test])
+    ])
+
+    # Substitute the variables
+    AC_SUBST(ax_warn_ldflags_variable)
+])dnl AX_COMPILER_FLAGS
diff --git a/m4/ax_cxx_compile_stdcxx.m4 b/m4/ax_cxx_compile_stdcxx.m4
new file mode 100644 (file)
index 0000000..9e9eaed
--- /dev/null
@@ -0,0 +1,948 @@
+# ===========================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_CXX_COMPILE_STDCXX(VERSION, [ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+#   Check for baseline language coverage in the compiler for the specified
+#   version of the C++ standard.  If necessary, add switches to CXX and
+#   CXXCPP to enable support.  VERSION may be '11' (for the C++11 standard)
+#   or '14' (for the C++14 standard).
+#
+#   The second argument, if specified, indicates whether you insist on an
+#   extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g.
+#   -std=c++11).  If neither is specified, you get whatever works, with
+#   preference for an extended mode.
+#
+#   The third argument, if specified 'mandatory' or if left unspecified,
+#   indicates that baseline support for the specified C++ standard is
+#   required and that the macro should error out if no mode with that
+#   support is found.  If specified 'optional', then configuration proceeds
+#   regardless, after defining HAVE_CXX${VERSION} if and only if a
+#   supporting mode is found.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Benjamin Kosnik <bkoz@redhat.com>
+#   Copyright (c) 2012 Zack Weinberg <zackw@panix.com>
+#   Copyright (c) 2013 Roy Stogner <roystgnr@ices.utexas.edu>
+#   Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov <sokolov@google.com>
+#   Copyright (c) 2015 Paul Norman <penorman@mac.com>
+#   Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+#   Copyright (c) 2016, 2018 Krzesimir Nowak <qdlacz@gmail.com>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 10
+
+dnl  This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro
+dnl  (serial version number 13).
+
+AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl
+  m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"],
+        [$1], [14], [ax_cxx_compile_alternatives="14 1y"],
+        [$1], [17], [ax_cxx_compile_alternatives="17 1z"],
+        [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl
+  m4_if([$2], [], [],
+        [$2], [ext], [],
+        [$2], [noext], [],
+        [m4_fatal([invalid second argument `$2' to AX_CXX_COMPILE_STDCXX])])dnl
+  m4_if([$3], [], [ax_cxx_compile_cxx$1_required=true],
+        [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true],
+        [$3], [optional], [ax_cxx_compile_cxx$1_required=false],
+        [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])])
+  AC_LANG_PUSH([C++])dnl
+  ac_success=no
+
+  m4_if([$2], [noext], [], [dnl
+  if test x$ac_success = xno; then
+    for alternative in ${ax_cxx_compile_alternatives}; do
+      switch="-std=gnu++${alternative}"
+      cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+      AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+                     $cachevar,
+        [ac_save_CXX="$CXX"
+         CXX="$CXX $switch"
+         AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+          [eval $cachevar=yes],
+          [eval $cachevar=no])
+         CXX="$ac_save_CXX"])
+      if eval test x\$$cachevar = xyes; then
+        CXX="$CXX $switch"
+        if test -n "$CXXCPP" ; then
+          CXXCPP="$CXXCPP $switch"
+        fi
+        ac_success=yes
+        break
+      fi
+    done
+  fi])
+
+  m4_if([$2], [ext], [], [dnl
+  if test x$ac_success = xno; then
+    dnl HP's aCC needs +std=c++11 according to:
+    dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf
+    dnl Cray's crayCC needs "-h std=c++11"
+    for alternative in ${ax_cxx_compile_alternatives}; do
+      for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do
+        cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+        AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+                       $cachevar,
+          [ac_save_CXX="$CXX"
+           CXX="$CXX $switch"
+           AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+            [eval $cachevar=yes],
+            [eval $cachevar=no])
+           CXX="$ac_save_CXX"])
+        if eval test x\$$cachevar = xyes; then
+          CXX="$CXX $switch"
+          if test -n "$CXXCPP" ; then
+            CXXCPP="$CXXCPP $switch"
+          fi
+          ac_success=yes
+          break
+        fi
+      done
+      if test x$ac_success = xyes; then
+        break
+      fi
+    done
+  fi])
+  AC_LANG_POP([C++])
+  if test x$ax_cxx_compile_cxx$1_required = xtrue; then
+    if test x$ac_success = xno; then
+      AC_MSG_ERROR([*** A compiler with support for C++$1 language features is required.])
+    fi
+  fi
+  if test x$ac_success = xno; then
+    HAVE_CXX$1=0
+    AC_MSG_NOTICE([No compiler with C++$1 support was found])
+  else
+    HAVE_CXX$1=1
+    AC_DEFINE(HAVE_CXX$1,1,
+              [define if the compiler supports basic C++$1 syntax])
+  fi
+  AC_SUBST(HAVE_CXX$1)
+])
+
+
+dnl  Test body for checking C++11 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11],
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+)
+
+
+dnl  Test body for checking C++14 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14],
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+)
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17],
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+  _AX_CXX_COMPILE_STDCXX_testbody_new_in_17
+)
+
+dnl  Tests for new features in C++11
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[
+
+// If the compiler admits that it is not ready for C++11, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201103L
+
+#error "This is not a C++11 compiler"
+
+#else
+
+namespace cxx11
+{
+
+  namespace test_static_assert
+  {
+
+    template <typename T>
+    struct check
+    {
+      static_assert(sizeof(int) <= sizeof(T), "not big enough");
+    };
+
+  }
+
+  namespace test_final_override
+  {
+
+    struct Base
+    {
+      virtual void f() {}
+    };
+
+    struct Derived : public Base
+    {
+      virtual void f() override {}
+    };
+
+  }
+
+  namespace test_double_right_angle_brackets
+  {
+
+    template < typename T >
+    struct check {};
+
+    typedef check<void> single_type;
+    typedef check<check<void>> double_type;
+    typedef check<check<check<void>>> triple_type;
+    typedef check<check<check<check<void>>>> quadruple_type;
+
+  }
+
+  namespace test_decltype
+  {
+
+    int
+    f()
+    {
+      int a = 1;
+      decltype(a) b = 2;
+      return a + b;
+    }
+
+  }
+
+  namespace test_type_deduction
+  {
+
+    template < typename T1, typename T2 >
+    struct is_same
+    {
+      static const bool value = false;
+    };
+
+    template < typename T >
+    struct is_same<T, T>
+    {
+      static const bool value = true;
+    };
+
+    template < typename T1, typename T2 >
+    auto
+    add(T1 a1, T2 a2) -> decltype(a1 + a2)
+    {
+      return a1 + a2;
+    }
+
+    int
+    test(const int c, volatile int v)
+    {
+      static_assert(is_same<int, decltype(0)>::value == true, "");
+      static_assert(is_same<int, decltype(c)>::value == false, "");
+      static_assert(is_same<int, decltype(v)>::value == false, "");
+      auto ac = c;
+      auto av = v;
+      auto sumi = ac + av + 'x';
+      auto sumf = ac + av + 1.0;
+      static_assert(is_same<int, decltype(ac)>::value == true, "");
+      static_assert(is_same<int, decltype(av)>::value == true, "");
+      static_assert(is_same<int, decltype(sumi)>::value == true, "");
+      static_assert(is_same<int, decltype(sumf)>::value == false, "");
+      static_assert(is_same<int, decltype(add(c, v))>::value == true, "");
+      return (sumf > 0.0) ? sumi : add(c, v);
+    }
+
+  }
+
+  namespace test_noexcept
+  {
+
+    int f() { return 0; }
+    int g() noexcept { return 0; }
+
+    static_assert(noexcept(f()) == false, "");
+    static_assert(noexcept(g()) == true, "");
+
+  }
+
+  namespace test_constexpr
+  {
+
+    template < typename CharT >
+    unsigned long constexpr
+    strlen_c_r(const CharT *const s, const unsigned long acc) noexcept
+    {
+      return *s ? strlen_c_r(s + 1, acc + 1) : acc;
+    }
+
+    template < typename CharT >
+    unsigned long constexpr
+    strlen_c(const CharT *const s) noexcept
+    {
+      return strlen_c_r(s, 0UL);
+    }
+
+    static_assert(strlen_c("") == 0UL, "");
+    static_assert(strlen_c("1") == 1UL, "");
+    static_assert(strlen_c("example") == 7UL, "");
+    static_assert(strlen_c("another\0example") == 7UL, "");
+
+  }
+
+  namespace test_rvalue_references
+  {
+
+    template < int N >
+    struct answer
+    {
+      static constexpr int value = N;
+    };
+
+    answer<1> f(int&)       { return answer<1>(); }
+    answer<2> f(const int&) { return answer<2>(); }
+    answer<3> f(int&&)      { return answer<3>(); }
+
+    void
+    test()
+    {
+      int i = 0;
+      const int c = 0;
+      static_assert(decltype(f(i))::value == 1, "");
+      static_assert(decltype(f(c))::value == 2, "");
+      static_assert(decltype(f(0))::value == 3, "");
+    }
+
+  }
+
+  namespace test_uniform_initialization
+  {
+
+    struct test
+    {
+      static const int zero {};
+      static const int one {1};
+    };
+
+    static_assert(test::zero == 0, "");
+    static_assert(test::one == 1, "");
+
+  }
+
+  namespace test_lambdas
+  {
+
+    void
+    test1()
+    {
+      auto lambda1 = [](){};
+      auto lambda2 = lambda1;
+      lambda1();
+      lambda2();
+    }
+
+    int
+    test2()
+    {
+      auto a = [](int i, int j){ return i + j; }(1, 2);
+      auto b = []() -> int { return '0'; }();
+      auto c = [=](){ return a + b; }();
+      auto d = [&](){ return c; }();
+      auto e = [a, &b](int x) mutable {
+        const auto identity = [](int y){ return y; };
+        for (auto i = 0; i < a; ++i)
+          a += b--;
+        return x + identity(a + b);
+      }(0);
+      return a + b + c + d + e;
+    }
+
+    int
+    test3()
+    {
+      const auto nullary = [](){ return 0; };
+      const auto unary = [](int x){ return x; };
+      using nullary_t = decltype(nullary);
+      using unary_t = decltype(unary);
+      const auto higher1st = [](nullary_t f){ return f(); };
+      const auto higher2nd = [unary](nullary_t f1){
+        return [unary, f1](unary_t f2){ return f2(unary(f1())); };
+      };
+      return higher1st(nullary) + higher2nd(nullary)(unary);
+    }
+
+  }
+
+  namespace test_variadic_templates
+  {
+
+    template <int...>
+    struct sum;
+
+    template <int N0, int... N1toN>
+    struct sum<N0, N1toN...>
+    {
+      static constexpr auto value = N0 + sum<N1toN...>::value;
+    };
+
+    template <>
+    struct sum<>
+    {
+      static constexpr auto value = 0;
+    };
+
+    static_assert(sum<>::value == 0, "");
+    static_assert(sum<1>::value == 1, "");
+    static_assert(sum<23>::value == 23, "");
+    static_assert(sum<1, 2>::value == 3, "");
+    static_assert(sum<5, 5, 11>::value == 21, "");
+    static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, "");
+
+  }
+
+  // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae
+  // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function
+  // because of this.
+  namespace test_template_alias_sfinae
+  {
+
+    struct foo {};
+
+    template<typename T>
+    using member = typename T::member_type;
+
+    template<typename T>
+    void func(...) {}
+
+    template<typename T>
+    void func(member<T>*) {}
+
+    void test();
+
+    void test() { func<foo>(0); }
+
+  }
+
+}  // namespace cxx11
+
+#endif  // __cplusplus >= 201103L
+
+]])
+
+
+dnl  Tests for new features in C++14
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[
+
+// If the compiler admits that it is not ready for C++14, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201402L
+
+#error "This is not a C++14 compiler"
+
+#else
+
+namespace cxx14
+{
+
+  namespace test_polymorphic_lambdas
+  {
+
+    int
+    test()
+    {
+      const auto lambda = [](auto&&... args){
+        const auto istiny = [](auto x){
+          return (sizeof(x) == 1UL) ? 1 : 0;
+        };
+        const int aretiny[] = { istiny(args)... };
+        return aretiny[0];
+      };
+      return lambda(1, 1L, 1.0f, '1');
+    }
+
+  }
+
+  namespace test_binary_literals
+  {
+
+    constexpr auto ivii = 0b0000000000101010;
+    static_assert(ivii == 42, "wrong value");
+
+  }
+
+  namespace test_generalized_constexpr
+  {
+
+    template < typename CharT >
+    constexpr unsigned long
+    strlen_c(const CharT *const s) noexcept
+    {
+      auto length = 0UL;
+      for (auto p = s; *p; ++p)
+        ++length;
+      return length;
+    }
+
+    static_assert(strlen_c("") == 0UL, "");
+    static_assert(strlen_c("x") == 1UL, "");
+    static_assert(strlen_c("test") == 4UL, "");
+    static_assert(strlen_c("another\0test") == 7UL, "");
+
+  }
+
+  namespace test_lambda_init_capture
+  {
+
+    int
+    test()
+    {
+      auto x = 0;
+      const auto lambda1 = [a = x](int b){ return a + b; };
+      const auto lambda2 = [a = lambda1(x)](){ return a; };
+      return lambda2();
+    }
+
+  }
+
+  namespace test_digit_separators
+  {
+
+    constexpr auto ten_million = 100'000'000;
+    static_assert(ten_million == 100000000, "");
+
+  }
+
+  namespace test_return_type_deduction
+  {
+
+    auto f(int& x) { return x; }
+    decltype(auto) g(int& x) { return x; }
+
+    template < typename T1, typename T2 >
+    struct is_same
+    {
+      static constexpr auto value = false;
+    };
+
+    template < typename T >
+    struct is_same<T, T>
+    {
+      static constexpr auto value = true;
+    };
+
+    int
+    test()
+    {
+      auto x = 0;
+      static_assert(is_same<int, decltype(f(x))>::value, "");
+      static_assert(is_same<int&, decltype(g(x))>::value, "");
+      return x;
+    }
+
+  }
+
+}  // namespace cxx14
+
+#endif  // __cplusplus >= 201402L
+
+]])
+
+
+dnl  Tests for new features in C++17
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[
+
+// If the compiler admits that it is not ready for C++17, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201703L
+
+#error "This is not a C++17 compiler"
+
+#else
+
+#include <initializer_list>
+#include <utility>
+#include <type_traits>
+
+namespace cxx17
+{
+
+  namespace test_constexpr_lambdas
+  {
+
+    constexpr int foo = [](){return 42;}();
+
+  }
+
+  namespace test::nested_namespace::definitions
+  {
+
+  }
+
+  namespace test_fold_expression
+  {
+
+    template<typename... Args>
+    int multiply(Args... args)
+    {
+      return (args * ... * 1);
+    }
+
+    template<typename... Args>
+    bool all(Args... args)
+    {
+      return (args && ...);
+    }
+
+  }
+
+  namespace test_extended_static_assert
+  {
+
+    static_assert (true);
+
+  }
+
+  namespace test_auto_brace_init_list
+  {
+
+    auto foo = {5};
+    auto bar {5};
+
+    static_assert(std::is_same<std::initializer_list<int>, decltype(foo)>::value);
+    static_assert(std::is_same<int, decltype(bar)>::value);
+  }
+
+  namespace test_typename_in_template_template_parameter
+  {
+
+    template<template<typename> typename X> struct D;
+
+  }
+
+  namespace test_fallthrough_nodiscard_maybe_unused_attributes
+  {
+
+    int f1()
+    {
+      return 42;
+    }
+
+    [[nodiscard]] int f2()
+    {
+      [[maybe_unused]] auto unused = f1();
+
+      switch (f1())
+      {
+      case 17:
+        f1();
+        [[fallthrough]];
+      case 42:
+        f1();
+      }
+      return f1();
+    }
+
+  }
+
+  namespace test_extended_aggregate_initialization
+  {
+
+    struct base1
+    {
+      int b1, b2 = 42;
+    };
+
+    struct base2
+    {
+      base2() {
+        b3 = 42;
+      }
+      int b3;
+    };
+
+    struct derived : base1, base2
+    {
+        int d;
+    };
+
+    derived d1 {{1, 2}, {}, 4};  // full initialization
+    derived d2 {{}, {}, 4};      // value-initialized bases
+
+  }
+
+  namespace test_general_range_based_for_loop
+  {
+
+    struct iter
+    {
+      int i;
+
+      int& operator* ()
+      {
+        return i;
+      }
+
+      const int& operator* () const
+      {
+        return i;
+      }
+
+      iter& operator++()
+      {
+        ++i;
+        return *this;
+      }
+    };
+
+    struct sentinel
+    {
+      int i;
+    };
+
+    bool operator== (const iter& i, const sentinel& s)
+    {
+      return i.i == s.i;
+    }
+
+    bool operator!= (const iter& i, const sentinel& s)
+    {
+      return !(i == s);
+    }
+
+    struct range
+    {
+      iter begin() const
+      {
+        return {0};
+      }
+
+      sentinel end() const
+      {
+        return {5};
+      }
+    };
+
+    void f()
+    {
+      range r {};
+
+      for (auto i : r)
+      {
+        [[maybe_unused]] auto v = i;
+      }
+    }
+
+  }
+
+  namespace test_lambda_capture_asterisk_this_by_value
+  {
+
+    struct t
+    {
+      int i;
+      int foo()
+      {
+        return [*this]()
+        {
+          return i;
+        }();
+      }
+    };
+
+  }
+
+  namespace test_enum_class_construction
+  {
+
+    enum class byte : unsigned char
+    {};
+
+    byte foo {42};
+
+  }
+
+  namespace test_constexpr_if
+  {
+
+    template <bool cond>
+    int f ()
+    {
+      if constexpr(cond)
+      {
+        return 13;
+      }
+      else
+      {
+        return 42;
+      }
+    }
+
+  }
+
+  namespace test_selection_statement_with_initializer
+  {
+
+    int f()
+    {
+      return 13;
+    }
+
+    int f2()
+    {
+      if (auto i = f(); i > 0)
+      {
+        return 3;
+      }
+
+      switch (auto i = f(); i + 4)
+      {
+      case 17:
+        return 2;
+
+      default:
+        return 1;
+      }
+    }
+
+  }
+
+  namespace test_template_argument_deduction_for_class_templates
+  {
+
+    template <typename T1, typename T2>
+    struct pair
+    {
+      pair (T1 p1, T2 p2)
+        : m1 {p1},
+          m2 {p2}
+      {}
+
+      T1 m1;
+      T2 m2;
+    };
+
+    void f()
+    {
+      [[maybe_unused]] auto p = pair{13, 42u};
+    }
+
+  }
+
+  namespace test_non_type_auto_template_parameters
+  {
+
+    template <auto n>
+    struct B
+    {};
+
+    B<5> b1;
+    B<'a'> b2;
+
+  }
+
+  namespace test_structured_bindings
+  {
+
+    int arr[2] = { 1, 2 };
+    std::pair<int, int> pr = { 1, 2 };
+
+    auto f1() -> int(&)[2]
+    {
+      return arr;
+    }
+
+    auto f2() -> std::pair<int, int>&
+    {
+      return pr;
+    }
+
+    struct S
+    {
+      int x1 : 2;
+      volatile double y1;
+    };
+
+    S f3()
+    {
+      return {};
+    }
+
+    auto [ x1, y1 ] = f1();
+    auto& [ xr1, yr1 ] = f1();
+    auto [ x2, y2 ] = f2();
+    auto& [ xr2, yr2 ] = f2();
+    const auto [ x3, y3 ] = f3();
+
+  }
+
+  namespace test_exception_spec_type_system
+  {
+
+    struct Good {};
+    struct Bad {};
+
+    void g1() noexcept;
+    void g2();
+
+    template<typename T>
+    Bad
+    f(T*, T*);
+
+    template<typename T1, typename T2>
+    Good
+    f(T1*, T2*);
+
+    static_assert (std::is_same_v<Good, decltype(f(g1, g2))>);
+
+  }
+
+  namespace test_inline_variables
+  {
+
+    template<class T> void f(T)
+    {}
+
+    template<class T> inline T g(T)
+    {
+      return T{};
+    }
+
+    template<> inline void f<>(int)
+    {}
+
+    template<> int g<>(int)
+    {
+      return 5;
+    }
+
+  }
+
+}  // namespace cxx17
+
+#endif  // __cplusplus < 201703L
+
+]])
diff --git a/m4/ax_cxx_compile_stdcxx_17.m4 b/m4/ax_cxx_compile_stdcxx_17.m4
new file mode 100644 (file)
index 0000000..a683417
--- /dev/null
@@ -0,0 +1,35 @@
+# =============================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx_17.html
+# =============================================================================
+#
+# SYNOPSIS
+#
+#   AX_CXX_COMPILE_STDCXX_17([ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+#   Check for baseline language coverage in the compiler for the C++17
+#   standard; if necessary, add switches to CXX and CXXCPP to enable
+#   support.
+#
+#   This macro is a convenience alias for calling the AX_CXX_COMPILE_STDCXX
+#   macro with the version set to C++17.  The two optional arguments are
+#   forwarded literally as the second and third argument respectively.
+#   Please see the documentation for the AX_CXX_COMPILE_STDCXX macro for
+#   more information.  If you want to use this macro, you also need to
+#   download the ax_cxx_compile_stdcxx.m4 file.
+#
+# LICENSE
+#
+#   Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+#   Copyright (c) 2016 Krzesimir Nowak <qdlacz@gmail.com>
+#
+#   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. This file is offered as-is, without any
+#   warranty.
+
+#serial 2
+
+AX_REQUIRE_DEFINED([AX_CXX_COMPILE_STDCXX])
+AC_DEFUN([AX_CXX_COMPILE_STDCXX_17], [AX_CXX_COMPILE_STDCXX([17], [$1], [$2])])
diff --git a/m4/ax_file_escapes.m4 b/m4/ax_file_escapes.m4
new file mode 100644 (file)
index 0000000..a86fdc3
--- /dev/null
@@ -0,0 +1,30 @@
+# ===========================================================================
+#     https://www.gnu.org/software/autoconf-archive/ax_file_escapes.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_FILE_ESCAPES
+#
+# DESCRIPTION
+#
+#   Writes the specified data to the specified file.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Tom Howard <tomhoward@users.sf.net>
+#
+#   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. This file is offered as-is, without any
+#   warranty.
+
+#serial 8
+
+AC_DEFUN([AX_FILE_ESCAPES],[
+AX_DOLLAR="\$"
+AX_SRB="\\135"
+AX_SLB="\\133"
+AX_BS="\\\\"
+AX_DQ="\""
+])
diff --git a/m4/ax_is_release.m4 b/m4/ax_is_release.m4
new file mode 100644 (file)
index 0000000..9097ddb
--- /dev/null
@@ -0,0 +1,80 @@
+# ===========================================================================
+#      https://www.gnu.org/software/autoconf-archive/ax_is_release.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_IS_RELEASE(POLICY)
+#
+# DESCRIPTION
+#
+#   Determine whether the code is being configured as a release, or from
+#   git. Set the ax_is_release variable to 'yes' or 'no'.
+#
+#   If building a release version, it is recommended that the configure
+#   script disable compiler errors and debug features, by conditionalising
+#   them on the ax_is_release variable.  If building from git, these
+#   features should be enabled.
+#
+#   The POLICY parameter specifies how ax_is_release is determined. It can
+#   take the following values:
+#
+#    * git-directory:  ax_is_release will be 'no' if a '.git' directory exists
+#    * minor-version:  ax_is_release will be 'no' if the minor version number
+#                      in $PACKAGE_VERSION is odd; this assumes
+#                      $PACKAGE_VERSION follows the 'major.minor.micro' scheme
+#    * micro-version:  ax_is_release will be 'no' if the micro version number
+#                      in $PACKAGE_VERSION is odd; this assumes
+#                      $PACKAGE_VERSION follows the 'major.minor.micro' scheme
+#    * dash-version:   ax_is_release will be 'no' if there is a dash '-'
+#                      in $PACKAGE_VERSION, for example 1.2-pre3, 1.2.42-a8b9
+#                      or 2.0-dirty (in particular this is suitable for use
+#                      with git-version-gen)
+#    * always:         ax_is_release will always be 'yes'
+#    * never:          ax_is_release will always be 'no'
+#
+#   Other policies may be added in future.
+#
+# LICENSE
+#
+#   Copyright (c) 2015 Philip Withnall <philip@tecnocode.co.uk>
+#   Copyright (c) 2016 Collabora Ltd.
+#
+#   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.
+
+#serial 7
+
+AC_DEFUN([AX_IS_RELEASE],[
+    AC_BEFORE([AC_INIT],[$0])
+
+    m4_case([$1],
+      [git-directory],[
+        # $is_release = (.git directory does not exist)
+        AS_IF([test -d ${srcdir}/.git],[ax_is_release=no],[ax_is_release=yes])
+      ],
+      [minor-version],[
+        # $is_release = ($minor_version is even)
+        minor_version=`echo "$PACKAGE_VERSION" | sed 's/[[^.]][[^.]]*.\([[^.]][[^.]]*\).*/\1/'`
+        AS_IF([test "$(( $minor_version % 2 ))" -ne 0],
+              [ax_is_release=no],[ax_is_release=yes])
+      ],
+      [micro-version],[
+        # $is_release = ($micro_version is even)
+        micro_version=`echo "$PACKAGE_VERSION" | sed 's/[[^.]]*\.[[^.]]*\.\([[^.]]*\).*/\1/'`
+        AS_IF([test "$(( $micro_version % 2 ))" -ne 0],
+              [ax_is_release=no],[ax_is_release=yes])
+      ],
+      [dash-version],[
+        # $is_release = ($PACKAGE_VERSION has a dash)
+        AS_CASE([$PACKAGE_VERSION],
+                [*-*], [ax_is_release=no],
+                [*], [ax_is_release=yes])
+      ],
+      [always],[ax_is_release=yes],
+      [never],[ax_is_release=no],
+      [
+        AC_MSG_ERROR([Invalid policy. Valid policies: git-directory, minor-version, micro-version, dash-version, always, never.])
+      ])
+])
diff --git a/m4/ax_lib_readline.m4 b/m4/ax_lib_readline.m4
new file mode 100644 (file)
index 0000000..2ca00a3
--- /dev/null
@@ -0,0 +1,109 @@
+# ===========================================================================
+#     https://www.gnu.org/software/autoconf-archive/ax_lib_readline.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_LIB_READLINE
+#
+# DESCRIPTION
+#
+#   Searches for a readline compatible library. If found, defines
+#   `HAVE_LIBREADLINE'. If the found library has the `add_history' function,
+#   sets also `HAVE_READLINE_HISTORY'. Also checks for the locations of the
+#   necessary include files and sets `HAVE_READLINE_H' or
+#   `HAVE_READLINE_READLINE_H' and `HAVE_READLINE_HISTORY_H' or
+#   'HAVE_HISTORY_H' if the corresponding include files exists.
+#
+#   The libraries that may be readline compatible are `libedit',
+#   `libeditline' and `libreadline'. Sometimes we need to link a termcap
+#   library for readline to work, this macro tests these cases too by trying
+#   to link with `libtermcap', `libcurses' or `libncurses' before giving up.
+#
+#   Here is an example of how to use the information provided by this macro
+#   to perform the necessary includes or declarations in a C file:
+#
+#     #ifdef HAVE_LIBREADLINE
+#     #  if defined(HAVE_READLINE_READLINE_H)
+#     #    include <readline/readline.h>
+#     #  elif defined(HAVE_READLINE_H)
+#     #    include <readline.h>
+#     #  else /* !defined(HAVE_READLINE_H) */
+#     extern char *readline ();
+#     #  endif /* !defined(HAVE_READLINE_H) */
+#     char *cmdline = NULL;
+#     #else /* !defined(HAVE_READLINE_READLINE_H) */
+#       /* no readline */
+#     #endif /* HAVE_LIBREADLINE */
+#
+#     #ifdef HAVE_READLINE_HISTORY
+#     #  if defined(HAVE_READLINE_HISTORY_H)
+#     #    include <readline/history.h>
+#     #  elif defined(HAVE_HISTORY_H)
+#     #    include <history.h>
+#     #  else /* !defined(HAVE_HISTORY_H) */
+#     extern void add_history ();
+#     extern int write_history ();
+#     extern int read_history ();
+#     #  endif /* defined(HAVE_READLINE_HISTORY_H) */
+#       /* no history */
+#     #endif /* HAVE_READLINE_HISTORY */
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Ville Laurikari <vl@iki.fi>
+#
+#   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. This file is offered as-is, without any
+#   warranty.
+
+#serial 8
+
+AU_ALIAS([VL_LIB_READLINE], [AX_LIB_READLINE])
+AC_DEFUN([AX_LIB_READLINE], [
+  AC_CACHE_CHECK([for a readline compatible library],
+                 ax_cv_lib_readline, [
+    ORIG_LIBS="$LIBS"
+    # djcb: we need the _real_ readline_
+    #for readline_lib in readline edit editline; do
+    for readline_lib in readline; do
+      for termcap_lib in "" termcap curses ncurses; do
+        if test -z "$termcap_lib"; then
+          TRY_LIB="-l$readline_lib"
+        else
+          TRY_LIB="-l$readline_lib -l$termcap_lib"
+        fi
+        LIBS="$ORIG_LIBS $TRY_LIB"
+        AC_LINK_IFELSE([AC_LANG_CALL([], [readline])], [ax_cv_lib_readline="$TRY_LIB"])
+        if test -n "$ax_cv_lib_readline"; then
+          break
+        fi
+      done
+      if test -n "$ax_cv_lib_readline"; then
+        break
+      fi
+    done
+    if test -z "$ax_cv_lib_readline"; then
+      ax_cv_lib_readline="no"
+    fi
+    LIBS="$ORIG_LIBS"
+  ])
+
+  if test "$ax_cv_lib_readline" != "no"; then
+    LIBS="$LIBS $ax_cv_lib_readline"
+    AC_DEFINE(HAVE_LIBREADLINE, 1,
+              [Define if you have a readline compatible library])
+    AC_CHECK_HEADERS(readline.h readline/readline.h)
+    AC_CACHE_CHECK([whether readline supports history],
+                   ax_cv_lib_readline_history, [
+      ax_cv_lib_readline_history="no"
+      AC_LINK_IFELSE([AC_LANG_CALL([], [add_history])], [ax_cv_lib_readline_history="yes"])
+    ])
+    if test "$ax_cv_lib_readline_history" = "yes"; then
+      AC_DEFINE(HAVE_READLINE_HISTORY, 1,
+                [Define if your readline library has \`add_history'])
+      AC_CHECK_HEADERS(history.h readline/history.h)
+    fi
+  fi
+])dnl
diff --git a/m4/ax_require_defined.m4 b/m4/ax_require_defined.m4
new file mode 100644 (file)
index 0000000..17c3eab
--- /dev/null
@@ -0,0 +1,37 @@
+# ===========================================================================
+#    https://www.gnu.org/software/autoconf-archive/ax_require_defined.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_REQUIRE_DEFINED(MACRO)
+#
+# DESCRIPTION
+#
+#   AX_REQUIRE_DEFINED is a simple helper for making sure other macros have
+#   been defined and thus are available for use.  This avoids random issues
+#   where a macro isn't expanded.  Instead the configure script emits a
+#   non-fatal:
+#
+#     ./configure: line 1673: AX_CFLAGS_WARN_ALL: command not found
+#
+#   It's like AC_REQUIRE except it doesn't expand the required macro.
+#
+#   Here's an example:
+#
+#     AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG])
+#
+# LICENSE
+#
+#   Copyright (c) 2014 Mike Frysinger <vapier@gentoo.org>
+#
+#   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. This file is offered as-is, without any
+#   warranty.
+
+#serial 2
+
+AC_DEFUN([AX_REQUIRE_DEFINED], [dnl
+  m4_ifndef([$1], [m4_fatal([macro ]$1[ is not defined; is a m4 file missing?])])
+])dnl AX_REQUIRE_DEFINED
diff --git a/m4/ax_valgrind_check.m4 b/m4/ax_valgrind_check.m4
new file mode 100644 (file)
index 0000000..7033798
--- /dev/null
@@ -0,0 +1,239 @@
+# ===========================================================================
+#    https://www.gnu.org/software/autoconf-archive/ax_valgrind_check.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_VALGRIND_DFLT(memcheck|helgrind|drd|sgcheck, on|off)
+#   AX_VALGRIND_CHECK()
+#
+# DESCRIPTION
+#
+#   AX_VALGRIND_CHECK checks whether Valgrind is present and, if so, allows
+#   running `make check` under a variety of Valgrind tools to check for
+#   memory and threading errors.
+#
+#   Defines VALGRIND_CHECK_RULES which should be substituted in your
+#   Makefile; and $enable_valgrind which can be used in subsequent configure
+#   output. VALGRIND_ENABLED is defined and substituted, and corresponds to
+#   the value of the --enable-valgrind option, which defaults to being
+#   enabled if Valgrind is installed and disabled otherwise. Individual
+#   Valgrind tools can be disabled via --disable-valgrind-<tool>, the
+#   default is configurable via the AX_VALGRIND_DFLT command or is to use
+#   all commands not disabled via AX_VALGRIND_DFLT. All AX_VALGRIND_DFLT
+#   calls must be made before the call to AX_VALGRIND_CHECK.
+#
+#   If unit tests are written using a shell script and automake's
+#   LOG_COMPILER system, the $(VALGRIND) variable can be used within the
+#   shell scripts to enable Valgrind, as described here:
+#
+#     https://www.gnu.org/software/gnulib/manual/html_node/Running-self_002dtests-under-valgrind.html
+#
+#   Usage example:
+#
+#   configure.ac:
+#
+#     AX_VALGRIND_DFLT([sgcheck], [off])
+#     AX_VALGRIND_CHECK
+#
+#   in each Makefile.am with tests:
+#
+#     @VALGRIND_CHECK_RULES@
+#     VALGRIND_SUPPRESSIONS_FILES = my-project.supp
+#     EXTRA_DIST = my-project.supp
+#
+#   This results in a "check-valgrind" rule being added. Running `make
+#   check-valgrind` in that directory will recursively run the module's test
+#   suite (`make check`) once for each of the available Valgrind tools (out
+#   of memcheck, helgrind and drd) while the sgcheck will be skipped unless
+#   enabled again on the commandline with --enable-valgrind-sgcheck. The
+#   results for each check will be output to test-suite-$toolname.log. The
+#   target will succeed if there are zero errors and fail otherwise.
+#
+#   Alternatively, a "check-valgrind-$TOOL" rule will be added, for $TOOL in
+#   memcheck, helgrind, drd and sgcheck. These are useful because often only
+#   some of those tools can be ran cleanly on a codebase.
+#
+#   The macro supports running with and without libtool.
+#
+# LICENSE
+#
+#   Copyright (c) 2014, 2015, 2016 Philip Withnall <philip.withnall@collabora.co.uk>
+#
+#   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.  This file is offered as-is, without any
+#   warranty.
+
+#serial 17
+
+dnl Configured tools
+m4_define([valgrind_tool_list], [[memcheck], [helgrind], [drd], [sgcheck]])
+m4_set_add_all([valgrind_exp_tool_set], [sgcheck])
+m4_foreach([vgtool], [valgrind_tool_list],
+           [m4_define([en_dflt_valgrind_]vgtool, [on])])
+
+AC_DEFUN([AX_VALGRIND_DFLT],[
+       m4_define([en_dflt_valgrind_$1], [$2])
+])dnl
+
+AM_EXTRA_RECURSIVE_TARGETS([check-valgrind])
+m4_foreach([vgtool], [valgrind_tool_list],
+       [AM_EXTRA_RECURSIVE_TARGETS([check-valgrind-]vgtool)])
+
+AC_DEFUN([AX_VALGRIND_CHECK],[
+       dnl Check for --enable-valgrind
+       AC_ARG_ENABLE([valgrind],
+                     [AS_HELP_STRING([--enable-valgrind], [Whether to enable Valgrind on the unit tests])],
+                     [enable_valgrind=$enableval],[enable_valgrind=])
+
+       AS_IF([test "$enable_valgrind" != "no"],[
+               # Check for Valgrind.
+               AC_CHECK_PROG([VALGRIND],[valgrind],[valgrind])
+               AS_IF([test "$VALGRIND" = ""],[
+                       AS_IF([test "$enable_valgrind" = "yes"],[
+                               AC_MSG_ERROR([Could not find valgrind; either install it or reconfigure with --disable-valgrind])
+                       ],[
+                               enable_valgrind=no
+                       ])
+               ],[
+                       enable_valgrind=yes
+               ])
+       ])
+
+       AM_CONDITIONAL([VALGRIND_ENABLED],[test "$enable_valgrind" = "yes"])
+       AC_SUBST([VALGRIND_ENABLED],[$enable_valgrind])
+
+       # Check for Valgrind tools we care about.
+       [valgrind_enabled_tools=]
+       m4_foreach([vgtool],[valgrind_tool_list],[
+               AC_ARG_ENABLE([valgrind-]vgtool,
+                   m4_if(m4_defn([en_dflt_valgrind_]vgtool),[off],dnl
+[AS_HELP_STRING([--enable-valgrind-]vgtool, [Whether to use ]vgtool[ during the Valgrind tests])],dnl
+[AS_HELP_STRING([--disable-valgrind-]vgtool, [Whether to skip ]vgtool[ during the Valgrind tests])]),
+                             [enable_valgrind_]vgtool[=$enableval],
+                             [enable_valgrind_]vgtool[=])
+               AS_IF([test "$enable_valgrind" = "no"],[
+                       enable_valgrind_]vgtool[=no],
+                     [test "$enable_valgrind_]vgtool[" ]dnl
+m4_if(m4_defn([en_dflt_valgrind_]vgtool), [off], [= "yes"], [!= "no"]),[
+                       AC_CACHE_CHECK([for Valgrind tool ]vgtool,
+                                      [ax_cv_valgrind_tool_]vgtool,[
+                               ax_cv_valgrind_tool_]vgtool[=no
+                               m4_set_contains([valgrind_exp_tool_set],vgtool,
+                                   [m4_define([vgtoolx],[exp-]vgtool)],
+                                   [m4_define([vgtoolx],vgtool)])
+                               AS_IF([`$VALGRIND --tool=]vgtoolx[ --help >/dev/null 2>&1`],[
+                                       ax_cv_valgrind_tool_]vgtool[=yes
+                               ])
+                       ])
+                       AS_IF([test "$ax_cv_valgrind_tool_]vgtool[" = "no"],[
+                               AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[
+                                       AC_MSG_ERROR([Valgrind does not support ]vgtool[; reconfigure with --disable-valgrind-]vgtool)
+                               ],[
+                                       enable_valgrind_]vgtool[=no
+                               ])
+                       ],[
+                               enable_valgrind_]vgtool[=yes
+                       ])
+               ])
+               AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[
+                       valgrind_enabled_tools="$valgrind_enabled_tools ]m4_bpatsubst(vgtool,[^exp-])["
+               ])
+               AC_SUBST([ENABLE_VALGRIND_]vgtool,[$enable_valgrind_]vgtool)
+       ])
+       AC_SUBST([valgrind_tools],["]m4_join([ ], valgrind_tool_list)["])
+       AC_SUBST([valgrind_enabled_tools],[$valgrind_enabled_tools])
+
+[VALGRIND_CHECK_RULES='
+# Valgrind check
+#
+# Optional:
+#  - VALGRIND_SUPPRESSIONS_FILES: Space-separated list of Valgrind suppressions
+#    files to load. (Default: empty)
+#  - VALGRIND_FLAGS: General flags to pass to all Valgrind tools.
+#    (Default: --num-callers=30)
+#  - VALGRIND_$toolname_FLAGS: Flags to pass to Valgrind $toolname (one of:
+#    memcheck, helgrind, drd, sgcheck). (Default: various)
+
+# Optional variables
+VALGRIND_SUPPRESSIONS ?= $(addprefix --suppressions=,$(VALGRIND_SUPPRESSIONS_FILES))
+VALGRIND_FLAGS ?= --num-callers=30
+VALGRIND_memcheck_FLAGS ?= --leak-check=full --show-reachable=no
+VALGRIND_helgrind_FLAGS ?= --history-level=approx
+VALGRIND_drd_FLAGS ?=
+VALGRIND_sgcheck_FLAGS ?=
+
+# Internal use
+valgrind_log_files = $(addprefix test-suite-,$(addsuffix .log,$(valgrind_tools)))
+
+valgrind_memcheck_flags = --tool=memcheck $(VALGRIND_memcheck_FLAGS)
+valgrind_helgrind_flags = --tool=helgrind $(VALGRIND_helgrind_FLAGS)
+valgrind_drd_flags = --tool=drd $(VALGRIND_drd_FLAGS)
+valgrind_sgcheck_flags = --tool=exp-sgcheck $(VALGRIND_sgcheck_FLAGS)
+
+valgrind_quiet = $(valgrind_quiet_$(V))
+valgrind_quiet_ = $(valgrind_quiet_$(AM_DEFAULT_VERBOSITY))
+valgrind_quiet_0 = --quiet
+valgrind_v_use   = $(valgrind_v_use_$(V))
+valgrind_v_use_  = $(valgrind_v_use_$(AM_DEFAULT_VERBOSITY))
+valgrind_v_use_0 = @echo "  USE   " $(patsubst check-valgrind-%-am,%,$''@):;
+
+# Support running with and without libtool.
+ifneq ($(LIBTOOL),)
+valgrind_lt = $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=execute
+else
+valgrind_lt =
+endif
+
+# Use recursive makes in order to ignore errors during check
+check-valgrind-am:
+ifeq ($(VALGRIND_ENABLED),yes)
+       $(A''M_V_at)$(MAKE) $(AM_MAKEFLAGS) -k \
+               $(foreach tool, $(valgrind_enabled_tools), check-valgrind-$(tool))
+else
+       @echo "Need to reconfigure with --enable-valgrind"
+endif
+
+# Valgrind running
+VALGRIND_TESTS_ENVIRONMENT = \
+       $(TESTS_ENVIRONMENT) \
+       env VALGRIND=$(VALGRIND) \
+       G_SLICE=always-malloc,debug-blocks \
+       G_DEBUG=fatal-warnings,fatal-criticals,gc-friendly
+
+VALGRIND_LOG_COMPILER = \
+       $(valgrind_lt) \
+       $(VALGRIND) $(VALGRIND_SUPPRESSIONS) --error-exitcode=1 $(VALGRIND_FLAGS)
+
+define valgrind_tool_rule
+check-valgrind-$(1)-am:
+ifeq ($$(VALGRIND_ENABLED)-$$(ENABLE_VALGRIND_$(1)),yes-yes)
+ifneq ($$(TESTS),)
+       $$(valgrind_v_use)$$(MAKE) check-TESTS \
+               TESTS_ENVIRONMENT="$$(VALGRIND_TESTS_ENVIRONMENT)" \
+               LOG_COMPILER="$$(VALGRIND_LOG_COMPILER)" \
+               LOG_FLAGS="$$(valgrind_$(1)_flags)" \
+               TEST_SUITE_LOG=test-suite-$(1).log
+endif
+else ifeq ($$(VALGRIND_ENABLED),yes)
+       @echo "Need to reconfigure with --enable-valgrind-$(1)"
+else
+       @echo "Need to reconfigure with --enable-valgrind"
+endif
+endef
+
+$(foreach tool,$(valgrind_tools),$(eval $(call valgrind_tool_rule,$(tool))))
+
+A''M_DISTCHECK_CONFIGURE_FLAGS ?=
+A''M_DISTCHECK_CONFIGURE_FLAGS += --disable-valgrind
+
+MOSTLYCLEANFILES ?=
+MOSTLYCLEANFILES += $(valgrind_log_files)
+
+.PHONY: check-valgrind $(add-prefix check-valgrind-,$(valgrind_tools))
+']
+
+       AC_SUBST([VALGRIND_CHECK_RULES])
+       m4_ifdef([_AM_SUBST_NOTMAKE], [_AM_SUBST_NOTMAKE([VALGRIND_CHECK_RULES])])
+])
diff --git a/m4/guile.m4 b/m4/guile.m4
new file mode 100644 (file)
index 0000000..6968973
--- /dev/null
@@ -0,0 +1,397 @@
+## Autoconf macros for working with Guile.
+##
+##   Copyright (C) 1998,2001, 2006, 2010, 2012, 2013, 2014 Free Software Foundation, Inc.
+##
+## This library is free software; you can redistribute it and/or
+## modify it under the terms of the GNU Lesser General Public License
+## as published by the Free Software Foundation; either version 3 of
+## the License, or (at your option) any later version.
+##
+## This library 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
+## Lesser General Public License for more details.
+##
+## You should have received a copy of the GNU Lesser General Public
+## License along with this library; if not, write to the Free Software
+## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+## 02110-1301 USA
+
+# serial 10
+
+## Index
+## -----
+##
+## GUILE_PKG -- find Guile development files
+## GUILE_PROGS -- set paths to Guile interpreter, config and tool programs
+## GUILE_FLAGS -- set flags for compiling and linking with Guile
+## GUILE_SITE_DIR -- find path to Guile "site" directories
+## GUILE_CHECK -- evaluate Guile Scheme code and capture the return value
+## GUILE_MODULE_CHECK -- check feature of a Guile Scheme module
+## GUILE_MODULE_AVAILABLE -- check availability of a Guile Scheme module
+## GUILE_MODULE_REQUIRED -- fail if a Guile Scheme module is unavailable
+## GUILE_MODULE_EXPORTS -- check if a module exports a variable
+## GUILE_MODULE_REQUIRED_EXPORT -- fail if a module doesn't export a variable
+
+## Code
+## ----
+
+## NOTE: Comments preceding an AC_DEFUN (starting from "Usage:") are massaged
+## into doc/ref/autoconf-macros.texi (see Makefile.am in that directory).
+
+# GUILE_PKG -- find Guile development files
+#
+# Usage: GUILE_PKG([VERSIONS])
+#
+# This macro runs the @code{pkg-config} tool to find development files
+# for an available version of Guile.
+#
+# By default, this macro will search for the latest stable version of
+# Guile (e.g. 3.0), falling back to the previous stable version
+# (e.g. 2.2) if it is available.  If no guile-@var{VERSION}.pc file is
+# found, an error is signalled.  The found version is stored in
+# @var{GUILE_EFFECTIVE_VERSION}.
+#
+# If @code{GUILE_PROGS} was already invoked, this macro ensures that the
+# development files have the same effective version as the Guile
+# program.
+#
+# @var{GUILE_EFFECTIVE_VERSION} is marked for substitution, as by
+# @code{AC_SUBST}.
+#
+AC_DEFUN([GUILE_PKG],
+ [AC_REQUIRE([PKG_PROG_PKG_CONFIG])
+  if test "x$PKG_CONFIG" = x; then
+    AC_MSG_ERROR([pkg-config is missing, please install it])
+  fi
+  _guile_versions_to_search="m4_default([$1], [3.0 2.2 2.0])"
+  if test -n "$GUILE_EFFECTIVE_VERSION"; then
+    _guile_tmp=""
+    for v in $_guile_versions_to_search; do
+      if test "$v" = "$GUILE_EFFECTIVE_VERSION"; then
+        _guile_tmp=$v
+      fi
+    done
+    if test -z "$_guile_tmp"; then
+      AC_MSG_FAILURE([searching for guile development files for versions $_guile_versions_to_search, but previously found $GUILE version $GUILE_EFFECTIVE_VERSION])
+    fi
+    _guile_versions_to_search=$GUILE_EFFECTIVE_VERSION
+  fi
+  GUILE_EFFECTIVE_VERSION=""
+  _guile_errors=""
+  for v in $_guile_versions_to_search; do
+    if test -z "$GUILE_EFFECTIVE_VERSION"; then
+      AC_MSG_NOTICE([checking for guile $v])
+      PKG_CHECK_EXISTS([guile-$v], [GUILE_EFFECTIVE_VERSION=$v], [])
+    fi
+  done
+
+  if test -z "$GUILE_EFFECTIVE_VERSION"; then
+    AC_MSG_ERROR([
+No Guile development packages were found.
+
+Please verify that you have Guile installed.  If you installed Guile
+from a binary distribution, please verify that you have also installed
+the development packages.  If you installed it yourself, you might need
+to adjust your PKG_CONFIG_PATH; see the pkg-config man page for more.
+])
+  fi
+  AC_MSG_NOTICE([found guile $GUILE_EFFECTIVE_VERSION])
+  AC_SUBST([GUILE_EFFECTIVE_VERSION])
+ ])
+
+# GUILE_FLAGS -- set flags for compiling and linking with Guile
+#
+# Usage: GUILE_FLAGS
+#
+# This macro runs the @code{pkg-config} tool to find out how to compile
+# and link programs against Guile.  It sets four variables:
+# @var{GUILE_CFLAGS}, @var{GUILE_LDFLAGS}, @var{GUILE_LIBS}, and
+# @var{GUILE_LTLIBS}.
+#
+# @var{GUILE_CFLAGS}: flags to pass to a C or C++ compiler to build code that
+# uses Guile header files.  This is almost always just one or more @code{-I}
+# flags.
+#
+# @var{GUILE_LDFLAGS}: flags to pass to the compiler to link a program
+# against Guile.  This includes @code{-lguile-@var{VERSION}} for the
+# Guile library itself, and may also include one or more @code{-L} flag
+# to tell the compiler where to find the libraries.  But it does not
+# include flags that influence the program's runtime search path for
+# libraries, and will therefore lead to a program that fails to start,
+# unless all necessary libraries are installed in a standard location
+# such as @file{/usr/lib}.
+#
+# @var{GUILE_LIBS} and @var{GUILE_LTLIBS}: flags to pass to the compiler or to
+# libtool, respectively, to link a program against Guile.  It includes flags
+# that augment the program's runtime search path for libraries, so that shared
+# libraries will be found at the location where they were during linking, even
+# in non-standard locations.  @var{GUILE_LIBS} is to be used when linking the
+# program directly with the compiler, whereas @var{GUILE_LTLIBS} is to be used
+# when linking the program is done through libtool.
+#
+# The variables are marked for substitution, as by @code{AC_SUBST}.
+#
+AC_DEFUN([GUILE_FLAGS],
+ [AC_REQUIRE([GUILE_PKG])
+  PKG_CHECK_MODULES(GUILE, [guile-$GUILE_EFFECTIVE_VERSION])
+
+  dnl GUILE_CFLAGS and GUILE_LIBS are already defined and AC_SUBST'd by
+  dnl PKG_CHECK_MODULES.  But GUILE_LIBS to pkg-config is GUILE_LDFLAGS
+  dnl to us.
+
+  GUILE_LDFLAGS=$GUILE_LIBS
+
+  dnl Determine the platform dependent parameters needed to use rpath.
+  dnl AC_LIB_LINKFLAGS_FROM_LIBS is defined in gnulib/m4/lib-link.m4 and needs
+  dnl the file gnulib/build-aux/config.rpath.
+  AC_LIB_LINKFLAGS_FROM_LIBS([GUILE_LIBS], [$GUILE_LDFLAGS], [])
+  GUILE_LIBS="$GUILE_LDFLAGS $GUILE_LIBS"
+  AC_LIB_LINKFLAGS_FROM_LIBS([GUILE_LTLIBS], [$GUILE_LDFLAGS], [yes])
+  GUILE_LTLIBS="$GUILE_LDFLAGS $GUILE_LTLIBS"
+
+  AC_SUBST([GUILE_EFFECTIVE_VERSION])
+  AC_SUBST([GUILE_CFLAGS])
+  AC_SUBST([GUILE_LDFLAGS])
+  AC_SUBST([GUILE_LIBS])
+  AC_SUBST([GUILE_LTLIBS])
+ ])
+
+# GUILE_SITE_DIR -- find path to Guile site directories
+#
+# Usage: GUILE_SITE_DIR
+#
+# This looks for Guile's "site" directories.  The variable @var{GUILE_SITE} will
+# be set to Guile's "site" directory for Scheme source files (usually something
+# like PREFIX/share/guile/site).  @var{GUILE_SITE_CCACHE} will be set to the
+# directory for compiled Scheme files also known as @code{.go} files
+# (usually something like
+# PREFIX/lib/guile/@var{GUILE_EFFECTIVE_VERSION}/site-ccache).
+# @var{GUILE_EXTENSION} will be set to the directory for compiled C extensions
+# (usually something like
+# PREFIX/lib/guile/@var{GUILE_EFFECTIVE_VERSION}/extensions). The latter two
+# are set to blank if the particular version of Guile does not support
+# them.  Note that this macro will run the macros @code{GUILE_PKG} and
+# @code{GUILE_PROGS} if they have not already been run.
+#
+# The variables are marked for substitution, as by @code{AC_SUBST}.
+#
+AC_DEFUN([GUILE_SITE_DIR],
+ [AC_REQUIRE([GUILE_PKG])
+  AC_REQUIRE([GUILE_PROGS])
+  AC_MSG_CHECKING(for Guile site directory)
+  GUILE_SITE=`$PKG_CONFIG --print-errors --variable=sitedir guile-$GUILE_EFFECTIVE_VERSION`
+  AC_MSG_RESULT($GUILE_SITE)
+  if test "$GUILE_SITE" = ""; then
+     AC_MSG_FAILURE(sitedir not found)
+  fi
+  AC_SUBST(GUILE_SITE)
+  AC_MSG_CHECKING([for Guile site-ccache directory using pkgconfig])
+  GUILE_SITE_CCACHE=`$PKG_CONFIG --variable=siteccachedir guile-$GUILE_EFFECTIVE_VERSION`
+  if test "$GUILE_SITE_CCACHE" = ""; then
+    AC_MSG_RESULT(no)
+    AC_MSG_CHECKING([for Guile site-ccache directory using interpreter])
+    GUILE_SITE_CCACHE=`$GUILE -c "(display (if (defined? '%site-ccache-dir) (%site-ccache-dir) \"\"))"`
+    if test $? != "0" -o "$GUILE_SITE_CCACHE" = ""; then
+      AC_MSG_RESULT(no)
+      GUILE_SITE_CCACHE=""
+      AC_MSG_WARN([siteccachedir not found])
+    fi
+  fi
+  AC_MSG_RESULT($GUILE_SITE_CCACHE)
+  AC_SUBST([GUILE_SITE_CCACHE])
+  AC_MSG_CHECKING(for Guile extensions directory)
+  GUILE_EXTENSION=`$PKG_CONFIG --print-errors --variable=extensiondir guile-$GUILE_EFFECTIVE_VERSION`
+  AC_MSG_RESULT($GUILE_EXTENSION)
+  if test "$GUILE_EXTENSION" = ""; then
+    GUILE_EXTENSION=""
+    AC_MSG_WARN(extensiondir not found)
+  fi
+  AC_SUBST(GUILE_EXTENSION)
+ ])
+
+# GUILE_PROGS -- set paths to Guile interpreter, config and tool programs
+#
+# Usage: GUILE_PROGS([VERSION])
+#
+# This macro looks for programs @code{guile} and @code{guild}, setting
+# variables @var{GUILE} and @var{GUILD} to their paths, respectively.
+# The macro will attempt to find @code{guile} with the suffix of
+# @code{-X.Y}, followed by looking for it with the suffix @code{X.Y}, and
+# then fall back to looking for @code{guile} with no suffix. If
+# @code{guile} is still not found, signal an error. The suffix, if any,
+# that was required to find @code{guile} will be used for @code{guild}
+# as well.
+#
+# By default, this macro will search for the latest stable version of
+# Guile (e.g. 3.0). x.y or x.y.z versions can be specified. If an older
+# version is found, the macro will signal an error.
+#
+# The effective version of the found @code{guile} is set to
+# @var{GUILE_EFFECTIVE_VERSION}.  This macro ensures that the effective
+# version is compatible with the result of a previous invocation of
+# @code{GUILE_FLAGS}, if any.
+#
+# As a legacy interface, it also looks for @code{guile-config} and
+# @code{guile-tools}, setting @var{GUILE_CONFIG} and @var{GUILE_TOOLS}.
+#
+# The variables are marked for substitution, as by @code{AC_SUBST}.
+#
+AC_DEFUN([GUILE_PROGS],
+ [_guile_required_version="m4_default([$1], [$GUILE_EFFECTIVE_VERSION])"
+  if test -z "$_guile_required_version"; then
+    _guile_required_version=3.0
+  fi
+
+  _guile_candidates=guile
+  _tmp=
+  for v in `echo "$_guile_required_version" | tr . ' '`; do
+    if test -n "$_tmp"; then _tmp=$_tmp.; fi
+    _tmp=$_tmp$v
+    _guile_candidates="guile-$_tmp guile$_tmp $_guile_candidates"
+  done
+
+  AC_PATH_PROGS(GUILE,[$_guile_candidates])
+  if test -z "$GUILE"; then
+      AC_MSG_ERROR([guile required but not found])
+  fi
+
+  _guile_suffix=`echo "$GUILE" | sed -e 's,^.*/guile\(.*\)$,\1,'`
+  _guile_effective_version=`$GUILE -c "(display (effective-version))"`
+  if test -z "$GUILE_EFFECTIVE_VERSION"; then
+    GUILE_EFFECTIVE_VERSION=$_guile_effective_version
+  elif test "$GUILE_EFFECTIVE_VERSION" != "$_guile_effective_version"; then
+    AC_MSG_ERROR([found development files for Guile $GUILE_EFFECTIVE_VERSION, but $GUILE has effective version $_guile_effective_version])
+  fi
+
+  _guile_major_version=`$GUILE -c "(display (major-version))"`
+  _guile_minor_version=`$GUILE -c "(display (minor-version))"`
+  _guile_micro_version=`$GUILE -c "(display (micro-version))"`
+  _guile_prog_version="$_guile_major_version.$_guile_minor_version.$_guile_micro_version"
+
+  AC_MSG_CHECKING([for Guile version >= $_guile_required_version])
+  _major_version=`echo $_guile_required_version | cut -d . -f 1`
+  _minor_version=`echo $_guile_required_version | cut -d . -f 2`
+  _micro_version=`echo $_guile_required_version | cut -d . -f 3`
+  if test "$_guile_major_version" -gt "$_major_version"; then
+    true
+  elif test "$_guile_major_version" -eq "$_major_version"; then
+    if test "$_guile_minor_version" -gt "$_minor_version"; then
+      true
+    elif test "$_guile_minor_version" -eq "$_minor_version"; then
+      if test -n "$_micro_version"; then
+        if test "$_guile_micro_version" -lt "$_micro_version"; then
+          AC_MSG_ERROR([Guile $_guile_required_version required, but $_guile_prog_version found])
+        fi
+      fi
+    elif test "$GUILE_EFFECTIVE_VERSION" = "$_major_version.$_minor_version" -a -z "$_micro_version"; then
+      # Allow prereleases that have the right effective version.
+      true
+    else
+      as_fn_error $? "Guile $_guile_required_version required, but $_guile_prog_version found" "$LINENO" 5
+    fi
+  elif test "$GUILE_EFFECTIVE_VERSION" = "$_major_version.$_minor_version" -a -z "$_micro_version"; then
+    # Allow prereleases that have the right effective version.
+    true
+  else
+    AC_MSG_ERROR([Guile $_guile_required_version required, but $_guile_prog_version found])
+  fi
+  AC_MSG_RESULT([$_guile_prog_version])
+
+  AC_PATH_PROG(GUILD,[guild$_guile_suffix])
+  AC_SUBST(GUILD)
+
+  AC_PATH_PROG(GUILE_CONFIG,[guile-config$_guile_suffix])
+  AC_SUBST(GUILE_CONFIG)
+  if test -n "$GUILD"; then
+    GUILE_TOOLS=$GUILD
+  else
+    AC_PATH_PROG(GUILE_TOOLS,[guile-tools$_guile_suffix])
+  fi
+  AC_SUBST(GUILE_TOOLS)
+ ])
+
+# GUILE_CHECK -- evaluate Guile Scheme code and capture the return value
+#
+# Usage: GUILE_CHECK_RETVAL(var,check)
+#
+# @var{var} is a shell variable name to be set to the return value.
+# @var{check} is a Guile Scheme expression, evaluated with "$GUILE -c", and
+#    returning either 0 or non-#f to indicate the check passed.
+#    Non-0 number or #f indicates failure.
+#    Avoid using the character "#" since that confuses autoconf.
+#
+AC_DEFUN([GUILE_CHECK],
+ [AC_REQUIRE([GUILE_PROGS])
+  $GUILE -c "$2" > /dev/null 2>&1
+  $1=$?
+ ])
+
+# GUILE_MODULE_CHECK -- check feature of a Guile Scheme module
+#
+# Usage: GUILE_MODULE_CHECK(var,module,featuretest,description)
+#
+# @var{var} is a shell variable name to be set to "yes" or "no".
+# @var{module} is a list of symbols, like: (ice-9 common-list).
+# @var{featuretest} is an expression acceptable to GUILE_CHECK, q.v.
+# @var{description} is a present-tense verb phrase (passed to AC_MSG_CHECKING).
+#
+AC_DEFUN([GUILE_MODULE_CHECK],
+         [AC_MSG_CHECKING([if $2 $4])
+         GUILE_CHECK($1,(use-modules $2) (exit ((lambda () $3))))
+         if test "$$1" = "0" ; then $1=yes ; else $1=no ; fi
+          AC_MSG_RESULT($$1)
+         ])
+
+# GUILE_MODULE_AVAILABLE -- check availability of a Guile Scheme module
+#
+# Usage: GUILE_MODULE_AVAILABLE(var,module)
+#
+# @var{var} is a shell variable name to be set to "yes" or "no".
+# @var{module} is a list of symbols, like: (ice-9 common-list).
+#
+AC_DEFUN([GUILE_MODULE_AVAILABLE],
+         [GUILE_MODULE_CHECK($1,$2,0,is available)
+         ])
+
+# GUILE_MODULE_REQUIRED -- fail if a Guile Scheme module is unavailable
+#
+# Usage: GUILE_MODULE_REQUIRED(symlist)
+#
+# @var{symlist} is a list of symbols, WITHOUT surrounding parens,
+# like: ice-9 common-list.
+#
+AC_DEFUN([GUILE_MODULE_REQUIRED],
+         [GUILE_MODULE_AVAILABLE(ac_guile_module_required, ($1))
+          if test "$ac_guile_module_required" = "no" ; then
+              AC_MSG_ERROR([required guile module not found: ($1)])
+          fi
+         ])
+
+# GUILE_MODULE_EXPORTS -- check if a module exports a variable
+#
+# Usage: GUILE_MODULE_EXPORTS(var,module,modvar)
+#
+# @var{var} is a shell variable to be set to "yes" or "no".
+# @var{module} is a list of symbols, like: (ice-9 common-list).
+# @var{modvar} is the Guile Scheme variable to check.
+#
+AC_DEFUN([GUILE_MODULE_EXPORTS],
+ [GUILE_MODULE_CHECK($1,$2,$3,exports `$3')
+ ])
+
+# GUILE_MODULE_REQUIRED_EXPORT -- fail if a module doesn't export a variable
+#
+# Usage: GUILE_MODULE_REQUIRED_EXPORT(module,modvar)
+#
+# @var{module} is a list of symbols, like: (ice-9 common-list).
+# @var{modvar} is the Guile Scheme variable to check.
+#
+AC_DEFUN([GUILE_MODULE_REQUIRED_EXPORT],
+ [GUILE_MODULE_EXPORTS(guile_module_required_export,$1,$2)
+  if test "$guile_module_required_export" = "no" ; then
+      AC_MSG_ERROR([module $1 does not export $2; required])
+  fi
+ ])
+
+## guile.m4 ends here
diff --git a/m4/host-cpu-c-abi.m4 b/m4/host-cpu-c-abi.m4
new file mode 100644 (file)
index 0000000..6db2aa2
--- /dev/null
@@ -0,0 +1,675 @@
+# host-cpu-c-abi.m4 serial 13
+dnl Copyright (C) 2002-2020 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+dnl From Bruno Haible and Sam Steingold.
+
+dnl Sets the HOST_CPU variable to the canonical name of the CPU.
+dnl Sets the HOST_CPU_C_ABI variable to the canonical name of the CPU with its
+dnl C language ABI (application binary interface).
+dnl Also defines __${HOST_CPU}__ and __${HOST_CPU_C_ABI}__ as C macros in
+dnl config.h.
+dnl
+dnl This canonical name can be used to select a particular assembly language
+dnl source file that will interoperate with C code on the given host.
+dnl
+dnl For example:
+dnl * 'i386' and 'sparc' are different canonical names, because code for i386
+dnl   will not run on SPARC CPUs and vice versa. They have different
+dnl   instruction sets.
+dnl * 'sparc' and 'sparc64' are different canonical names, because code for
+dnl   'sparc' and code for 'sparc64' cannot be linked together: 'sparc' code
+dnl   contains 32-bit instructions, whereas 'sparc64' code contains 64-bit
+dnl   instructions. A process on a SPARC CPU can be in 32-bit mode or in 64-bit
+dnl   mode, but not both.
+dnl * 'mips' and 'mipsn32' are different canonical names, because they use
+dnl   different argument passing and return conventions for C functions, and
+dnl   although the instruction set of 'mips' is a large subset of the
+dnl   instruction set of 'mipsn32'.
+dnl * 'mipsn32' and 'mips64' are different canonical names, because they use
+dnl   different sizes for the C types like 'int' and 'void *', and although
+dnl   the instruction sets of 'mipsn32' and 'mips64' are the same.
+dnl * The same canonical name is used for different endiannesses. You can
+dnl   determine the endianness through preprocessor symbols:
+dnl   - 'arm': test __ARMEL__.
+dnl   - 'mips', 'mipsn32', 'mips64': test _MIPSEB vs. _MIPSEL.
+dnl   - 'powerpc64': test _BIG_ENDIAN vs. _LITTLE_ENDIAN.
+dnl * The same name 'i386' is used for CPUs of type i386, i486, i586
+dnl   (Pentium), AMD K7, Pentium II, Pentium IV, etc., because
+dnl   - Instructions that do not exist on all of these CPUs (cmpxchg,
+dnl     MMX, SSE, SSE2, 3DNow! etc.) are not frequently used. If your
+dnl     assembly language source files use such instructions, you will
+dnl     need to make the distinction.
+dnl   - Speed of execution of the common instruction set is reasonable across
+dnl     the entire family of CPUs. If you have assembly language source files
+dnl     that are optimized for particular CPU types (like GNU gmp has), you
+dnl     will need to make the distinction.
+dnl   See <https://en.wikipedia.org/wiki/X86_instruction_listings>.
+AC_DEFUN([gl_HOST_CPU_C_ABI],
+[
+  AC_REQUIRE([AC_CANONICAL_HOST])
+  AC_REQUIRE([gl_C_ASM])
+  AC_CACHE_CHECK([host CPU and C ABI], [gl_cv_host_cpu_c_abi],
+    [case "$host_cpu" in
+
+changequote(,)dnl
+       i[34567]86 )
+changequote([,])dnl
+         gl_cv_host_cpu_c_abi=i386
+         ;;
+
+       x86_64 )
+         # On x86_64 systems, the C compiler may be generating code in one of
+         # these ABIs:
+         # - 64-bit instruction set, 64-bit pointers, 64-bit 'long': x86_64.
+         # - 64-bit instruction set, 64-bit pointers, 32-bit 'long': x86_64
+         #   with native Windows (mingw, MSVC).
+         # - 64-bit instruction set, 32-bit pointers, 32-bit 'long': x86_64-x32.
+         # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': i386.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#if (defined __x86_64__ || defined __amd64__ \
+                     || defined _M_X64 || defined _M_AMD64)
+                 int ok;
+                #else
+                 error fail
+                #endif
+              ]])],
+           [AC_COMPILE_IFELSE(
+              [AC_LANG_SOURCE(
+                 [[#if defined __ILP32__ || defined _ILP32
+                    int ok;
+                   #else
+                    error fail
+                   #endif
+                 ]])],
+              [gl_cv_host_cpu_c_abi=x86_64-x32],
+              [gl_cv_host_cpu_c_abi=x86_64])],
+           [gl_cv_host_cpu_c_abi=i386])
+         ;;
+
+changequote(,)dnl
+       alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] )
+changequote([,])dnl
+         gl_cv_host_cpu_c_abi=alpha
+         ;;
+
+       arm* | aarch64 )
+         # Assume arm with EABI.
+         # On arm64 systems, the C compiler may be generating code in one of
+         # these ABIs:
+         # - aarch64 instruction set, 64-bit pointers, 64-bit 'long': arm64.
+         # - aarch64 instruction set, 32-bit pointers, 32-bit 'long': arm64-ilp32.
+         # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': arm or armhf.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#ifdef __aarch64__
+                 int ok;
+                #else
+                 error fail
+                #endif
+              ]])],
+           [AC_COMPILE_IFELSE(
+              [AC_LANG_SOURCE(
+                [[#if defined __ILP32__ || defined _ILP32
+                   int ok;
+                  #else
+                   error fail
+                  #endif
+                ]])],
+              [gl_cv_host_cpu_c_abi=arm64-ilp32],
+              [gl_cv_host_cpu_c_abi=arm64])],
+           [# Don't distinguish little-endian and big-endian arm, since they
+            # don't require different machine code for simple operations and
+            # since the user can distinguish them through the preprocessor
+            # defines __ARMEL__ vs. __ARMEB__.
+            # But distinguish arm which passes floating-point arguments and
+            # return values in integer registers (r0, r1, ...) - this is
+            # gcc -mfloat-abi=soft or gcc -mfloat-abi=softfp - from arm which
+            # passes them in float registers (s0, s1, ...) and double registers
+            # (d0, d1, ...) - this is gcc -mfloat-abi=hard. GCC 4.6 or newer
+            # sets the preprocessor defines __ARM_PCS (for the first case) and
+            # __ARM_PCS_VFP (for the second case), but older GCC does not.
+            echo 'double ddd; void func (double dd) { ddd = dd; }' > conftest.c
+            # Look for a reference to the register d0 in the .s file.
+            AC_TRY_COMMAND(${CC-cc} $CFLAGS $CPPFLAGS $gl_c_asm_opt conftest.c) >/dev/null 2>&1
+            if LC_ALL=C grep 'd0,' conftest.$gl_asmext >/dev/null; then
+              gl_cv_host_cpu_c_abi=armhf
+            else
+              gl_cv_host_cpu_c_abi=arm
+            fi
+            rm -f conftest*
+           ])
+         ;;
+
+       hppa1.0 | hppa1.1 | hppa2.0* | hppa64 )
+         # On hppa, the C compiler may be generating 32-bit code or 64-bit
+         # code. In the latter case, it defines _LP64 and __LP64__.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#ifdef __LP64__
+                 int ok;
+                #else
+                 error fail
+                #endif
+              ]])],
+           [gl_cv_host_cpu_c_abi=hppa64],
+           [gl_cv_host_cpu_c_abi=hppa])
+         ;;
+
+       ia64* )
+         # On ia64 on HP-UX, the C compiler may be generating 64-bit code or
+         # 32-bit code. In the latter case, it defines _ILP32.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#ifdef _ILP32
+                 int ok;
+                #else
+                 error fail
+                #endif
+              ]])],
+           [gl_cv_host_cpu_c_abi=ia64-ilp32],
+           [gl_cv_host_cpu_c_abi=ia64])
+         ;;
+
+       mips* )
+         # We should also check for (_MIPS_SZPTR == 64), but gcc keeps this
+         # at 32.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#if defined _MIPS_SZLONG && (_MIPS_SZLONG == 64)
+                 int ok;
+                #else
+                 error fail
+                #endif
+              ]])],
+           [gl_cv_host_cpu_c_abi=mips64],
+           [# In the n32 ABI, _ABIN32 is defined, _ABIO32 is not defined (but
+            # may later get defined by <sgidefs.h>), and _MIPS_SIM == _ABIN32.
+            # In the 32 ABI, _ABIO32 is defined, _ABIN32 is not defined (but
+            # may later get defined by <sgidefs.h>), and _MIPS_SIM == _ABIO32.
+            AC_COMPILE_IFELSE(
+              [AC_LANG_SOURCE(
+                 [[#if (_MIPS_SIM == _ABIN32)
+                    int ok;
+                   #else
+                    error fail
+                   #endif
+                 ]])],
+              [gl_cv_host_cpu_c_abi=mipsn32],
+              [gl_cv_host_cpu_c_abi=mips])])
+         ;;
+
+       powerpc* )
+         # Different ABIs are in use on AIX vs. Mac OS X vs. Linux,*BSD.
+         # No need to distinguish them here; the caller may distinguish
+         # them based on the OS.
+         # On powerpc64 systems, the C compiler may still be generating
+         # 32-bit code. And on powerpc-ibm-aix systems, the C compiler may
+         # be generating 64-bit code.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#if defined __powerpc64__ || defined _ARCH_PPC64
+                 int ok;
+                #else
+                 error fail
+                #endif
+              ]])],
+           [# On powerpc64, there are two ABIs on Linux: The AIX compatible
+            # one and the ELFv2 one. The latter defines _CALL_ELF=2.
+            AC_COMPILE_IFELSE(
+              [AC_LANG_SOURCE(
+                 [[#if defined _CALL_ELF && _CALL_ELF == 2
+                    int ok;
+                   #else
+                    error fail
+                   #endif
+                 ]])],
+              [gl_cv_host_cpu_c_abi=powerpc64-elfv2],
+              [gl_cv_host_cpu_c_abi=powerpc64])
+           ],
+           [gl_cv_host_cpu_c_abi=powerpc])
+         ;;
+
+       rs6000 )
+         gl_cv_host_cpu_c_abi=powerpc
+         ;;
+
+       riscv32 | riscv64 )
+         # There are 2 architectures (with variants): rv32* and rv64*.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#if __riscv_xlen == 64
+                  int ok;
+                #else
+                  error fail
+                #endif
+              ]])],
+           [cpu=riscv64],
+           [cpu=riscv32])
+         # There are 6 ABIs: ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d.
+         # Size of 'long' and 'void *':
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#if defined __LP64__
+                  int ok;
+                #else
+                  error fail
+                #endif
+              ]])],
+           [main_abi=lp64],
+           [main_abi=ilp32])
+         # Float ABIs:
+         # __riscv_float_abi_double:
+         #   'float' and 'double' are passed in floating-point registers.
+         # __riscv_float_abi_single:
+         #   'float' are passed in floating-point registers.
+         # __riscv_float_abi_soft:
+         #   No values are passed in floating-point registers.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#if defined __riscv_float_abi_double
+                  int ok;
+                #else
+                  error fail
+                #endif
+              ]])],
+           [float_abi=d],
+           [AC_COMPILE_IFELSE(
+              [AC_LANG_SOURCE(
+                 [[#if defined __riscv_float_abi_single
+                     int ok;
+                   #else
+                     error fail
+                   #endif
+                 ]])],
+              [float_abi=f],
+              [float_abi=''])
+           ])
+         gl_cv_host_cpu_c_abi="${cpu}-${main_abi}${float_abi}"
+         ;;
+
+       s390* )
+         # On s390x, the C compiler may be generating 64-bit (= s390x) code
+         # or 31-bit (= s390) code.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#if defined __LP64__ || defined __s390x__
+                  int ok;
+                #else
+                  error fail
+                #endif
+              ]])],
+           [gl_cv_host_cpu_c_abi=s390x],
+           [gl_cv_host_cpu_c_abi=s390])
+         ;;
+
+       sparc | sparc64 )
+         # UltraSPARCs running Linux have `uname -m` = "sparc64", but the
+         # C compiler still generates 32-bit code.
+         AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#if defined __sparcv9 || defined __arch64__
+                 int ok;
+                #else
+                 error fail
+                #endif
+              ]])],
+           [gl_cv_host_cpu_c_abi=sparc64],
+           [gl_cv_host_cpu_c_abi=sparc])
+         ;;
+
+       *)
+         gl_cv_host_cpu_c_abi="$host_cpu"
+         ;;
+     esac
+    ])
+
+  dnl In most cases, $HOST_CPU and $HOST_CPU_C_ABI are the same.
+  HOST_CPU=`echo "$gl_cv_host_cpu_c_abi" | sed -e 's/-.*//'`
+  HOST_CPU_C_ABI="$gl_cv_host_cpu_c_abi"
+  AC_SUBST([HOST_CPU])
+  AC_SUBST([HOST_CPU_C_ABI])
+
+  # This was
+  #   AC_DEFINE_UNQUOTED([__${HOST_CPU}__])
+  #   AC_DEFINE_UNQUOTED([__${HOST_CPU_C_ABI}__])
+  # earlier, but KAI C++ 3.2d doesn't like this.
+  sed -e 's/-/_/g' >> confdefs.h <<EOF
+#ifndef __${HOST_CPU}__
+#define __${HOST_CPU}__ 1
+#endif
+#ifndef __${HOST_CPU_C_ABI}__
+#define __${HOST_CPU_C_ABI}__ 1
+#endif
+EOF
+  AH_TOP([/* CPU and C ABI indicator */
+#ifndef __i386__
+#undef __i386__
+#endif
+#ifndef __x86_64_x32__
+#undef __x86_64_x32__
+#endif
+#ifndef __x86_64__
+#undef __x86_64__
+#endif
+#ifndef __alpha__
+#undef __alpha__
+#endif
+#ifndef __arm__
+#undef __arm__
+#endif
+#ifndef __armhf__
+#undef __armhf__
+#endif
+#ifndef __arm64_ilp32__
+#undef __arm64_ilp32__
+#endif
+#ifndef __arm64__
+#undef __arm64__
+#endif
+#ifndef __hppa__
+#undef __hppa__
+#endif
+#ifndef __hppa64__
+#undef __hppa64__
+#endif
+#ifndef __ia64_ilp32__
+#undef __ia64_ilp32__
+#endif
+#ifndef __ia64__
+#undef __ia64__
+#endif
+#ifndef __m68k__
+#undef __m68k__
+#endif
+#ifndef __mips__
+#undef __mips__
+#endif
+#ifndef __mipsn32__
+#undef __mipsn32__
+#endif
+#ifndef __mips64__
+#undef __mips64__
+#endif
+#ifndef __powerpc__
+#undef __powerpc__
+#endif
+#ifndef __powerpc64__
+#undef __powerpc64__
+#endif
+#ifndef __powerpc64_elfv2__
+#undef __powerpc64_elfv2__
+#endif
+#ifndef __riscv32__
+#undef __riscv32__
+#endif
+#ifndef __riscv64__
+#undef __riscv64__
+#endif
+#ifndef __riscv32_ilp32__
+#undef __riscv32_ilp32__
+#endif
+#ifndef __riscv32_ilp32f__
+#undef __riscv32_ilp32f__
+#endif
+#ifndef __riscv32_ilp32d__
+#undef __riscv32_ilp32d__
+#endif
+#ifndef __riscv64_ilp32__
+#undef __riscv64_ilp32__
+#endif
+#ifndef __riscv64_ilp32f__
+#undef __riscv64_ilp32f__
+#endif
+#ifndef __riscv64_ilp32d__
+#undef __riscv64_ilp32d__
+#endif
+#ifndef __riscv64_lp64__
+#undef __riscv64_lp64__
+#endif
+#ifndef __riscv64_lp64f__
+#undef __riscv64_lp64f__
+#endif
+#ifndef __riscv64_lp64d__
+#undef __riscv64_lp64d__
+#endif
+#ifndef __s390__
+#undef __s390__
+#endif
+#ifndef __s390x__
+#undef __s390x__
+#endif
+#ifndef __sh__
+#undef __sh__
+#endif
+#ifndef __sparc__
+#undef __sparc__
+#endif
+#ifndef __sparc64__
+#undef __sparc64__
+#endif
+])
+
+])
+
+
+dnl Sets the HOST_CPU_C_ABI_32BIT variable to 'yes' if the C language ABI
+dnl (application binary interface) is a 32-bit one, to 'no' if it is a 64-bit
+dnl one, or to 'unknown' if unknown.
+dnl This is a simplified variant of gl_HOST_CPU_C_ABI.
+AC_DEFUN([gl_HOST_CPU_C_ABI_32BIT],
+[
+  AC_REQUIRE([AC_CANONICAL_HOST])
+  AC_CACHE_CHECK([32-bit host C ABI], [gl_cv_host_cpu_c_abi_32bit],
+    [if test -n "$gl_cv_host_cpu_c_abi"; then
+       case "$gl_cv_host_cpu_c_abi" in
+         i386 | x86_64-x32 | arm | armhf | arm64-ilp32 | hppa | ia64-ilp32 | mips | mipsn32 | powerpc | riscv*-ilp32* | s390 | sparc)
+           gl_cv_host_cpu_c_abi_32bit=yes ;;
+         x86_64 | alpha | arm64 | hppa64 | ia64 | mips64 | powerpc64 | powerpc64-elfv2 | riscv*-lp64* | s390x | sparc64 )
+           gl_cv_host_cpu_c_abi_32bit=no ;;
+         *)
+           gl_cv_host_cpu_c_abi_32bit=unknown ;;
+       esac
+     else
+       case "$host_cpu" in
+
+         # CPUs that only support a 32-bit ABI.
+         arc \
+         | bfin \
+         | cris* \
+         | csky \
+         | epiphany \
+         | ft32 \
+         | h8300 \
+         | m68k \
+         | microblaze | microblazeel \
+         | nds32 | nds32le | nds32be \
+         | nios2 | nios2eb | nios2el \
+         | or1k* \
+         | or32 \
+         | sh | sh[1234] | sh[1234]e[lb] \
+         | tic6x \
+         | xtensa* )
+           gl_cv_host_cpu_c_abi_32bit=yes
+           ;;
+
+         # CPUs that only support a 64-bit ABI.
+changequote(,)dnl
+         alpha | alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] \
+         | mmix )
+changequote([,])dnl
+           gl_cv_host_cpu_c_abi_32bit=no
+           ;;
+
+changequote(,)dnl
+         i[34567]86 )
+changequote([,])dnl
+           gl_cv_host_cpu_c_abi_32bit=yes
+           ;;
+
+         x86_64 )
+           # On x86_64 systems, the C compiler may be generating code in one of
+           # these ABIs:
+           # - 64-bit instruction set, 64-bit pointers, 64-bit 'long': x86_64.
+           # - 64-bit instruction set, 64-bit pointers, 32-bit 'long': x86_64
+           #   with native Windows (mingw, MSVC).
+           # - 64-bit instruction set, 32-bit pointers, 32-bit 'long': x86_64-x32.
+           # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': i386.
+           AC_COMPILE_IFELSE(
+             [AC_LANG_SOURCE(
+                [[#if (defined __x86_64__ || defined __amd64__ \
+                       || defined _M_X64 || defined _M_AMD64) \
+                      && !(defined __ILP32__ || defined _ILP32)
+                   int ok;
+                  #else
+                   error fail
+                  #endif
+                ]])],
+             [gl_cv_host_cpu_c_abi_32bit=no],
+             [gl_cv_host_cpu_c_abi_32bit=yes])
+           ;;
+
+         arm* | aarch64 )
+           # Assume arm with EABI.
+           # On arm64 systems, the C compiler may be generating code in one of
+           # these ABIs:
+           # - aarch64 instruction set, 64-bit pointers, 64-bit 'long': arm64.
+           # - aarch64 instruction set, 32-bit pointers, 32-bit 'long': arm64-ilp32.
+           # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': arm or armhf.
+           AC_COMPILE_IFELSE(
+             [AC_LANG_SOURCE(
+                [[#if defined __aarch64__ && !(defined __ILP32__ || defined _ILP32)
+                   int ok;
+                  #else
+                   error fail
+                  #endif
+                ]])],
+             [gl_cv_host_cpu_c_abi_32bit=no],
+             [gl_cv_host_cpu_c_abi_32bit=yes])
+           ;;
+
+         hppa1.0 | hppa1.1 | hppa2.0* | hppa64 )
+           # On hppa, the C compiler may be generating 32-bit code or 64-bit
+           # code. In the latter case, it defines _LP64 and __LP64__.
+           AC_COMPILE_IFELSE(
+             [AC_LANG_SOURCE(
+                [[#ifdef __LP64__
+                   int ok;
+                  #else
+                   error fail
+                  #endif
+                ]])],
+             [gl_cv_host_cpu_c_abi_32bit=no],
+             [gl_cv_host_cpu_c_abi_32bit=yes])
+           ;;
+
+         ia64* )
+           # On ia64 on HP-UX, the C compiler may be generating 64-bit code or
+           # 32-bit code. In the latter case, it defines _ILP32.
+           AC_COMPILE_IFELSE(
+             [AC_LANG_SOURCE(
+                [[#ifdef _ILP32
+                   int ok;
+                  #else
+                   error fail
+                  #endif
+                ]])],
+             [gl_cv_host_cpu_c_abi_32bit=yes],
+             [gl_cv_host_cpu_c_abi_32bit=no])
+           ;;
+
+         mips* )
+           # We should also check for (_MIPS_SZPTR == 64), but gcc keeps this
+           # at 32.
+           AC_COMPILE_IFELSE(
+             [AC_LANG_SOURCE(
+                [[#if defined _MIPS_SZLONG && (_MIPS_SZLONG == 64)
+                   int ok;
+                  #else
+                   error fail
+                  #endif
+                ]])],
+             [gl_cv_host_cpu_c_abi_32bit=no],
+             [gl_cv_host_cpu_c_abi_32bit=yes])
+           ;;
+
+         powerpc* )
+           # Different ABIs are in use on AIX vs. Mac OS X vs. Linux,*BSD.
+           # No need to distinguish them here; the caller may distinguish
+           # them based on the OS.
+           # On powerpc64 systems, the C compiler may still be generating
+           # 32-bit code. And on powerpc-ibm-aix systems, the C compiler may
+           # be generating 64-bit code.
+           AC_COMPILE_IFELSE(
+             [AC_LANG_SOURCE(
+                [[#if defined __powerpc64__ || defined _ARCH_PPC64
+                   int ok;
+                  #else
+                   error fail
+                  #endif
+                ]])],
+             [gl_cv_host_cpu_c_abi_32bit=no],
+             [gl_cv_host_cpu_c_abi_32bit=yes])
+           ;;
+
+         rs6000 )
+           gl_cv_host_cpu_c_abi_32bit=yes
+           ;;
+
+         riscv32 | riscv64 )
+           # There are 6 ABIs: ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d.
+           # Size of 'long' and 'void *':
+           AC_COMPILE_IFELSE(
+             [AC_LANG_SOURCE(
+                [[#if defined __LP64__
+                    int ok;
+                  #else
+                    error fail
+                  #endif
+                ]])],
+             [gl_cv_host_cpu_c_abi_32bit=no],
+             [gl_cv_host_cpu_c_abi_32bit=yes])
+           ;;
+
+         s390* )
+           # On s390x, the C compiler may be generating 64-bit (= s390x) code
+           # or 31-bit (= s390) code.
+           AC_COMPILE_IFELSE(
+             [AC_LANG_SOURCE(
+                [[#if defined __LP64__ || defined __s390x__
+                    int ok;
+                  #else
+                    error fail
+                  #endif
+                ]])],
+             [gl_cv_host_cpu_c_abi_32bit=no],
+             [gl_cv_host_cpu_c_abi_32bit=yes])
+           ;;
+
+         sparc | sparc64 )
+           # UltraSPARCs running Linux have `uname -m` = "sparc64", but the
+           # C compiler still generates 32-bit code.
+           AC_COMPILE_IFELSE(
+             [AC_LANG_SOURCE(
+                [[#if defined __sparcv9 || defined __arch64__
+                   int ok;
+                  #else
+                   error fail
+                  #endif
+                ]])],
+             [gl_cv_host_cpu_c_abi_32bit=no],
+             [gl_cv_host_cpu_c_abi_32bit=yes])
+           ;;
+
+         *)
+           gl_cv_host_cpu_c_abi_32bit=unknown
+           ;;
+       esac
+     fi
+    ])
+
+  HOST_CPU_C_ABI_32BIT="$gl_cv_host_cpu_c_abi_32bit"
+])
diff --git a/m4/lib-ld.m4 b/m4/lib-ld.m4
new file mode 100644 (file)
index 0000000..a187196
--- /dev/null
@@ -0,0 +1,168 @@
+# lib-ld.m4 serial 9
+dnl Copyright (C) 1996-2003, 2009-2019 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+dnl Subroutines of libtool.m4,
+dnl with replacements s/_*LT_PATH/AC_LIB_PROG/ and s/lt_/acl_/ to avoid
+dnl collision with libtool.m4.
+
+dnl From libtool-2.4. Sets the variable with_gnu_ld to yes or no.
+AC_DEFUN([AC_LIB_PROG_LD_GNU],
+[AC_CACHE_CHECK([if the linker ($LD) is GNU ld], [acl_cv_prog_gnu_ld],
+[# I'd rather use --version here, but apparently some GNU lds only accept -v.
+case `$LD -v 2>&1 </dev/null` in
+*GNU* | *'with BFD'*)
+  acl_cv_prog_gnu_ld=yes
+  ;;
+*)
+  acl_cv_prog_gnu_ld=no
+  ;;
+esac])
+with_gnu_ld=$acl_cv_prog_gnu_ld
+])
+
+dnl From libtool-2.4. Sets the variable LD.
+AC_DEFUN([AC_LIB_PROG_LD],
+[AC_REQUIRE([AC_PROG_CC])dnl
+AC_REQUIRE([AC_CANONICAL_HOST])dnl
+
+AC_ARG_WITH([gnu-ld],
+    [AS_HELP_STRING([--with-gnu-ld],
+        [assume the C compiler uses GNU ld [default=no]])],
+    [test "$withval" = no || with_gnu_ld=yes],
+    [with_gnu_ld=no])dnl
+
+# Prepare PATH_SEPARATOR.
+# The user is always right.
+if test "${PATH_SEPARATOR+set}" != set; then
+  # Determine PATH_SEPARATOR by trying to find /bin/sh in a PATH which
+  # contains only /bin. Note that ksh looks also at the FPATH variable,
+  # so we have to set that as well for the test.
+  PATH_SEPARATOR=:
+  (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 \
+    && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 \
+           || PATH_SEPARATOR=';'
+       }
+fi
+
+if test -n "$LD"; then
+  AC_MSG_CHECKING([for ld])
+elif test "$GCC" = yes; then
+  AC_MSG_CHECKING([for ld used by $CC])
+elif test "$with_gnu_ld" = yes; then
+  AC_MSG_CHECKING([for GNU ld])
+else
+  AC_MSG_CHECKING([for non-GNU ld])
+fi
+if test -n "$LD"; then
+  # Let the user override the test with a path.
+  :
+else
+  AC_CACHE_VAL([acl_cv_path_LD],
+  [
+    acl_cv_path_LD= # Final result of this test
+    ac_prog=ld # Program to search in $PATH
+    if test "$GCC" = yes; then
+      # Check if gcc -print-prog-name=ld gives a path.
+      case $host in
+        *-*-mingw*)
+          # gcc leaves a trailing carriage return which upsets mingw
+          acl_output=`($CC -print-prog-name=ld) 2>&5 | tr -d '\015'` ;;
+        *)
+          acl_output=`($CC -print-prog-name=ld) 2>&5` ;;
+      esac
+      case $acl_output in
+        # Accept absolute paths.
+        [[\\/]]* | ?:[[\\/]]*)
+          re_direlt='/[[^/]][[^/]]*/\.\./'
+          # Canonicalize the pathname of ld
+          acl_output=`echo "$acl_output" | sed 's%\\\\%/%g'`
+          while echo "$acl_output" | grep "$re_direlt" > /dev/null 2>&1; do
+            acl_output=`echo $acl_output | sed "s%$re_direlt%/%"`
+          done
+          # Got the pathname. No search in PATH is needed.
+          acl_cv_path_LD="$acl_output"
+          ac_prog=
+          ;;
+        "")
+          # If it fails, then pretend we aren't using GCC.
+          ;;
+        *)
+          # If it is relative, then search for the first ld in PATH.
+          with_gnu_ld=unknown
+          ;;
+      esac
+    fi
+    if test -n "$ac_prog"; then
+      # Search for $ac_prog in $PATH.
+      acl_save_ifs="$IFS"; IFS=$PATH_SEPARATOR
+      for ac_dir in $PATH; do
+        IFS="$acl_save_ifs"
+        test -z "$ac_dir" && ac_dir=.
+        if test -f "$ac_dir/$ac_prog" || test -f "$ac_dir/$ac_prog$ac_exeext"; then
+          acl_cv_path_LD="$ac_dir/$ac_prog"
+          # Check to see if the program is GNU ld.  I'd rather use --version,
+          # but apparently some variants of GNU ld only accept -v.
+          # Break only if it was the GNU/non-GNU ld that we prefer.
+          case `"$acl_cv_path_LD" -v 2>&1 </dev/null` in
+            *GNU* | *'with BFD'*)
+              test "$with_gnu_ld" != no && break
+              ;;
+            *)
+              test "$with_gnu_ld" != yes && break
+              ;;
+          esac
+        fi
+      done
+      IFS="$acl_save_ifs"
+    fi
+    case $host in
+      *-*-aix*)
+        AC_COMPILE_IFELSE(
+          [AC_LANG_SOURCE(
+             [[#if defined __powerpc64__ || defined _ARCH_PPC64
+                int ok;
+               #else
+                error fail
+               #endif
+             ]])],
+          [# The compiler produces 64-bit code. Add option '-b64' so that the
+           # linker groks 64-bit object files.
+           case "$acl_cv_path_LD " in
+             *" -b64 "*) ;;
+             *) acl_cv_path_LD="$acl_cv_path_LD -b64" ;;
+           esac
+          ], [])
+        ;;
+      sparc64-*-netbsd*)
+        AC_COMPILE_IFELSE(
+          [AC_LANG_SOURCE(
+             [[#if defined __sparcv9 || defined __arch64__
+                int ok;
+               #else
+                error fail
+               #endif
+             ]])],
+          [],
+          [# The compiler produces 32-bit code. Add option '-m elf32_sparc'
+           # so that the linker groks 32-bit object files.
+           case "$acl_cv_path_LD " in
+             *" -m elf32_sparc "*) ;;
+             *) acl_cv_path_LD="$acl_cv_path_LD -m elf32_sparc" ;;
+           esac
+          ])
+        ;;
+    esac
+  ])
+  LD="$acl_cv_path_LD"
+fi
+if test -n "$LD"; then
+  AC_MSG_RESULT([$LD])
+else
+  AC_MSG_RESULT([no])
+  AC_MSG_ERROR([no acceptable ld found in \$PATH])
+fi
+AC_LIB_PROG_LD_GNU
+])
diff --git a/m4/lib-link.m4 b/m4/lib-link.m4
new file mode 100644 (file)
index 0000000..0ff1073
--- /dev/null
@@ -0,0 +1,774 @@
+# lib-link.m4 serial 28
+dnl Copyright (C) 2001-2019 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+dnl From Bruno Haible.
+
+AC_PREREQ([2.61])
+
+dnl AC_LIB_LINKFLAGS(name [, dependencies]) searches for libname and
+dnl the libraries corresponding to explicit and implicit dependencies.
+dnl Sets and AC_SUBSTs the LIB${NAME} and LTLIB${NAME} variables and
+dnl augments the CPPFLAGS variable.
+dnl Sets and AC_SUBSTs the LIB${NAME}_PREFIX variable to nonempty if libname
+dnl was found in ${LIB${NAME}_PREFIX}/$acl_libdirstem.
+AC_DEFUN([AC_LIB_LINKFLAGS],
+[
+  AC_REQUIRE([AC_LIB_PREPARE_PREFIX])
+  AC_REQUIRE([AC_LIB_RPATH])
+  pushdef([Name],[m4_translit([$1],[./+-], [____])])
+  pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-],
+                                   [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+  AC_CACHE_CHECK([how to link with lib[]$1], [ac_cv_lib[]Name[]_libs], [
+    AC_LIB_LINKFLAGS_BODY([$1], [$2])
+    ac_cv_lib[]Name[]_libs="$LIB[]NAME"
+    ac_cv_lib[]Name[]_ltlibs="$LTLIB[]NAME"
+    ac_cv_lib[]Name[]_cppflags="$INC[]NAME"
+    ac_cv_lib[]Name[]_prefix="$LIB[]NAME[]_PREFIX"
+  ])
+  LIB[]NAME="$ac_cv_lib[]Name[]_libs"
+  LTLIB[]NAME="$ac_cv_lib[]Name[]_ltlibs"
+  INC[]NAME="$ac_cv_lib[]Name[]_cppflags"
+  LIB[]NAME[]_PREFIX="$ac_cv_lib[]Name[]_prefix"
+  AC_LIB_APPENDTOVAR([CPPFLAGS], [$INC]NAME)
+  AC_SUBST([LIB]NAME)
+  AC_SUBST([LTLIB]NAME)
+  AC_SUBST([LIB]NAME[_PREFIX])
+  dnl Also set HAVE_LIB[]NAME so that AC_LIB_HAVE_LINKFLAGS can reuse the
+  dnl results of this search when this library appears as a dependency.
+  HAVE_LIB[]NAME=yes
+  popdef([NAME])
+  popdef([Name])
+])
+
+dnl AC_LIB_HAVE_LINKFLAGS(name, dependencies, includes, testcode, [missing-message])
+dnl searches for libname and the libraries corresponding to explicit and
+dnl implicit dependencies, together with the specified include files and
+dnl the ability to compile and link the specified testcode. The missing-message
+dnl defaults to 'no' and may contain additional hints for the user.
+dnl If found, it sets and AC_SUBSTs HAVE_LIB${NAME}=yes and the LIB${NAME}
+dnl and LTLIB${NAME} variables and augments the CPPFLAGS variable, and
+dnl #defines HAVE_LIB${NAME} to 1. Otherwise, it sets and AC_SUBSTs
+dnl HAVE_LIB${NAME}=no and LIB${NAME} and LTLIB${NAME} to empty.
+dnl Sets and AC_SUBSTs the LIB${NAME}_PREFIX variable to nonempty if libname
+dnl was found in ${LIB${NAME}_PREFIX}/$acl_libdirstem.
+AC_DEFUN([AC_LIB_HAVE_LINKFLAGS],
+[
+  AC_REQUIRE([AC_LIB_PREPARE_PREFIX])
+  AC_REQUIRE([AC_LIB_RPATH])
+  pushdef([Name],[m4_translit([$1],[./+-], [____])])
+  pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-],
+                                   [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+
+  dnl Search for lib[]Name and define LIB[]NAME, LTLIB[]NAME and INC[]NAME
+  dnl accordingly.
+  AC_LIB_LINKFLAGS_BODY([$1], [$2])
+
+  dnl Add $INC[]NAME to CPPFLAGS before performing the following checks,
+  dnl because if the user has installed lib[]Name and not disabled its use
+  dnl via --without-lib[]Name-prefix, he wants to use it.
+  ac_save_CPPFLAGS="$CPPFLAGS"
+  AC_LIB_APPENDTOVAR([CPPFLAGS], [$INC]NAME)
+
+  AC_CACHE_CHECK([for lib[]$1], [ac_cv_lib[]Name], [
+    ac_save_LIBS="$LIBS"
+    dnl If $LIB[]NAME contains some -l options, add it to the end of LIBS,
+    dnl because these -l options might require -L options that are present in
+    dnl LIBS. -l options benefit only from the -L options listed before it.
+    dnl Otherwise, add it to the front of LIBS, because it may be a static
+    dnl library that depends on another static library that is present in LIBS.
+    dnl Static libraries benefit only from the static libraries listed after
+    dnl it.
+    case " $LIB[]NAME" in
+      *" -l"*) LIBS="$LIBS $LIB[]NAME" ;;
+      *)       LIBS="$LIB[]NAME $LIBS" ;;
+    esac
+    AC_LINK_IFELSE(
+      [AC_LANG_PROGRAM([[$3]], [[$4]])],
+      [ac_cv_lib[]Name=yes],
+      [ac_cv_lib[]Name='m4_if([$5], [], [no], [[$5]])'])
+    LIBS="$ac_save_LIBS"
+  ])
+  if test "$ac_cv_lib[]Name" = yes; then
+    HAVE_LIB[]NAME=yes
+    AC_DEFINE([HAVE_LIB]NAME, 1, [Define if you have the lib][$1 library.])
+    AC_MSG_CHECKING([how to link with lib[]$1])
+    AC_MSG_RESULT([$LIB[]NAME])
+  else
+    HAVE_LIB[]NAME=no
+    dnl If $LIB[]NAME didn't lead to a usable library, we don't need
+    dnl $INC[]NAME either.
+    CPPFLAGS="$ac_save_CPPFLAGS"
+    LIB[]NAME=
+    LTLIB[]NAME=
+    LIB[]NAME[]_PREFIX=
+  fi
+  AC_SUBST([HAVE_LIB]NAME)
+  AC_SUBST([LIB]NAME)
+  AC_SUBST([LTLIB]NAME)
+  AC_SUBST([LIB]NAME[_PREFIX])
+  popdef([NAME])
+  popdef([Name])
+])
+
+dnl Determine the platform dependent parameters needed to use rpath:
+dnl   acl_libext,
+dnl   acl_shlibext,
+dnl   acl_libname_spec,
+dnl   acl_library_names_spec,
+dnl   acl_hardcode_libdir_flag_spec,
+dnl   acl_hardcode_libdir_separator,
+dnl   acl_hardcode_direct,
+dnl   acl_hardcode_minus_L.
+AC_DEFUN([AC_LIB_RPATH],
+[
+  dnl Complain if config.rpath is missing.
+  AC_REQUIRE_AUX_FILE([config.rpath])
+  AC_REQUIRE([AC_PROG_CC])                dnl we use $CC, $GCC, $LDFLAGS
+  AC_REQUIRE([AC_LIB_PROG_LD])            dnl we use $LD, $with_gnu_ld
+  AC_REQUIRE([AC_CANONICAL_HOST])         dnl we use $host
+  AC_REQUIRE([AC_CONFIG_AUX_DIR_DEFAULT]) dnl we use $ac_aux_dir
+  AC_CACHE_CHECK([for shared library run path origin], [acl_cv_rpath], [
+    CC="$CC" GCC="$GCC" LDFLAGS="$LDFLAGS" LD="$LD" with_gnu_ld="$with_gnu_ld" \
+    ${CONFIG_SHELL-/bin/sh} "$ac_aux_dir/config.rpath" "$host" > conftest.sh
+    . ./conftest.sh
+    rm -f ./conftest.sh
+    acl_cv_rpath=done
+  ])
+  wl="$acl_cv_wl"
+  acl_libext="$acl_cv_libext"
+  acl_shlibext="$acl_cv_shlibext"
+  acl_libname_spec="$acl_cv_libname_spec"
+  acl_library_names_spec="$acl_cv_library_names_spec"
+  acl_hardcode_libdir_flag_spec="$acl_cv_hardcode_libdir_flag_spec"
+  acl_hardcode_libdir_separator="$acl_cv_hardcode_libdir_separator"
+  acl_hardcode_direct="$acl_cv_hardcode_direct"
+  acl_hardcode_minus_L="$acl_cv_hardcode_minus_L"
+  dnl Determine whether the user wants rpath handling at all.
+  AC_ARG_ENABLE([rpath],
+    [  --disable-rpath         do not hardcode runtime library paths],
+    :, enable_rpath=yes)
+])
+
+dnl AC_LIB_FROMPACKAGE(name, package)
+dnl declares that libname comes from the given package. The configure file
+dnl will then not have a --with-libname-prefix option but a
+dnl --with-package-prefix option. Several libraries can come from the same
+dnl package. This declaration must occur before an AC_LIB_LINKFLAGS or similar
+dnl macro call that searches for libname.
+AC_DEFUN([AC_LIB_FROMPACKAGE],
+[
+  pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-],
+                                   [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+  define([acl_frompackage_]NAME, [$2])
+  popdef([NAME])
+  pushdef([PACK],[$2])
+  pushdef([PACKUP],[m4_translit(PACK,[abcdefghijklmnopqrstuvwxyz./+-],
+                                     [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+  define([acl_libsinpackage_]PACKUP,
+    m4_ifdef([acl_libsinpackage_]PACKUP, [m4_defn([acl_libsinpackage_]PACKUP)[, ]],)[lib$1])
+  popdef([PACKUP])
+  popdef([PACK])
+])
+
+dnl AC_LIB_LINKFLAGS_BODY(name [, dependencies]) searches for libname and
+dnl the libraries corresponding to explicit and implicit dependencies.
+dnl Sets the LIB${NAME}, LTLIB${NAME} and INC${NAME} variables.
+dnl Also, sets the LIB${NAME}_PREFIX variable to nonempty if libname was found
+dnl in ${LIB${NAME}_PREFIX}/$acl_libdirstem.
+AC_DEFUN([AC_LIB_LINKFLAGS_BODY],
+[
+  AC_REQUIRE([AC_LIB_PREPARE_MULTILIB])
+  pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-],
+                                   [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+  pushdef([PACK],[m4_ifdef([acl_frompackage_]NAME, [acl_frompackage_]NAME, lib[$1])])
+  pushdef([PACKUP],[m4_translit(PACK,[abcdefghijklmnopqrstuvwxyz./+-],
+                                     [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])])
+  pushdef([PACKLIBS],[m4_ifdef([acl_frompackage_]NAME, [acl_libsinpackage_]PACKUP, lib[$1])])
+  dnl By default, look in $includedir and $libdir.
+  use_additional=yes
+  AC_LIB_WITH_FINAL_PREFIX([
+    eval additional_includedir=\"$includedir\"
+    eval additional_libdir=\"$libdir\"
+  ])
+  AC_ARG_WITH(PACK[-prefix],
+[[  --with-]]PACK[[-prefix[=DIR]  search for ]PACKLIBS[ in DIR/include and DIR/lib
+  --without-]]PACK[[-prefix     don't search for ]PACKLIBS[ in includedir and libdir]],
+[
+    if test "X$withval" = "Xno"; then
+      use_additional=no
+    else
+      if test "X$withval" = "X"; then
+        AC_LIB_WITH_FINAL_PREFIX([
+          eval additional_includedir=\"$includedir\"
+          eval additional_libdir=\"$libdir\"
+        ])
+      else
+        additional_includedir="$withval/include"
+        additional_libdir="$withval/$acl_libdirstem"
+        if test "$acl_libdirstem2" != "$acl_libdirstem" \
+           && test ! -d "$withval/$acl_libdirstem"; then
+          additional_libdir="$withval/$acl_libdirstem2"
+        fi
+      fi
+    fi
+])
+  dnl Search the library and its dependencies in $additional_libdir and
+  dnl $LDFLAGS. Using breadth-first-seach.
+  LIB[]NAME=
+  LTLIB[]NAME=
+  INC[]NAME=
+  LIB[]NAME[]_PREFIX=
+  dnl HAVE_LIB${NAME} is an indicator that LIB${NAME}, LTLIB${NAME} have been
+  dnl computed. So it has to be reset here.
+  HAVE_LIB[]NAME=
+  rpathdirs=
+  ltrpathdirs=
+  names_already_handled=
+  names_next_round='$1 $2'
+  while test -n "$names_next_round"; do
+    names_this_round="$names_next_round"
+    names_next_round=
+    for name in $names_this_round; do
+      already_handled=
+      for n in $names_already_handled; do
+        if test "$n" = "$name"; then
+          already_handled=yes
+          break
+        fi
+      done
+      if test -z "$already_handled"; then
+        names_already_handled="$names_already_handled $name"
+        dnl See if it was already located by an earlier AC_LIB_LINKFLAGS
+        dnl or AC_LIB_HAVE_LINKFLAGS call.
+        uppername=`echo "$name" | sed -e 'y|abcdefghijklmnopqrstuvwxyz./+-|ABCDEFGHIJKLMNOPQRSTUVWXYZ____|'`
+        eval value=\"\$HAVE_LIB$uppername\"
+        if test -n "$value"; then
+          if test "$value" = yes; then
+            eval value=\"\$LIB$uppername\"
+            test -z "$value" || LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$value"
+            eval value=\"\$LTLIB$uppername\"
+            test -z "$value" || LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }$value"
+          else
+            dnl An earlier call to AC_LIB_HAVE_LINKFLAGS has determined
+            dnl that this library doesn't exist. So just drop it.
+            :
+          fi
+        else
+          dnl Search the library lib$name in $additional_libdir and $LDFLAGS
+          dnl and the already constructed $LIBNAME/$LTLIBNAME.
+          found_dir=
+          found_la=
+          found_so=
+          found_a=
+          eval libname=\"$acl_libname_spec\"    # typically: libname=lib$name
+          if test -n "$acl_shlibext"; then
+            shrext=".$acl_shlibext"             # typically: shrext=.so
+          else
+            shrext=
+          fi
+          if test $use_additional = yes; then
+            dir="$additional_libdir"
+            dnl The same code as in the loop below:
+            dnl First look for a shared library.
+            if test -n "$acl_shlibext"; then
+              if test -f "$dir/$libname$shrext"; then
+                found_dir="$dir"
+                found_so="$dir/$libname$shrext"
+              else
+                if test "$acl_library_names_spec" = '$libname$shrext$versuffix'; then
+                  ver=`(cd "$dir" && \
+                        for f in "$libname$shrext".*; do echo "$f"; done \
+                        | sed -e "s,^$libname$shrext\\\\.,," \
+                        | sort -t '.' -n -r -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 \
+                        | sed 1q ) 2>/dev/null`
+                  if test -n "$ver" && test -f "$dir/$libname$shrext.$ver"; then
+                    found_dir="$dir"
+                    found_so="$dir/$libname$shrext.$ver"
+                  fi
+                else
+                  eval library_names=\"$acl_library_names_spec\"
+                  for f in $library_names; do
+                    if test -f "$dir/$f"; then
+                      found_dir="$dir"
+                      found_so="$dir/$f"
+                      break
+                    fi
+                  done
+                fi
+              fi
+            fi
+            dnl Then look for a static library.
+            if test "X$found_dir" = "X"; then
+              if test -f "$dir/$libname.$acl_libext"; then
+                found_dir="$dir"
+                found_a="$dir/$libname.$acl_libext"
+              fi
+            fi
+            if test "X$found_dir" != "X"; then
+              if test -f "$dir/$libname.la"; then
+                found_la="$dir/$libname.la"
+              fi
+            fi
+          fi
+          if test "X$found_dir" = "X"; then
+            for x in $LDFLAGS $LTLIB[]NAME; do
+              AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+              case "$x" in
+                -L*)
+                  dir=`echo "X$x" | sed -e 's/^X-L//'`
+                  dnl First look for a shared library.
+                  if test -n "$acl_shlibext"; then
+                    if test -f "$dir/$libname$shrext"; then
+                      found_dir="$dir"
+                      found_so="$dir/$libname$shrext"
+                    else
+                      if test "$acl_library_names_spec" = '$libname$shrext$versuffix'; then
+                        ver=`(cd "$dir" && \
+                              for f in "$libname$shrext".*; do echo "$f"; done \
+                              | sed -e "s,^$libname$shrext\\\\.,," \
+                              | sort -t '.' -n -r -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 \
+                              | sed 1q ) 2>/dev/null`
+                        if test -n "$ver" && test -f "$dir/$libname$shrext.$ver"; then
+                          found_dir="$dir"
+                          found_so="$dir/$libname$shrext.$ver"
+                        fi
+                      else
+                        eval library_names=\"$acl_library_names_spec\"
+                        for f in $library_names; do
+                          if test -f "$dir/$f"; then
+                            found_dir="$dir"
+                            found_so="$dir/$f"
+                            break
+                          fi
+                        done
+                      fi
+                    fi
+                  fi
+                  dnl Then look for a static library.
+                  if test "X$found_dir" = "X"; then
+                    if test -f "$dir/$libname.$acl_libext"; then
+                      found_dir="$dir"
+                      found_a="$dir/$libname.$acl_libext"
+                    fi
+                  fi
+                  if test "X$found_dir" != "X"; then
+                    if test -f "$dir/$libname.la"; then
+                      found_la="$dir/$libname.la"
+                    fi
+                  fi
+                  ;;
+              esac
+              if test "X$found_dir" != "X"; then
+                break
+              fi
+            done
+          fi
+          if test "X$found_dir" != "X"; then
+            dnl Found the library.
+            LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-L$found_dir -l$name"
+            if test "X$found_so" != "X"; then
+              dnl Linking with a shared library. We attempt to hardcode its
+              dnl directory into the executable's runpath, unless it's the
+              dnl standard /usr/lib.
+              if test "$enable_rpath" = no \
+                 || test "X$found_dir" = "X/usr/$acl_libdirstem" \
+                 || test "X$found_dir" = "X/usr/$acl_libdirstem2"; then
+                dnl No hardcoding is needed.
+                LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so"
+              else
+                dnl Use an explicit option to hardcode DIR into the resulting
+                dnl binary.
+                dnl Potentially add DIR to ltrpathdirs.
+                dnl The ltrpathdirs will be appended to $LTLIBNAME at the end.
+                haveit=
+                for x in $ltrpathdirs; do
+                  if test "X$x" = "X$found_dir"; then
+                    haveit=yes
+                    break
+                  fi
+                done
+                if test -z "$haveit"; then
+                  ltrpathdirs="$ltrpathdirs $found_dir"
+                fi
+                dnl The hardcoding into $LIBNAME is system dependent.
+                if test "$acl_hardcode_direct" = yes; then
+                  dnl Using DIR/libNAME.so during linking hardcodes DIR into the
+                  dnl resulting binary.
+                  LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so"
+                else
+                  if test -n "$acl_hardcode_libdir_flag_spec" && test "$acl_hardcode_minus_L" = no; then
+                    dnl Use an explicit option to hardcode DIR into the resulting
+                    dnl binary.
+                    LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so"
+                    dnl Potentially add DIR to rpathdirs.
+                    dnl The rpathdirs will be appended to $LIBNAME at the end.
+                    haveit=
+                    for x in $rpathdirs; do
+                      if test "X$x" = "X$found_dir"; then
+                        haveit=yes
+                        break
+                      fi
+                    done
+                    if test -z "$haveit"; then
+                      rpathdirs="$rpathdirs $found_dir"
+                    fi
+                  else
+                    dnl Rely on "-L$found_dir".
+                    dnl But don't add it if it's already contained in the LDFLAGS
+                    dnl or the already constructed $LIBNAME
+                    haveit=
+                    for x in $LDFLAGS $LIB[]NAME; do
+                      AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+                      if test "X$x" = "X-L$found_dir"; then
+                        haveit=yes
+                        break
+                      fi
+                    done
+                    if test -z "$haveit"; then
+                      LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$found_dir"
+                    fi
+                    if test "$acl_hardcode_minus_L" != no; then
+                      dnl FIXME: Not sure whether we should use
+                      dnl "-L$found_dir -l$name" or "-L$found_dir $found_so"
+                      dnl here.
+                      LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so"
+                    else
+                      dnl We cannot use $acl_hardcode_runpath_var and LD_RUN_PATH
+                      dnl here, because this doesn't fit in flags passed to the
+                      dnl compiler. So give up. No hardcoding. This affects only
+                      dnl very old systems.
+                      dnl FIXME: Not sure whether we should use
+                      dnl "-L$found_dir -l$name" or "-L$found_dir $found_so"
+                      dnl here.
+                      LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-l$name"
+                    fi
+                  fi
+                fi
+              fi
+            else
+              if test "X$found_a" != "X"; then
+                dnl Linking with a static library.
+                LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_a"
+              else
+                dnl We shouldn't come here, but anyway it's good to have a
+                dnl fallback.
+                LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$found_dir -l$name"
+              fi
+            fi
+            dnl Assume the include files are nearby.
+            additional_includedir=
+            case "$found_dir" in
+              */$acl_libdirstem | */$acl_libdirstem/)
+                basedir=`echo "X$found_dir" | sed -e 's,^X,,' -e "s,/$acl_libdirstem/"'*$,,'`
+                if test "$name" = '$1'; then
+                  LIB[]NAME[]_PREFIX="$basedir"
+                fi
+                additional_includedir="$basedir/include"
+                ;;
+              */$acl_libdirstem2 | */$acl_libdirstem2/)
+                basedir=`echo "X$found_dir" | sed -e 's,^X,,' -e "s,/$acl_libdirstem2/"'*$,,'`
+                if test "$name" = '$1'; then
+                  LIB[]NAME[]_PREFIX="$basedir"
+                fi
+                additional_includedir="$basedir/include"
+                ;;
+            esac
+            if test "X$additional_includedir" != "X"; then
+              dnl Potentially add $additional_includedir to $INCNAME.
+              dnl But don't add it
+              dnl   1. if it's the standard /usr/include,
+              dnl   2. if it's /usr/local/include and we are using GCC on Linux,
+              dnl   3. if it's already present in $CPPFLAGS or the already
+              dnl      constructed $INCNAME,
+              dnl   4. if it doesn't exist as a directory.
+              if test "X$additional_includedir" != "X/usr/include"; then
+                haveit=
+                if test "X$additional_includedir" = "X/usr/local/include"; then
+                  if test -n "$GCC"; then
+                    case $host_os in
+                      linux* | gnu* | k*bsd*-gnu) haveit=yes;;
+                    esac
+                  fi
+                fi
+                if test -z "$haveit"; then
+                  for x in $CPPFLAGS $INC[]NAME; do
+                    AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+                    if test "X$x" = "X-I$additional_includedir"; then
+                      haveit=yes
+                      break
+                    fi
+                  done
+                  if test -z "$haveit"; then
+                    if test -d "$additional_includedir"; then
+                      dnl Really add $additional_includedir to $INCNAME.
+                      INC[]NAME="${INC[]NAME}${INC[]NAME:+ }-I$additional_includedir"
+                    fi
+                  fi
+                fi
+              fi
+            fi
+            dnl Look for dependencies.
+            if test -n "$found_la"; then
+              dnl Read the .la file. It defines the variables
+              dnl dlname, library_names, old_library, dependency_libs, current,
+              dnl age, revision, installed, dlopen, dlpreopen, libdir.
+              save_libdir="$libdir"
+              case "$found_la" in
+                */* | *\\*) . "$found_la" ;;
+                *) . "./$found_la" ;;
+              esac
+              libdir="$save_libdir"
+              dnl We use only dependency_libs.
+              for dep in $dependency_libs; do
+                case "$dep" in
+                  -L*)
+                    additional_libdir=`echo "X$dep" | sed -e 's/^X-L//'`
+                    dnl Potentially add $additional_libdir to $LIBNAME and $LTLIBNAME.
+                    dnl But don't add it
+                    dnl   1. if it's the standard /usr/lib,
+                    dnl   2. if it's /usr/local/lib and we are using GCC on Linux,
+                    dnl   3. if it's already present in $LDFLAGS or the already
+                    dnl      constructed $LIBNAME,
+                    dnl   4. if it doesn't exist as a directory.
+                    if test "X$additional_libdir" != "X/usr/$acl_libdirstem" \
+                       && test "X$additional_libdir" != "X/usr/$acl_libdirstem2"; then
+                      haveit=
+                      if test "X$additional_libdir" = "X/usr/local/$acl_libdirstem" \
+                         || test "X$additional_libdir" = "X/usr/local/$acl_libdirstem2"; then
+                        if test -n "$GCC"; then
+                          case $host_os in
+                            linux* | gnu* | k*bsd*-gnu) haveit=yes;;
+                          esac
+                        fi
+                      fi
+                      if test -z "$haveit"; then
+                        haveit=
+                        for x in $LDFLAGS $LIB[]NAME; do
+                          AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+                          if test "X$x" = "X-L$additional_libdir"; then
+                            haveit=yes
+                            break
+                          fi
+                        done
+                        if test -z "$haveit"; then
+                          if test -d "$additional_libdir"; then
+                            dnl Really add $additional_libdir to $LIBNAME.
+                            LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$additional_libdir"
+                          fi
+                        fi
+                        haveit=
+                        for x in $LDFLAGS $LTLIB[]NAME; do
+                          AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+                          if test "X$x" = "X-L$additional_libdir"; then
+                            haveit=yes
+                            break
+                          fi
+                        done
+                        if test -z "$haveit"; then
+                          if test -d "$additional_libdir"; then
+                            dnl Really add $additional_libdir to $LTLIBNAME.
+                            LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-L$additional_libdir"
+                          fi
+                        fi
+                      fi
+                    fi
+                    ;;
+                  -R*)
+                    dir=`echo "X$dep" | sed -e 's/^X-R//'`
+                    if test "$enable_rpath" != no; then
+                      dnl Potentially add DIR to rpathdirs.
+                      dnl The rpathdirs will be appended to $LIBNAME at the end.
+                      haveit=
+                      for x in $rpathdirs; do
+                        if test "X$x" = "X$dir"; then
+                          haveit=yes
+                          break
+                        fi
+                      done
+                      if test -z "$haveit"; then
+                        rpathdirs="$rpathdirs $dir"
+                      fi
+                      dnl Potentially add DIR to ltrpathdirs.
+                      dnl The ltrpathdirs will be appended to $LTLIBNAME at the end.
+                      haveit=
+                      for x in $ltrpathdirs; do
+                        if test "X$x" = "X$dir"; then
+                          haveit=yes
+                          break
+                        fi
+                      done
+                      if test -z "$haveit"; then
+                        ltrpathdirs="$ltrpathdirs $dir"
+                      fi
+                    fi
+                    ;;
+                  -l*)
+                    dnl Handle this in the next round.
+                    names_next_round="$names_next_round "`echo "X$dep" | sed -e 's/^X-l//'`
+                    ;;
+                  *.la)
+                    dnl Handle this in the next round. Throw away the .la's
+                    dnl directory; it is already contained in a preceding -L
+                    dnl option.
+                    names_next_round="$names_next_round "`echo "X$dep" | sed -e 's,^X.*/,,' -e 's,^lib,,' -e 's,\.la$,,'`
+                    ;;
+                  *)
+                    dnl Most likely an immediate library name.
+                    LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$dep"
+                    LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }$dep"
+                    ;;
+                esac
+              done
+            fi
+          else
+            dnl Didn't find the library; assume it is in the system directories
+            dnl known to the linker and runtime loader. (All the system
+            dnl directories known to the linker should also be known to the
+            dnl runtime loader, otherwise the system is severely misconfigured.)
+            LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-l$name"
+            LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-l$name"
+          fi
+        fi
+      fi
+    done
+  done
+  if test "X$rpathdirs" != "X"; then
+    if test -n "$acl_hardcode_libdir_separator"; then
+      dnl Weird platform: only the last -rpath option counts, the user must
+      dnl pass all path elements in one option. We can arrange that for a
+      dnl single library, but not when more than one $LIBNAMEs are used.
+      alldirs=
+      for found_dir in $rpathdirs; do
+        alldirs="${alldirs}${alldirs:+$acl_hardcode_libdir_separator}$found_dir"
+      done
+      dnl Note: acl_hardcode_libdir_flag_spec uses $libdir and $wl.
+      acl_save_libdir="$libdir"
+      libdir="$alldirs"
+      eval flag=\"$acl_hardcode_libdir_flag_spec\"
+      libdir="$acl_save_libdir"
+      LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$flag"
+    else
+      dnl The -rpath options are cumulative.
+      for found_dir in $rpathdirs; do
+        acl_save_libdir="$libdir"
+        libdir="$found_dir"
+        eval flag=\"$acl_hardcode_libdir_flag_spec\"
+        libdir="$acl_save_libdir"
+        LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$flag"
+      done
+    fi
+  fi
+  if test "X$ltrpathdirs" != "X"; then
+    dnl When using libtool, the option that works for both libraries and
+    dnl executables is -R. The -R options are cumulative.
+    for found_dir in $ltrpathdirs; do
+      LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-R$found_dir"
+    done
+  fi
+  popdef([PACKLIBS])
+  popdef([PACKUP])
+  popdef([PACK])
+  popdef([NAME])
+])
+
+dnl AC_LIB_APPENDTOVAR(VAR, CONTENTS) appends the elements of CONTENTS to VAR,
+dnl unless already present in VAR.
+dnl Works only for CPPFLAGS, not for LIB* variables because that sometimes
+dnl contains two or three consecutive elements that belong together.
+AC_DEFUN([AC_LIB_APPENDTOVAR],
+[
+  for element in [$2]; do
+    haveit=
+    for x in $[$1]; do
+      AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+      if test "X$x" = "X$element"; then
+        haveit=yes
+        break
+      fi
+    done
+    if test -z "$haveit"; then
+      [$1]="${[$1]}${[$1]:+ }$element"
+    fi
+  done
+])
+
+dnl For those cases where a variable contains several -L and -l options
+dnl referring to unknown libraries and directories, this macro determines the
+dnl necessary additional linker options for the runtime path.
+dnl AC_LIB_LINKFLAGS_FROM_LIBS([LDADDVAR], [LIBSVALUE], [USE-LIBTOOL])
+dnl sets LDADDVAR to linker options needed together with LIBSVALUE.
+dnl If USE-LIBTOOL evaluates to non-empty, linking with libtool is assumed,
+dnl otherwise linking without libtool is assumed.
+AC_DEFUN([AC_LIB_LINKFLAGS_FROM_LIBS],
+[
+  AC_REQUIRE([AC_LIB_RPATH])
+  AC_REQUIRE([AC_LIB_PREPARE_MULTILIB])
+  $1=
+  if test "$enable_rpath" != no; then
+    if test -n "$acl_hardcode_libdir_flag_spec" && test "$acl_hardcode_minus_L" = no; then
+      dnl Use an explicit option to hardcode directories into the resulting
+      dnl binary.
+      rpathdirs=
+      next=
+      for opt in $2; do
+        if test -n "$next"; then
+          dir="$next"
+          dnl No need to hardcode the standard /usr/lib.
+          if test "X$dir" != "X/usr/$acl_libdirstem" \
+             && test "X$dir" != "X/usr/$acl_libdirstem2"; then
+            rpathdirs="$rpathdirs $dir"
+          fi
+          next=
+        else
+          case $opt in
+            -L) next=yes ;;
+            -L*) dir=`echo "X$opt" | sed -e 's,^X-L,,'`
+                 dnl No need to hardcode the standard /usr/lib.
+                 if test "X$dir" != "X/usr/$acl_libdirstem" \
+                    && test "X$dir" != "X/usr/$acl_libdirstem2"; then
+                   rpathdirs="$rpathdirs $dir"
+                 fi
+                 next= ;;
+            *) next= ;;
+          esac
+        fi
+      done
+      if test "X$rpathdirs" != "X"; then
+        if test -n ""$3""; then
+          dnl libtool is used for linking. Use -R options.
+          for dir in $rpathdirs; do
+            $1="${$1}${$1:+ }-R$dir"
+          done
+        else
+          dnl The linker is used for linking directly.
+          if test -n "$acl_hardcode_libdir_separator"; then
+            dnl Weird platform: only the last -rpath option counts, the user
+            dnl must pass all path elements in one option.
+            alldirs=
+            for dir in $rpathdirs; do
+              alldirs="${alldirs}${alldirs:+$acl_hardcode_libdir_separator}$dir"
+            done
+            acl_save_libdir="$libdir"
+            libdir="$alldirs"
+            eval flag=\"$acl_hardcode_libdir_flag_spec\"
+            libdir="$acl_save_libdir"
+            $1="$flag"
+          else
+            dnl The -rpath options are cumulative.
+            for dir in $rpathdirs; do
+              acl_save_libdir="$libdir"
+              libdir="$dir"
+              eval flag=\"$acl_hardcode_libdir_flag_spec\"
+              libdir="$acl_save_libdir"
+              $1="${$1}${$1:+ }$flag"
+            done
+          fi
+        fi
+      fi
+    fi
+  fi
+  AC_SUBST([$1])
+])
diff --git a/m4/lib-prefix.m4 b/m4/lib-prefix.m4
new file mode 100644 (file)
index 0000000..8adb17b
--- /dev/null
@@ -0,0 +1,249 @@
+# lib-prefix.m4 serial 14
+dnl Copyright (C) 2001-2005, 2008-2019 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+dnl From Bruno Haible.
+
+dnl AC_LIB_PREFIX adds to the CPPFLAGS and LDFLAGS the flags that are needed
+dnl to access previously installed libraries. The basic assumption is that
+dnl a user will want packages to use other packages he previously installed
+dnl with the same --prefix option.
+dnl This macro is not needed if only AC_LIB_LINKFLAGS is used to locate
+dnl libraries, but is otherwise very convenient.
+AC_DEFUN([AC_LIB_PREFIX],
+[
+  AC_BEFORE([$0], [AC_LIB_LINKFLAGS])
+  AC_REQUIRE([AC_PROG_CC])
+  AC_REQUIRE([AC_CANONICAL_HOST])
+  AC_REQUIRE([AC_LIB_PREPARE_MULTILIB])
+  AC_REQUIRE([AC_LIB_PREPARE_PREFIX])
+  dnl By default, look in $includedir and $libdir.
+  use_additional=yes
+  AC_LIB_WITH_FINAL_PREFIX([
+    eval additional_includedir=\"$includedir\"
+    eval additional_libdir=\"$libdir\"
+  ])
+  AC_ARG_WITH([lib-prefix],
+[[  --with-lib-prefix[=DIR] search for libraries in DIR/include and DIR/lib
+  --without-lib-prefix    don't search for libraries in includedir and libdir]],
+[
+    if test "X$withval" = "Xno"; then
+      use_additional=no
+    else
+      if test "X$withval" = "X"; then
+        AC_LIB_WITH_FINAL_PREFIX([
+          eval additional_includedir=\"$includedir\"
+          eval additional_libdir=\"$libdir\"
+        ])
+      else
+        additional_includedir="$withval/include"
+        additional_libdir="$withval/$acl_libdirstem"
+      fi
+    fi
+])
+  if test $use_additional = yes; then
+    dnl Potentially add $additional_includedir to $CPPFLAGS.
+    dnl But don't add it
+    dnl   1. if it's the standard /usr/include,
+    dnl   2. if it's already present in $CPPFLAGS,
+    dnl   3. if it's /usr/local/include and we are using GCC on Linux,
+    dnl   4. if it doesn't exist as a directory.
+    if test "X$additional_includedir" != "X/usr/include"; then
+      haveit=
+      for x in $CPPFLAGS; do
+        AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+        if test "X$x" = "X-I$additional_includedir"; then
+          haveit=yes
+          break
+        fi
+      done
+      if test -z "$haveit"; then
+        if test "X$additional_includedir" = "X/usr/local/include"; then
+          if test -n "$GCC"; then
+            case $host_os in
+              linux* | gnu* | k*bsd*-gnu) haveit=yes;;
+            esac
+          fi
+        fi
+        if test -z "$haveit"; then
+          if test -d "$additional_includedir"; then
+            dnl Really add $additional_includedir to $CPPFLAGS.
+            CPPFLAGS="${CPPFLAGS}${CPPFLAGS:+ }-I$additional_includedir"
+          fi
+        fi
+      fi
+    fi
+    dnl Potentially add $additional_libdir to $LDFLAGS.
+    dnl But don't add it
+    dnl   1. if it's the standard /usr/lib,
+    dnl   2. if it's already present in $LDFLAGS,
+    dnl   3. if it's /usr/local/lib and we are using GCC on Linux,
+    dnl   4. if it doesn't exist as a directory.
+    if test "X$additional_libdir" != "X/usr/$acl_libdirstem"; then
+      haveit=
+      for x in $LDFLAGS; do
+        AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"])
+        if test "X$x" = "X-L$additional_libdir"; then
+          haveit=yes
+          break
+        fi
+      done
+      if test -z "$haveit"; then
+        if test "X$additional_libdir" = "X/usr/local/$acl_libdirstem"; then
+          if test -n "$GCC"; then
+            case $host_os in
+              linux*) haveit=yes;;
+            esac
+          fi
+        fi
+        if test -z "$haveit"; then
+          if test -d "$additional_libdir"; then
+            dnl Really add $additional_libdir to $LDFLAGS.
+            LDFLAGS="${LDFLAGS}${LDFLAGS:+ }-L$additional_libdir"
+          fi
+        fi
+      fi
+    fi
+  fi
+])
+
+dnl AC_LIB_PREPARE_PREFIX creates variables acl_final_prefix,
+dnl acl_final_exec_prefix, containing the values to which $prefix and
+dnl $exec_prefix will expand at the end of the configure script.
+AC_DEFUN([AC_LIB_PREPARE_PREFIX],
+[
+  dnl Unfortunately, prefix and exec_prefix get only finally determined
+  dnl at the end of configure.
+  if test "X$prefix" = "XNONE"; then
+    acl_final_prefix="$ac_default_prefix"
+  else
+    acl_final_prefix="$prefix"
+  fi
+  if test "X$exec_prefix" = "XNONE"; then
+    acl_final_exec_prefix='${prefix}'
+  else
+    acl_final_exec_prefix="$exec_prefix"
+  fi
+  acl_save_prefix="$prefix"
+  prefix="$acl_final_prefix"
+  eval acl_final_exec_prefix=\"$acl_final_exec_prefix\"
+  prefix="$acl_save_prefix"
+])
+
+dnl AC_LIB_WITH_FINAL_PREFIX([statement]) evaluates statement, with the
+dnl variables prefix and exec_prefix bound to the values they will have
+dnl at the end of the configure script.
+AC_DEFUN([AC_LIB_WITH_FINAL_PREFIX],
+[
+  acl_save_prefix="$prefix"
+  prefix="$acl_final_prefix"
+  acl_save_exec_prefix="$exec_prefix"
+  exec_prefix="$acl_final_exec_prefix"
+  $1
+  exec_prefix="$acl_save_exec_prefix"
+  prefix="$acl_save_prefix"
+])
+
+dnl AC_LIB_PREPARE_MULTILIB creates
+dnl - a variable acl_libdirstem, containing the basename of the libdir, either
+dnl   "lib" or "lib64" or "lib/64",
+dnl - a variable acl_libdirstem2, as a secondary possible value for
+dnl   acl_libdirstem, either the same as acl_libdirstem or "lib/sparcv9" or
+dnl   "lib/amd64".
+AC_DEFUN([AC_LIB_PREPARE_MULTILIB],
+[
+  dnl There is no formal standard regarding lib and lib64.
+  dnl On glibc systems, the current practice is that on a system supporting
+  dnl 32-bit and 64-bit instruction sets or ABIs, 64-bit libraries go under
+  dnl $prefix/lib64 and 32-bit libraries go under $prefix/lib. We determine
+  dnl the compiler's default mode by looking at the compiler's library search
+  dnl path. If at least one of its elements ends in /lib64 or points to a
+  dnl directory whose absolute pathname ends in /lib64, we assume a 64-bit ABI.
+  dnl Otherwise we use the default, namely "lib".
+  dnl On Solaris systems, the current practice is that on a system supporting
+  dnl 32-bit and 64-bit instruction sets or ABIs, 64-bit libraries go under
+  dnl $prefix/lib/64 (which is a symlink to either $prefix/lib/sparcv9 or
+  dnl $prefix/lib/amd64) and 32-bit libraries go under $prefix/lib.
+  AC_REQUIRE([AC_CANONICAL_HOST])
+  AC_REQUIRE([gl_HOST_CPU_C_ABI_32BIT])
+
+  case "$host_os" in
+    solaris*)
+      AC_CACHE_CHECK([for 64-bit host], [gl_cv_solaris_64bit],
+        [AC_COMPILE_IFELSE(
+           [AC_LANG_SOURCE(
+              [[#ifdef _LP64
+                 int ok;
+                #else
+                 error fail
+                #endif
+              ]])],
+           [gl_cv_solaris_64bit=yes],
+           [gl_cv_solaris_64bit=no])
+        ]);;
+  esac
+
+  dnl Allow the user to override the result by setting acl_cv_libdirstems.
+  AC_CACHE_CHECK([for the common suffixes of directories in the library search path],
+    [acl_cv_libdirstems],
+    [acl_libdirstem=lib
+     acl_libdirstem2=
+     case "$host_os" in
+       solaris*)
+         dnl See Solaris 10 Software Developer Collection > Solaris 64-bit Developer's Guide > The Development Environment
+         dnl <https://docs.oracle.com/cd/E19253-01/816-5138/dev-env/index.html>.
+         dnl "Portable Makefiles should refer to any library directories using the 64 symbolic link."
+         dnl But we want to recognize the sparcv9 or amd64 subdirectory also if the
+         dnl symlink is missing, so we set acl_libdirstem2 too.
+         if test $gl_cv_solaris_64bit = yes; then
+           acl_libdirstem=lib/64
+           case "$host_cpu" in
+             sparc*)        acl_libdirstem2=lib/sparcv9 ;;
+             i*86 | x86_64) acl_libdirstem2=lib/amd64 ;;
+           esac
+         fi
+         ;;
+       *)
+         dnl If $CC generates code for a 32-bit ABI, the libraries are
+         dnl surely under $prefix/lib, not $prefix/lib64.
+         if test "$HOST_CPU_C_ABI_32BIT" != yes; then
+           dnl The result is a property of the system. However, non-system
+           dnl compilers sometimes have odd library search paths. Therefore
+           dnl prefer asking /usr/bin/gcc, if available, rather than $CC.
+           searchpath=`(if test -f /usr/bin/gcc \
+                           && LC_ALL=C /usr/bin/gcc -print-search-dirs >/dev/null 2>/dev/null; then \
+                          LC_ALL=C /usr/bin/gcc -print-search-dirs; \
+                        else \
+                          LC_ALL=C $CC -print-search-dirs; \
+                        fi) 2>/dev/null \
+                       | sed -n -e 's,^libraries: ,,p' | sed -e 's,^=,,'`
+           if test -n "$searchpath"; then
+             acl_save_IFS="${IFS=      }"; IFS=":"
+             for searchdir in $searchpath; do
+               if test -d "$searchdir"; then
+                 case "$searchdir" in
+                   */lib64/ | */lib64 ) acl_libdirstem=lib64 ;;
+                   */../ | */.. )
+                     # Better ignore directories of this form. They are misleading.
+                     ;;
+                   *) searchdir=`cd "$searchdir" && pwd`
+                      case "$searchdir" in
+                        */lib64 ) acl_libdirstem=lib64 ;;
+                      esac ;;
+                 esac
+               fi
+             done
+             IFS="$acl_save_IFS"
+           fi
+         fi
+         ;;
+     esac
+     test -n "$acl_libdirstem2" || acl_libdirstem2="$acl_libdirstem"
+     acl_cv_libdirstems="$acl_libdirstem,$acl_libdirstem2"
+    ])
+  # Decompose acl_cv_libdirstems into acl_libdirstem and acl_libdirstem2.
+  acl_libdirstem=`echo "$acl_cv_libdirstems" | sed -e 's/,.*//'`
+  acl_libdirstem2=`echo "$acl_cv_libdirstems" | sed -e '/,/s/.*,//'`
+])
diff --git a/man/Makefile.am b/man/Makefile.am
new file mode 100644 (file)
index 0000000..ac7fb93
--- /dev/null
@@ -0,0 +1,38 @@
+## Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+dist_man_MANS =                \
+       mu-add.1        \
+       mu-bookmarks.5  \
+       mu-cfind.1      \
+       mu-easy.1       \
+       mu-extract.1    \
+       mu-fields.1     \
+       mu-find.1       \
+       mu-help.1       \
+       mu-index.1      \
+       mu-info.1       \
+       mu-init.1       \
+       mu-mkdir.1      \
+       mu-query.7      \
+       mu-remove.1     \
+       mu-server.1     \
+       mu-script.1     \
+       mu-verify.1     \
+       mu-view.1       \
+       mu.1
diff --git a/man/meson.build b/man/meson.build
new file mode 100644 (file)
index 0000000..81d7e4c
--- /dev/null
@@ -0,0 +1,36 @@
+## Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+install_man(
+  ['mu.1',
+   'mu-add.1',
+   'mu-bookmarks.5',
+   'mu-cfind.1',
+   'mu-easy.1',
+   'mu-extract.1',
+   'mu-fields.1',
+   'mu-find.1',
+   'mu-help.1',
+   'mu-index.1',
+   'mu-info.1',
+   'mu-init.1',
+   'mu-mkdir.1',
+   'mu-query.7',
+   'mu-remove.1',
+   'mu-script.1',
+   'mu-server.1',
+   'mu-verify.1',
+   'mu-view.1'])
diff --git a/man/mu-add.1 b/man/mu-add.1
new file mode 100644 (file)
index 0000000..1bf1ed9
--- /dev/null
@@ -0,0 +1,47 @@
+.TH MU ADD 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+mu add\-  add one or more messages to the database
+
+.SH SYNOPSIS
+
+.B mu add <file> [<files>]
+
+.SH DESCRIPTION
+
+\fBmu add\fR is the command to add specific message files to the
+database. Each file must be specified with an absolute path.
+
+.SH OPTIONS
+
+\fBmu add\fR does not have its own options, but the general options for
+determining the location of the database (\fI--muhome\fR) are available. See
+\fBmu-index\fR(1) for more information.
+
+.SH RETURN VALUE
+
+\fBmu add\fR returns 0 upon success; in general, the following error codes are
+returned:
+
+.nf
+| code | meaning                           |
+|------+-----------------------------------|
+|    0 | ok                                |
+|    1 | general error                     |
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR mu-index (1),
+.BR mu-remove (1)
diff --git a/man/mu-bookmarks.5 b/man/mu-bookmarks.5
new file mode 100644 (file)
index 0000000..f68a335
--- /dev/null
@@ -0,0 +1,36 @@
+.TH MU-BOOKMARKS 5 "July 2019" "User Manuals"
+
+.SH NAME
+
+bookmarks \- file with bookmarks (shortcuts) for mu search expressions
+
+.SH DESCRIPTION
+
+Bookmarks are named shortcuts for search queries. They allow using a convenient
+name for often-used queries. The bookmarks are also visible as shortcuts in the
+mu experimental user interfaces, \fImug\fR and \fImug2\fR.
+
+The bookmarks file is read from \fI<muhome>/bookmarks\fR. On Unix this would
+typically be w be \fI~/.config/mu/bookmarks\fR, but this can be influenced using
+the \fB\-\-muhome\fR parameter for \fBmu-find\fR(1) and \fBmug\fR(1).
+
+The bookmarks file is a typical key=value \fB.ini\fR-file, which is best shown
+by means of an example:
+
+.nf
+    [mu]
+    inbox=maildir:/inbox                  # inbox
+    oldhat=maildir:/archive subject:hat   # archived with subject containing 'hat'
+.fi
+
+The \fB[mu]\fR group header is required.
+
+For practical uses of bookmarks, see \fBmu-find\fR(1).
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1), mu-find (1)
diff --git a/man/mu-cfind.1 b/man/mu-cfind.1
new file mode 100644 (file)
index 0000000..a3618e1
--- /dev/null
@@ -0,0 +1,133 @@
+.TH MU CFIND 1 "April 2019" "User Manuals"
+
+.SH NAME
+
+\fBmu cfind\fR is the \fBmu\fR command to find contacts in the \fBmu\fR
+database and export them for use in other programs.
+
+.SH SYNOPSIS
+
+.B mu cfind [options] [<pattern>]
+
+.SH DESCRIPTION
+
+\fBmu cfind\fR is the \fBmu\fR command for finding \fIcontacts\fR (name and
+e-mail address of people who were either an e-mail's sender or
+receiver). There are different output formats available, for importing the
+contacts into other programs.
+
+.SH SEARCHING CONTACTS
+
+When you index your messages (see \fBmu index\fR), \fBmu\fR creates a list of
+unique e-mail addresses found and the accompanying name, and caches this
+list. In case the same e-mail address is used with different names, the most
+recent non-empty name is used.
+
+\fBmu cfind\fR starts a search for contacts that match a \fIregular
+expression\fR. For example:
+
+.nf
+   $ mu cfind '@gmail\.com'
+.fi
+
+would find all contacts with a gmail-address, while
+
+.nf
+   $ mu cfind Mary
+.fi
+
+lists all contacts with Mary in either name or e-mail address.
+
+If you do not specify a search expression, \fBmu cfind\fR returns the full list
+of contacts. Note, \fBmu cfind\fR uses a cache with the e-mail information,
+which is populated during the indexing process.
+
+The regular expressions are Perl-compatible (as per the PCRE-library used by
+GRegex).
+
+.SH OPTIONS
+
+.TP
+\fB\-\-format\fR=\fIplain|mutt-alias|mutt-ab|wl|org-contact|bbdb|csv\fR
+sets the output format to the given value. The following are available:
+
+.nf
+| --format=   | description                       |
+|-------------+-----------------------------------|
+| plain       | default, simple list              |
+| mutt-alias  | mutt alias-format                 |
+| mutt-ab     | mutt external address book format |
+| wl          | wanderlust addressbook format     |
+| org-contact | org-mode org-contact format       |
+| bbdb        | BBDB format                       |
+| csv         | comma-separated values (*)       |
+.fi
+
+
+(*) CSV is not fully standardized, but \fBmu cfind\fR follows some common
+practices: any double-quote is replaced by a double-double quote (thus, "hello"
+become ""hello"", and fields with commas are put in double-quotes. Normally,
+this should only apply to name fields.
+
+.TP
+\fB\-\-personal\fR only show addresses seen in messages where one of 'my' e-mail
+addresses was seen in one of the address fields; this is to exclude addresses
+only seen in mailing-list messages. See the \fB\-\-my-address\fR parameter in
+\fBmu index\fR.
+
+.TP
+\fB\-\-after=\fR\fI<timestamp>\fR only show addresses last seen after
+\fI<timestamp>\fR. \fI<timestamp>\fR is a UNIX \fBtime_t\fR value, the number of
+seconds since 1970-01-01 (in UTC).
+
+From the command line, you can use the \fBdate\fR command to get this value. For
+example, only consider addresses last seen after 2009-06-01, you could specify
+.nf
+  --after=`date +%s --date='2009-06-01'`
+.fi
+
+.SH RETURN VALUE
+
+\fBmu cfind\fR returns 0 upon successful completion -- that is, at least one
+contact was found. Anything else leads to a non-zero return value:
+
+.nf
+| code | meaning                        |
+|------+--------------------------------|
+|    0 | ok                             |
+|    1 | general error                  |
+|    2 | no matches (for 'mu cfind')    |
+.fi
+
+.SH INTEGRATION WITH MUTT
+
+You can use \fBmu cfind\fR as an external address book server for \fBmutt\fR.
+For this to work, add the following to your \fImuttrc\fR:
+
+.nf
+set query_command = "mu cfind --format=mutt-ab '%s'"
+.fi
+
+Now, in mutt, you can search for e-mail addresses using the \fBquery\fR-command,
+which is (by default) accessible by pressing \fBQ\fR.
+
+.SH ENCODING
+
+\fBmu cfind\fR output is encoded according to the current locale except for
+\fI--format=bbdb\fR. This is hard-coded to UTF-8, and as such specified in the
+output-file, so emacs/bbdb can handle things correctly, without guessing.
+
+.SH BUGS
+
+Please report bugs if you find them at \fBhttps://github.com/djcb/mu/issues\fR.
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR mu-index (1),
+.BR mu-find (1),
+.BR pcrepattern(3)
diff --git a/man/mu-easy.1 b/man/mu-easy.1
new file mode 100644 (file)
index 0000000..27e9258
--- /dev/null
@@ -0,0 +1,313 @@
+.TH MU-EASY 1 "February 2020" "User Manuals"
+
+.SH NAME
+
+mu easy \- a quick introduction to mu
+
+.SH DESCRIPTION
+
+\fBmu\fR is a set of tools for dealing with e-mail messages in Maildirs. There
+are many options, which are all described in the man pages for the various
+sub-commands. This man pages jumps over all of the details and gives examples of
+some common use cases. If the use cases described here do not precisely do what
+you want, please check the more extensive information in the man page about the
+sub-command you are using -- for example, the \fBmu-index\fR(1) or
+\fBmu-find\fR(1) man pages.
+
+\fBNOTE\fR: the \fBindex\fR command (and therefore, the ones that depend on
+that, such as \fBfind\fR), require that you store your mail in the
+Maildir-format. If you don't do so, you can still use the other commands, but
+you won't be able to index/search your mail.
+
+By default, \fBmu\fR uses colorized output when it thinks your terminal is
+capable of doing so. If you don't like color, you can use the \fB--nocolor\fR
+command-line option, or set either the \fBMU_NOCOLOR\fR or the \fBNO_COLOR\fR
+environment variable to non-empty.
+
+.SH SETTING THINGS UP
+
+The first time you run the mu commands, you need to initialize it. This is done
+with the \fBinit\fR command.
+
+.nf
+  \fB$ mu init\fR
+.fi
+
+This uses the defaults (see \fBmu-init(1)\fR for details on how to change that).
+
+
+.SH INDEXING YOUR E-MAIL
+
+Before you can search e-mails, you'll first need to index them:
+
+.nf
+  \fB$ mu index\fR
+.fi
+
+The process can take a few minutes, depending on the amount of mail
+you have, the speed of your computer, hard drive etc. Usually,
+indexing should be able to reach a speed of a few hundred messages per
+second.
+
+\fBmu index\fR guesses the top-level Maildir to do its job; if it guesses wrong,
+you can use the \fI--maildir\fR option to specify the top-level directory that
+should be processed. See the \fBmu-index\fR(1) man page for more details.
+
+Normally, \fBmu index\fR visits all the directories under the top-level Maildir;
+however, you can exclude certain directories (say, the 'trash' or 'spam'
+folders) by creating a file called \fI.noindex\fR in the directory. When
+\fBmu\fR sees such a file, it will exclude this directory and its
+sub-directories from indexing. Also see \fB.noupdate\fR in the \fBmu-index\fR(1)
+manpage.
+
+.SH SEARCHING YOUR E-MAIL
+
+After you have indexed your mail, you can start searching it. By default, the
+search results are printed on standard output. Alternatively, the output can
+take the form of Maildir with symbolic links to the found messages. This enables
+integration with e-mail clients; see the \fBmu-find\fR(1) man page for details,
+the syntax of the search parameters and so on. Here, we just give some examples
+for common cases.
+
+You can use the \fBmu fields\fR command to get information about all possible
+fields and flags.
+
+First, let's search for all messages sent to Julius (Caesar) regarding
+fruit:
+
+.nf
+\fB$ mu find t:julius fruit\fR
+.fi
+
+This should return something like:
+
+.nf
+  2008-07-31T21:57:25 EEST John Milton <jm@example.com> Fere libenter homines id quod volunt credunt
+.fi
+
+This means there is a message to 'julius' with 'fruit' somewhere in the message.
+In this case, it's a message from John Milton. Note that the date format depends
+on your the language/locale you are using.
+
+How do we know that the message was sent to Julius Caesar? Well, it's not
+visible from the results above, because the default fields that are shown are
+date/sender/subject. However, we can change this using the \fI--fields\fR
+parameter (try \fBmu fields\fR to see all the details):
+
+.nf
+  \fB$ mu find --fields="t s" t:julius fruit\fR
+.fi
+
+In other words, display the 'To:'-field (t) and the subject (s). This should
+return something like:
+.nf
+  Julius Caesar <jc@example.com> Fere libenter homines id quod volunt credunt
+.fi
+
+This is the same message found before, only with some different fields
+displayed.
+
+By default, \fBmu\fR uses the logical AND for the search parameters -- that
+is, it displays messages that match all the parameters. However, we can use
+logical OR as well:
+
+.nf
+  \fB$ mu find t:julius OR f:socrates\fR
+.fi
+
+In other words, display messages that are either sent to Julius Caesar
+\fBor\fR are from Socrates. This could return something like:
+
+.nf
+  2008-07-31T21:57:25 EEST Socrates <soc@example.com> cool stuff
+  2008-07-31T21:57:25 EEST John Milton <jm@example.com> Fere libenter homines id quod volunt credunt
+.fi
+
+What if we want to see some of the body of the message?  You can get
+a 'summary' of the first lines of the message using the \fI--summary-len\fR
+option, which will 'summarize' the first \fIn\fR lines of the message:
+
+.nf
+  \fB$ mu find --summary-len=3 napoleon m:/archive\fR
+.fi
+
+.nf
+  1970-01-01T02:00:00 EET Napoleon Bonaparte <nb@example.com> rock on dude
+  Summary: Le 24 février 1815, la vigie de Notre-Dame de la Garde signala le
+  trois-mâts le Pharaon, venant de Smyrne, Trieste et Naples. Comme
+  d'habitude, un pilote côtier partit aussitôt du port, rasa le château
+.fi
+
+The summary consists of the first n lines of the message with all superfluous
+whitespace removed.
+
+Also note the \fBm:/archive\fR parameter in the query. This means that we only
+match messages in a maildir called '/archive'.
+
+.SH MORE QUERIES
+
+Let's list a few more queries that may be interesting; please note that
+searches for message flags, priority and date ranges are only available in mu
+version 0.9 or later.
+
+Get all important messages which are signed:
+.nf
+  \fB$ mu find flag:signed prio:high \fR
+.fi
+
+Get all messages from Jim without an attachment:
+.nf
+  \fB$ mu find from:jim AND NOT flag:attach\fR
+.fi
+
+Get all messages where Jack is in one of the contact fields:
+.nf
+  \fB$ mu find contact:jack\fR
+.fi
+This uses the special contact: pseudo-field which matches (\fBfrom\fR,
+\fBto\fR, \fBcc\fR and \fBbcc\fR).
+
+Get all messages in the Sent Items folder about yoghurt:
+.nf
+ \fB$mu find maildir:'/Sent Items' yoghurt\fR
+.fi
+Note how we need to quote search terms that include spaces.
+
+
+Get all unread messages where the subject mentions Ångström:
+.nf
+  \fB$ mu find subject:Ångström flag:unread\fR
+.fi
+which is equivalent to:
+.nf
+  \fB$ mu find subject:angstrom flag:unread\fR
+.fi
+because does mu is case-insensitive and accent-insensitive.
+
+Get all unread messages between March 2002 and August 2003 about some bird (or
+a Swedish rock band):
+.nf
+  \fB$ mu find date:20020301..20030831 nightingale flag:unread\fR
+.fi
+
+Get all messages received today:
+.nf
+  \fB$ mu find date:today..now\fR
+.fi
+
+Get all messages we got in the last two weeks about emacs:
+.nf
+  \fB$ mu find date:2w..now emacs\fR
+.fi
+
+Another powerful feature (since 0.9.6) are wildcard searches, where you can
+search for the last \fIn\fR characters in a word. For example, you can search
+for:
+.nf
+  \fB$ mu find 'subject:soc*'\fR
+.fi
+and get mails about soccer, Socrates, society, and so on. Note, it's important
+to quote the search query, otherwise the shell will interpret
+the '*'.
+
+You can also search for messages with a certain attachment using their
+filename, for example:
+
+.nf
+  \fB$ mu find 'file:pic*'\fR
+.fi
+will get you all messages with an attachment starting with 'pic'.
+
+If you want to find attachments with a certain MIME-type, you can use the
+following:
+
+Get all messages with PDF attachments:
+.nf
+  \fB$ mu find mime:application/pdf\fR
+.fi
+
+or even:
+
+Get all messages with image attachments:
+.nf
+  \fB$ mu find 'mime:image/*'\fR
+.fi
+
+
+Note that (1) the '*' wildcard can only be used as the rightmost thing in a
+search query, and (2) that you need to quote the search term, because
+otherwise your shell will interpret the '*' (expanding it to all files in the
+current directory -- probably not what you want).
+
+.SH DISPLAYING MESSAGES
+
+We might also want to display the complete messages instead of the header
+information. This can be done using \fBmu view\fR command. Note that this
+command does not use the database; you simply provide it the path to a
+message.
+
+Therefore, if you want to display some message from a search query, you'll
+need its path. To get the path (think \fBl\fRocation) for our first example we
+can use:
+
+.nf
+  \fB$ mu find --fields="l" t:julius fruit\fR
+.fi
+
+And we'll get something like:
+.nf
+  /home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2,
+.fi
+We can now display this message:
+
+.nf
+  \fB$ mu view /home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2,\fR
+
+     From: John Milton <jm@example.com>
+     To: Julius Caesar <jc@example.com>
+     Subject: Fere libenter homines id quod volunt credunt
+     Date: 2008-07-31T21:57:25 EEST
+
+     OF Mans First Disobedience, and the Fruit
+     Of that Forbidden Tree, whose mortal taste
+     Brought Death into the World, and all our woe,
+     [...]
+.fi
+
+.SH FINDING CONTACTS
+
+While \fBmu find\fR searches for messages, there is also \fBmu cfind\fR to
+find \fIcontacts\fR, that is, names + addresses. Without any search
+expression, \fBmu cfind\fR lists all of your contacts.
+
+.nf
+  \fB$ mu cfind julius\fR
+.fi
+
+will find all contacts with 'julius' in either name or e-mail address. Note
+that \fBmu cfind\fR accepts a \fIregular expression\fR.
+
+\fBmu cfind\fR also supports a \fI--format=\fR-parameter, which sets the
+output to some specific format, so the results can be imported into another
+program. For example, to export your contact information to a \fBmutt\fR
+address book file, you can use something like:
+
+.nf
+  \fB$ mu cfind --format=mutt-alias > ~/mutt-aliases \fR
+.fi
+
+Then, you can use them in \fBmutt\fR if you add something like \fBsource
+~/mutt-aliases\fR to your \fImuttrc\fR.
+
+.SH AUTHOR
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+.BR mu (1),
+.BR mu-init (1),
+.BR mu-index (1),
+.BR mu-find (1),
+.BR mu-mfind (1),
+.BR mu-mkdir (1),
+.BR mu-view (1),
+.BR mu-extract (1)
diff --git a/man/mu-extract.1 b/man/mu-extract.1
new file mode 100644 (file)
index 0000000..35d96ce
--- /dev/null
@@ -0,0 +1,100 @@
+.TH MU EXTRACT 1 "July 2012" "User Manuals"
+
+.SH NAME
+
+\fBmu extract\fR is the \fBmu\fR command to display and save message parts
+(attachments), and open them with other tools.
+
+.SH SYNOPSIS
+
+.B mu extract [options] <file>
+
+.B mu extract [options] <file> <pattern>
+
+.SH DESCRIPTION
+
+\fBmu extract\fR is the \fBmu\fR sub-command for extracting MIME-parts (e.g.,
+attachments) from mail messages. The sub-command works on message files, and
+does not require the message to be indexed in the database.
+
+For attachments, the file name used when saving it is the name of the
+attachment in the message. If there is no such name, or when saving
+non-attachment MIME-parts, a name is derived from the message-id of the
+message.
+
+If you specify a pattern (a case-insensitive regular expression) as the second
+argument, all attachments with filenames matching that pattern will be
+extracted. The regular expressions are Perl-compatible (as per the
+PCRE-library).
+
+Without any options, \fBmu extract\fR simply outputs the list of leaf
+MIME-parts in the message. Only 'leaf' MIME-parts (including RFC822
+attachments) are considered, \fBmultipart/*\fR etc. are ignored.
+
+.SH OPTIONS
+
+.TP
+\fB\-a\fR, \fB\-\-save\-attachments\fR
+save all MIME-parts that look like attachments.
+
+.TP
+\fB\-\-save\-all\fR
+save all non-multipart MIME-parts.
+
+.TP
+\fB\-\-parts\fR=<parts>
+only consider the following numbered parts
+(comma-separated list). The numbers for the parts can be seen from running
+\fBmu extract\fR without any options but only the message file.
+
+.TP
+\fB\-\-target\-dir\fR=<dir>
+save the parts in the target directory rather than
+the current working directory.
+
+.TP
+\fB\-\-overwrite\fR
+overwrite existing files with the same name; by default overwriting is not
+allowed.
+
+.TP
+\fB\-\-play\fR Try to 'play' (open) the attachment with the default
+application for the particular file type. On MacOS, this uses the \fBopen\fR
+program, on other platforms it uses \fBxdg-open\fR. You can choose a different
+program by setting the \fBMU_PLAY_PROGRAM\fR environment variable.
+
+.SH EXAMPLES
+
+To display information about all the MIME-parts in a message file:
+.nf
+   $ mu extract msgfile
+.fi
+
+To extract MIME-part 3 and 4 from this message, overwriting existing files
+with the same name:
+.nf
+   $ mu extract --parts=3,4 --overwrite msgfile
+.fi
+
+To extract all files ending in '.jpg' (case-insensitive):
+.nf
+   $ mu extract msgfile '.*\.jpg'
+.fi
+
+To extract an mp3-file, and play it in the default mp3-playing application:
+.nf
+   $ mu extract --play msgfile 'whoopsididitagain.mp3'
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1)
diff --git a/man/mu-fields.1 b/man/mu-fields.1
new file mode 100644 (file)
index 0000000..e86c22a
--- /dev/null
@@ -0,0 +1,32 @@
+.TH MU FIELDS 1 "April 2022" "User Manuals"
+
+.SH NAME
+
+mu fields\-  list all message fields
+
+.SH SYNOPSIS
+
+.B mu fields [options]
+
+.SH DESCRIPTION
+
+\fBmu fields\fR is the \fBmu\fR command for showing a table of message fields
+and their properties.
+
+.SH OPTIONS
+
+Inherits common options from
+.BR mu(1)
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1)
diff --git a/man/mu-find.1 b/man/mu-find.1
new file mode 100644 (file)
index 0000000..df2f4d8
--- /dev/null
@@ -0,0 +1,338 @@
+.TH MU FIND 1 "29 April 2022" "User Manuals"
+
+.SH NAME
+
+mu find \- find e-mail messages in the \fBmu\fR database.
+
+.SH SYNOPSIS
+
+.B mu find [options] <search expression>
+
+.SH DESCRIPTION
+
+\fBmu find\fR is the \fBmu\fR command for searching e-mail message
+that were stored earlier using \fBmu index\fR(1).
+
+.SH SEARCHING MAIL
+
+\fBmu find\fR starts a search for messages in the database that match
+some search pattern. The search patterns are described in detail in
+.BR mu-query (7).
+.
+
+For example:
+
+.nf
+   $ mu find subject:snow and date:2009..
+.fi
+
+would find all messages in 2009 with 'snow' in the subject field, e.g:
+
+.nf
+  2009-03-05 17:57:33 EET Lucia  <lucia@example.com> running in the snow
+  2009-03-05 18:38:24 EET Marius <marius@foobar.com> Re: running in the snow
+.fi
+
+Note, this the default, plain-text output, which is the default, so you don't
+have to use \fB--format=plain\fR. For other types of output (such as symlinks,
+XML or s-expressions), see the discussion in the \fBOPTIONS\fR-section below
+about \fB--format\fR.
+
+The search pattern is taken as a command-line parameter. If the search
+parameter consists of multiple parts (as in the example) they are
+treated as if there were a logical \fBand\fR between them.
+
+For details on the possible queries, see
+.BR mu-query (7).
+
+.SH OPTIONS
+
+Note, some of the important options are described in the \fBmu\fR(1) man-page
+and not here, as they apply to multiple mu-commands.
+
+The \fBfind\fR-command has various options that influence the way \fBmu\fR
+displays the results. If you don't specify anything, the defaults are
+\fI\-\-fields="d f s"\fR, \fI\-\-sortfield=date\fR and \fI\-\-reverse\fR.
+
+.TP
+\fB\-f\fR, \fB\-\-fields\fR=\fI<fields>\fR
+specifies a string that determines which fields are shown in the output. This
+string consists of a number of characters (such as 's' for subject or 'f' for
+from), which will replace with the actual field in the output. Fields that are
+not known will be output as-is, allowing for some simple formatting.
+
+For example:
+
+.nf
+  $ mu find subject:snow --fields "d f s"
+.fi
+
+would list the date, subject and sender of all messages with 'snow' in the
+their subject.
+
+The table of replacement characters is superset of the list mentions for
+search parameters, such as:
+.nf
+       t       \fBt\fRo: recipient
+       d       Sent \fBd\fRate of the message
+       f       Message sender (\fBf\fRrom:)
+       g       Message flags (fla\fBg\fRs)
+       l       Full path to the message (\fBl\fRocation)
+       s       Message \fBs\fRubject
+       i       Message-\fBi\fRd
+       m       \fBm\fRaildir
+.fi
+
+For the complete, up-to-date list, see:
+.BR mu-fields(1)
+
+The message flags are described in \fBmu-query\fR(7). As an example, a
+message which is 'seen', has an attachment and is signed would
+have 'asz' as its corresponding output string, while an encrypted new
+message would have 'nx'.
+
+.TP
+\fB\-s\fR, \fB\-\-sortfield\fR \fR=\fI<field>\fR and \fB\-z\fR,
+\fB\-\-reverse\fR specifies the field to sort the search results by, and the
+direction (i.e., 'reverse' means that the sort should be reverted - Z-A). Examples include:
+
+.nf
+       cc,c            Cc (carbon-copy) recipient(s)
+       date,d          Message sent date
+       from,f          Message sender
+       maildir,m       Maildir
+       msgid,i         Message id
+       prio,p          Nessage priority
+       subject,s       Message subject
+       to,t            To:-recipient(s)
+.fi
+
+For the complete list use can use the \fBmu fields\fR command; see:
+.BR mu-fields(1)
+
+Thus, for example, to sort messages by date, you could specify:
+
+.nf
+  $ mu find fahrrad --fields "d f s" --sortfield=date --reverse
+.fi
+
+Note, if you specify a sortfield, by default, messages are sorted in reverse
+(descending) order (e.g., from lowest to highest). This is usually a good
+choice, but for dates it may be more useful to sort in the opposite direction.
+
+.TP
+\fB\-n\fR, \fB\-\-maxnum=<number>\fR
+If > 0, display maximally that number of entries.  If not specified, all matching entries are displayed.
+
+.TP
+\fB\-\-summary-len=<number>\fR
+If > 0, use that number of lines of the message to provide a summary.
+
+.TP
+\fB\-\-format\fR=\fIplain|links|xquery|xml|sexp\fR
+output results in the specified format.
+
+The default is \fBplain\fR, i.e normal output with one line per message.
+
+\fBlinks\fR outputs the results as a maildir with symbolic links to the found
+messages. This enables easy integration with mail-clients (see below for more
+information).
+
+\fBxml\fR formats the search results as XML.
+
+\fBsexp\fR formats the search results as an s-expression as used in Lisp
+programming environments.
+
+\fBxquery\fR shows the Xapian query corresponding to your search terms. This
+is meant for for debugging purposes.
+
+.TP
+\fB\-\-linksdir\fR \fR=\fI<dir>\fR and \fB\-c\fR, \fB\-\-clearlinks\fR
+output the results as a maildir with symbolic links to the found
+messages. This enables easy integration with mail-clients (see below
+for more information). \fBmu\fR will create the maildir if it does not
+exist yet.
+
+If you specify \fB\-\-clearlinks\fR, all existing symlinks will be
+cleared from the target directories; this allows for re-use of the
+same maildir. However, this option will delete any symlink it finds,
+so be careful.
+
+.nf
+  $ mu find grolsch --format=links --linksdir=~/Maildir/search --clearlinks
+.fi
+
+will store links to found messages in \fI~/Maildir/search\fR. If the directory
+does not exist yet, it will be created.
+
+Note: when \fBmu\fR creates a Maildir for these links, it automatically
+inserts a \fI.noindex\fR file, to exclude the directory from \fBmu
+index\fR.
+
+.TP
+\fB\-\-after=\fR\fI<timestamp>\fR only show messages whose message files were
+last modified (\fBmtime\fR) after \fI<timestamp>\fR. \fI<timestamp>\fR is a
+UNIX \fBtime_t\fR value, the number of seconds since 1970-01-01 (in UTC).
+
+From the command line, you can use the \fBdate\fR command to get this
+value. For example, only consider messages modified (or created) in the last 5
+minutes, you could specify
+.nf
+  --after=`date +%s --date='5 min ago'`
+.fi
+This is assuming the GNU \fBdate\fR command.
+
+
+.TP
+\fB\-\-exec\fR=\fI<command>\fR
+the \fB\-\-exec\fR command causes the \fIcommand\fR to be executed on each
+matched message; for example, to see the raw text of all messages
+matching 'milkshake', you could use:
+.nf
+  $ mu find milkshake --exec='less'
+.fi
+which is roughly equivalent to:
+.nf
+  $ mu find milkshake --fields="l" | xargs less
+.fi
+
+
+.TP
+\fB\-b\fR, \fB\-\-bookmark\fR=\fI<bookmark>\fR
+use a bookmarked search query. Using this option, a query from your bookmark
+file will be prepended to other search queries. See \fBmu-bookmarks\fR(1) for the
+details of the bookmarks file.
+
+
+.TP
+\fB\-\-skip\-dups\fR,\fB-u\fR whenever there are multiple messages with the
+same name, only show the first one. This is useful if you have copies of the
+same message, which is a common occurrence when using e.g. Gmail together with
+\fBofflineimap\fR.
+
+.TP
+\fB\-\-include\-related\fR,\fB-r\fR also include messages being referred to by
+the matched messages -- i.e.. include messages that are part of the same
+message thread as some matched messages. This is useful if you want
+Gmail-style 'conversations'. Note, finding these related messages make
+searches slower.
+
+.TP
+\fB\-t\fR, \fB\-\-threads\fR show messages in a 'threaded' format --
+that is, with indentation and arrows showing the conversation threads
+in the list of matching messages. When using this, sorting is
+chronological (by date), based on the newest message in a thread.
+
+Messages in the threaded list are indented based on the depth in the
+discussion, and are prefix with a kind of arrow with thread-related
+information about the message, as in the following table:
+
+.nf
+|             | normal | orphan | duplicate |
+|-------------+--------+--------+-----------|
+| first child | `->    | `*>    | `=>       |
+| other       | |->    | |*>    | |=>       |
+.fi
+
+Here, an 'orphan' is a message without a parent message (in the list of
+matches), and a duplicate is a message whose message-id was already seen
+before; not this may not really be the same message, if the message-id was
+copied.
+
+The algorithm used for determining the threads is based on Jamie Zawinksi's
+description:
+.BR http://www.jwz.org/doc/threading.html
+
+
+.SS Integrating mu find with mail clients
+
+.TP
+
+\fBmutt\fR
+
+For \fBmutt\fR you can use the following in your \fImuttrc\fR; pressing the F8
+key will start a search, and F9 will take you to the results.
+
+.nf
+# mutt macros for mu
+macro index <F8> "<shell-escape>mu find --clearlinks --format=links --linksdir=~/Maildir/search " \\
+                                       "mu find"
+macro index <F9> "<change-folder-readonly>~/Maildir/search" \\
+                                       "mu find results"
+.fi
+
+
+.TP
+
+\fBWanderlust\fR
+
+\fBSam B\fR suggested the following on the \fBmu\fR-mailing list. First add
+the following to your Wanderlust configuration file:
+
+.nf
+(require 'elmo-search)
+(elmo-search-register-engine
+    'mu 'local-file
+    :prog "/usr/local/bin/mu" ;; or wherever you've installed it
+    :args '("find" pattern "--fields" "l") :charset 'utf-8)
+
+(setq elmo-search-default-engine 'mu)
+;; for when you type "g" in folder or summary.
+(setq wl-default-spec "[")
+.fi
+
+Now, you can search using the \fBg\fR key binding; you can also create
+permanent virtual folders when the messages matching some expression by adding
+something like the following to your \fIfolders\fR file.
+
+.nf
+VFolders {
+  [date:today..now]!mu  "Today"
+
+  [size:1m..100m]!mu    "Big"
+
+  [flag:unread]!mu      "Unread"
+}
+.fi
+
+After restarting Wanderlust, the virtual folders should appear.
+
+.SH RETURN VALUE
+
+\fBmu find\fR returns 0 upon successful completion; if the search was
+performed, there needs to be a least one match. Anything else leads to a
+non-zero return value, for example:
+
+.nf
+| code | meaning                        |
+|------+--------------------------------|
+|    0 | ok                             |
+|    1 | general error                  |
+|    4 | no matches (for 'mu find')     |
+.fi
+
+
+.SH ENCODING
+
+\fBmu find\fR output is encoded according the locale for \fI--format=plain\fR
+(the default), and UTF-8 for all other formats (\fIsexp\fR,
+\fIxml\fR).
+
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+If you have specific messages which are not matched correctly, please attach
+them (appropriately censored if needed).
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR mu-index (1),
+.BR mu-query (7)
+.BR mu-fields (1)
diff --git a/man/mu-help.1 b/man/mu-help.1
new file mode 100644 (file)
index 0000000..80ba090
--- /dev/null
@@ -0,0 +1,34 @@
+.TH MU HELP 1 "July 2012" "User Manuals"
+
+.SH NAME
+
+\fBmu help\fR is a \fBmu\fR command that gives help information about mu
+commands.
+
+.SH SYNOPSIS
+
+.B mu help <command>
+
+.SH DESCRIPTION
+
+\fBmu help\fR provides help information about mu commands.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu-index (1),
+.BR mu-find (1),
+.BR mu-cfind (1),
+.BR mu-mkdir (1),
+.BR mu-view (1),
+.BR mu-extract (1),
+.BR mu-easy (1),
+.BR mu-bookmarks (5)
diff --git a/man/mu-index.1 b/man/mu-index.1
new file mode 100644 (file)
index 0000000..d9e9ea3
--- /dev/null
@@ -0,0 +1,196 @@
+.TH MU-INDEX 1 "June 2022" "User Manuals"
+
+.SH NAME
+
+mu index \- index e-mail messages stored in Maildirs
+
+.SH SYNOPSIS
+
+.B mu index [options]
+
+.SH DESCRIPTION
+
+\fBmu index\fR is the \fBmu\fR command for scanning the contents of Maildir
+directories and storing the results in a Xapian database. The data can then be
+queried using
+.BR mu-find (1)\.
+
+Before the first time you run \fBmu index\fR, you must run \fBmu init\fR to
+initialize the database.
+
+\fBindex\fR understands Maildirs as defined by Daniel Bernstein for
+\fBqmail\fR(7). In addition, it understands recursive Maildirs (Maildirs within
+Maildirs), Maildir++. It can also deal with VFAT-based Maildirs which use '!'
+or ';' as the separators instead of ':'.
+
+E-mail messages which are not stored in something resembling a maildir
+leaf-directory (\fIcur\fR and \fInew\fR) are ignored, as are the cache
+directories for \fInotmuch\fR and \fIgnus\fR, and any dot-directory.
+
+Starting with mu 1.5.x, symlinks are followed, and can be spread over multiple
+filesystems; however note that moving files around is much faster when multiple
+filesystems are not involved.
+
+If there is a file called \fI.noindex\fR in a directory, the contents of that
+directory and all of its subdirectories will be ignored. This can be useful to
+exclude certain directories from the indexing process, for example directories
+with spam-messages.
+
+If there is a file called \fI.noupdate\fR in a directory, the contents of that
+directory and all of its subdirectories will be ignored, unless we do a full
+rebuild (with \fBmu init\fR). This can be useful to speed up things you have
+some maildirs that never change. Note that you can still search for these
+messages, this only affects updating the database. \fI.noupdate\fR is ignored
+when you start indexing with an empty database (such as directly after \fImu
+init\fR.
+
+There also the \fB--lazy-check\fR which can greatly speed up indexing; see below
+for details.
+
+The first run of \fBmu index\fR may take a few minutes if you have a lot of mail
+(tens of thousands of messages). Fortunately, such a full scan needs to be done
+only once; after that it suffices to index the changes, which goes much faster.
+See the 'Note on performance (i,ii,iii)' below for more information.
+
+The optional 'phase two' of the indexing-process is the removal of messages from
+the database for which there is no longer a corresponding file in the Maildir.
+If you do not want this, you can use \fB\-n\fR, \fB\-\-nocleanup\fR.
+
+When \fBmu index\fR catches one of the signals \fBSIGINT\fR, \fBSIGHUP\fR or
+\fBSIGTERM\fR (e.g., when you press Ctrl-C during the indexing process), it
+tries to shutdown gracefully; it tries to save and commit data, and close the
+database etc. If it receives another signal (e.g., when pressing Ctrl-C once
+more), \fBmu index\fR will terminate immediately.
+
+.SH OPTIONS
+
+Some of the general options are described in the \fBmu(1)\fR man-page and not
+here, as they apply to multiple mu commands.
+
+.TP
+\fB\-\-lazy-check\fR
+in lazy-check mode, \fBmu\fR does not consider messages for which the
+time-stamp (ctime) of the directory they reside in has not changed
+since the previous indexing run. This is much faster than the non-lazy
+check, but won't update messages that have change (rather than having
+been added or removed), since merely editing a message does not update
+the directory time-stamp. Of course, you can run \fBmu-index\fR
+occasionally without \fB\-\-lazy-check\fR, to pick up such messages.
+
+.TP
+\fB\-\-nocleanup\fR
+disables the database cleanup that \fBmu\fR does by default after indexing.
+
+.SS A note on performance (i)
+As a non-scientific benchmark, a simple test on the author's machine (a
+Thinkpad X61s laptop using Linux 2.6.35 and an ext3 file system) with no
+existing database, and a maildir with 27273 messages:
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 66,65s user 6,05s system 27% cpu 4:24,20 total
+.fi
+(about 103 messages per second)
+
+A second run, which is the more typical use case when there is a database
+already, goes much faster:
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 0,48s user 0,76s system 10% cpu 11,796 total
+.fi
+(more than 56818 messages per second)
+
+Note that each test flushes the caches first; a more common use case might
+be to run \fBmu index\fR when new mail has arrived; the cache may stay
+quite 'warm' in that case:
+
+.nf
+ $ time mu index --quiet
+ 0,33s user 0,40s system 80% cpu 0,905 total
+.fi
+which is more than 30000 messages per second.
+
+
+.SS A note on performance (ii)
+As per June 2012, we did the same non-scientific benchmark, this time with an
+Intel i5-2500 CPU @ 3.30GHz, an ext4 file system and a maildir with 22589
+messages. We start without an existing database.
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 27,79s user 2,17s system 48% cpu 1:01,47 total
+.fi
+(about 813 messages per second)
+
+A second run, which is the more typical use case when there is a database
+already, goes much faster:
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 0,13s user 0,30s system 19% cpu 2,162 total
+.fi
+(more than 173000 messages per second)
+
+
+.SS A note on performance (iii)
+As per July 2016, we did the same non-scientific benchmark, again with
+the Intel i5-2500 CPU @ 3.30GHz, an ext4 file system. This time, the
+maildir contains 72525 messages.
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 40,34s user 2,56s system 64% cpu 1:06,17 total
+.fi
+(about 1099 messages per second).
+
+.SS A note on performance (iv)
+A few years later and its June 2022. There's a lot more happening during indexing, but indexing became multi-threaded and machines are faster; e.g. this
+is with an AMD Ryzen Threadripper 1950X (32) @ 3.399GHz.
+
+The instructions are a little different since we have a proper repeatable
+benchmark now. After building,
+
+.nf
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+% THREAD_NUM=4 build/lib/tests/bench-indexer -m perf
+# random seed: R02Sf5c50e4851ec51adaf301e0e054bd52b
+1..1
+# Start of bench tests
+# Start of indexer tests
+indexed 5000 messages in 20 maildirs in 3763ms; 752 μs/message; 1328 messages/s (4 thread(s))
+ok 1 /bench/indexer/4-cores
+# End of indexer tests
+# End of bench tests
+.fi
+
+Things are again a little faster, even though the index does a lot more now
+(text-normalizatian, and pre-generating message-sexps). A faster machine helps,
+too!
+
+.SH RETURN VALUE
+
+\fBmu index\fR return 0 upon successful completion; any other number signals an
+error.
+
+.SH BUGS
+
+Please report bugs if you find any:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR maildir (5),
+.BR mu (1),
+.BR mu-init (1),
+.BR mu-find (1),
+.BR mu-cfind (1)
diff --git a/man/mu-info.1 b/man/mu-info.1
new file mode 100644 (file)
index 0000000..ea8e703
--- /dev/null
@@ -0,0 +1,47 @@
+.TH MU-INFO 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+mu info \- show information about the mu database
+
+.SH SYNOPSIS
+
+.B mu info [options]
+
+.SH DESCRIPTION
+
+\fBmu info\fR is the \fBmu\fR command for getting information about the mu
+database. Note that while running (e.g. \fBmu4e\fR), some of the information
+may be slightly delayed due to database caching.
+
+.SH OPTIONS
+
+Note, some of the general options are described in the \fBmu(1)\fR man-page and
+not here, as they apply to multiple mu commands.
+
+.TP
+\fB\-\-muhome\fR
+use an alternative directory to store and read the database, write the logs,
+etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux
+this defaults to \fI~/.cache/mu\fR, \fI~/.config/mu\fR). Earlier versions of
+\fBmu\fR defaulted to \fI~/.mu\fR, which now requires \fI\-\-muhome=~/.mu\fR.
+
+.SH RETURN VALUE
+
+\fBmu init\fR returns 0 upon successful completion, or a non-zero exit code if
+there was some error.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR maildir (5),
+.BR mu (1),
+.BR mu-index (1)
diff --git a/man/mu-init.1 b/man/mu-init.1
new file mode 100644 (file)
index 0000000..e5697d4
--- /dev/null
@@ -0,0 +1,79 @@
+.TH MU-INIT 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+mu init \- initialize the mu message database
+
+.SH SYNOPSIS
+
+.B mu init [options]
+
+.SH DESCRIPTION
+
+\fBmu init\fR is the subcommand for setting up the mu message
+database. After \fBmu init\fR has completed, you can run \fBmu
+index\fR
+
+.SH OPTIONS
+
+Note, some of the general options are described in the \fBmu(1)\fR
+man-page and not here, as they apply to multiple mu commands.
+
+.TP
+\fB\-\-muhome\fR
+use an alternative directory to store and read the database, write the logs,
+etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux
+this defaults to \fI~/.cache/mu\fR, \fI~/.config/mu\fR).
+
+Earlier versions of \fBmu\fR defaulted to \fI~/.mu\fR, which now requires
+\fI\-\-muhome=~/.mu\fR.
+
+Alternatively, use can use the \fBMUHOME\fR environment variable (the command-line takes precedence).
+
+.TP
+\fB\-m\fR, \fB\-\-maildir\fR=\fI<maildir>\fR
+starts searching at \fI<maildir>\fR. By default, \fBmu\fR uses whatever the
+\fBMAILDIR\fR environment variable is set to; if it is not set, it tries
+\fI~/Maildir\fR if it already exists.
+
+.TP
+\fB\-\-my-address\fR=\fI<my-email-address>\fR
+specifies that some e-mail addresses are 'my-address' (\fB\-\-my-address\fR can
+be used multiple times). This is used by \fBmu cfind\fR -- any e-mail address
+found in the address fields of a message which also has \fI<my-email-address>\fR
+in one of its address fields is considered a \fIpersonal\fR e-mail address. This
+allows you, for example, to filter out (\fBmu cfind --personal\fR) addresses
+which were merely seen in mailing list messages.
+
+\fI<my-email-address>\fR can be either a plain e-mail address (such as
+\fBfoo@example.com\fR), or a regular-expression (of the 'Basic POSIX' flavor),
+wrapped in \fB/\fR (such as \fB/foo-.*@example\\.com/\fR). Depending on your
+shell program, the argument may need to b quoted.
+
+.SH ENVIRONMENT
+
+\fBmu init\fR uses \fBMAILDIR\fR to find the user's Maildir if it has not been
+specified explicitly with \fB\-\-maildir\fR=\fI<maildir>\fR. If \fBMAILDIR\fR is
+not set, \fBmu init\fR uses \fI~/Maildir\fR.
+
+\fBMUHOME\fR can be used as an alternative to \fB\-\-muhome\fR.
+
+.SH RETURN VALUE
+
+\fBmu init\fR returns 0 upon successful completion, or a non-zero exit code if
+there was some error.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR maildir (5),
+.BR mu (1),
+.BR mu-index (1)
diff --git a/man/mu-mkdir.1 b/man/mu-mkdir.1
new file mode 100644 (file)
index 0000000..6f9ddd2
--- /dev/null
@@ -0,0 +1,45 @@
+.TH MU MKDIR 1 "July 2012" "User Manuals"
+
+.SH NAME
+
+mu mkdir\-  create a new Maildir
+
+.SH SYNOPSIS
+
+.B mu mkdir [options] <dir> [<dirs>]
+
+.SH DESCRIPTION
+
+\fBmu mkdir\fR is the \fBmu\fR command for creating Maildirs. It does
+\fBnot\fR use the mu database. With the \fBmkdir\fR command, you can create
+new Maildirs with permissions 0755. For example,
+
+.nf
+   mu mkdir tom dick harry
+.fi
+
+creates three maildirs, \fItom\fR, \fIdick\fR and \fIharry\fR.
+
+If creation fails for any reason, \fBno\fR attempt is made to remove any parts
+that were created. This is for safety reasons.
+
+.SH OPTIONS
+
+.TP
+\fB\-\-mode\fR=<mode>
+set the file access mode for the new maildir(s) as in \fBchmod(1)\fR.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR maildir (5),
+.BR mu (1),
+.BR chmod (1)
diff --git a/man/mu-query.7 b/man/mu-query.7
new file mode 100644 (file)
index 0000000..ca1800a
--- /dev/null
@@ -0,0 +1,361 @@
+.TH MU QUERY 7 "22 April 2022" "User Manuals"
+
+.SH NAME
+
+mu query language \- a language for finding messages in \fBmu\fR databases.
+
+.SH DESCRIPTION
+
+The mu query language is a language used by \fBmu find\fR and \fBmu4e\fR to find
+messages in \fBmu\fR's Xapian databases. The language is quite similar to
+Xapian's default query-parser, but is an independent implementation that is
+customized for the mu/mu4e use-case.
+
+In this article, we give a structured but informal overview of the query
+language and provide examples.
+
+As a companion to this, we recommend the \fBmu fields\fR and \fBmu flags\fR
+commands to get an up-to-date list of the available fields and flags.
+
+\fBNOTE:\fR if you use queries on the command-line (say, for \fBmu find\fR), you
+need to quote any characters that would otherwise be interpreted by the shell,
+such as \fB""\fR, \fB(\fR and \fB)\fR and whitespace.
+
+.de EX1
+.nf
+.RS
+..
+
+.de EX2
+.RE
+.fi
+..
+
+.SH TERMS
+
+The basic building blocks of a query are \fBterms\fR; these are just
+normal words like 'banana' or 'hello', or words prefixed with a
+field-name which make them apply to just that field. See
+\fBmu find\fR
+for all the available fields.
+
+Some example queries:
+.EX1
+vacation
+subject:capybara
+maildir:/inbox
+.EX2
+
+Terms without an explicit field-prefix, (like 'vacation' above) are
+interpreted like:
+.EX1
+to:vacation or subject:vacation or body:vacation or ...
+.EX2
+
+The language is case-insensitive for terms and attempts to 'flatten'
+any diacritics, so \fIangtrom\fR matches \fIÅngström\fR.
+
+.PP
+If terms contain whitespace, they need to be quoted:
+.EX1
+subject:"hi there"
+.EX2
+This is a so-called \fIphrase query\fR, which means that we match
+against subjects that contain the literal phrase "hi there".
+
+Remember that you need to escape those quotes when using this from the
+command-line:
+.EX1
+mu find subject:\\"hi there\\"
+.EX2
+
+.SH LOGICAL OPERATORS
+
+We can combine terms with logical operators -- binary ones: \fBand\fR,
+\fBor\fR, \fBxor\fR and the unary \fBnot\fR, with the conventional
+rules for precedence and association, and are case-insensitive.
+
+.PP
+You can also group things with \fB(\fR and \fB)\fR, so you can do
+things like:
+.EX1
+(subject:beethoven or subject:bach) and not body:elvis
+.EX2
+
+If you do not explicitly specify an operator between terms, \fBand\fR
+is implied, so the queries
+.EX1
+subject:chip subject:dale
+.EX2
+.EX1
+subject:chip AND subject:dale
+.EX2
+are equivalent. For readability, we recommend the second version.
+
+Note that a \fIpure not\fR - e.g. searching for \fBnot apples\fR is
+quite a 'heavy' query.
+
+.SH REGULAR EXPRESSIONS AND WILDCARDS
+
+The language supports matching regular expressions that follow
+ECMAScript; for details, see
+
+.BR http://www.cplusplus.com/reference/regex/ECMAScript/
+
+Regular expressions must be enclosed in \fB//\fR. Some examples:
+.EX1
+subject:/h.llo/                # match hallo, hello, ...
+subject:/
+.EX2
+
+Note the difference between 'maildir:/foo' and 'maildir:/foo/'; the
+former matches messages in the '/foo' maildir, while the latter
+matches all messages in all maildirs that match 'foo', such
+as '/foo', '/bar/cuux/foo', '/fooishbar' etc.
+
+Wildcards are an older mechanism for matching where a term with a
+rightmost \fB*\fR (and \fIonly\fR in that position) matches any term
+that starts with the part before the \fB*\fR; they are supported for
+backward compatibility and \fBmu\fR translates them to regular
+expressions internally:
+.EX1
+foo*
+.EX2
+is equivalent to
+.EX1
+/foo.*/
+.EX2
+
+As a note of caution, certain wild-cards and regular expression can
+take quite a bit longer than 'normal' queries.
+
+.SH FIELDS
+
+We already saw a number of search fields, such as \fBsubject:\fR and
+\fBbody:\fR. For the full table, see \fBmu fields\fR.
+.EX1
+       bcc,h           Bcc (blind-carbon-copy) recipient(s)
+       body,b          Message body
+       cc,c            Cc (carbon-copy) recipient(s)
+       changed,k       Last change to message file (range)
+       date,d          Send date (range)
+       embed,e         Search inside embedded text parts
+       file,j          Attachment filename
+       flag,g          Message Flags
+       from,f          Message sender
+       list,v          Mailing list (e.g. the List-Id value)
+       maildir,m       Maildir
+       mime,y          MIME-type of one or more message parts
+       msgid,i         Message-ID
+       prio,p          Message priority (\fIlow\fR, \fInormal\fR or \fIhigh\fR)
+       size,z          Message size range
+       subject,s       Message subject
+       tag,x           Tags for the message
+       thread,w        Thread a message belongs to
+       to,t            To: recipient(s)
+
+The \fBmu fields\fR command is recommended to get the latest version.
+.EX2
+The shortcut character can be used instead of the full name:
+.EX1
+f:foo@bar
+.EX2
+is the same as
+.EX1
+from:foo@bar
+.EX2
+For queries that are not one-off, we would recommend the longer name
+for readability.
+
+There are also the special fields \fBcontact:\fR, which matches all
+contact-fields (\fIfrom\fR, \fIto\fR, \fIcc\fR and \fIbcc\fR), and
+\fBrecip\fR, which matches all recipient-fields (\fIto\fR, \fIcc\fR
+and \fIbcc\fR). Hence, for instance,
+.EX1
+contact:fnorb@example.com
+.EX2
+is equivalent to
+.EX1
+(from:fnorb@example.com or to:fnorb@example.com or
+      cc:from:fnorb@example.com or bcc:fnorb@example.com)
+.EX2
+
+.SH DATE RANGES
+
+The \fBdate:\fR field takes a date-range, expressed as the lower and
+upper bound, separated by \fB..\fR. Either lower or upper (but not
+both) can be omitted to create an open range.
+
+Dates are expressed in local time and using ISO-8601 format
+(YYYY-MM-DD HH:MM:SS); you can leave out the right part, and \fBmu\fR
+adds the rest, depending on whether this is the beginning or end of
+the range (e.g., as a lower bound, '2015' would be interpreted as the
+start of that year; as an upper bound as the end of the year).
+
+You can use '/' , '.', '-' and 'T' to make dates more human readable.
+
+Some examples:
+.EX1
+date:20170505..20170602
+date:2017-05-05..2017-06-02
+date:..2017-10-01T12:00
+date:2015-06-01..
+date:2016..2016
+.EX2
+
+You can also use the special 'dates' \fBnow\fR and \fBtoday\fR:
+.EX1
+date:20170505..now
+date:today..
+.EX2
+
+Finally, you can use relative 'ago' times which express some time
+before now and consist of a number followed by a unit, with units
+\fBs\fR for seconds, \fBM\fR for minutes, \fBh\fR for hours, \fBd\fR
+for days, \fBw\fR for week, \fBm\fR for months and \fBy\fR for years.
+Some examples:
+
+.EX1
+date:3m..
+date:2017.01.01..5w
+.EX2
+
+.SH SIZE RANGES
+
+The \fBsize\fR or \fBz\fR field allows you to match \fIsize ranges\fR
+-- that is, match messages that have a byte-size within a certain
+range. Units (b (for bytes), K (for 1000 bytes) and M (for 1000 * 1000
+bytes) are supported). Some examples:
+
+.EX1
+size:10k..2m
+size:10m..
+.EX2
+
+.SH FLAG FIELDS
+
+The \fBflag\fR/\fBg\fR field allows you to match message flags. The
+following fields are available:
+.EX1
+       a,attach        Message with attachment
+       d,draft         Draft Message
+       f,flagged       Flagged
+       l,list          Mailing-list message
+       n,new           New message (in new/ Maildir)
+       p,passed        Passed ('Handled')
+       r,replied       Replied
+       s,seen          Seen
+       t,trashed       Marked for deletion
+       u,unread        new OR NOT seen
+       x,encrypted     Encrypted message
+       z,signed        Signed message
+.EX2
+
+Some examples:
+.EX1
+flag:attach
+flag:replied
+g:x
+.EX2
+
+Encrypted messages may be signed as well, but this is only visible
+after decrypting and thus, invisible to \fBmu\fR.
+
+.SH PRIORITY FIELD
+
+The message priority field (\fBprio:\fR) has three possible values:
+\fBlow\fR, \fBnormal\fR or \fBhigh\fR. For instance, to match
+high-priority messages:
+.EX1
+ prio:high
+.EX2
+
+.SH MAILDIR
+
+The Maildir field describes the directory path starting \fBafter\fR
+the Maildir-base path, and before the \fI/cur/\fR or \fI/new/\fR part.
+So for example, if there's a message with the file name
+\fI~/Maildir/lists/running/cur/1234.213:2,\fR, you could find it (and
+all the other messages in the same maildir) with:
+.EX1
+maildir:/lists/running
+.EX2
+
+Note the starting '/'. If you want to match mails in the 'root'
+maildir, you can do with a single '/':
+.EX1
+maildir:/
+.EX2
+
+If you have maildirs (or any fields) that include spaces, you need to
+quote them, ie.
+.EX1
+maildir:"/Sent Items"
+.EX2
+
+Note that from the command-line, such queries must be quoted:
+.EX1
+mu find 'maildir:"/Sent Items"'
+.EX2
+
+.SH MORE EXAMPLES
+
+Here are some simple examples of \fBmu\fR queries; you can make many
+more complicated queries using various logical operators, parentheses
+and so on, but in the author's experience, it's usually faster to find
+a message with a simple query just searching for some words.
+
+Find all messages with both 'bee' and 'bird' (in any field)
+.EX1
+bee AND bird
+.EX2
+
+Find all messages with either Frodo or Sam:
+.EX1
+Frodo OR Sam
+.EX2
+
+Find all messages with the 'wombat' as subject, and 'capybara' anywhere:
+.EX1
+subject:wombat and capybara
+.EX2
+
+Find all messages in the 'Archive' folder from Fred:
+.EX1
+from:fred and maildir:/Archive
+.EX2
+
+Find all unread messages with attachments:
+.EX1
+flag:attach and flag:unread
+.EX2
+
+
+Find all messages with PDF-attachments:
+.EX1
+mime:application/pdf
+.EX2
+
+Find all messages with attached images:
+.EX1
+mime:image/*
+.EX2
+
+.SH CAVEATS
+
+With current Xapian versions, the apostroph character is considered part of a
+word. Thus, you cannot find \fID'Artagnan\fR by searching for \fIArtagnan\fR.
+So, include the apostroph in search or use a regexp search.
+
+Matching on spaces has changed compared to the old query-parser; this applies
+e.g. to Maildirs that have spaces in their name, such as \fISent Items\fR. See
+\fBMAILDIR\fR above.
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu-find (1)
+.BR mu-fields (1)
diff --git a/man/mu-remove.1 b/man/mu-remove.1
new file mode 100644 (file)
index 0000000..e0aa212
--- /dev/null
@@ -0,0 +1,48 @@
+.TH MU REMOVE 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+\fBmu remove\fR is the \fBmu\fR command to remove messages from the database.
+
+.SH SYNOPSIS
+
+.B mu remove [options] <file> [<files>]
+
+.SH DESCRIPTION
+
+\fBmu remove\fR removes specific messages from the database, each of them
+specified by their filename. The files do not have to exist in the file
+system.
+
+.SH OPTIONS
+
+\fBmu remove\fR does not have its own options, but the general options for
+determining the location of the database (\fI--muhome\fR) are available. See
+\fBmu-index(1)\fR for more information.
+
+.SH RETURN VALUE
+
+\fBmu remove\fR returns 0 upon success; in general, the following error codes are
+returned:
+
+.nf
+| code | meaning                           |
+|------+-----------------------------------|
+|    0 | ok                                |
+|    1 | general error                     |
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR mu-index (1),
+.BR mu-add (1)
diff --git a/man/mu-script.1 b/man/mu-script.1
new file mode 100644 (file)
index 0000000..ec3f032
--- /dev/null
@@ -0,0 +1,84 @@
+.TH MU SCRIPT 1 "October 2021" "User Manuals"
+
+.SH NAME
+
+mu script\- show the available mu scripts, and/or run them.
+
+.SH SYNOPSIS
+
+.B mu script [options] [<pattern>]
+
+.B mu <script-name> [<script-options>]
+
+.SH DESCRIPTION
+
+\fBmu script\fR is the \fBmu\fR command to list available \fBmu\fR scripts.
+The scripts are to be implemented in the Guile programming language, and
+therefore only work if your \fBmu\fR is built with support for Guile. In
+addition, many scripts require you to have \fBgnuplot\fR installed.
+
+Without any parameters, \fBmu script\fR lists the available scripts. If you
+provide a pattern (a regular expression), only the scripts whose name or
+one-line description match this pattern are listed. See the examples below.
+
+\fBmu\fR ships with a number of scripts.
+
+.SH OPTIONS
+
+.TP
+\fB\-\-verbose\fR,\fB\-v\fR
+when listing the available scripts, show the long descriptions.
+
+\fB\-\-\fR
+all options on the right side of the \fB\-\-\fR are passed to the script.
+
+.SH EXAMPLES
+
+List all available scripts (one-line descriptions):
+.nf
+  $ mu script
+.fi
+
+List all available scripts matching \fImonth\fR (long descriptions):
+.nf
+  $ mu script -v month
+.fi
+
+Run the \fImsgs-per-month\fR script for messages matching 'hello', and pass it
+the \fI--textonly\fR parameter:
+.nf
+  $ mu msgs-per-month --query=hello --textonly
+.fi
+
+.SH RETURN VALUE
+
+\fBmu script\fR returns 0 when all went well, and returns some non-zero error
+code when this is not the case.
+
+.SH FILES
+
+You can make your own Scheme scripts accessible through \fBmu script\fR by
+putting them in either \fI<XDG_DATA_HOME>/mu/scripts\fR (e.g., \fI~/.local/share/mu/scripts\fR) or, if \fImuhome\fR is specified, in 
+
+It is a good idea to document the scripts by using some
+special comments in the source code:
+.nf
+;; INFO: this is my script -- one-line description
+;; INFO: (longer description)
+;; INFO: --option1=<foo> (describe option1)
+;; INFO: etc.
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1),
+.BR guile (1)
diff --git a/man/mu-server.1 b/man/mu-server.1
new file mode 100644 (file)
index 0000000..087483c
--- /dev/null
@@ -0,0 +1,61 @@
+.TH MU-SERVER 1 "January 2020" "User Manuals"
+
+.SH NAME
+
+mu server \- the mu backend for the mu4e e-mail client
+
+.SH SYNOPSIS
+
+.B mu server [options]
+
+.SH DESCRIPTION
+
+\fBmu server\fR starts a simple shell in which one can query and manipulate the
+mu database. The output uses s-expressions. \fBmu server\fR is not meant for use
+by humans, except for debugging purposes. Instead, it is designed specifically
+for the \fBmu4e\fR e-mail client.
+
+In this man-page, we document the commands \fBmu server\fR accepts, as well as
+their responses. In general, the commands sent to the server are s-expressions
+of the form:
+
+.nf
+   (<command-name> :param1 value1 :param2 value2)
+.fi
+
+For example, to view a certain message, the command would be:
+
+.nf
+   (view :docid 12345)
+.fi
+
+Parameters can be sent in any order; they must be of the correct type though.
+See \fBlib/utils/mu-sexp-parser.hh\fR and \fBlib/utils/mu-sexp-parser.cc\fR in
+source-tree for the details.
+
+
+.SH OUTPUT FORMAT
+
+\fBmu server\fR accepts a number of commands, and delivers its results in
+the form:
+
+.nf
+   \\376<length>\\377<s-expr>
+.fi
+
+\\376 (one byte 0xfe), followed by the length of the s-expression expressed as
+an hexadecimal number, followed by another \\377 (one byte 0xff), followed by
+the actual s-expression.
+
+By prefixing the expression with its length, it can be processed more
+efficiently. The \\376 and \\377 were chosen since they never occur in valid
+UTF-8 (in which the s-expressions are encoded).
+
+.sh COMMANDS
+
+
+.SH AUTHOR
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+.BR mu (1)
diff --git a/man/mu-verify.1 b/man/mu-verify.1
new file mode 100644 (file)
index 0000000..652db95
--- /dev/null
@@ -0,0 +1,69 @@
+.TH MU VERIFY 1 "April 2022" "User Manuals"
+
+.SH NAME
+
+mu verify\- verify message signatures and display information about them
+
+.SH SYNOPSIS
+
+.B mu verify [options] <msgfile>
+
+.SH DESCRIPTION
+
+\fBmu verify\fR is the \fBmu\fR command for verifying message signatures (such
+as PGP/GPG signatures) and displaying information about them. The sub-command
+works on message files, and does not require the message to be indexed in the
+database.
+
+.SH OPTIONS
+
+.TP
+\fB\-r\fR, \fB\-\-auto\-retrieve\fR
+attempt to find keys online (see the \fBauto-key-retrieve\fR option in the
+\fBgnupg(1)\fR documentation).
+
+.SH EXAMPLES
+
+To display aggregated (one-line) information about the verification status in a
+message:
+.nf
+   $ mu verify msgfile
+.fi
+
+To display information about all the signatures:
+.nf
+   $ mu verify --verbose msgfile
+.fi
+
+If you only want to use the exit code, you can use:
+.nf
+   $ mu verify --quiet msgfile
+.fi
+which does not give any output unless there is an error.
+
+.SH RETURN VALUE
+
+\fBmu verify\fR returns 0 when all signatures could be verified to be good, and
+returns some non-zero error code when this is not the case.
+
+When there are no signatures, returns 0 as well.
+
+.nf
+| code | meaning                        |
+|------+--------------------------------|
+|    0 | ok                             |
+|    1 | some non-verified signature(s) |
+.fi
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1)
diff --git a/man/mu-view.1 b/man/mu-view.1
new file mode 100644 (file)
index 0000000..8dc746d
--- /dev/null
@@ -0,0 +1,53 @@
+.TH MU VIEW 1 "April 2022" "User Manuals"
+
+.SH NAME
+
+mu view\- display an e-mail message file
+
+.SH SYNOPSIS
+
+.B mu view [options] <file> [<files>]
+
+.SH DESCRIPTION
+
+\fBmu view\fR is the \fBmu\fR command for displaying e-mail message files. It
+works on message files and does \fInot\fR require the message to be indexed in
+the database.
+
+The command shows some common headers (From:, To:, Cc:, Bcc:, Subject: and
+Date:), the list of attachments and the plain-text body of the message (if
+any).
+
+.SH OPTIONS
+
+.TP
+\fB\-\-summary-len\fR=\fI<number>\fR
+instead of displaying the full message, output a summary based upon the first
+\fI<number>\fR lines of the message.
+
+.TP
+\fB\-\-terminate\fR
+terminate messages with \\f (\fIform-feed\fR) characters when displaying
+them. This is useful when you want to further process them.
+
+.TP
+\fB\-\-decrypt\fR
+attempt to decrypt encrypted message bodies. This is only possible if \fBmu\fR
+was built with crypto-support.
+
+.TP
+\fB\-\-auto-retrieve\fR
+attempt to retrieve crypto-keys automatically from the network, when needed.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+
+.BR mu (1)
diff --git a/man/mu.1 b/man/mu.1
new file mode 100644 (file)
index 0000000..95469e9
--- /dev/null
+++ b/man/mu.1
@@ -0,0 +1,188 @@
+.TH MU 1 "May 2022" "User Manuals"
+
+.SH NAME
+
+mu \- a set of tools to deal with Maildirs and message files, in particular to
+index and search e-mail messages.
+
+.SH SYNOPSIS
+
+In alphabetical order:
+
+.B mu [options]
+general mu command.
+
+.B mu add
+add specific messages to the database. See
+.BR mu-add(1)
+
+.B mu cfind [options] [<regexp>]
+find contacts. See
+.BR mu-cfind(1)
+
+.B mu extract [options] <file> [<parts>] [<regexp>]
+extract attachments and other MIME-parts. See
+.BR mu-extract(1)
+
+.B mu find [options] <search expression>
+find messages. See
+.BR mu-find(1)
+
+.B mu help [command]
+get help for some command. See
+.BR mu-help(1)
+
+.B mu index [options]
+(re)index the messages in a Maildir. See
+.BR mu-index(1)
+
+.B mu info [options]
+show information about the mu database
+.BR mu-info(1)
+
+.B mu init [options]
+initialize the mu database
+.BR mu-init(1)
+
+.B mu mkdir [options] <dir> [<dirs>]
+create a new Maildir. See
+.BR mu-mkdir(1)
+
+.B mu remove [options]
+remove specific messages from the database. See
+.BR mu-remove(1)
+
+.B mu script [options]
+run a mu (Guile) script. See
+.BR mu-script(1)
+
+.B mu server [options]
+start a server process (for \fBmu4e\fR-internal use). See
+.BR mu-server(1)
+
+.B mu view <file> [<files>]
+view a specific message. See
+.BR mu-view(1)
+
+.SH DESCRIPTION
+
+\fBmu\fR is a set of tools for dealing with Maildirs and the e-mail messages
+in them.
+
+\fBmu\fR's main purpose is to enable searching of e-mail messages. It
+does so by periodically scanning a Maildir directory tree and
+analyzing the e-mail messages found (this is called 'indexing'). The
+results of this analysis are stored in a database, which can then be
+queried.
+
+In addition to indexing and searching, \fBmu\fR also offers
+functionality for viewing messages, extracting attachments and
+creating maildirs, and searching and exporting contact information.
+
+\fBmu\fR can be used from the command line or can be integrated with various
+e-mail clients.
+
+This manpage gives a general overview of the available commands
+(\fBindex\fR, \fBfind\fR, etc.); each \fBmu\fR command has its own
+man-page as well.
+
+.SH COLORS
+
+Some \fBmu\fR sub-commands support colorized output, and do so by
+default. If you don't want colors, you can use \fI--nocolor\fR.
+
+Currently, \fBmu find\fR, \fBmu view\fR, \fBmu cfind\fR and \fBmu extract\fR
+support colors.
+
+.SH ENCODING
+
+\fBmu\fR's output is in the current locale, with the exceptions of the output
+specifically meant for output to UTF8-encoded files. In practice, this means
+that the output of commands \fBindex\fR, \fBview\fR,
+\fBextract\fR is always encoded according to the current locale.
+
+The same is true for \fBfind\fR and \fBcfind\fR, with some exceptions, where
+the output is always UTF-8, regardless of the locale.
+
+For \fBcfind\fR the exception is \fI--format=bbdb\fR. This is hard-coded to
+UTF-8, and as such specified in the output-file, so emacs/bbdb can handle it
+correctly without guessing.
+
+For \fBfind\fR the output is encoded according the locale for
+\fI--format=plain\fR (the default), and UTF-8 for all other formats
+(\fIjson\fR, \fIsexp\fR, \fIxml\fR).
+
+.SH DATABASE AND FILE
+
+Commands \fBmu index\fR and \fBfind\fR and \fBcfind\fR work with the database,
+while the other ones work on individual mail files. Hence, running \fBview\fR,
+\fBmkdir\fR and \fBextract\fR does not require the mu database.
+
+The various commands are discussed in more detail in their own separate
+man-pages; here the general options are discussed.
+
+.SH OPTIONS
+
+\fBmu\fR offers several general options that apply to all commands,
+including \fBmu\fR without any command.
+
+.TP
+\fB\-\-muhome\fR
+use an alternative directory to store and read the database, write the logs,
+etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux
+by default \fI~/.cache/mu\fR, \fI~/.config/mu\fR). Earlier versions of \fBmu\fR defaulted
+to \fI~/.mu\fR, which now requires \fI\-\-muhome=~/.mu\fR.
+
+.TP
+\fB\-d\fR, \fB\-\-debug\fR
+makes \fBmu\fR generate extra debug information,
+useful for debugging the program itself. By default, debug information goes to
+the log file, \fI~/.cache/mu/mu.log\fR. It can safely be deleted when \fBmu\fR is
+not running. When running with \fB--debug\fR option, the log file can grow
+rather quickly. See the note on logging below.
+
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+causes \fBmu\fR not to output informational
+messages and progress information to standard output, but only to the log
+file. Error messages will still be sent to standard error. Note that \fBmu
+index\fR is \fBmuch\fR faster with \fB\-\-quiet\fR, so it is recommended you
+use this option when using \fBmu\fR from scripts etc.
+
+.TP
+\fB\-\-log-stderr\fR
+causes \fBmu\fR to \fBnot\fR output log messages to standard error, in
+addition to sending them to the log file.
+
+.TP
+\fB\-V\fR, \fB\-\-version\fR
+prints \fBmu\fR version and copyright information.
+
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+lists the various command line options.
+
+.SH ENVIRONMENT
+
+\fBMUHOME\fR can be used as an alternative to \fB\-\-muhome\fR. The latter has precedence.
+
+\fBNO_COLOR\fR can be used as an alternative to \fB\-\-nocolor\fR.
+
+.SH ERROR CODES
+
+The various mu subcommands typically exit with 0 (zero) upon success, and
+non-zero when some error occurred.
+
+.SH BUGS
+
+Please report bugs if you find them:
+.BR https://github.com/djcb/mu/issues
+
+.SH AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+.SH "SEE ALSO"
+.BR mu-index (1), mu-find (1), mu-cfind (1), mu-mkdir (1), mu-view (1),
+.BR mu-extract (1), mu-easy (1), mu-bookmarks (5), mu-query (7)
+.BR https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
diff --git a/meson.build b/meson.build
new file mode 100644 (file)
index 0000000..3d2839a
--- /dev/null
@@ -0,0 +1,212 @@
+## Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+################################################################################
+# project setup
+#
+project('mu', ['c', 'cpp'],
+        version: '1.8.14',
+        meson_version: '>= 0.52.0', # debian 10
+        license: 'GPL-3.0-or-later',
+        default_options : [
+          'buildtype=debugoptimized',
+          'warning_level=3',
+          'c_std=c11',
+          'cpp_std=c++17'
+        ]
+       )
+
+# installation paths
+prefixdir = get_option('prefix')
+bindir   = prefixdir /  get_option('bindir')
+datadir          = prefixdir / get_option('datadir')
+mandir   = prefixdir / get_option('mandir')
+infodir          = prefixdir / get_option('infodir')
+
+# allow for configuring lispdir, as with autotools.
+# default to <p
+if get_option('lispdir') == ''
+  mu4e_lispdir= datadir / join_paths('emacs', 'site-lisp', 'mu4e')
+else
+  mu4e_lispdir= get_option('lispdir') / 'mu4e'
+endif
+
+################################################################################
+# compilers / flags
+#
+extra_flags = [
+  '-Wno-unused-parameter',
+  '-Wno-cast-function-type',
+  '-Wformat-security',
+  '-Wformat=2',
+  '-Wstack-protector',
+  '-Wno-switch-enum',
+  '-Wno-keyword-macro',
+  '-Wno-#warnings']
+
+if get_option('buildtype') == 'debug'
+  extra_flags += [
+    '-ggdb',
+    '-fvar-tracking',
+    '-fvar-tracking-assignments']
+endif
+
+# compilers
+cc = meson.get_compiler('c')
+cxx= meson.get_compiler('cpp')
+
+# extra arguments, if available
+foreach extra_arg : extra_flags
+  if cc.has_argument (extra_arg)
+    add_project_arguments([extra_arg], language: 'c')
+  endif
+  if cxx.has_argument (extra_arg)
+    add_project_arguments([extra_arg], language: 'cpp')
+  endif
+endforeach
+
+# some clang don't have charconv, but we need it.
+# https://github.com/djcb/mu/issues/2347
+cxx.check_header('charconv', required:true)
+
+################################################################################
+# config.h setup
+#
+config_h_data=configuration_data()
+config_h_data.set_quoted('MU_STORE_SCHEMA_VERSION', '465')
+config_h_data.set_quoted('PACKAGE_VERSION', meson.project_version())
+config_h_data.set_quoted('PACKAGE_STRING', meson.project_name() + ' ' +
+                                           meson.project_version())
+config_h_data.set_quoted('VERSION', meson.project_version())
+config_h_data.set_quoted('PACKAGE_NAME', meson.project_name())
+
+add_project_arguments(['-DHAVE_CONFIG_H'], language: 'c')
+add_project_arguments(['-DHAVE_CONFIG_H'], language: 'cpp')
+config_h_dep=declare_dependency(
+  include_directories: include_directories(['.']))
+
+functions=[
+  'setsid'
+]
+foreach f : functions
+  if cc.has_function(f)
+    define = 'HAVE_' + f.underscorify().to_upper()
+    config_h_data.set(define, 1)
+  endif
+endforeach
+
+if cc.has_function('wordexp')
+  config_h_data.set('HAVE_WORDEXP_H',1)
+endif
+
+testmaildir=join_paths(meson.current_source_dir(), 'lib', 'tests')
+config_h_data.set_quoted('MU_TESTMAILDIR',  join_paths(testmaildir, 'testdir'))
+config_h_data.set_quoted('MU_TESTMAILDIR2',  join_paths(testmaildir, 'testdir2'))
+config_h_data.set_quoted('MU_TESTMAILDIR4',  join_paths(testmaildir, 'testdir4'))
+config_h_data.set_quoted('MU_TESTMAILDIR_CJK',  join_paths(testmaildir, 'cjk'))
+
+################################################################################
+# hard dependencies
+#
+glib_dep       = dependency('glib-2.0', version: '>= 2.58')
+gobject_dep    = dependency('gobject-2.0', version: '>= 2.58')
+gio_dep        = dependency('gio-2.0', version: '>= 2.50')
+gmime_dep      = dependency('gmime-3.0', version: '>= 3.2')
+xapian_dep     = dependency('xapian-core', version:'>= 1.4')
+thread_dep     = dependency('threads')
+
+awk=find_program(['gawk', 'awk'])
+gzip=find_program('gzip')
+
+# soft dependencies
+guile_dep = dependency('guile-3.0', required: get_option('guile'))
+# soft dependencies
+
+# emacs -- needed for mu4e compilation
+emacs_name=get_option('emacs')
+emacs=find_program([emacs_name], version: '>=25.3', required:false)
+if not emacs.found()
+  message('emacs not found; not pre-compiling mu4e sources')
+endif
+
+makeinfo=find_program(['makeinfo'], required:false)
+if not makeinfo.found()
+  message('makeinfo (texinfo) not found; not building info documentation')
+else
+  install_info=find_program(['install-info'], required:false)
+  if not install_info.found()
+    message('install-info not found')
+  else
+    install_info_script=join_paths(meson.current_source_dir(), 'build-aux',
+                                   'meson-install-info.sh')
+  endif
+endif
+
+# readline. annoyingly, macos has an incompatible libedit claiming to be
+# readline. this is only a dev/debug convenience for the mu4e repl.
+readline_dep=[]
+if get_option('readline').enabled()
+  readline_dep = dependency('readline', version:'>= 8.0')
+  config_h_data.set('HAVE_LIBREADLINE', 1)
+  config_h_data.set('HAVE_READLINE_READLINE_H', 1)
+  config_h_data.set('HAVE_READLINE_HISTORY', 1)
+  config_h_data.set('HAVE_READLINE_HISTORY_H', 1)
+endif
+
+
+################################################################################
+# write out version.texi (for texiinfo builds in mu4e, guile)
+version_texi_data=configuration_data()
+version_texi_data.set('VERSION', meson.project_version())
+version_texi_data.set('EDITION', meson.project_version())
+version_texi_data.set('UPDATED',
+                      run_command('date', '+%d %B %Y', check:true).stdout().strip())
+version_texi_data.set('UPDATEDMONTH',
+                      run_command('date', '+%B %Y', check:true).stdout().strip())
+
+configure_file(input: 'version.texi.in',
+               output: 'version.texi',
+               configuration: version_texi_data)
+
+################################################################################
+# install some data files
+install_data('NEWS.org',
+            install_dir : join_paths(datadir,'doc', 'mu'))
+
+################################################################################
+# subdirs
+subdir('lib')
+subdir('mu')
+subdir('man')
+
+if emacs.found()
+   subdir('mu4e')
+endif
+
+if not get_option('guile').disabled() and guile_dep.found()
+  config_h_data.set('BUILD_GUILE', 1)
+  config_h_data.set_quoted('GUILE_BINARY',
+                           guile_dep.get_pkgconfig_variable('guile'))
+  #message('guile is disabled for now')
+  subdir('guile')
+endif
+
+config_h_data.set_quoted('MU_PROGRAM', mu.full_path())
+################################################################################
+
+################################################################################
+# write-out config.h
+configure_file(output : 'config.h', configuration : config_h_data)
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644 (file)
index 0000000..587b479
--- /dev/null
@@ -0,0 +1,35 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+
+option('guile',
+       type : 'feature',
+       value: 'auto',
+       description: 'build the guile scripting support (requires guile-3.x)')
+
+option('readline',
+       type: 'feature',
+       value: 'auto',
+       description: 'enable readline support for the mu4e repl')
+
+option('emacs',
+       type: 'string',
+       value: 'emacs',
+       description: 'name/path of the emacs executable')
+
+option('lispdir',
+       type: 'string',
+       description: 'path under which to install emacs-lisp files')
diff --git a/mu/Makefile.am b/mu/Makefile.am
new file mode 100644 (file)
index 0000000..e9f6f6e
--- /dev/null
@@ -0,0 +1,74 @@
+## Copyright (C) 2010-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+AM_CPPFLAGS=                                                    \
+       -I${top_srcdir}/lib                                     \
+       $(GLIB_CFLAGS)                                          \
+       $(XAPIAN_CFLAGS)                                        \
+       $(CODE_COVERAGE_CFLAGS)
+
+AM_CXXFLAGS=                                                    \
+       $(GMIME_CFLAGS)                                         \
+       -DMU_SCRIPTS_DIR="\"$(pkgdatadir)/scripts/\""           \
+       $(ASAN_CXXCFLAGS)                                       \
+       $(WARN_CXXFLAGS)                                        \
+       $(CODE_COVERAGE_CFLAGS)                                 \
+       -Wno-switch-enum
+
+AM_LDFLAGS=                                                     \
+       $(ASAN_LDFLAGS)
+
+bin_PROGRAMS=                                                   \
+       mu
+
+# note, mu.cc is only '.cc' and not '.c' because libmu must explicitly
+# be linked as c++, not c.
+mu_SOURCES=                                                     \
+       mu.cc                                                   \
+       mu-cmd-cfind.cc                                         \
+       mu-config.cc                                            \
+       mu-config.hh                                            \
+       mu-cmd-extract.cc                                       \
+       mu-cmd-find.cc                                          \
+       mu-cmd-index.cc                                         \
+       mu-cmd-server.cc                                        \
+       mu-cmd-script.cc                                        \
+       mu-cmd-fields.cc                                        \
+       mu-cmd.cc                                               \
+       mu-cmd.hh
+
+BUILT_SOURCES=                                                  \
+       mu-help-strings.inc
+
+mu-help-strings.inc: mu-help-strings.txt mu-help-strings.awk
+        $(AM_V_GEN) $(AWK) -f ${top_srcdir}/mu/mu-help-strings.awk < $< > $@
+
+mu_LDADD=                                                       \
+       ${top_builddir}/lib/libmu.la                            \
+       ${top_builddir}/lib/utils/libmu-utils.la                \
+       $(GLIB_LIBS)                                            \
+       $(XAPIAN_LIBS)                                          \
+       $(READLINE_LIBS)                                        \
+       $(CODE_COVERAGE_LIBS)
+
+EXTRA_DIST=                                                     \
+       mu-help-strings.awk                                     \
+       mu-help-strings.txt
+
+CLEANFILES=                                                     \
+       $(BUILT_SOURCES)
diff --git a/mu/meson.build b/mu/meson.build
new file mode 100644 (file)
index 0000000..99659ea
--- /dev/null
@@ -0,0 +1,43 @@
+## Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+awk_script=join_paths(meson.current_source_dir(), 'mu-help-strings.awk')
+mu_help_strings_h=custom_target('mu_help',
+                               input: 'mu-help-strings.txt',
+                               output: 'mu-help-strings.inc',
+                               command: [awk, '-f', awk_script, '@INPUT@'],
+                               capture: true)
+mu = executable(
+  'mu', [
+  'mu.cc',
+  'mu-cmd-cfind.cc',
+  'mu-cmd-extract.cc',
+  'mu-cmd-fields.cc',
+  'mu-cmd-find.cc',
+  'mu-cmd-index.cc',
+  'mu-cmd-script.cc',
+  'mu-cmd-server.cc',
+  'mu-cmd.cc',
+  'mu-cmd.hh',
+  'mu-config.cc',
+  'mu-config.hh',
+  mu_help_strings_h
+],
+  dependencies: [ glib_dep, gmime_dep, lib_mu_dep, thread_dep, config_h_dep ],
+  cpp_args: ['-DMU_SCRIPTS_DIR="'+ join_paths(datadir, 'mu', 'scripts') + '"'],
+  install: true)
+
+subdir('tests')
diff --git a/mu/mu-cmd-cfind.cc b/mu/mu-cmd-cfind.cc
new file mode 100644 (file)
index 0000000..893ce84
--- /dev/null
@@ -0,0 +1,439 @@
+/*
+** Copyright (C) 2011-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it under
+** the terms of the GNU General Public License as published by the Free Software
+** Foundation; either version 3, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful, but WITHOUT
+** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+** FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+** details.
+**
+** You should have received a copy of the GNU General Public License along with
+** this program; if not, write to the Free Software Foundation, Inc., 51
+** Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <string>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "mu-cmd.hh"
+#include "mu-contacts-cache.hh"
+#include "mu-runtime.hh"
+
+#include "utils/mu-util.h"
+#include "utils/mu-utils.hh"
+#include "utils/mu-error.hh"
+
+using namespace Mu;
+
+/**
+ * macro to check whether the string is empty, ie. if it's NULL or
+ * it's length is 0
+ *
+ * @param S a string
+ *
+ * @return TRUE if the string is empty, FALSE otherwise
+ */
+#define mu_str_is_empty(S) ((!(S)||!(*S))?TRUE:FALSE)
+
+
+/**
+ * guess the last name for the given name; clearly,
+ * this is just a rough guess for setting an initial value.
+ *
+ * @param name a name
+ *
+ * @return the last name, as a newly allocated string (free with
+ * g_free)
+ */
+static gchar*
+guess_last_name(const char* name)
+{
+       const gchar* lastsp;
+
+       if (!name)
+               return g_strdup("");
+
+       lastsp = g_strrstr(name, " ");
+
+       return g_strdup(lastsp ? lastsp + 1 : "");
+}
+
+/**
+ * guess the first name for the given name; clearly,
+ * this is just a rough guess for setting an initial value.
+ *
+ * @param name a name
+ *
+ * @return the first name, as a newly allocated string (free with
+ * g_free)
+ */
+static gchar*
+guess_first_name(const char* name)
+{
+       const gchar* lastsp;
+
+       if (!name)
+               return g_strdup("");
+
+       lastsp = g_strrstr(name, " ");
+
+       if (lastsp)
+               return g_strndup(name, lastsp - name);
+       else
+               return g_strdup(name);
+}
+
+/**
+ * guess some nick name for the given name; if we can determine an
+ * first name, last name, the nick will be first name + the first char
+ * of the last name. otherwise, it's just the first name. clearly,
+ * this is just a rough guess for setting an initial value for nicks.
+ *
+ * @param name a name
+ *
+ * @return the guessed nick, as a newly allocated string (free with g_free)
+ */
+static gchar*
+cleanup_str(const char* str)
+{
+       gchar*       s;
+       const gchar* cur;
+       unsigned     i;
+
+       if (mu_str_is_empty(str))
+               return g_strdup("");
+
+       s = g_new0(char, strlen(str) + 1);
+
+       for (cur = str, i = 0; *cur; ++cur) {
+               if (ispunct(*cur) || isspace(*cur))
+                       continue;
+               else
+                       s[i++] = *cur;
+       }
+
+       return s;
+}
+
+static char*
+uniquify_nick(const char* nick, GHashTable* nicks)
+{
+       guint u;
+
+       for (u = 2; u != 1000; ++u) {
+               char* cand;
+               cand = g_strdup_printf("%s%u", nick, u);
+               if (!g_hash_table_contains(nicks, cand))
+                       return cand;
+       }
+
+       return g_strdup(nick); /* if all else fails */
+}
+
+static gchar*
+guess_nick(const char* name, GHashTable* nicks)
+{
+       gchar *fname, *lname, *nick;
+       gchar  initial[7];
+
+       fname = guess_first_name(name);
+       lname = guess_last_name(name);
+
+       /* if there's no last name, use first name as the nick */
+       if (mu_str_is_empty(fname) || mu_str_is_empty(lname)) {
+               g_free(lname);
+               nick = fname;
+               goto leave;
+       }
+
+       memset(initial, 0, sizeof(initial));
+       /* couldn't we get an initial for the last name? */
+       if (g_unichar_to_utf8(g_utf8_get_char(lname), initial) == 0) {
+               g_free(lname);
+               nick = fname;
+               goto leave;
+       }
+
+       nick = g_strdup_printf("%s%s", fname, initial);
+       g_free(fname);
+       g_free(lname);
+
+leave : {
+       gchar* tmp;
+       tmp = cleanup_str(nick);
+       g_free(nick);
+       nick = tmp;
+}
+
+       if (g_hash_table_contains(nicks, nick)) {
+               char* tmp;
+               tmp = uniquify_nick(nick, nicks);
+               g_free(nick);
+               nick = tmp;
+       }
+
+       g_hash_table_add(nicks, g_strdup(nick));
+
+       return nick;
+}
+
+static void
+print_header(const MuConfigFormat format)
+{
+       switch (format) {
+       case MU_CONFIG_FORMAT_BBDB:
+               g_print(";; -*-coding: utf-8-emacs;-*-\n"
+                       ";;; file-version: 6\n");
+               break;
+       case MU_CONFIG_FORMAT_MUTT_AB:
+               g_print("Matching addresses in the mu database:\n");
+               break;
+       default:
+               break;
+       }
+}
+
+static void
+each_contact_bbdb(const std::string& email, const std::string& name, time_t tstamp)
+{
+       char *fname, *lname;
+
+       fname = guess_first_name(name.c_str());
+       lname = guess_last_name(name.c_str());
+
+       const auto now{time_to_string("%Y-%m-%d", time(NULL))};
+       const auto timestamp{time_to_string("%Y-%m-%d", tstamp)};
+
+       g_print("[\"%s\" \"%s\" nil nil nil nil (\"%s\") "
+               "((creation-date . \"%s\") (time-stamp . \"%s\")) nil]\n",
+               fname,
+               lname,
+               email.c_str(),
+               now.c_str(),
+               timestamp.c_str());
+
+       g_free(fname);
+       g_free(lname);
+}
+
+static void
+each_contact_mutt_alias(const std::string& email,
+                       const std::string& name,
+                       GHashTable*        nicks)
+{
+       if (name.empty())
+               return;
+
+       char* nick = guess_nick(name.c_str(), nicks);
+       mu_util_print_encoded("alias %s %s <%s>\n", nick, name.c_str(), email.c_str());
+
+       g_free(nick);
+}
+
+static void
+each_contact_wl(const std::string& email,
+               const std::string& name,
+               GHashTable*        nicks)
+{
+       if (name.empty())
+               return;
+
+       char* nick = guess_nick(name.c_str(), nicks);
+       mu_util_print_encoded("%s \"%s\" \"%s\"\n", email.c_str(), nick, name.c_str());
+       g_free(nick);
+}
+
+static void
+print_plain(const std::string& email, const std::string& name, bool color)
+{
+       if (!name.empty()) {
+               if (color)
+                       ::fputs(MU_COLOR_MAGENTA, stdout);
+               mu_util_fputs_encoded(name.c_str(), stdout);
+               ::fputs(" ", stdout);
+       }
+
+       if (color)
+               ::fputs(MU_COLOR_GREEN, stdout);
+
+       mu_util_fputs_encoded(email.c_str(), stdout);
+
+       if (color)
+               fputs(MU_COLOR_DEFAULT, stdout);
+
+       fputs("\n", stdout);
+}
+
+struct ECData {
+       MuConfigFormat  format;
+       gboolean        color, personal;
+       time_t          after;
+       GRegex*         rx;
+       GHashTable*     nicks;
+       size_t          maxnum;
+       size_t          n;
+};
+
+static void
+each_contact(const Mu::Contact& ci, ECData& ecdata)
+{
+       if (ecdata.personal && !ci.personal)
+               return;
+
+       if (ci.message_date < ecdata.after)
+               return;
+
+       if (ecdata.rx &&
+           !g_regex_match(ecdata.rx, ci.email.c_str(), (GRegexMatchFlags)0, NULL) &&
+           !g_regex_match(ecdata.rx,
+                          ci.name.empty() ? "" : ci.name.c_str(),
+                          (GRegexMatchFlags)0,
+                          NULL))
+               return;
+
+       ++ecdata.n;
+
+       switch (ecdata.format) {
+       case MU_CONFIG_FORMAT_MUTT_ALIAS:
+               each_contact_mutt_alias(ci.email, ci.name, ecdata.nicks);
+               break;
+       case MU_CONFIG_FORMAT_MUTT_AB:
+               mu_util_print_encoded("%s\t%s\t\n", ci.email.c_str(), ci.name.c_str());
+               break;
+       case MU_CONFIG_FORMAT_WL: each_contact_wl(ci.email, ci.name, ecdata.nicks);
+               break;
+       case MU_CONFIG_FORMAT_ORG_CONTACT:
+               if (!ci.name.empty())
+                       mu_util_print_encoded("* %s\n:PROPERTIES:\n:EMAIL: %s\n:END:\n\n",
+                                             ci.name.c_str(),
+                                             ci.email.c_str());
+               break;
+       case MU_CONFIG_FORMAT_BBDB: each_contact_bbdb(ci.email, ci.name, ci.message_date);
+               break;
+       case MU_CONFIG_FORMAT_CSV:
+               mu_util_print_encoded("%s,%s\n",
+                                     ci.name.empty() ? "" : Mu::quote(ci.name).c_str(),
+                                     Mu::quote(ci.email).c_str());
+               break;
+       case MU_CONFIG_FORMAT_DEBUG: {
+               char datebuf[32];
+               const auto mdate(static_cast<::time_t>(ci.message_date));
+               ::strftime(datebuf, sizeof(datebuf), "%F %T", ::gmtime(&mdate));
+               g_print("%s\n\tname: %s\n\t%s\n\tpersonal: %s\n\tfreq: %zu\n"
+                       "\tlast-seen: %s\n",
+                       ci.email.c_str(),
+                       ci.name.empty() ? "<none>" : ci.name.c_str(),
+                       ci.display_name(true).c_str(),
+                       ci.personal ? "yes" : "no",
+                       ci.frequency,
+                       datebuf);
+       }
+               break;
+       default:
+               print_plain(ci.email, ci.name, ecdata.color);
+       }
+}
+
+static Result<void>
+run_cmd_cfind(const Mu::Store&         store,
+             const char*               pattern,
+             gboolean                  personal,
+             time_t                    after,
+             int                       maxnum,
+             const MuConfigFormat      format,
+             gboolean                  color)
+{
+       ECData ecdata{};
+       GError *err{};
+
+       memset(&ecdata, 0, sizeof(ecdata));
+
+       if (pattern) {
+               ecdata.rx = g_regex_new(
+                   pattern,
+                   (GRegexCompileFlags)(G_REGEX_CASELESS | G_REGEX_OPTIMIZE),
+                   (GRegexMatchFlags)0, &err);
+
+               if (!ecdata.rx)
+                       return Err(Error::Code::Internal, &err, "invalid cfind regexp");
+       }
+
+       ecdata.personal = personal;
+       ecdata.n        = 0;
+       ecdata.after    = after;
+       ecdata.maxnum   = maxnum;
+       ecdata.format   = format;
+       ecdata.color    = color;
+       ecdata.nicks    = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+
+       print_header(format);
+
+       store.contacts_cache().for_each([&](const auto& ci) {
+               each_contact(ci, ecdata);
+               return ecdata.maxnum == 0 || ecdata.n < ecdata.maxnum;
+       });
+       g_hash_table_unref(ecdata.nicks);
+
+       if (ecdata.rx)
+               g_regex_unref(ecdata.rx);
+
+       if (ecdata.n == 0)
+               return Err(Error::Code::ContactNotFound, "no matching contacts found");
+       else
+               return Ok();
+}
+
+static gboolean
+cfind_params_valid(const MuConfig* opts)
+{
+       if (!opts || opts->cmd != MU_CONFIG_CMD_CFIND)
+               return FALSE;
+
+       switch (opts->format) {
+       case MU_CONFIG_FORMAT_PLAIN:
+       case MU_CONFIG_FORMAT_MUTT_ALIAS:
+       case MU_CONFIG_FORMAT_MUTT_AB:
+       case MU_CONFIG_FORMAT_WL:
+       case MU_CONFIG_FORMAT_BBDB:
+       case MU_CONFIG_FORMAT_CSV:
+       case MU_CONFIG_FORMAT_ORG_CONTACT:
+       case MU_CONFIG_FORMAT_DEBUG: break;
+       default:
+               g_printerr("invalid output format %s\n",
+                          opts->formatstr ? opts->formatstr : "<none>");
+               return FALSE;
+       }
+
+       /* only one pattern allowed */
+       if (opts->params[1] && opts->params[2]) {
+               g_printerr("usage: mu cfind [options] [<ptrn>]\n");
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+Result<void>
+Mu::mu_cmd_cfind(const Mu::Store& store, const MuConfig* opts)
+{
+       if (!cfind_params_valid(opts))
+               return Err(Error::Code::InvalidArgument, "error in parameters");
+       else
+               return run_cmd_cfind(store,
+                                    opts->params[1],
+                                    opts->personal,
+                                    opts->after,
+                                    opts->maxnum,
+                                    opts->format,
+                                     !opts->nocolor);
+}
diff --git a/mu/mu-cmd-extract.cc b/mu/mu-cmd-extract.cc
new file mode 100644 (file)
index 0000000..d071441
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+** Copyright (C) 2010-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+#include "mu-config.hh"
+#include "utils/mu-util.h"
+#include "utils/mu-utils.hh"
+#include <message/mu-message.hh>
+#include <regex>
+
+using namespace Mu;
+
+
+static Result<void>
+save_part(const Message::Part& part, size_t idx, const MuConfig* opts)
+{
+       const auto targetdir = std::invoke([&]{
+               auto tdir{std::string{opts->targetdir ? opts->targetdir : ""}};
+               return tdir.empty() ? tdir : tdir + G_DIR_SEPARATOR_S;
+       });
+       const auto path{targetdir +
+               part.cooked_filename().value_or(format("part-%zu", idx))};
+
+       if (auto&& res{part.to_file(path, opts->overwrite)}; !res)
+               return Err(res.error());
+
+       if (opts->play) {
+               GError *err{};
+               if (auto res{mu_util_play(path.c_str(), &err)};
+                   res != MU_OK)
+                       return Err(Error::Code::Play, &err, "playing '%s' failed",
+                                  path.c_str());
+       }
+
+       return Ok();
+}
+
+static Result<void>
+save_parts(const std::string& path, Option<std::string>& filename_rx,
+          const MuConfig* opts)
+{
+       auto message{Message::make_from_path(path, mu_config_message_options(opts))};
+       if (!message)
+               return Err(std::move(message.error()));
+
+
+       size_t partnum{}, saved_num{};
+       const auto partnums = std::invoke([&]()->std::vector<size_t> {
+                       std::vector<size_t> nums;
+                       for (auto&& numstr : split(opts->parts ? opts->parts : "", ','))
+                               nums.emplace_back(
+                                       static_cast<size_t>(::atoi(numstr.c_str())));
+                       return nums;
+               });
+
+
+       for (auto&& part: message->parts()) {
+
+               ++partnum;
+
+               if (!opts->save_all) {
+
+                       if (!partnums.empty() &&
+                           !seq_some(partnums, [&](auto&& num){return num==partnum;}))
+                               continue; // not a wanted partnum.
+
+                       if (filename_rx && (!part.raw_filename() ||
+                                           !std::regex_match(*part.raw_filename(),
+                                                             std::regex{*filename_rx})))
+                               continue; // not a wanted pattern.
+               }
+
+               if (auto res = save_part(part, partnum, opts); !res)
+                       return res;
+
+               ++saved_num;
+       }
+
+       // if (saved_num == 0)
+       //      return Err(Error::Code::File,
+       //                 "no %s extracted from this message",
+       //                 opts->save_attachments ? "attachments" : "parts");
+       // else
+               return Ok();
+}
+
+#define color_maybe(C)                          \
+       do {                                    \
+               if (color)                      \
+                       fputs((C), stdout);     \
+       } while (0)
+
+static void
+show_part(const MessagePart& part, size_t index, bool color)
+{
+       /* index */
+       g_print("  %zu ", index);
+
+       /* filename */
+       color_maybe(MU_COLOR_GREEN);
+       const auto fname{part.raw_filename()};
+       mu_util_fputs_encoded(fname ? fname->c_str() : "<none>", stdout);
+
+       mu_util_fputs_encoded(" ", stdout);
+
+       /* content-type */
+       color_maybe(MU_COLOR_BLUE);
+       const auto ctype{part.mime_type()};
+       mu_util_fputs_encoded(ctype ? ctype->c_str() :  "<none>", stdout);
+
+       /* /\* disposition *\/ */
+       color_maybe(MU_COLOR_MAGENTA);
+       mu_util_print_encoded(" [%s]", part.is_attachment() ?
+                             "attachment" : "inline");
+       /* size */
+       if (part.size() > 0) {
+               color_maybe(MU_COLOR_CYAN);
+               g_print(" (%zu bytes)", part.size());
+       }
+
+       color_maybe(MU_COLOR_DEFAULT);
+       fputs("\n", stdout);
+}
+
+static Mu::Result<void>
+show_parts(const char* path, const MuConfig* opts)
+{
+       //msgopts = mu_config_get_msg_options(opts);
+
+       auto msg_res{Message::make_from_path(path, mu_config_message_options(opts))};
+       if (!msg_res)
+               return Err(std::move(msg_res.error()));
+
+       /* TODO: update this for crypto */
+       size_t index{};
+       g_print("MIME-parts in this message:\n");
+       for (auto&& part: msg_res->parts())
+               show_part(part, ++index, !opts->nocolor);
+
+       return Ok();
+}
+
+static Mu::Result<void>
+check_params(const MuConfig* opts)
+{
+       size_t param_num;
+       param_num = mu_config_param_num(opts);
+
+       if (param_num < 2)
+               return Err(Error::Code::InvalidArgument, "parameters missing");
+
+       if (opts->save_attachments || opts->save_all)
+               if (opts->parts || param_num == 3)
+                       return Err(Error::Code::User,
+                                  "--save-attachments and --save-all don't "
+                                  "accept a filename pattern or --parts");
+
+       if (opts->save_attachments && opts->save_all)
+               return Err(Error::Code::User,
+                          "only one of --save-attachments and"
+                          " --save-all is allowed");
+       return Ok();
+}
+
+
+Mu::Result<void>
+Mu::mu_cmd_extract(const MuConfig* opts)
+{
+       if (!opts ||  opts->cmd != MU_CONFIG_CMD_EXTRACT)
+               return Err(Error::Code::Internal, "error in arguments");
+       if (auto res = check_params(opts); !res)
+               return Err(std::move(res.error()));
+
+       if (!opts->params[2] && !opts->parts &&
+           !opts->save_attachments && !opts->save_all)
+               return show_parts(opts->params[1], opts); /* show, don't save */
+
+       if (!mu_util_check_dir(opts->targetdir, FALSE, TRUE))
+               return Err(Error::Code::File,
+                          "target '%s' is not a writable directory",
+                          opts->targetdir);
+
+       Option<std::string> pattern{};
+       if (opts->params[2])
+               pattern = opts->params[2];
+
+       return save_parts(opts->params[1], pattern, opts);
+}
diff --git a/mu/mu-cmd-fields.cc b/mu/mu-cmd-fields.cc
new file mode 100644 (file)
index 0000000..729ccf4
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it under
+** the terms of the GNU General Public License as published by the Free Software
+** Foundation; either version 3, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful, but WITHOUT
+** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+** FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+** details.
+**
+** You should have received a copy of the GNU General Public License along with
+** this program; if not, write to the Free Software Foundation, Inc., 51
+** Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <iostream>
+#include <functional>
+
+#include "mu-cmd.hh"
+#include <message/mu-message.hh>
+#include "utils/mu-utils.hh"
+
+#include "thirdparty/tabulate.hpp"
+
+
+using namespace Mu;
+using namespace tabulate;
+
+
+static void
+table_header(Table& table, const MuConfig* opts)
+{
+       if (opts->nocolor)
+               return;
+
+       (*table.begin()).format()
+               .font_style({FontStyle::bold})
+               .font_color(Color::blue);
+
+}
+
+static void
+show_fields(const MuConfig* opts)
+{
+       using namespace std::string_literals;
+
+       Table fields;
+       fields.add_row({"field-name", "alias", "short", "search",
+                       "value", "sexp", "example query", "description"});
+
+       auto disp= [&](std::string_view sv)->std::string {
+               if (sv.empty())
+                       return "";
+               else
+                       return format("%*s", STR_V(sv));
+       };
+
+       auto searchable=[&](const Field& field)->std::string {
+               if (field.is_boolean_term())
+                       return "boolean";
+               if (field.is_indexable_term())
+                       return "index";
+               if (field.is_normal_term())
+                       return "yes";
+               if (field.is_contact())
+                       return "contact";
+               if (field.is_range())
+                       return "range";
+               return "no";
+       };
+
+       size_t row{};
+       field_for_each([&](auto&& field){
+               if (field.is_internal())
+                       return; // skip.
+
+               fields.add_row({format("%*s", STR_V(field.name)),
+                               field.alias.empty() ? "" : format("%*s", STR_V(field.alias)),
+                               field.shortcut ? format("%c", field.shortcut) : ""s,
+                               searchable(field),
+                               field.is_value() ? "yes" : "no",
+                               field.include_in_sexp() ? "yes" : "no",
+                               disp(field.example_query),
+                               disp(field.description)});
+               ++row;
+       });
+
+       table_header(fields, opts);
+
+       std::cout << fields << '\n';
+}
+
+static void
+show_flags(const MuConfig* opts)
+{
+       using namespace tabulate;
+       using namespace std::string_literals;
+
+       Table flags;
+       flags.add_row({"flag", "shortcut", "category", "description"});
+
+       flag_infos_for_each([&](const MessageFlagInfo& info) {
+
+               const auto catname = std::invoke(
+                       [](MessageFlagCategory cat)->std::string {
+                               switch(cat){
+                               case MessageFlagCategory::Mailfile:
+                                       return "file";
+                               case MessageFlagCategory::Maildir:
+                                       return "maildir";
+                               case MessageFlagCategory::Content:
+                                       return "content";
+                               case MessageFlagCategory::Pseudo:
+                                       return "pseudo";
+                               default:
+                                       return {};
+                               }
+                       }, info.category);
+
+               flags.add_row({format("%*s", STR_V(info.name)),
+                               format("%c", info.shortcut),
+                               catname,
+                               std::string{info.description}});
+       });
+
+       table_header(flags, opts);
+
+       std::cout << flags << '\n';
+}
+
+
+
+Result<void>
+Mu::mu_cmd_fields(const MuConfig* opts)
+{
+       g_return_val_if_fail(opts, Err(Error::Code::Internal, "no opts"));
+
+       if (!locale_workaround())
+               return Err(Error::Code::User, "failed to find a working locale");
+
+
+       std::cout << "#\n# message fields\n#\n";
+       show_fields(opts);
+       std::cout << "\n#\n# message flags\n#\n";
+       show_flags(opts);
+
+       return Ok();
+
+}
diff --git a/mu/mu-cmd-find.cc b/mu/mu-cmd-find.cc
new file mode 100644 (file)
index 0000000..898244f
--- /dev/null
@@ -0,0 +1,612 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <array>
+
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <signal.h>
+
+#include "message/mu-message.hh"
+#include "mu-maildir.hh"
+#include "mu-query-match-deciders.hh"
+#include "mu-query.hh"
+#include "mu-bookmarks.hh"
+#include "mu-runtime.hh"
+#include "message/mu-message.hh"
+
+#include "utils/mu-option.hh"
+#include "utils/mu-util.h"
+
+#include "mu-cmd.hh"
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+struct OutputInfo {
+       Xapian::docid       docid{};
+       bool                header{};
+       bool                footer{};
+       bool                last{};
+       Option<QueryMatch&> match_info;
+};
+
+constexpr auto FirstOutput{OutputInfo{0, true, false, {}, {}}};
+constexpr auto LastOutput{OutputInfo{0, false, true, {}, {}}};
+
+using OutputFunc = std::function<bool(const Option<Message>& msg, const OutputInfo&,
+                                     const MuConfig*, GError**)>;
+
+static Result<void>
+print_internal(const Store&       store,
+              const std::string& expr,
+              gboolean           xapian,
+              gboolean           warn)
+{
+       std::cout << store.parse_query(expr, xapian) << "\n";
+       return Ok();
+}
+
+static Result<QueryResults>
+run_query(const Store& store, const std::string& expr, const MuConfig* opts)
+{
+       const auto sortfield{field_from_name(opts->sortfield ? opts->sortfield : "")};
+       if (!sortfield && opts->sortfield)
+               return Err(Error::Code::InvalidArgument,
+                          "invalid sort field: '%s'", opts->sortfield);
+
+       Mu::QueryFlags qflags{QueryFlags::SkipUnreadable};
+       if (opts->reverse)
+               qflags |= QueryFlags::Descending;
+       if (opts->skip_dups)
+               qflags |= QueryFlags::SkipDuplicates;
+       if (opts->include_related)
+               qflags |= QueryFlags::IncludeRelated;
+       if (opts->threads)
+               qflags |= QueryFlags::Threading;
+
+       return store.run_query(expr, sortfield.value_or(field_from_id(Field::Id::Date)).id,
+                              qflags, opts->maxnum);
+}
+
+static gboolean
+exec_cmd(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+       if (!msg)
+               return TRUE;
+
+       gint     status;
+       char *   cmdline, *escpath;
+       gboolean rv;
+
+       escpath = g_shell_quote(msg->path().c_str());
+       cmdline = g_strdup_printf("%s %s", opts->exec, escpath);
+
+       rv = g_spawn_command_line_sync(cmdline, NULL, NULL, &status, err);
+
+       g_free(cmdline);
+       g_free(escpath);
+
+       return rv;
+}
+
+static gchar*
+resolve_bookmark(const MuConfig* opts, GError** err)
+{
+       MuBookmarks* bm;
+       char*        val;
+       const gchar* bmfile;
+
+       bmfile = mu_runtime_path(MU_RUNTIME_PATH_BOOKMARKS);
+       bm     = mu_bookmarks_new(bmfile);
+       if (!bm) {
+               g_set_error(err,
+                           MU_ERROR_DOMAIN,
+                           MU_ERROR_FILE_CANNOT_OPEN,
+                           "failed to open bookmarks file '%s'",
+                           bmfile);
+               return FALSE;
+       }
+
+       val = (gchar*)mu_bookmarks_lookup(bm, opts->bookmark);
+       if (!val)
+               g_set_error(err,
+                           MU_ERROR_DOMAIN,
+                           MU_ERROR_NO_MATCHES,
+                           "bookmark '%s' not found",
+                           opts->bookmark);
+       else
+               val = g_strdup(val);
+
+       mu_bookmarks_destroy(bm);
+       return val;
+}
+
+static Result<std::string>
+get_query(const MuConfig* opts)
+{
+       GError *err{};
+       gchar *query, *bookmarkval;
+
+       /* params[0] is 'find', actual search params start with [1] */
+       if (!opts->bookmark && !opts->params[1])
+               return Err(Error::Code::InvalidArgument, "error in parameters");
+
+       bookmarkval = {};
+       if (opts->bookmark) {
+               bookmarkval = resolve_bookmark(opts, &err);
+               if (!bookmarkval)
+                       return Err(Error::Code::Command, &err,
+                                  "failed to resolve bookmark");
+       }
+
+       query = g_strjoinv(" ", &opts->params[1]);
+       if (bookmarkval) {
+               gchar* tmp;
+               tmp = g_strdup_printf("%s %s", bookmarkval, query);
+               g_free(query);
+               query = tmp;
+       }
+       g_free(bookmarkval);
+
+       return Ok(to_string_gchar(std::move(query)));
+}
+
+static bool
+prepare_links(const MuConfig* opts, GError** err)
+{
+       /* note, mu_maildir_mkdir simply ignores whatever part of the
+        * mail dir already exists */
+       if (auto&& res = maildir_mkdir(opts->linksdir, 0700, true); !res) {
+               res.error().fill_g_error(err);
+               return false;
+       }
+
+       if (!opts->clearlinks)
+               return false;
+
+       if (auto&& res = maildir_clear_links(opts->linksdir); !res) {
+               res.error().fill_g_error(err);
+               return false;
+       }
+
+       return true;
+}
+
+static bool
+output_link(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+       if (info.header)
+               return prepare_links(opts, err);
+       else if (info.footer)
+               return true;
+
+       if (auto&& res = maildir_link(msg->path(), opts->linksdir); !res) {
+               res.error().fill_g_error(err);
+               return false;
+       }
+
+       return true;
+}
+
+static void
+ansi_color_maybe(Field::Id field_id, gboolean color)
+{
+       const char* ansi;
+
+       if (!color)
+               return; /* nothing to do */
+
+       switch (field_id) {
+       case Field::Id::From: ansi = MU_COLOR_CYAN; break;
+
+       case Field::Id::To:
+       case Field::Id::Cc:
+       case Field::Id::Bcc: ansi     = MU_COLOR_BLUE; break;
+       case Field::Id::Subject: ansi = MU_COLOR_GREEN; break;
+       case Field::Id::Date: ansi    = MU_COLOR_MAGENTA; break;
+
+       default:
+               if (field_from_id(field_id).type != Field::Type::String)
+                       ansi = MU_COLOR_YELLOW;
+               else
+                       ansi = MU_COLOR_RED;
+       }
+
+       fputs(ansi, stdout);
+}
+
+static void
+ansi_reset_maybe(Field::Id field_id, gboolean color)
+{
+       if (!color)
+               return; /* nothing to do */
+
+       fputs(MU_COLOR_DEFAULT, stdout);
+}
+
+static std::string
+display_field(const Message& msg, Field::Id field_id)
+{
+       switch (field_from_id(field_id).type) {
+       case Field::Type::String:
+               return msg.document().string_value(field_id);
+       case Field::Type::Integer:
+               if (field_id == Field::Id::Priority) {
+                       return to_string(msg.priority());
+               } else if (field_id == Field::Id::Flags) {
+                       return to_string(msg.flags());
+               } else /* as string */
+                       return msg.document().string_value(field_id);
+       case Field::Type::TimeT:
+               return time_to_string(
+                       "%c", static_cast<::time_t>(msg.document().integer_value(field_id)));
+       case Field::Type::ByteSize:
+               return to_string(msg.document().integer_value(field_id));
+       case Field::Type::StringList:
+               return join(msg.document().string_vec_value(field_id), ',');
+       case Field::Type::ContactList:
+               return to_string(msg.document().contacts_value(field_id));
+       default:
+               g_return_val_if_reached("");
+               return "";
+       }
+}
+
+static void
+print_summary(const Message& msg, const MuConfig* opts)
+{
+       const auto body{msg.body_text()};
+       if (!body)
+               return;
+
+       const auto summ{to_string_opt_gchar(
+                       mu_str_summarize(body->c_str(),
+                                        opts->summary_len))};
+
+       g_print("Summary: ");
+       mu_util_fputs_encoded(summ ? summ->c_str() : "<none>", stdout);
+       g_print("\n");
+}
+
+static void
+thread_indent(const QueryMatch& info, const MuConfig* opts)
+{
+       const auto is_root{any_of(info.flags & QueryMatch::Flags::Root)};
+       const auto first_child{any_of(info.flags & QueryMatch::Flags::First)};
+       const auto last_child{any_of(info.flags & QueryMatch::Flags::Last)};
+       const auto empty_parent{any_of(info.flags & QueryMatch::Flags::Orphan)};
+       const auto is_dup{any_of(info.flags & QueryMatch::Flags::Duplicate)};
+       // const auto is_related{any_of(info.flags & QueryMatch::Flags::Related)};
+
+       /* indent */
+       if (opts->debug) {
+               ::fputs(info.thread_path.c_str(), stdout);
+               ::fputs(" ", stdout);
+       } else
+               for (auto i = info.thread_level; i > 1; --i)
+                       ::fputs("  ", stdout);
+
+       if (!is_root) {
+               if (first_child)
+                       ::fputs("\\", stdout);
+               else if (last_child)
+                       ::fputs("/", stdout);
+               else
+                       ::fputs(" ", stdout);
+               ::fputs(empty_parent ? "*> " : is_dup ? "=> "
+                                                     : "-> ",
+                       stdout);
+       }
+}
+
+static void
+output_plain_fields(const Message& msg, const char* fields,
+                   gboolean color, gboolean threads)
+{
+       const char* myfields;
+       int         nonempty;
+
+       g_return_if_fail(fields);
+
+       for (myfields = fields, nonempty = 0; *myfields; ++myfields) {
+               const auto field_opt{field_from_shortcut(*myfields)};
+               if (!field_opt || (!field_opt->is_value() && !field_opt->is_contact()))
+                       nonempty += printf("%c", *myfields);
+
+               else {
+                       ansi_color_maybe(field_opt->id, color);
+                       nonempty += mu_util_fputs_encoded(
+                               display_field(msg, field_opt->id).c_str(), stdout);
+                       ansi_reset_maybe(field_opt->id, color);
+               }
+       }
+
+       if (nonempty)
+               fputs("\n", stdout);
+}
+
+static gboolean
+output_plain(const Option<Message>& msg, const OutputInfo& info,
+            const MuConfig* opts, GError** err)
+{
+       if (!msg)
+               return true;
+
+       /* we reuse the color (whatever that may be)
+        * for message-priority for threads, too */
+       ansi_color_maybe(Field::Id::Priority, !opts->nocolor);
+       if (opts->threads && info.match_info)
+               thread_indent(*info.match_info, opts);
+
+       output_plain_fields(*msg, opts->fields, !opts->nocolor, opts->threads);
+
+       if (opts->summary_len > 0)
+               print_summary(*msg, opts);
+
+       return TRUE;
+}
+
+static bool
+output_sexp(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+       if (msg) {
+
+               if (const auto sexp{msg->cached_sexp()}; !sexp.empty())
+                       fputs(sexp.c_str(), stdout);
+               else
+                       fputs(msg->to_sexp().to_sexp_string().c_str(), stdout);
+
+               fputs("\n", stdout);
+       }
+
+       return true;
+}
+
+static bool
+output_json(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+       if (info.header) {
+               g_print("[\n");
+               return true;
+       }
+
+       if (info.footer) {
+               g_print("]\n");
+               return true;
+       }
+
+       if (!msg)
+               return true;
+
+       g_print("%s%s\n",
+               msg->to_sexp().to_json_string().c_str(),
+               info.last ? "" : ",");
+
+       return true;
+}
+
+static void
+print_attr_xml(const std::string& elm, const std::string& str)
+{
+       if (str.empty())
+               return; /* empty: don't include */
+
+       auto&& esc{to_string_opt_gchar(g_markup_escape_text(str.c_str(), -1))};
+       g_print("\t\t<%s>%s</%s>\n", elm.c_str(), esc.value_or("").c_str(), elm.c_str());
+}
+
+static bool
+output_xml(const Option<Message>& msg, const OutputInfo& info, const MuConfig* opts, GError** err)
+{
+       if (info.header) {
+               g_print("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
+               g_print("<messages>\n");
+               return true;
+       }
+
+       if (info.footer) {
+               g_print("</messages>\n");
+               return true;
+       }
+
+       g_print("\t<message>\n");
+       print_attr_xml("from", to_string(msg->from()));
+       print_attr_xml("to", to_string(msg->to()));
+       print_attr_xml("cc", to_string(msg->cc()));
+       print_attr_xml("subject", msg->subject());
+       g_print("\t\t<date>%u</date>\n", (unsigned)msg->date());
+       g_print("\t\t<size>%u</size>\n", (unsigned)msg->size());
+       print_attr_xml("msgid", msg->message_id());
+       print_attr_xml("path", msg->path());
+       print_attr_xml("maildir", msg->maildir());
+       g_print("\t</message>\n");
+
+       return true;
+}
+
+static OutputFunc
+get_output_func(const MuConfig* opts, GError** err)
+{
+       switch (opts->format) {
+       case MU_CONFIG_FORMAT_LINKS: return output_link;
+       case MU_CONFIG_FORMAT_EXEC: return exec_cmd;
+       case MU_CONFIG_FORMAT_PLAIN: return output_plain;
+       case MU_CONFIG_FORMAT_XML: return output_xml;
+       case MU_CONFIG_FORMAT_SEXP: return output_sexp;
+       case MU_CONFIG_FORMAT_JSON: return output_json;
+
+       default: g_return_val_if_reached(NULL); return NULL;
+       }
+}
+
+static Result<void>
+output_query_results(const QueryResults& qres, const MuConfig* opts)
+{
+       GError* err{};
+       const auto output_func{get_output_func(opts, &err)};
+       if (!output_func)
+               return Err(Error::Code::Query, &err, "failed to find output function");
+
+       gboolean rv{true};
+       output_func(Nothing, FirstOutput, opts, {});
+
+       size_t n{0};
+       for (auto&& item : qres) {
+               n++;
+               auto msg{item.message()};
+               if (!msg)
+                       continue;
+
+               if (opts->after != 0 && msg->changed() < opts->after)
+                       continue;
+
+               rv = output_func(msg,
+                                {item.doc_id(),
+                                 false,
+                                 false,
+                                 n == qres.size(), /* last? */
+                                 item.query_match()},
+                                opts,
+                                &err);
+               if (!rv)
+                       break;
+       }
+       output_func(Nothing, LastOutput, opts, {});
+
+
+       if (rv)
+               return Ok();
+       else
+               return Err(Error::Code::Query, &err, "error in query results output");
+}
+
+static Result<void>
+process_query(const Store& store, const std::string& expr, const MuConfig* opts)
+{
+       auto qres{run_query(store, expr, opts)};
+       if (!qres)
+               return Err(qres.error());
+
+       if (qres->empty())
+               return Err(Error::Code::NoMatches,  "no matches for search expression");
+
+       return output_query_results(*qres, opts);
+}
+
+static Result<void>
+execute_find(const Store& store, const MuConfig* opts)
+{
+       auto expr{get_query(opts)};
+       if (!expr)
+               return Err(expr.error());
+
+       if (opts->format == MU_CONFIG_FORMAT_XQUERY)
+               return print_internal(store, *expr, TRUE, FALSE);
+       else if (opts->format == MU_CONFIG_FORMAT_MQUERY)
+               return print_internal(store, *expr, FALSE, opts->verbose);
+       else
+               return process_query(store, *expr, opts);
+}
+
+static gboolean
+format_params_valid(const MuConfig* opts, GError** err)
+{
+       switch (opts->format) {
+       case MU_CONFIG_FORMAT_EXEC: break;
+       case MU_CONFIG_FORMAT_PLAIN:
+       case MU_CONFIG_FORMAT_SEXP:
+       case MU_CONFIG_FORMAT_JSON:
+       case MU_CONFIG_FORMAT_LINKS:
+       case MU_CONFIG_FORMAT_XML:
+       case MU_CONFIG_FORMAT_XQUERY:
+       case MU_CONFIG_FORMAT_MQUERY:
+               if (opts->exec) {
+                       mu_util_g_set_error(err,
+                                           MU_ERROR_IN_PARAMETERS,
+                                           "--exec and --format cannot be combined");
+                       return FALSE;
+               }
+               break;
+       default:
+               mu_util_g_set_error(err,
+                                   MU_ERROR_IN_PARAMETERS,
+                                   "invalid output format %s",
+                                   opts->formatstr ? opts->formatstr : "<none>");
+               return FALSE;
+       }
+
+       if (opts->format == MU_CONFIG_FORMAT_LINKS && !opts->linksdir) {
+               mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS, "missing --linksdir argument");
+               return FALSE;
+       }
+
+       if (opts->linksdir && opts->format != MU_CONFIG_FORMAT_LINKS) {
+               mu_util_g_set_error(err,
+                                   MU_ERROR_IN_PARAMETERS,
+                                   "--linksdir is only valid with --format=links");
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+query_params_valid(const MuConfig* opts, GError** err)
+{
+       const gchar* xpath;
+
+       if (!opts->params[1]) {
+               mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS, "missing query");
+               return FALSE;
+       }
+
+       xpath = mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB);
+       if (mu_util_check_dir(xpath, TRUE, FALSE))
+               return TRUE;
+
+       mu_util_g_set_error(err,
+                           MU_ERROR_FILE_CANNOT_READ,
+                           "'%s' is not a readable Xapian directory",
+                           xpath);
+       return FALSE;
+}
+
+Result<void>
+Mu::mu_cmd_find(const Store& store, const MuConfig* opts)
+{
+       g_return_val_if_fail(opts, Err(Error::Code::Internal, "no opts"));
+       g_return_val_if_fail(opts->cmd == MU_CONFIG_CMD_FIND, Err(Error::Code::Internal,
+                                                                 "wrong command"));
+       MuConfig myopts{*opts};
+
+       if (myopts.exec)
+               myopts.format = MU_CONFIG_FORMAT_EXEC; /* pseudo format */
+
+       GError *err{};
+       if (!query_params_valid(&myopts, &err) || !format_params_valid(&myopts, &err))
+               return Err(Error::Code::InvalidArgument, &err, "invalid argument");
+       else
+               return execute_find(store, &myopts);
+}
diff --git a/mu/mu-cmd-index.cc b/mu/mu-cmd-index.cc
new file mode 100644 (file)
index 0000000..1559cd6
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+
+#include <chrono>
+#include <thread>
+#include <atomic>
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include "index/mu-indexer.hh"
+#include "mu-store.hh"
+#include "mu-runtime.hh"
+
+#include "utils/mu-util.h"
+
+using namespace Mu;
+
+static std::atomic<bool> caught_signal;
+
+static void
+sig_handler(int _sig)
+{
+       caught_signal = true;
+}
+
+static void
+install_sig_handler(void)
+{
+       struct sigaction action;
+       int              i, sigs[] = {SIGINT, SIGHUP, SIGTERM};
+
+       sigemptyset(&action.sa_mask);
+       action.sa_flags   = SA_RESETHAND;
+       action.sa_handler = sig_handler;
+
+       for (i = 0; i != G_N_ELEMENTS(sigs); ++i)
+               if (sigaction(sigs[i], &action, NULL) != 0)
+                       g_critical("set sigaction for %d failed: %s",
+                                  sigs[i], g_strerror(errno));
+}
+
+static void
+print_stats(const Indexer::Progress& stats, bool color)
+{
+       const char* kars = "-\\|/";
+       static auto i    = 0U;
+
+       MaybeAnsi col{color};
+       using Color = MaybeAnsi::Color;
+
+       std::cout << col.fg(Color::Yellow) << kars[++i % 4] << col.reset() << " indexing messages; "
+                 << "checked: " << col.fg(Color::Green) << stats.checked << col.reset()
+                 << "; updated/new: " << col.fg(Color::Green) << stats.updated << col.reset()
+                 << "; cleaned-up: " << col.fg(Color::Green) << stats.removed << col.reset();
+}
+
+Result<void>
+Mu::mu_cmd_index(Mu::Store& store, const MuConfig* opts)
+{
+       if (!opts || opts->cmd != MU_CONFIG_CMD_INDEX || opts->params[1])
+               return Err(Error::Code::InvalidArgument, "error in parameters");
+
+       if (opts->max_msg_size < 0)
+               return Err(Error::Code::InvalidArgument,
+                                   "the maximum message size must be >= 0");
+
+       const auto mdir{store.properties().root_maildir};
+       if (G_UNLIKELY(access(mdir.c_str(), R_OK) != 0))
+               return Err(Error::Code::File, "'%s' is not readable: %s",
+                          mdir.c_str(), g_strerror(errno));
+
+       MaybeAnsi col{!opts->nocolor};
+       using Color = MaybeAnsi::Color;
+       if (!opts->quiet) {
+               if (opts->lazycheck)
+                       std::cout << "lazily ";
+
+               std::cout << "indexing maildir " << col.fg(Color::Green)
+                         << store.properties().root_maildir << col.reset() << " -> store "
+                         << col.fg(Color::Green) << store.properties().database_path << col.reset()
+                         << std::endl;
+       }
+
+       Mu::Indexer::Config conf{};
+       conf.cleanup    = !opts->nocleanup;
+       conf.lazy_check = opts->lazycheck;
+       // ignore .noupdate with an empty store.
+       conf.ignore_noupdate = store.empty();
+
+       install_sig_handler();
+
+       auto& indexer{store.indexer()};
+       indexer.start(conf);
+       while (!caught_signal && indexer.is_running()) {
+               if (!opts->quiet)
+                       print_stats(indexer.progress(), !opts->nocolor);
+
+               std::this_thread::sleep_for(std::chrono::milliseconds(250));
+
+               if (!opts->quiet) {
+                       std::cout << "\r";
+                       std::cout.flush();
+               }
+       }
+
+       store.indexer().stop();
+
+       if (!opts->quiet) {
+               print_stats(store.indexer().progress(), !opts->nocolor);
+               std::cout << std::endl;
+       }
+
+       return Ok();
+}
diff --git a/mu/mu-cmd-script.cc b/mu/mu-cmd-script.cc
new file mode 100644 (file)
index 0000000..62ef9c1
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+** Copyright (C) 2012-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <errno.h>
+
+#include "mu-cmd.hh"
+#include "mu-script.hh"
+#include "mu-runtime.hh"
+
+#include "utils/mu-util.h"
+
+#define MU_GUILE_EXT          ".scm"
+#define MU_GUILE_DESCR_PREFIX ";; INFO: "
+
+#define COL(C) ((color) ? C : "")
+
+using namespace Mu;
+
+static void
+print_script(const char* name,
+            const char* oneline,
+            const char* descr,
+            gboolean    color,
+            gboolean    verbose)
+{
+       g_print("%s%s%s%s%s%s%s%s",
+               verbose ? "\n" : "  * ",
+               COL(MU_COLOR_GREEN),
+               name,
+               COL(MU_COLOR_DEFAULT),
+               oneline ? ": " : "",
+               COL(MU_COLOR_BLUE),
+               oneline ? oneline : "",
+               MU_COLOR_DEFAULT);
+
+       if (verbose && descr)
+               g_print("%s%s%s", COL(MU_COLOR_MAGENTA), descr, COL(MU_COLOR_DEFAULT));
+}
+
+static gboolean
+print_scripts(GSList* scripts, gboolean color, gboolean verbose, const char* rxstr, GError** err)
+{
+       GSList*     cur;
+       const char* verb;
+
+       if (!scripts) {
+               g_print("No scripts available\n");
+               return TRUE; /* not an error */
+       }
+
+       verb = verbose ? "" : " (use --verbose for details)";
+
+       if (rxstr)
+               g_print("Available scripts matching '%s'%s:\n", rxstr, verb);
+       else
+               g_print("Available scripts%s:\n", verb);
+
+       for (cur = scripts; cur; cur = g_slist_next(cur)) {
+               MuScriptInfo* msi;
+               const char *  descr, *oneline, *name;
+
+               msi     = (MuScriptInfo*)cur->data;
+               name    = mu_script_info_name(msi);
+               oneline = mu_script_info_one_line(msi);
+               descr   = mu_script_info_description(msi);
+
+               /* if rxstr is provide, only consider matching scriptinfos */
+               if (rxstr && !mu_script_info_matches_regex(msi, rxstr, err)) {
+                       if (err && *err)
+                               return FALSE;
+                       continue;
+               }
+
+               print_script(name, oneline, descr, color, verbose);
+       }
+
+       return TRUE;
+}
+
+static char*
+get_userpath(const char* muhome)
+{
+       if (muhome)
+               return g_build_path(G_DIR_SEPARATOR_S, muhome, "scripts", NULL);
+       else
+               return g_build_path(G_DIR_SEPARATOR_S,
+                                   g_get_user_data_dir(),
+                                   "mu",
+                                   "scripts",
+                                   NULL);
+}
+
+static GSList*
+get_script_info_list(const char* muhome, GError** err)
+{
+       GSList *scripts, *userscripts, *last;
+       char*   userpath;
+
+       scripts = mu_script_get_script_info_list(MU_SCRIPTS_DIR,
+                                                MU_GUILE_EXT,
+                                                MU_GUILE_DESCR_PREFIX,
+                                                err);
+
+       if (err && *err)
+               return NULL;
+
+       userpath = get_userpath(muhome);
+
+       /* is there are userdir for scripts? */
+       if (!mu_util_check_dir(userpath, TRUE, FALSE)) {
+               g_free(userpath);
+               return scripts;
+       }
+
+       /* append it to the list we already have */
+       userscripts =
+           mu_script_get_script_info_list(userpath, MU_GUILE_EXT, MU_GUILE_DESCR_PREFIX, err);
+       g_free(userpath);
+
+       /* some error, return nothing */
+       if (err && *err) {
+               mu_script_info_list_destroy(userscripts);
+               mu_script_info_list_destroy(scripts);
+               return NULL;
+       }
+
+       /* append the user scripts */
+       last = g_slist_last(scripts);
+       if (last) {
+               last->next = userscripts;
+               return scripts;
+       } else
+               return userscripts; /* apparently, scripts was NULL */
+}
+
+Mu::Result<void>
+Mu::mu_cmd_script(const MuConfig* opts)
+{
+       GError         *err{};
+       MuScriptInfo*   msi;
+       GSList*         scripts;
+
+       if (!mu_util_supports(MU_FEATURE_GUILE))
+               return Err(Error::Code::InvalidArgument,
+                          "<script> sub-command not available (requires guile)");
+
+       scripts = get_script_info_list(opts->muhome, &err);
+       if (err)
+               goto leave;
+
+       if (g_strcmp0(opts->cmdstr, "script") == 0) {
+               print_scripts(scripts, !opts->nocolor, opts->verbose,
+                             opts->script_params[0], &err);
+               goto leave;
+       }
+
+       msi = mu_script_find_script_with_name(scripts, opts->script);
+       if (!msi) {
+               mu_util_g_set_error(&err, MU_ERROR_SCRIPT_NOT_FOUND,
+                                   "command or script not found");
+               goto leave;
+       }
+
+       /* do it! */
+       mu_script_guile_run(msi, mu_runtime_path(MU_RUNTIME_PATH_CACHE),
+                           opts->script_params, &err);
+leave:
+       /* this won't be reached, unless there is some error */
+       mu_script_info_list_destroy(scripts);
+
+       if (err)
+               return Err(Error::Code::InvalidArgument, &err,
+                          "error running script");
+       else
+               return Ok();
+}
diff --git a/mu/mu-cmd-server.cc b/mu/mu-cmd-server.cc
new file mode 100644 (file)
index 0000000..779b9cb
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <string>
+#include <algorithm>
+#include <atomic>
+#include <cstdio>
+
+#include <unistd.h>
+
+#include "mu-runtime.hh"
+#include "mu-cmd.hh"
+#include "mu-server.hh"
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-command-parser.hh"
+#include "utils/mu-readline.hh"
+
+using namespace Mu;
+static std::atomic<int> MuTerminate{0};
+static bool              tty;
+
+static void
+sig_handler(int sig)
+{
+       MuTerminate = sig;
+}
+
+static void
+install_sig_handler(void)
+{
+       static struct sigaction action;
+       int                     i, sigs[] = {SIGINT, SIGHUP, SIGTERM, SIGPIPE};
+
+       MuTerminate = 0;
+
+       action.sa_handler = sig_handler;
+       sigemptyset(&action.sa_mask);
+       action.sa_flags = SA_RESETHAND;
+
+       for (i = 0; i != G_N_ELEMENTS(sigs); ++i)
+               if (sigaction(sigs[i], &action, NULL) != 0)
+                       g_critical("set sigaction for %d failed: %s",
+                                  sigs[i], g_strerror(errno));
+}
+
+/*
+ * Markers for/after the length cookie that precedes the expression we write to
+ * output. We use octal 376, 377 (ie, 0xfe, 0xff) as they will never occur in
+ * utf8 */
+
+#define COOKIE_PRE  "\376"
+#define COOKIE_POST "\377"
+
+static void
+cookie(size_t n)
+{
+       const auto num{static_cast<unsigned>(n)};
+
+       if (tty) // for testing.
+               ::printf("[%x]", num);
+       else
+               ::printf(COOKIE_PRE "%x" COOKIE_POST, num);
+}
+
+static void
+output_sexp_stdout(Sexp&& sexp, Server::OutputFlags flags)
+{
+       /* if requested, insert \n between list elements; note:
+        * is _not_ inherited by children */
+       if (any_of(flags & Server::OutputFlags::SplitList))
+               sexp.formatting_opts |= Sexp::FormattingOptions::SplitList;
+
+       const auto str{sexp.to_sexp_string()};
+
+       cookie(str.size() + 1);
+       if (G_UNLIKELY(::puts(str.c_str()) < 0)) {
+               g_critical("failed to write output '%s'", str.c_str());
+               ::raise(SIGTERM); /* terminate ourselves */
+       }
+
+       if (any_of(flags & Server::OutputFlags::Flush))
+               std::fflush(stdout);
+}
+
+static void
+report_error(const Mu::Error& err) noexcept
+{
+       Sexp::List e;
+
+       e.add_prop(":error", Sexp::make_number(static_cast<size_t>(err.code())));
+       e.add_prop(":message", Sexp::make_string(err.what()));
+
+       output_sexp_stdout(Sexp::make_list(std::move(e)),
+                          Server::OutputFlags::Flush);
+}
+
+
+Result<void>
+Mu::mu_cmd_server(const MuConfig* opts) try {
+
+       auto store = Store::make(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB),
+                                Store::Options::Writable);
+       if (!store)
+               return Err(store.error());
+
+       Server server{*store, output_sexp_stdout};
+       g_message("created server with store @ %s; maildir @ %s; debug-mode %s"
+                 "readline: %s",
+                 store->properties().database_path.c_str(),
+                 store->properties().root_maildir.c_str(),
+                 opts->debug ? "yes" : "no",
+                 have_readline() ? "yes" : "no");
+
+       tty = ::isatty(::fileno(stdout));
+       const auto eval = std::string{opts->commands ? "(help :full t)"
+                                     : opts->eval   ? opts->eval
+                                                    : ""};
+       if (!eval.empty()) {
+               server.invoke(eval);
+               return Ok();
+       }
+
+       // Note, the readline stuff is inactive unless on a tty.
+       const auto histpath{std::string{mu_runtime_path(MU_RUNTIME_PATH_CACHE)} + "/history"};
+       setup_readline(histpath, 50);
+
+       install_sig_handler();
+       std::cout << ";; Welcome to the " << PACKAGE_STRING << " command-server\n"
+                 << ";; Use (help) to get a list of commands, (quit) to quit.\n";
+
+       bool do_quit{};
+       while (!MuTerminate && !do_quit) {
+               std::fflush(stdout); // Needed for Windows, see issue #1827.
+               const auto line{read_line(do_quit)};
+               if (line.find_first_not_of(" \t") == std::string::npos)
+                       continue; // skip whitespace-only lines
+
+               do_quit = server.invoke(line) ? false : true;
+               save_line(line);
+       }
+
+       if (MuTerminate != 0)
+               g_message ("shutting down due to signal %d", MuTerminate.load());
+
+       shutdown_readline();
+
+       return Ok();
+
+} catch (const Error& er) {
+       /* note: user-level error, "OK" for mu */
+       report_error(er);
+       g_warning("server caught exception: %s", er.what());
+       return Ok();
+} catch (...) {
+       g_critical("server caught exception");
+       return Err(Error::Code::Internal, "caught exception");
+}
diff --git a/mu/mu-cmd.cc b/mu/mu-cmd.cc
new file mode 100644 (file)
index 0000000..a171389
--- /dev/null
@@ -0,0 +1,602 @@
+/*
+** Copyright (C) 2010-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <iostream>
+#include <iomanip>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "mu-config.hh"
+#include "mu-cmd.hh"
+#include "mu-maildir.hh"
+#include "mu-contacts-cache.hh"
+#include "mu-runtime.hh"
+#include "message/mu-message.hh"
+#include "message/mu-mime-object.hh"
+
+#include "utils/mu-util.h"
+
+#include "utils/mu-error.hh"
+#include "utils/mu-utils.hh"
+#include "message/mu-message.hh"
+
+#include <thirdparty/tabulate.hpp>
+
+#define VIEW_TERMINATOR '\f' /* form-feed */
+
+using namespace Mu;
+
+static Mu::Result<void>
+view_msg_sexp(const Message& message, const MuConfig* opts)
+{
+       ::fputs(message.to_sexp().to_sexp_string().c_str(), stdout);
+       ::fputs("\n", stdout);
+
+       return Ok();
+}
+
+
+static std::string /* return comma-sep'd list of attachments */
+get_attach_str(const Message& message, const MuConfig* opts)
+{
+       std::string str;
+       seq_for_each(message.parts(), [&](auto&& part) {
+               if (auto fname = part.raw_filename(); fname) {
+                       if (str.empty())
+                               str = fname.value();
+                       else
+                               str += ", " + fname.value();
+               }
+       });
+
+       return str;
+}
+
+#define color_maybe(C)                          \
+       do {                                    \
+               if (color)                      \
+                       fputs((C), stdout);     \
+       } while (0)
+
+static void
+print_field(const std::string& field, const std::string& val, bool color)
+{
+       if (val.empty())
+               return;
+
+       color_maybe(MU_COLOR_MAGENTA);
+       mu_util_fputs_encoded(field.c_str(), stdout);
+       color_maybe(MU_COLOR_DEFAULT);
+       fputs(": ", stdout);
+
+       color_maybe(MU_COLOR_GREEN);
+       mu_util_fputs_encoded(val.c_str(), stdout);
+
+       color_maybe(MU_COLOR_DEFAULT);
+       fputs("\n", stdout);
+}
+
+/* a summary_len of 0 mean 'don't show summary, show body */
+static void
+body_or_summary(const Message& message, const MuConfig* opts)
+{
+       gboolean    color;
+       //int         my_opts = mu_config_get_msg_options(opts) | MU_MSG_OPTION_CONSOLE_PASSWORD;
+
+       color = !opts->nocolor;
+
+       const auto body{message.body_text()};
+       if (!body || body->empty()) {
+               if (any_of(message.flags() & Flags::Encrypted)) {
+                       color_maybe(MU_COLOR_CYAN);
+                       g_print("[No text body found; "
+                               "message has encrypted parts]\n");
+               } else {
+                       color_maybe(MU_COLOR_MAGENTA);
+                       g_print("[No text body found]\n");
+               }
+               color_maybe(MU_COLOR_DEFAULT);
+               return;
+       }
+
+       if (opts->summary_len != 0) {
+               gchar* summ;
+               summ = mu_str_summarize(body->c_str(), opts->summary_len);
+               print_field("Summary", summ, color);
+               g_free(summ);
+       } else {
+               mu_util_print_encoded("%s", body->c_str());
+               if (!g_str_has_suffix(body->c_str(), "\n"))
+                       g_print("\n");
+       }
+}
+
+/* we ignore fields for now */
+/* summary_len == 0 means "no summary */
+static Mu::Result<void>
+view_msg_plain(const Message& message, const MuConfig* opts)
+{
+       const auto color{!opts->nocolor};
+
+       print_field("From",    to_string(message.from()), color);
+       print_field("To",      to_string(message.to()), color);
+       print_field("Cc",      to_string(message.cc()), color);
+       print_field("Bcc",     to_string(message.bcc()), color);
+       print_field("Subject", message.subject(), color);
+
+       if (auto&& date = message.date(); date != 0)
+               print_field("Date", time_to_string("%c", date), color);
+
+       print_field("Tags", join(message.tags(), ", "), color);
+
+       print_field("Attachments",get_attach_str(message, opts), color);
+       body_or_summary(message, opts);
+
+       return Ok();
+}
+
+static Mu::Result<void>
+handle_msg(const std::string& fname, const MuConfig* opts)
+{
+       auto message{Message::make_from_path(fname, mu_config_message_options(opts))};
+       if (!message)
+               return Err(message.error());
+
+       switch (opts->format) {
+       case MU_CONFIG_FORMAT_PLAIN:
+               return view_msg_plain(*message, opts);
+       case MU_CONFIG_FORMAT_SEXP:
+               return view_msg_sexp(*message, opts);
+       default:
+               g_critical("bug: should not be reached");
+               return Err(Error::Code::Internal, "error");
+       }
+}
+
+static Mu::Result<void>
+view_params_valid(const MuConfig* opts)
+{
+       /* note: params[0] will be 'view' */
+       if (!opts->params[0] || !opts->params[1])
+               return Err(Error::Code::InvalidArgument, "error in parameters");
+
+       switch (opts->format) {
+       case MU_CONFIG_FORMAT_PLAIN:
+       case MU_CONFIG_FORMAT_SEXP: break;
+       default:
+               return Err(Error::Code::InvalidArgument, "invalid output format");
+       }
+
+       return Ok();
+}
+
+static Mu::Result<void>
+cmd_view(const MuConfig* opts)
+{
+       if (!opts || opts->cmd != Mu::MU_CONFIG_CMD_VIEW)
+               return Err(Error::Code::InvalidArgument, "invalid parameters");
+       if (auto res = view_params_valid(opts); !res)
+               return res;
+
+       for (auto i = 1; opts->params[i]; ++i) {
+               if (auto res = handle_msg(opts->params[i], opts); !res)
+                       return res;
+               /* add a separator between two messages? */
+               if (opts->terminator)
+                       g_print("%c", VIEW_TERMINATOR);
+       }
+
+       return Ok();
+}
+
+static Mu::Result<void>
+cmd_mkdir(const MuConfig* opts)
+{
+       int i;
+
+       if (!opts->params[1])
+               return Err(Error::Code::InvalidArgument,
+                          "missing directory parameter");
+
+       for (i = 1; opts->params[i]; ++i) {
+               if (auto&& res =
+                   maildir_mkdir(opts->params[i], opts->dirmode, FALSE); !res)
+                       return res;
+       }
+
+       return Ok();
+}
+
+static Result<void>
+cmd_add(Mu::Store& store, const MuConfig* opts)
+{
+       /* note: params[0] will be 'add' */
+       if (!opts->params[0] || !opts->params[1])
+               return Err(Error::Code::InvalidArgument,
+                          "expected some files to add");
+
+       for (auto u = 1; opts->params[u]; ++u) {
+
+               const auto docid{store.add_message(opts->params[u])};
+               if (!docid)
+                       return Err(docid.error());
+               else
+                       g_debug("added message @ %s, docid=%u",
+                               opts->params[u], docid.value());
+       }
+
+       return Ok();
+}
+
+static Result<void>
+cmd_remove(Mu::Store& store, const MuConfig* opts)
+{
+       /* note: params[0] will be 'remove' */
+       if (!opts->params[0] || !opts->params[1])
+               return Err(Error::Code::InvalidArgument,
+                          "expected some files to remove");
+
+       for (auto u = 1; opts->params[u]; ++u) {
+
+               const auto res = store.remove_message(opts->params[u]);
+               if (!res)
+                       return Err(Error::Code::File, "failed to remove %s",
+                                  opts->params[u]);
+               else
+                       g_debug("removed message @ %s", opts->params[u]);
+       }
+
+       return Ok();
+}
+
+
+template <typename T>
+static void
+key_val(const Mu::MaybeAnsi& col, const std::string& key, T val)
+{
+       using Color = Mu::MaybeAnsi::Color;
+
+       std::cout << col.fg(Color::BrightBlue) << std::left << std::setw(18) << key << col.reset()
+                 << ": ";
+
+       std::cout << col.fg(Color::Green) << val << col.reset() << "\n";
+}
+
+
+static void
+print_signature(const Mu::MimeSignature& sig, const MuConfig *opts)
+{
+       Mu::MaybeAnsi col{!opts->nocolor};
+
+       const auto created{sig.created()};
+       key_val(col, "created",
+               created == 0 ? "unknown" :
+               time_to_string("%c", sig.created()).c_str());
+
+       const auto expires{sig.expires()};
+       key_val(col, "expires", expires==0 ? "never" :
+               time_to_string("%c", sig.expires()).c_str());
+
+       const auto cert{sig.certificate()};
+       key_val(col, "public-key algo",
+               to_string_view_opt(cert.pubkey_algo()).value_or("unknown"));
+       key_val(col, "digest algo",
+               to_string_view_opt(cert.digest_algo()).value_or("unknown"));
+       key_val(col, "id-validity",
+               to_string_view_opt(cert.id_validity()).value_or("unknown"));
+       key_val(col, "trust",
+               to_string_view_opt(cert.trust()).value_or("unknown"));
+       key_val(col, "issuer-serial", cert.issuer_serial().value_or("unknown"));
+       key_val(col, "issuer-name", cert.issuer_name().value_or("unknown"));
+       key_val(col, "finger-print", cert.fingerprint().value_or("unknown"));
+       key_val(col, "key-id", cert.key_id().value_or("unknown"));
+       key_val(col, "name", cert.name().value_or("unknown"));
+       key_val(col, "user-id", cert.user_id().value_or("unknown"));
+}
+
+
+static bool
+verify(const MimeMultipartSigned& sigpart, const MuConfig *opts)
+{
+       using VFlags = MimeMultipartSigned::VerifyFlags;
+       const auto vflags{opts->auto_retrieve ?
+               VFlags::EnableKeyserverLookups: VFlags::None};
+
+       auto ctx{MimeCryptoContext::make_gpg()};
+       if (!ctx)
+               return false;
+
+       const auto sigs{sigpart.verify(*ctx, vflags)};
+       Mu::MaybeAnsi col{!opts->nocolor};
+
+       if (!sigs || sigs->empty()) {
+
+               if (!opts->quiet)
+                       g_print("cannot find signatures in part\n");
+
+               return true;
+       }
+
+       bool valid{true};
+       for (auto&& sig: *sigs) {
+
+               const auto status{sig.status()};
+
+               if (!opts->quiet)
+                       key_val(col, "status", to_string(status));
+
+               if (opts->verbose)
+                       print_signature(sig, opts);
+
+               if (none_of(sig.status() & MimeSignature::Status::Green))
+                       valid = false;
+       }
+
+       return valid;
+}
+
+static Mu::Result<void>
+cmd_verify(const MuConfig* opts)
+{
+       if (!opts || opts->cmd != MU_CONFIG_CMD_VERIFY)
+               return Err(Error::Code::Internal, "error in parameters");
+
+       if (!opts->params[1])
+               return Err(Error::Code::InvalidArgument,
+                          "missing message-file parameter");
+
+       auto message{Message::make_from_path(opts->params[1],
+                                            mu_config_message_options(opts))};
+       if (!message)
+               return Err(message.error());
+
+
+       if (none_of(message->flags() & Flags::Signed)) {
+               if (!opts->quiet)
+                       g_print("no signed parts found\n");
+               return Ok();
+       }
+
+       bool verified{true}; /* innocent until proven guilty */
+       for(auto&& part: message->parts()) {
+
+               if (!part.is_signed())
+                       continue;
+
+               const auto& mobj{part.mime_object()};
+               if (!mobj.is_multipart_signed())
+                       continue;
+
+               if (!verify(MimeMultipartSigned(mobj), opts))
+                       verified = false;
+       }
+
+       if (verified)
+               return Ok();
+       else
+               return Err(Error::Code::UnverifiedSignature,
+                          "failed to verify one or more signatures");
+}
+
+static Result<void>
+cmd_info(const Mu::Store& store, const MuConfig* opts)
+{
+       using namespace tabulate;
+
+       if (!locale_workaround())
+               return Err(Error::Code::User, "failed to find a working locale");
+
+       auto colorify = [](Table& table) {
+               for (auto&& row: table) {
+
+                       if (row.cells().size() < 2)
+                               continue;
+
+                       row.cells().at(0)->format().font_style({FontStyle::bold})
+                               .font_color(Color::green);
+                       row.cells().at(1)->format().font_color(Color::blue);
+               }
+       };
+
+       auto tstamp = [](::time_t t)->std::string {
+               if (t == 0)
+                       return "never";
+               else
+                       return time_to_string("%c", t);
+
+       };
+
+       Table info;
+       info.add_row({"maildir", store.properties().root_maildir});
+       info.add_row({"database-path", store.properties().database_path});
+       info.add_row({"schema-version", store.properties().schema_version});
+       info.add_row({"max-message-size", format("%zu", store.properties().max_message_size)});
+       info.add_row({"batch-size", format("%zu", store.properties().batch_size)});
+       info.add_row({"created", tstamp(store.properties().created)});
+       for (auto&& c : store.properties().personal_addresses)
+                       info.add_row({"personal-address", c});
+
+       info.add_row({"messages in store", format("%zu", store.size())});
+       info.add_row({"last-change", tstamp(store.statistics().last_change)});
+       info.add_row({"last-index",  tstamp(store.statistics().last_index)});
+
+       if (!opts->nocolor)
+               colorify(info);
+
+       std::cout << info << '\n';
+
+       return Ok();
+}
+
+static Result<void>
+cmd_init(const MuConfig* opts)
+{
+       /* not provided, nor could we find a good default */
+       if (!opts->maildir)
+               return Err(Error::Code::InvalidArgument,
+                          "missing --maildir parameter and could "
+                          "not determine default");
+
+       if (opts->max_msg_size < 0)
+               return Err(Error::Code::InvalidArgument,
+                          "invalid value for max-message-size");
+       else if (opts->batch_size < 0)
+               return Err(Error::Code::InvalidArgument,
+                          "invalid value for batch-size");
+
+       Mu::Store::Config conf{};
+       conf.max_message_size = opts->max_msg_size;
+       conf.batch_size       = opts->batch_size;
+
+       Mu::StringVec my_addrs;
+       auto          addrs = opts->my_addresses;
+       while (addrs && *addrs) {
+               my_addrs.emplace_back(*addrs);
+               ++addrs;
+       }
+
+       auto store = Store::make_new(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB),
+                                    opts->maildir, my_addrs, conf);
+       if (!store)
+               return Err(store.error());
+
+       if (!opts->quiet) {
+               cmd_info(*store, opts);
+               std::cout << "\nstore created; use the 'index' command to fill/update it.\n";
+       }
+
+       return Ok();
+}
+
+static Result<void>
+cmd_find(const MuConfig* opts)
+{
+       auto store{Store::make(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB))};
+       if (!store)
+               return Err(store.error());
+       else
+               return mu_cmd_find(*store, opts);
+}
+
+static void
+show_usage(void)
+{
+       g_print("usage: mu command [options] [parameters]\n");
+       g_print("where command is one of index, find, cfind, view, mkdir, "
+               "extract, add, remove, script, verify or server\n");
+       g_print("see the mu, mu-<command> or mu-easy manpages for "
+               "more information\n");
+}
+
+
+using ReadOnlyStoreFunc = std::function<Result<void>(const Store&, const MuConfig*)>;
+using WritableStoreFunc = std::function<Result<void>(Store&, const MuConfig*)>;
+
+static Result<void>
+with_readonly_store(const ReadOnlyStoreFunc& func, const MuConfig* opts)
+{
+       auto store{Store::make(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB))};
+       if (!store)
+               return Err(store.error());
+
+       return func(store.value(), opts);
+}
+
+static Result<void>
+with_writable_store(const WritableStoreFunc func, const MuConfig* opts)
+{
+       auto store{Store::make(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB),
+                              Store::Options::Writable)};
+       if (!store)
+               return Err(store.error());
+
+       return func(store.value(), opts);
+}
+
+Result<void>
+Mu::mu_cmd_execute(const MuConfig* opts) try {
+
+       if (!opts || !opts->params || !opts->params[0])
+               return Err(Error::Code::InvalidArgument, "error in parameters");
+
+       switch (opts->cmd) {
+       case MU_CONFIG_CMD_HELP: /* already handled in mu-config.c */
+               return Ok();
+
+       /*
+        * no store needed
+        */
+       case MU_CONFIG_CMD_FIELDS:
+               return mu_cmd_fields(opts);
+       case MU_CONFIG_CMD_MKDIR:
+               return cmd_mkdir(opts);
+       case MU_CONFIG_CMD_SCRIPT:
+               return mu_cmd_script(opts);
+       case MU_CONFIG_CMD_VIEW:
+               return cmd_view(opts);
+       case MU_CONFIG_CMD_VERIFY:
+               return cmd_verify(opts);
+       case MU_CONFIG_CMD_EXTRACT:
+               return mu_cmd_extract(opts);
+       /*
+        * read-only store
+        */
+
+       case MU_CONFIG_CMD_CFIND:
+               return with_readonly_store(mu_cmd_cfind, opts);
+       case MU_CONFIG_CMD_FIND:
+               return cmd_find(opts);
+       case MU_CONFIG_CMD_INFO:
+               return with_readonly_store(cmd_info, opts);
+
+       /* writable store */
+
+       case MU_CONFIG_CMD_ADD:
+               return with_writable_store(cmd_add, opts);
+       case MU_CONFIG_CMD_REMOVE:
+               return with_writable_store(cmd_remove, opts);
+       case MU_CONFIG_CMD_INDEX:
+               return with_writable_store(mu_cmd_index, opts);
+
+       /* commands instantiate store themselves */
+       case MU_CONFIG_CMD_INIT:
+               return cmd_init(opts);
+       case MU_CONFIG_CMD_SERVER:
+               return mu_cmd_server(opts);
+
+       default:
+               show_usage();
+               return Ok();
+       }
+
+} catch (const Mu::Error& er) {
+       return Err(er);
+} catch (const std::runtime_error& re) {
+       return Err(Error::Code::Internal, "runtime-error: %s", re.what());
+} catch (const std::exception& ex) {
+       return Err(Error::Code::Internal, "error: %s", ex.what());
+} catch (...) {
+       return Err(Error::Code::Internal, "caught exception");
+}
diff --git a/mu/mu-cmd.hh b/mu/mu-cmd.hh
new file mode 100644 (file)
index 0000000..12b2ddf
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+** Copyright (C) 2008-2022-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_CMD_HH__
+#define MU_CMD_HH__
+
+#include <glib.h>
+#include <mu-config.hh>
+#include <mu-store.hh>
+#include <utils/mu-result.hh>
+
+namespace Mu {
+/**
+ * execute the 'find' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_find(const Mu::Store& store, const MuConfig* opts);
+
+/**
+ * execute the 'extract' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_extract(const MuConfig* opts);
+
+/**
+ * execute the 'fields' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_fields(const MuConfig* opts);
+
+/**
+ * execute the 'script' command
+ *
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_script(const MuConfig* opts);
+
+/**
+ * execute the cfind command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_cfind(const Mu::Store& store, const MuConfig* opts);
+
+/**
+ * execute the 'index' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_index(Mu::Store& store, const MuConfig* opt);
+
+/**
+ * execute the server command
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return MU_OK (0) if the command succeeds, some error code otherwise
+ */
+Result<void> mu_cmd_server(const MuConfig* opts);
+
+/**
+ * execute some mu command, based on 'opts'
+ *
+ * @param opts configuration option
+ * @param err receives error information, or NULL
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_execute(const MuConfig* opts);
+
+} // namespace Mu
+
+#endif /*__MU_CMD_H__*/
diff --git a/mu/mu-config.cc b/mu/mu-config.cc
new file mode 100644 (file)
index 0000000..2dcb3a3
--- /dev/null
@@ -0,0 +1,738 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <string.h> /* memset */
+#include <unistd.h>
+#include <stdio.h>
+
+#include "mu-config.hh"
+#include "mu-cmd.hh"
+
+using namespace Mu;
+
+static MuConfig MU_CONFIG;
+
+#define color_maybe(C) (MU_CONFIG.nocolor ? "" : (C))
+
+static MuConfigFormat
+get_output_format(const char* formatstr)
+{
+       int i;
+       struct {
+               const char*    name;
+               MuConfigFormat format;
+       } formats[] = {{"mutt-alias", MU_CONFIG_FORMAT_MUTT_ALIAS},
+                      {"mutt-ab", MU_CONFIG_FORMAT_MUTT_AB},
+                      {"wl", MU_CONFIG_FORMAT_WL},
+                      {"csv", MU_CONFIG_FORMAT_CSV},
+                      {"org-contact", MU_CONFIG_FORMAT_ORG_CONTACT},
+                      {"bbdb", MU_CONFIG_FORMAT_BBDB},
+                      {"links", MU_CONFIG_FORMAT_LINKS},
+                      {"plain", MU_CONFIG_FORMAT_PLAIN},
+                      {"sexp", MU_CONFIG_FORMAT_SEXP},
+                      {"json", MU_CONFIG_FORMAT_JSON},
+                      {"xml", MU_CONFIG_FORMAT_XML},
+                      {"xquery", MU_CONFIG_FORMAT_XQUERY},
+                      {"mquery", MU_CONFIG_FORMAT_MQUERY},
+                      {"debug", MU_CONFIG_FORMAT_DEBUG}};
+
+       for (i = 0; i != G_N_ELEMENTS(formats); i++)
+               if (strcmp(formats[i].name, formatstr) == 0)
+                       return formats[i].format;
+
+       return MU_CONFIG_FORMAT_UNKNOWN;
+}
+
+#define expand_dir(D)                                                                    \
+       if ((D)) {                                                                       \
+               char* exp;                                                               \
+               exp = mu_util_dir_expand((D));                                           \
+               if (exp) {                                                               \
+                       g_free((D));                                                     \
+                       (D) = exp;                                                       \
+               }                                                                        \
+       }
+
+static void
+set_group_mu_defaults()
+{
+       /* try to determine muhome from command-line or environment;
+        * note: if not specified, we use XDG defaults */
+
+       if (!MU_CONFIG.muhome) {
+               /* if not set explicity, try the environment */
+               const char* muhome;
+               muhome = g_getenv("MUHOME");
+               if (muhome)
+                       MU_CONFIG.muhome = g_strdup(muhome);
+       }
+
+       if (MU_CONFIG.muhome)
+               expand_dir(MU_CONFIG.muhome);
+
+       /* check for the MU_NOCOLOR or NO_COLOR env vars; but in any case don't
+        * use colors unless we're writing to a tty */
+       if (g_getenv(MU_NOCOLOR) != NULL || g_getenv("NO_COLOR") != NULL)
+               MU_CONFIG.nocolor = TRUE;
+
+       if (!isatty(fileno(stdout)) || !isatty(fileno(stderr)))
+               MU_CONFIG.nocolor = TRUE;
+}
+
+static GOptionGroup*
+config_options_group_mu()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {
+           {"debug", 'd', 0, G_OPTION_ARG_NONE, &MU_CONFIG.debug,
+            "print debug output to standard error (false)", NULL},
+           {"quiet", 'q', 0, G_OPTION_ARG_NONE, &MU_CONFIG.quiet,
+            "don't give any progress information (false)", NULL},
+           {"version", 'V', 0, G_OPTION_ARG_NONE, &MU_CONFIG.version,
+            "display version and copyright information (false)", NULL},
+           {"muhome", 0, 0, G_OPTION_ARG_FILENAME, &MU_CONFIG.muhome,
+            "specify an alternative mu directory", "<dir>"},
+           {"log-stderr", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.log_stderr,
+            "log to standard error (false)", NULL},
+           {"nocolor", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.nocolor,
+            "don't use ANSI-colors in output (false)", NULL},
+           {"verbose", 'v', 0, G_OPTION_ARG_NONE, &MU_CONFIG.verbose,
+            "verbose output (false)", NULL},
+
+           {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &MU_CONFIG.params,
+            "parameters", NULL},
+           {NULL, 0, 0, (GOptionArg)0, NULL, NULL, NULL}};
+
+       og = g_option_group_new("mu", "general mu options", "", NULL, NULL);
+       g_option_group_add_entries(og, entries);
+
+       return og;
+}
+
+static void
+set_group_init_defaults()
+{
+       if (!MU_CONFIG.maildir)
+               MU_CONFIG.maildir = mu_util_guess_maildir();
+
+       expand_dir(MU_CONFIG.maildir);
+}
+
+static GOptionGroup*
+config_options_group_init()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {
+           {"maildir", 'm', 0, G_OPTION_ARG_FILENAME, &MU_CONFIG.maildir,
+            "top of the maildir", "<maildir>"},
+           {"my-address", 0, 0, G_OPTION_ARG_STRING_ARRAY, &MU_CONFIG.my_addresses,
+            "my e-mail address; can be used multiple times", "<address>"},
+           {"max-message-size", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.max_msg_size,
+            "Maximum allowed size for messages", "<size-in-bytes>"},
+           {"batch-size", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.batch_size,
+            "Number of changes in a database transaction batch", "<number>"},
+           {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+       og = g_option_group_new("init", "Options for the 'init' command", "", NULL, NULL);
+       g_option_group_add_entries(og, entries);
+
+       return og;
+}
+
+static gboolean
+index_post_parse_func(GOptionContext* context, GOptionGroup* group, gpointer data,
+                     GError** error)
+{
+       if (!MU_CONFIG.maildir && !MU_CONFIG.my_addresses)
+               return TRUE;
+
+       g_printerr("%sNOTE%s: as of mu 1.3.8, 'mu index' no longer uses the\n"
+                  "--maildir/-m or --my-address options.\n\n",
+                  color_maybe(MU_COLOR_RED), color_maybe(MU_COLOR_DEFAULT));
+       g_printerr("Instead, these options should be passed to 'mu init'.\n");
+       g_printerr(
+           "See the mu-init(1) or the mu4e reference manual,\n'Initializing the message "
+           "store' for details.\n\n");
+
+       return TRUE;
+}
+
+static GOptionGroup*
+config_options_group_index()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {
+           /* only here so we can tell users they are deprecated */
+           {"maildir", 'm', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_FILENAME,
+            &MU_CONFIG.maildir, "top of the maildir", "<maildir>"},
+           {"my-address", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING_ARRAY,
+            &MU_CONFIG.my_addresses, "my e-mail address; can be used multiple times",
+            "<address>"},
+
+           {"lazy-check", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.lazycheck,
+            "only check dir-timestamps (false)", NULL},
+           {"nocleanup", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.nocleanup,
+            "don't clean up the database after indexing (false)", NULL},
+           {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+       og = g_option_group_new("index", "Options for the 'index' command", "", NULL,
+                               NULL);
+       g_option_group_add_entries(og, entries);
+       g_option_group_set_parse_hooks(og, NULL, (GOptionParseFunc)index_post_parse_func);
+
+       return og;
+}
+
+static void
+set_group_find_defaults()
+{
+       /* note, when no fields are specified, we use date-from-subject */
+       if (!MU_CONFIG.fields || !*MU_CONFIG.fields) {
+               MU_CONFIG.fields = g_strdup("d f s");
+               if (!MU_CONFIG.sortfield) {
+                       MU_CONFIG.sortfield = g_strdup("d");
+               }
+       }
+
+       if (!MU_CONFIG.formatstr) /* by default, use plain output */
+               MU_CONFIG.format = MU_CONFIG_FORMAT_PLAIN;
+       else
+               MU_CONFIG.format = get_output_format(MU_CONFIG.formatstr);
+
+       expand_dir(MU_CONFIG.linksdir);
+}
+
+static GOptionGroup*
+config_options_group_find()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {
+           {"fields", 'f', 0, G_OPTION_ARG_STRING, &MU_CONFIG.fields,
+            "fields to display in the output", "<fields>"},
+           {"sortfield", 's', 0, G_OPTION_ARG_STRING, &MU_CONFIG.sortfield,
+            "field to sort on", "<field>"},
+           {"maxnum", 'n', 0, G_OPTION_ARG_INT, &MU_CONFIG.maxnum,
+            "number of entries to display in the output", "<number>"},
+           {"threads", 't', 0, G_OPTION_ARG_NONE, &MU_CONFIG.threads,
+            "show message threads", NULL},
+           {"bookmark", 'b', 0, G_OPTION_ARG_STRING, &MU_CONFIG.bookmark,
+            "use a bookmarked query", "<bookmark>"},
+           {"reverse", 'z', 0, G_OPTION_ARG_NONE, &MU_CONFIG.reverse,
+            "sort in reverse (descending) order (z -> a)", NULL},
+           {"skip-dups", 'u', 0, G_OPTION_ARG_NONE, &MU_CONFIG.skip_dups,
+            "show only the first of messages duplicates (false)", NULL},
+           {"include-related", 'r', 0, G_OPTION_ARG_NONE, &MU_CONFIG.include_related,
+            "include related messages in results (false)", NULL},
+           {"linksdir", 0, 0, G_OPTION_ARG_STRING, &MU_CONFIG.linksdir,
+            "output as symbolic links to a target maildir", "<dir>"},
+           {"clearlinks", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.clearlinks,
+            "clear old links before filling a linksdir (false)", NULL},
+           {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr,
+            "output format ('plain'(*), 'links', 'xml',"
+             "'sexp', 'xquery')",
+            "<format>"},
+           {"summary-len", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.summary_len,
+            "use up to <n> lines for the summary, or 0 for none (0)", "<len>"},
+           {"exec", 'e', 0, G_OPTION_ARG_STRING, &MU_CONFIG.exec,
+            "execute command on each match message", "<command>"},
+           {"after", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.after,
+            "only show messages whose m_time > T (t_time)", "<timestamp>"},
+           {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+       og = g_option_group_new("find", "Options for the 'find' command", "", NULL, NULL);
+       g_option_group_add_entries(og, entries);
+
+       return og;
+}
+
+static GOptionGroup*
+config_options_group_mkdir()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {{"mode", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.dirmode,
+                                  "set the mode (as in chmod), in octal notation",
+                                  "<mode>"},
+                                 {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+       /* set dirmode before, because '0000' is a valid mode */
+       MU_CONFIG.dirmode = 0755;
+
+       og = g_option_group_new("mkdir", "Options for the 'mkdir' command", "", NULL,
+                               NULL);
+       g_option_group_add_entries(og, entries);
+
+       return og;
+}
+
+static void
+set_group_cfind_defaults()
+{
+       if (!MU_CONFIG.formatstr) /* by default, use plain output */
+               MU_CONFIG.format = MU_CONFIG_FORMAT_PLAIN;
+       else
+               MU_CONFIG.format = get_output_format(MU_CONFIG.formatstr);
+}
+
+static GOptionGroup*
+config_options_group_cfind()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {
+           {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr,
+            "output format (plain(*), mutt-alias, mutt-ab, wl, "
+             "org-contact, bbdb, csv)",
+            "<format>"},
+           {"personal", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.personal,
+            "whether to only get 'personal' contacts", NULL},
+           {"after", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.after,
+            "only get addresses last seen after T", "<timestamp>"},
+           {"maxnum", 'n', 0, G_OPTION_ARG_INT, &MU_CONFIG.maxnum,
+            "maximum number of contacts", "<number>"},
+           {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+       og = g_option_group_new("cfind", "Options for the 'cfind' command", "", NULL,
+                               NULL);
+       g_option_group_add_entries(og, entries);
+
+       return og;
+}
+
+static GOptionGroup*
+config_options_group_script()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY,
+                                  &MU_CONFIG.params, "script parameters", NULL},
+                                 {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+       og = g_option_group_new("script", "Options for the 'script' command", "", NULL,
+                               NULL);
+
+       g_option_group_add_entries(og, entries);
+
+       return og;
+}
+
+static void
+set_group_view_defaults()
+{
+       if (!MU_CONFIG.formatstr) /* by default, use plain output */
+               MU_CONFIG.format = MU_CONFIG_FORMAT_PLAIN;
+       else
+               MU_CONFIG.format = get_output_format(MU_CONFIG.formatstr);
+}
+
+/* crypto options are used in a few different commands */
+static GOptionEntry*
+crypto_option_entries()
+{
+       static GOptionEntry entries[] = {
+           {"auto-retrieve", 'r', 0, G_OPTION_ARG_NONE, &MU_CONFIG.auto_retrieve,
+            "attempt to retrieve keys online (false)", NULL},
+           {"decrypt", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.decrypt,
+            "attempt to decrypt the message", NULL},
+           {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+       return entries;
+}
+
+static GOptionGroup*
+config_options_group_view()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {
+           {"summary-len", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.summary_len,
+            "use up to <n> lines for the summary, or 0 for none (0)", "<len>"},
+           {"terminate", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.terminator,
+            "terminate messages with ascii-0x07 (\\f, form-feed)", NULL},
+           {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr,
+            "output format ('plain'(*), 'sexp')", "<format>"},
+           {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+       og = g_option_group_new("view", "Options for the 'view' command", "", NULL, NULL);
+
+       g_option_group_add_entries(og, entries);
+       g_option_group_add_entries(og, crypto_option_entries());
+
+       return og;
+}
+
+static void
+set_group_extract_defaults()
+{
+       if (!MU_CONFIG.targetdir)
+               MU_CONFIG.targetdir = g_strdup(".");
+
+       expand_dir(MU_CONFIG.targetdir);
+}
+
+static GOptionGroup*
+config_options_group_extract()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {
+           {"save-attachments", 'a', 0, G_OPTION_ARG_NONE, &MU_CONFIG.save_attachments,
+            "save all attachments (false)", NULL},
+           {"save-all", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.save_all,
+            "save all parts (incl. non-attachments) (false)", NULL},
+           {"parts", 0, 0, G_OPTION_ARG_STRING, &MU_CONFIG.parts,
+            "save specific parts (comma-separated list)", "<parts>"},
+           {"target-dir", 0, 0, G_OPTION_ARG_FILENAME, &MU_CONFIG.targetdir,
+            "target directory for saving", "<dir>"},
+           {"overwrite", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.overwrite,
+            "overwrite existing files (false)", NULL},
+           {"play", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.play,
+            "try to 'play' (open) the extracted parts", NULL},
+           {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+       og = g_option_group_new("extract", "Options for the 'extract' command", "", NULL,
+                               NULL);
+       g_option_group_add_entries(og, entries);
+       g_option_group_add_entries(og, crypto_option_entries());
+
+       return og;
+}
+
+static GOptionGroup*
+config_options_group_verify()
+{
+       GOptionGroup* og;
+       og = g_option_group_new("verify", "Options for the 'verify' command", "", NULL,
+                               NULL);
+       g_option_group_add_entries(og, crypto_option_entries());
+
+       return og;
+}
+
+static GOptionGroup*
+config_options_group_server()
+{
+       GOptionGroup* og;
+       GOptionEntry  entries[] = {
+           {"commands", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.commands,
+            "list the available command and their parameters, then exit", NULL},
+           {"eval", 'e', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &MU_CONFIG.eval,
+            "expression to evaluate", "<expr>"},
+           {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}};
+
+       og = g_option_group_new("server", "Options for the 'server' command", "", NULL,
+                               NULL);
+       g_option_group_add_entries(og, entries);
+
+       return og;
+}
+
+static MuConfigCmd
+cmd_from_string(const char* str)
+{
+       int i;
+       struct {
+               const gchar* name;
+               MuConfigCmd  cmd;
+       } cmd_map[] = {
+           {"add", MU_CONFIG_CMD_ADD},         {"cfind", MU_CONFIG_CMD_CFIND},
+           {"extract", MU_CONFIG_CMD_EXTRACT}, {"find", MU_CONFIG_CMD_FIND},
+           {"help", MU_CONFIG_CMD_HELP},       {"index", MU_CONFIG_CMD_INDEX},
+           {"info", MU_CONFIG_CMD_INFO},       {"init", MU_CONFIG_CMD_INIT},
+           {"mkdir", MU_CONFIG_CMD_MKDIR},     {"remove", MU_CONFIG_CMD_REMOVE},
+           {"script", MU_CONFIG_CMD_SCRIPT},   {"server", MU_CONFIG_CMD_SERVER},
+           {"verify", MU_CONFIG_CMD_VERIFY},   {"view", MU_CONFIG_CMD_VIEW},
+           {"fields", MU_CONFIG_CMD_FIELDS},
+       };
+
+       if (!str)
+               return MU_CONFIG_CMD_UNKNOWN;
+
+       for (i = 0; i != G_N_ELEMENTS(cmd_map); ++i)
+               if (strcmp(str, cmd_map[i].name) == 0)
+                       return cmd_map[i].cmd;
+#ifdef BUILD_GUILE
+       /* if we don't recognize it and it's not an option, it may be
+        * some script */
+       if (str[0] != '-')
+               return MU_CONFIG_CMD_SCRIPT;
+#endif /*BUILD_GUILE*/
+
+       return MU_CONFIG_CMD_UNKNOWN;
+}
+
+static gboolean
+parse_cmd(int* argcp, char*** argvp, GError** err)
+{
+       MU_CONFIG.cmd    = MU_CONFIG_CMD_NONE;
+       MU_CONFIG.cmdstr = NULL;
+
+       if (*argcp < 2) /* no command found at all */
+               return TRUE;
+       else if ((**argvp)[1] == '-')
+               /* if the first param starts with '-', there is no
+                * command, just some option (like --version, --help
+                * etc.)*/
+               return TRUE;
+
+       MU_CONFIG.cmdstr = g_strdup((*argvp)[1]);
+       MU_CONFIG.cmd    = cmd_from_string(MU_CONFIG.cmdstr);
+
+#ifndef BUILD_GUILE
+       if (MU_CONFIG.cmd == MU_CONFIG_CMD_SCRIPT) {
+               mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS,
+                                   "command 'script' not supported");
+               return FALSE;
+       }
+#endif /*!BUILD_GUILE*/
+
+       if (MU_CONFIG.cmdstr && MU_CONFIG.cmdstr[0] != '-' &&
+           MU_CONFIG.cmd == MU_CONFIG_CMD_UNKNOWN) {
+               mu_util_g_set_error(err, MU_ERROR_IN_PARAMETERS, "unknown command '%s'",
+                                   MU_CONFIG.cmdstr);
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static GOptionGroup*
+get_option_group(MuConfigCmd cmd)
+{
+       switch (cmd) {
+       case MU_CONFIG_CMD_CFIND: return config_options_group_cfind();
+       case MU_CONFIG_CMD_EXTRACT: return config_options_group_extract();
+       case MU_CONFIG_CMD_FIND: return config_options_group_find();
+       case MU_CONFIG_CMD_INDEX: return config_options_group_index();
+       case MU_CONFIG_CMD_INIT: return config_options_group_init();
+       case MU_CONFIG_CMD_MKDIR: return config_options_group_mkdir();
+       case MU_CONFIG_CMD_SERVER: return config_options_group_server();
+       case MU_CONFIG_CMD_SCRIPT: return config_options_group_script();
+       case MU_CONFIG_CMD_VERIFY: return config_options_group_verify();
+       case MU_CONFIG_CMD_VIEW: return config_options_group_view();
+       default: return NULL; /* no group to add */
+       }
+}
+
+/* ugh yuck massaging the GOption text output; glib prepares some text
+ * which has a 'Usage:' for the 'help' command. However, we need the
+ * help for the command we're asking help for. So, we remove the Usage:
+ * from what glib generates. :-( */
+static gchar*
+massage_help(const char* help)
+{
+       GRegex* rx;
+       char*   str;
+
+       rx  = g_regex_new("^Usage:.*\n.*\n", (GRegexCompileFlags)0,
+                         G_REGEX_MATCH_NEWLINE_ANY, NULL);
+       str = g_regex_replace(rx, help, -1, 0, "", G_REGEX_MATCH_NEWLINE_ANY, NULL);
+       g_regex_unref(rx);
+       return str;
+}
+
+static const char*
+get_help_string(MuConfigCmd cmd, bool long_help)
+{
+       struct Help {
+               MuConfigCmd cmd;
+               const char* usage;
+               const char* long_help;
+       };
+       constexpr std::array all_help = {
+#include "mu-help-strings.inc"
+       };
+
+       const auto help_it = std::find_if(all_help.begin(), all_help.end(),
+                                         [&](auto&& info) { return info.cmd == cmd; });
+       if (help_it == all_help.end()) {
+               g_critical("cannot find info for %u", cmd);
+               return "";
+       } else
+               return long_help ? help_it->long_help : help_it->usage;
+}
+
+void
+Mu::mu_config_show_help(MuConfigCmd cmd)
+{
+       GOptionContext* ctx;
+       GOptionGroup*   group;
+       char *          help, *cleanhelp;
+
+       g_return_if_fail(mu_config_cmd_is_valid(cmd));
+
+       ctx = g_option_context_new("- mu help");
+       g_option_context_set_main_group(ctx, config_options_group_mu());
+
+       group = get_option_group(cmd);
+       if (group)
+               g_option_context_add_group(ctx, group);
+
+       g_option_context_set_description(ctx, get_help_string(cmd, TRUE));
+       help      = g_option_context_get_help(ctx, TRUE, group);
+       cleanhelp = massage_help(help);
+
+       g_print("usage:\n\t%s%s", get_help_string(cmd, FALSE), cleanhelp);
+
+       g_free(help);
+       g_free(cleanhelp);
+       g_option_context_free(ctx);
+}
+
+static gboolean
+cmd_help()
+{
+       MuConfigCmd cmd;
+
+       if (!MU_CONFIG.params)
+               cmd = MU_CONFIG_CMD_UNKNOWN;
+       else
+               cmd = cmd_from_string(MU_CONFIG.params[1]);
+
+       if (cmd == MU_CONFIG_CMD_UNKNOWN) {
+               mu_config_show_help(MU_CONFIG_CMD_HELP);
+               return TRUE;
+       }
+
+       mu_config_show_help(cmd);
+
+       return TRUE;
+}
+
+static gboolean
+parse_params(int* argcp, char*** argvp, GError** err)
+{
+       GOptionContext* context;
+       GOptionGroup*   group;
+       gboolean        rv;
+
+       context = g_option_context_new("- mu general options");
+
+       g_option_context_set_help_enabled(context, FALSE);
+       g_option_context_set_main_group(context, config_options_group_mu());
+       g_option_context_set_ignore_unknown_options(context, FALSE);
+
+       switch (MU_CONFIG.cmd) {
+       case MU_CONFIG_CMD_NONE:
+       case MU_CONFIG_CMD_HELP:
+               /* 'help' is special; sucks in the options of the
+                * command after it */
+               rv = g_option_context_parse(context, argcp, argvp, err) && cmd_help();
+               break;
+       case MU_CONFIG_CMD_SCRIPT:
+               /* all unknown commands are passed to 'script' */
+               g_option_context_set_ignore_unknown_options(context, TRUE);
+               group = get_option_group(MU_CONFIG.cmd);
+               g_option_context_add_group(context, group);
+               rv               = g_option_context_parse(context, argcp, argvp, err);
+               MU_CONFIG.script = g_strdup(MU_CONFIG.cmdstr);
+               /* argvp contains the script parameters */
+               MU_CONFIG.script_params = (const char**)&((*argvp)[1]);
+               break;
+
+       default:
+               group = get_option_group(MU_CONFIG.cmd);
+               if (group)
+                       g_option_context_add_group(context, group);
+
+               rv = g_option_context_parse(context, argcp, argvp, err);
+               break;
+       }
+
+       g_option_context_free(context);
+
+       return rv ? TRUE : FALSE;
+}
+
+MuConfig*
+Mu::mu_config_init(int* argcp, char*** argvp, GError** err)
+{
+       g_return_val_if_fail(argcp && argvp, NULL);
+
+       memset(&MU_CONFIG, 0, sizeof(MU_CONFIG));
+
+       if (!parse_cmd(argcp, argvp, err))
+               goto errexit;
+
+       if (!parse_params(argcp, argvp, err))
+               goto errexit;
+
+       /* fill in the defaults if user did not specify */
+       set_group_mu_defaults();
+       set_group_init_defaults();
+       set_group_find_defaults();
+       set_group_cfind_defaults();
+       set_group_view_defaults();
+       set_group_extract_defaults();
+       /* set_group_mkdir_defaults (config); */
+
+       return &MU_CONFIG;
+
+errexit:
+       mu_config_uninit(&MU_CONFIG);
+       return NULL;
+}
+
+void
+Mu::mu_config_uninit(MuConfig* opts)
+{
+       if (!opts)
+               return;
+
+       g_free(opts->cmdstr);
+       g_free(opts->muhome);
+       g_free(opts->maildir);
+       g_free(opts->fields);
+       g_free(opts->sortfield);
+       g_free(opts->bookmark);
+       g_free(opts->formatstr);
+       g_free(opts->exec);
+       g_free(opts->linksdir);
+       g_free(opts->targetdir);
+       g_free(opts->parts);
+       g_free(opts->script);
+       g_free(opts->eval);
+
+       g_strfreev(opts->my_addresses);
+       g_strfreev(opts->params);
+
+       memset(opts, 0, sizeof(MU_CONFIG));
+}
+
+size_t
+Mu::mu_config_param_num(const MuConfig* opts)
+{
+       size_t n;
+
+       g_return_val_if_fail(opts && opts->params, 0);
+       for (n = 0; opts->params[n]; ++n)
+               ;
+
+       return n;
+}
+
+Message::Options
+Mu::mu_config_message_options(const MuConfig *conf)
+{
+       Message::Options opts{};
+
+       if (conf->decrypt)
+               opts |= Message::Options::Decrypt;
+       if (conf->auto_retrieve)
+               opts |= Message::Options::RetrieveKeys;
+
+       return opts;
+}
diff --git a/mu/mu-config.hh b/mu/mu-config.hh
new file mode 100644 (file)
index 0000000..9b07d1e
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_CONFIG_HH__
+#define MU_CONFIG_HH__
+
+#include <glib.h>
+#include <sys/types.h> /* for mode_t */
+#include <message/mu-message.hh>
+#include <utils/mu-util.h>
+
+namespace Mu {
+
+/* env var; if non-empty, color are disabled */
+#define MU_NOCOLOR "MU_NOCOLOR"
+
+typedef enum {
+       MU_CONFIG_FORMAT_UNKNOWN = 0,
+
+       /* for cfind, find, view */
+       MU_CONFIG_FORMAT_PLAIN, /* plain output */
+
+       /* for cfind */
+       MU_CONFIG_FORMAT_MUTT_ALIAS,  /* mutt alias style */
+       MU_CONFIG_FORMAT_MUTT_AB,     /* mutt ext abook */
+       MU_CONFIG_FORMAT_WL,          /* Wanderlust abook */
+       MU_CONFIG_FORMAT_CSV,         /* comma-sep'd values */
+       MU_CONFIG_FORMAT_ORG_CONTACT, /* org-contact */
+       MU_CONFIG_FORMAT_BBDB,        /* BBDB */
+       MU_CONFIG_FORMAT_DEBUG,
+
+       /* for find, view */
+       MU_CONFIG_FORMAT_SEXP, /* output sexps (emacs) */
+       MU_CONFIG_FORMAT_JSON, /* output JSON */
+
+       /* for find */
+       MU_CONFIG_FORMAT_LINKS,  /* output as symlinks */
+       MU_CONFIG_FORMAT_XML,    /* output xml */
+       MU_CONFIG_FORMAT_XQUERY, /* output the xapian query */
+       MU_CONFIG_FORMAT_MQUERY, /* output the mux query */
+
+       MU_CONFIG_FORMAT_EXEC /* execute some command */
+} MuConfigFormat;
+
+typedef enum {
+       MU_CONFIG_CMD_UNKNOWN = 0,
+
+       MU_CONFIG_CMD_ADD,
+       MU_CONFIG_CMD_CFIND,
+       MU_CONFIG_CMD_EXTRACT,
+       MU_CONFIG_CMD_FIELDS,
+       MU_CONFIG_CMD_FIND,
+       MU_CONFIG_CMD_HELP,
+       MU_CONFIG_CMD_INDEX,
+       MU_CONFIG_CMD_INFO,
+       MU_CONFIG_CMD_INIT,
+       MU_CONFIG_CMD_MKDIR,
+       MU_CONFIG_CMD_REMOVE,
+       MU_CONFIG_CMD_SCRIPT,
+       MU_CONFIG_CMD_SERVER,
+       MU_CONFIG_CMD_VERIFY,
+       MU_CONFIG_CMD_VIEW,
+
+       MU_CONFIG_CMD_NONE
+} MuConfigCmd;
+
+#define mu_config_cmd_is_valid(C) ((C) > MU_CONFIG_CMD_UNKNOWN && (C) < MU_CONFIG_CMD_NONE)
+
+/* struct with all configuration options for mu; it will be filled
+ * from the config file, and/or command line arguments */
+
+struct _MuConfig {
+       MuConfigCmd cmd; /* the command, or
+                         * MU_CONFIG_CMD_NONE */
+       char* cmdstr;    /* cmd string, for user
+                         * info */
+       /* general options */
+       gboolean quiet;      /* don't give any output */
+       gboolean debug;      /* log debug-level info */
+       gchar*   muhome;     /* the House of Mu */
+       gboolean version;    /* request mu version */
+       gboolean log_stderr; /* log to stderr (not logfile) */
+       gchar**  params;     /* parameters (for querying) */
+       gboolean nocolor;    /* don't use use ansi-colors
+                             * in some output */
+       gboolean verbose;    /* verbose output */
+
+       /* options for init */
+       gchar* maildir;      /* where the mails are */
+       char** my_addresses; /* 'my e-mail address', for mu cfind;
+                             * can be use multiple times */
+       int max_msg_size;    /* maximum size for message files */
+       int batch_size;      /* database transaction batch size */
+
+       /* options for indexing */
+
+       gboolean nocleanup; /* don't cleanup del'd mails from db */
+       gboolean lazycheck; /* don't check dirs with up-to-date
+                            * timestamps */
+
+       /* options for querying 'find' (and view-> 'summary') */
+       gchar*   fields;    /* fields to show in output */
+       gchar*   sortfield; /* field to sort by (string) */
+       int      maxnum;    /* max # of entries to print */
+       gboolean reverse;   /* sort in revers order (z->a) */
+       gboolean threads;   /* show message threads */
+
+       int      summary_len; /* max # of lines for summary */
+
+       gchar* bookmark;          /* use bookmark */
+       gchar* formatstr;         /* output type for find
+                                  * (plain,links,xml,json,sexp)
+                                  * and view (plain, sexp) and cfind
+                                  */
+       MuConfigFormat format;    /* the decoded formatstr */
+       gchar*         exec;      /* command to execute on the
+                                  * files for the matched
+                                  * messages */
+       gboolean skip_dups;       /* if there are multiple
+                                  * messages with the same
+                                  * msgid, show only the first
+                                  * one */
+       gboolean include_related; /* included related messages
+                                  * in results */
+       /* for find and cind */
+       time_t after; /* only show messages or
+                      * addresses last seen after
+                      * T */
+       /* options for crypto
+        * ie, 'view', 'extract' */
+       gboolean auto_retrieve; /* assume we're online */
+       gboolean decrypt;       /* try to decrypt the
+                                * message body, if any */
+
+       /* options for view */
+       gboolean terminator; /* add separator \f between
+                             * multiple messages in mu
+                             * view */
+
+       /* options for cfind (and 'find' --> "after") */
+       gboolean personal; /* only show 'personal' addresses */
+       /* also 'after' --> see above */
+
+       /* output to a maildir with symlinks */
+       gchar*   linksdir;   /* maildir to output symlinks */
+       gboolean clearlinks; /* clear a linksdir before filling */
+       mode_t   dirmode;    /* mode for the created maildir */
+
+       /* options for extracting parts */
+       gboolean save_all;         /* extract all parts */
+       gboolean save_attachments; /* extract all attachment parts */
+       gchar*   parts;            /* comma-sep'd list of parts
+                                   * to save /  open */
+       gchar*   targetdir;        /* where to save the attachments */
+       gboolean overwrite;        /* should we overwrite same-named files */
+       gboolean play;             /* after saving, try to 'play'
+                                   * (open) the attmnt using xdgopen */
+       /* for server */
+       gboolean commands; /* dump documentations for server
+                           * commands */
+       gchar* eval;       /* command to evaluate */
+
+       /* options for mu-script */
+       gchar*       script;        /* script to run */
+       const char** script_params; /* parameters for scripts */
+};
+typedef struct _MuConfig MuConfig;
+
+/**
+ * initialize a mu config object
+ *
+ * set default values for the configuration options; when you call
+ * mu_config_init, you should also call mu_config_uninit when the data
+ * is no longer needed.
+ *
+ * Note that this is _static_ data, ie., mu_config_init will always
+ * return the same pointer
+ *
+ * @param argcp: pointer to argc
+ * @param argvp: pointer to argv
+ * @param err: receives error information
+ */
+MuConfig* mu_config_init(int* argcp, char*** argvp, GError** err) G_GNUC_WARN_UNUSED_RESULT;
+/**
+ * free the MuConfig structure
+ *
+ * @param opts a MuConfig struct, or NULL
+ */
+void mu_config_uninit(MuConfig* conf);
+
+/**
+ * execute the command / options in this config
+ *
+ * @param opts a MuConfig struct
+ *
+ * @return a value denoting the success/failure of the execution;
+ * MU_ERROR_NONE (0) for success, non-zero for a failure. This is to used for
+ * the exit code of the process
+ *
+ */
+MuError mu_config_execute(const MuConfig* conf);
+
+/**
+ * count the number of non-option parameters
+ *
+ * @param opts a MuConfig struct
+ *
+ * @return the number of non-option parameters, or 0 in case of error
+ */
+size_t mu_config_param_num(const MuConfig* conf);
+
+/**
+ * determine Message::Options from command line args
+ *
+ * @param opts a MuConfig struct
+ *
+ * @return the corresponding Message::Options
+ */
+Message::Options mu_config_message_options(const MuConfig* opts);
+
+/**
+ * print help text for the current command
+ *
+ * @param cmd the command to show help for
+ */
+void mu_config_show_help(const MuConfigCmd cmd);
+
+} // namespace Mu.
+
+#endif /*__MU_CONFIG_H__*/
diff --git a/mu/mu-help-strings.awk b/mu/mu-help-strings.awk
new file mode 100644 (file)
index 0000000..24a6951
--- /dev/null
@@ -0,0 +1,58 @@
+## Copyright (C) 2012-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+
+## convert text blobs statements into c-strings
+
+BEGIN {
+       in_def=0;
+       in_string=0;
+       print "/* Do not edit - auto-generated. */"
+}
+
+
+/^#BEGIN/ {
+       printf "\tHelp {\n\t\t" $2 ","    # e.g., MU_CONFIG_CMD_ADD
+       in_def=1
+}
+
+/^#STRING/ {
+       if (in_def== 1) {
+               if (in_string==1) {
+                       print ",";
+               }
+               in_string=1
+       }
+}
+
+/^#END/ {
+       if (in_string==1) {
+               in_string=0;
+       }
+       in_def=0;
+       printf "\n\t},\n"
+}
+
+
+!/^#/  {
+       if (in_string==1) {
+               printf "\n\t\t\"" $0 "\\n\""
+       }
+}
+
+END {
+       print "/* the end */"
+}
diff --git a/mu/mu-help-strings.txt b/mu/mu-help-strings.txt
new file mode 100644 (file)
index 0000000..be545ff
--- /dev/null
@@ -0,0 +1,200 @@
+#-*-mode:org-*-
+#
+# Copyright (C) 2012-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, 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.
+
+
+#BEGIN MU_CONFIG_CMD_ADD
+#STRING
+mu add <file> [<files>]
+#STRING
+mu add is the command to add specific measage files to the database. Each of the
+files must be specified with an absolute path.
+#END
+
+#BEGIN MU_CONFIG_CMD_CFIND
+#STRING
+mu cfind [options] [--format=<format>] [--personal] [--after=<T>] [<pattern>]
+#STRING
+mu cfind is the mu command to find contacts in the mu database and export them
+for use in other programs.
+
+<format> is one of:
+ plain
+ mutt-alias
+ mutt-ab
+ wl
+ csv
+ org-contact
+ bbdb
+
+'plain' is the default.
+
+If you specify '--personal', only addresses that were found in mails
+that include 'my' e-mail address will be listed - so to exclude e.g.
+mailing-list posts. Use the --my-address= option in 'mu index' to
+specify what addresses are considered 'my' address.
+
+With '--after=T' you can tell mu to only show addresses that were seen after
+T. T is a Unix timestamp. For example, to get only addresses seen after the
+beginning of 2012, you could use
+         --after=`date +%%s -d 2012-01-01`
+#END
+
+#BEGIN MU_CONFIG_CMD_EXTRACT
+#STRING
+mu extract [options] <file>
+#STRING
+mu extract is the mu command to display and save message parts
+(attachments), and open them with other tools.
+#END
+
+#BEGIN MU_CONFIG_CMD_FIELDS
+#STRING
+mu fields
+#STRING
+mu fields produces a table with all messages fields and flags. This
+is useful for writing query expressions.
+#END
+
+
+#BEGIN MU_CONFIG_CMD_FIND
+#STRING
+mu find [options] <search expression>
+#STRING
+mu find is the mu command for searching e-mail message that were
+stored earlier using mu index(1).
+
+Some examples:
+ # get all messages with 'bananas' in body, subject or recipient fields:
+ $ mu find bananas
+
+ # get all messages regarding bananas from John with an attachment:
+ $ mu find from:john flag:attach bananas
+
+ # get all messages with subject wombat in June 2009
+ $ mu find subject:wombat date:20090601..20090630
+
+See the `mu-find' and `mu-easy' man-pages for more information.
+#END
+
+#BEGIN MU_CONFIG_CMD_HELP
+#STRING
+mu help <command>
+#STRING
+mu help is the mu command to get help about <command>, where <command>
+is one of:
+  add     - add message to database
+  cfind   - find a contact
+  extract - extract parts/attachments from messages
+  fields  - show table of all query fields and flags
+  find    - query the message database
+  help    - get help
+  index   - index messages
+  init    - init the mu database
+  mkdir   - create a maildir
+  remove  - remove a message from the database
+  script  - run a script (available only when mu was built with guile-support)
+  server  - start mu server
+  verify  - verify signatures of a message
+  view    - view a specific message
+#END
+
+#BEGIN MU_CONFIG_CMD_INDEX
+#STRING
+mu index [options]
+#STRING
+mu index is the mu command for scanning the contents of Maildir
+directories and storing the results in a Xapian database.The
+data can then be queried using mu-find(1).
+#END
+
+#BEGIN MU_CONFIG_CMD_INIT
+#STRING
+mu init [options]
+#STRING
+mu init is the mu command for setting up the mu database.
+#END
+
+#BEGIN MU_CONFIG_CMD_INFO
+#STRING
+mu init [options]
+#STRING
+mu info is the command for getting information about a mu database.
+#END
+
+#BEGIN MU_CONFIG_CMD_MKDIR
+#STRING
+mu mkdir [options] <dir> [<dirs>]
+#STRING
+mu mkdir is the command for creating Maildirs.It does not
+use the mu database.
+#END
+
+#BEGIN MU_CONFIG_CMD_REMOVE
+#STRING
+mu remove [options] <file> [<files>]
+#STRING
+mu remove is the mu command to remove messages from the database.
+#END
+
+#BEGIN MU_CONFIG_CMD_SERVER
+#STRING
+mu server [options]
+#STRING
+mu server starts a simple shell in which one can query and
+manipulate the mu database.The output of the commands is terms
+of Lisp symbolic expressions (s-exps). Its main use is for
+the mu4e e-mail client.
+#END
+
+#BEGIN MU_CONFIG_CMD_SCRIPT
+#STRING
+mu script [<pattern>] [-v]
+mu <script-name> [<script options>]
+#STRING
+
+List the available scripts and/or run them (if mu was built with support for
+scripts). With <pattern>, list only those scripts whose name or one-line
+description matches it.  With -v, get a longer description for each script.
+
+Some examples:
+
+List all available scripts matching 'month' (long descriptions):
+  $ mu script -v month
+
+Run the 'msgs-per-month' script, and pass it the '--textonly' parameter:
+  $ mu msgs-per-month --textonly
+#END
+
+#BEGIN MU_CONFIG_CMD_VERIFY
+#STRING
+mu verify [options] <msgfile>
+#STRING
+mu verify is the mu command for verifying message signatures
+(such as PGP/GPG signatures)and displaying information about them.
+The command works on message files, and does not require
+the message to be indexed in the database.
+#END
+
+#BEGIN MU_CONFIG_CMD_VIEW
+#STRING
+mu view [options] <file> [<files>]
+#STRING
+mu view is the mu command for displaying e-mail message files. It
+works on message files and does not require the message to be
+indexed in the database.
+#END
diff --git a/mu/mu-memcheck.in b/mu/mu-memcheck.in
new file mode 100644 (file)
index 0000000..73a2329
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+export G_SLICE=always-malloc
+export G_DEBUG=gc-friendly
+
+libtool --mode=execute valgrind --tool=memcheck --leak-check=full --show-possibly-lost=no --leak-resolution=med --track-origins=yes --num-callers=20 --log-file='@abs_top_builddir@/mu-%p.vgdump' @abs_top_builddir@/mu/mu $@
diff --git a/mu/mu.cc b/mu/mu.cc
new file mode 100644 (file)
index 0000000..fa045ab
--- /dev/null
+++ b/mu/mu.cc
@@ -0,0 +1,125 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include <config.h>
+
+#include <glib.h>
+#include <glib-object.h>
+#include <locale.h>
+
+#include "mu-config.hh"
+#include "mu-cmd.hh"
+#include "mu-runtime.hh"
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+static void
+show_version(void)
+{
+       const char* blurb = "mu (mail indexer/searcher) version " VERSION "\n"
+                           "Copyright (C) 2008-2022 Dirk-Jan C. Binnema\n"
+                           "License GPLv3+: GNU GPL version 3 or later "
+                           "<http://gnu.org/licenses/gpl.html>.\n"
+                           "This is free software: you are free to change "
+                           "and redistribute it.\n"
+                           "There is NO WARRANTY, to the extent permitted by law.";
+
+       g_print("%s\n", blurb);
+}
+
+static int
+handle_result(const Result<void>& res, MuConfig* conf)
+{
+       if (res)
+               return 0;
+
+       using Color = MaybeAnsi::Color;
+       MaybeAnsi col{conf ? !conf->nocolor : false};
+
+       // show the error and some help, but not if it's only a softerror.
+       if (!res.error().is_soft_error()) {
+               std::cerr << col.fg(Color::Red) << "error" << col.reset() << ": "
+                         << col.fg(Color::BrightYellow)
+                         << res.error().what() << "\n";
+       } else
+               std::cerr <<  col.fg(Color::BrightBlue) << res.error().what() << '\n';
+
+       std::cerr << col.fg(Color::Green);
+
+       // perhaps give some useful hint on how to solve it.
+       switch (res.error().code()) {
+       case Error::Code::InvalidArgument:
+               if (conf && mu_config_cmd_is_valid(conf->cmd))
+                       mu_config_show_help(conf->cmd);
+               break;
+       case Error::Code::StoreLock:
+               std::cerr << "Perhaps mu is already running?\n";
+               break;
+       case Error::Code::SchemaMismatch:
+               std::cerr << "Please (re)initialize mu with 'mu init' "
+                         << "see mu-init(1) for details\n";
+               break;
+       default:
+               break; /* nothing to do */
+       }
+
+       std::cerr << col.reset();
+
+       return res.error().exit_code();
+}
+
+int
+main(int argc, char* argv[])
+{
+       int rv{};
+       MuConfig *conf{};
+       GError*   err{};
+
+       using Color = MaybeAnsi::Color;
+       MaybeAnsi col{conf ? !conf->nocolor : false};
+
+       setlocale(LC_ALL, "");
+
+       conf = mu_config_init(&argc, &argv, &err);
+       if (!conf) {
+               std::cerr << col.fg(Color::Red) << "error" << col.reset() << ": "
+                         << col.fg(Color::BrightYellow)
+                         << (err ? err->message : "something went wrong") << "\n";
+               rv = 1;
+               goto cleanup;
+       } else if (conf->version) {
+               show_version();
+               goto cleanup;
+       } else if (conf->cmd == MU_CONFIG_CMD_NONE) /* nothing to do */
+               goto cleanup;
+       else if (!mu_runtime_init(conf->muhome, PACKAGE_NAME, conf->debug)) {
+               std::cerr << col.fg(Color::Red) << "error initializing mu\n"
+                         << col.reset();
+               rv = 2;
+       } else
+               rv = handle_result(mu_cmd_execute(conf), conf);
+
+cleanup:
+       g_clear_error(&err);
+       mu_config_uninit(conf);
+       mu_runtime_uninit();
+
+       return rv;
+}
diff --git a/mu/tests/gmime-test.c b/mu/tests/gmime-test.c
new file mode 100644 (file)
index 0000000..45282f9
--- /dev/null
@@ -0,0 +1,260 @@
+/*
+** Copyright (C) 2011-2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#define _POSIX_C_SOURCE 1
+
+#include <gmime/gmime.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <locale.h>
+
+static gchar*
+get_recip(GMimeMessage* msg, GMimeAddressType atype)
+{
+       char*                recep;
+       InternetAddressList* receps;
+
+       receps = g_mime_message_get_addresses(msg, atype);
+       recep  = (char*)internet_address_list_to_string(receps, NULL, FALSE);
+
+       if (!recep || !*recep) {
+               g_free(recep);
+               return NULL;
+       }
+
+       return recep;
+}
+
+static gchar*
+get_refs_str(GMimeMessage* msg)
+{
+       const gchar*     str;
+       GMimeReferences* mime_refs;
+       int              i, refs_len;
+       gchar*           rv;
+
+       str = g_mime_object_get_header(GMIME_OBJECT(msg), "References");
+       if (!str)
+               return NULL;
+
+       mime_refs = g_mime_references_parse(NULL, str);
+       refs_len  = g_mime_references_length(mime_refs);
+       for (rv = NULL, i = 0; i < refs_len; ++i) {
+               const char* msgid;
+               msgid = g_mime_references_get_message_id(mime_refs, i);
+               rv    = g_strdup_printf("%s%s%s", rv ? rv : "", rv ? "," : "", msgid);
+       }
+       g_mime_references_free(mime_refs);
+
+       return rv;
+}
+
+static void
+print_date(GMimeMessage* msg)
+{
+       GDateTime* dt;
+       gchar*     buf;
+
+       dt = g_mime_message_get_date(msg);
+       if (!dt)
+               return;
+
+       dt  = g_date_time_to_local(dt);
+       buf = g_date_time_format(dt, "%c");
+       g_date_time_unref(dt);
+
+       if (buf) {
+               g_print("Date   : %s\n", buf);
+               g_free(buf);
+       }
+}
+
+static void
+print_body(GMimeMessage* msg)
+{
+       GMimeObject*      body;
+       GMimeDataWrapper* wrapper;
+       GMimeStream*      stream;
+
+       body = g_mime_message_get_body(msg);
+
+       if (GMIME_IS_MULTIPART(body))
+               body = g_mime_multipart_get_part(GMIME_MULTIPART(body), 0);
+       if (!GMIME_IS_PART(body))
+               return;
+
+       wrapper = g_mime_part_get_content(GMIME_PART(body));
+       if (!GMIME_IS_DATA_WRAPPER(wrapper))
+               return;
+
+       stream = g_mime_data_wrapper_get_stream(wrapper);
+       if (!GMIME_IS_STREAM(stream))
+               return;
+
+       do {
+               char    buf[512];
+               ssize_t len;
+
+               len = g_mime_stream_read(stream, buf, sizeof(buf));
+               if (len == -1)
+                       break;
+
+               if (write(fileno(stdout), buf, len) == -1)
+                       break;
+
+               if (len < (int)sizeof(buf))
+                       break;
+
+       } while (1);
+}
+
+static gboolean
+test_message(GMimeMessage* msg)
+{
+       gchar*       val;
+       const gchar* str;
+
+       val = get_recip(msg, GMIME_ADDRESS_TYPE_FROM);
+       g_print("From   : %s\n", val ? val : "<none>");
+       g_free(val);
+
+       val = get_recip(msg, GMIME_ADDRESS_TYPE_TO);
+       g_print("To     : %s\n", val ? val : "<none>");
+       g_free(val);
+
+       val = get_recip(msg, GMIME_ADDRESS_TYPE_CC);
+       g_print("Cc     : %s\n", val ? val : "<none>");
+       g_free(val);
+
+       val = get_recip(msg, GMIME_ADDRESS_TYPE_BCC);
+       g_print("Bcc    : %s\n", val ? val : "<none>");
+       g_free(val);
+
+       str = g_mime_message_get_subject(msg);
+       g_print("Subject: %s\n", str ? str : "<none>");
+
+       print_date(msg);
+
+       str = g_mime_message_get_message_id(msg);
+       g_print("Msg-id : %s\n", str ? str : "<none>");
+
+       {
+               gchar* refsstr;
+               refsstr = get_refs_str(msg);
+               g_print("Refs   : %s\n", refsstr ? refsstr : "<none>");
+               g_free(refsstr);
+       }
+
+       print_body(msg);
+
+       return TRUE;
+}
+
+static gboolean
+test_stream(GMimeStream* stream)
+{
+       GMimeParser*  parser;
+       GMimeMessage* msg;
+       gboolean      rv;
+
+       parser = NULL;
+       msg    = NULL;
+
+       parser = g_mime_parser_new_with_stream(stream);
+       if (!parser) {
+               g_warning("failed to create parser");
+               rv = FALSE;
+               goto leave;
+       }
+
+       msg = g_mime_parser_construct_message(parser, NULL);
+       if (!msg) {
+               g_warning("failed to construct message");
+               rv = FALSE;
+               goto leave;
+       }
+
+       rv = test_message(msg);
+
+leave:
+       if (parser)
+               g_object_unref(parser);
+
+       if (msg)
+               g_object_unref(msg);
+
+       return rv;
+}
+
+static gboolean
+test_file(const char* path)
+{
+       FILE*        file;
+       GMimeStream* stream;
+       gboolean     rv;
+
+       stream = NULL;
+       file   = NULL;
+
+       file = fopen(path, "r");
+       if (!file) {
+               g_warning("cannot open file '%s': %s", path, g_strerror(errno));
+               rv = FALSE;
+               goto leave;
+       }
+
+       stream = g_mime_stream_file_new(file);
+       if (!stream) {
+               g_warning("cannot open stream for '%s'", path);
+               rv = FALSE;
+               goto leave;
+       }
+
+       rv = test_stream(stream);
+       g_object_unref(stream);
+       return rv;
+
+leave:
+       if (file)
+               fclose(file);
+
+       return rv;
+}
+
+int
+main(int argc, char* argv[])
+{
+       gboolean rv;
+
+       if (argc != 2) {
+               g_printerr("usage: %s <msg-file>\n", argv[0]);
+               return 1;
+       }
+
+       setlocale(LC_ALL, "");
+
+       g_mime_init();
+
+       rv = test_file(argv[1]);
+
+       g_mime_shutdown();
+
+       return rv ? 0 : 1;
+}
diff --git a/mu/tests/meson.build b/mu/tests/meson.build
new file mode 100644 (file)
index 0000000..8aeaec5
--- /dev/null
@@ -0,0 +1,44 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+#
+# tests
+#
+
+# BROKEN on ubuntu CI, but works elsewhere. Investigate
+test('test-cmd',
+     executable('test-cmd',
+               'test-mu-cmd.cc',
+               install: false,
+               dependencies: [glib_dep, config_h_dep, lib_mu_dep]))
+
+test('test-cmd-cfind',
+     executable('test-cmd-cfind',
+               'test-mu-cmd-cfind.cc',
+               install: false,
+               dependencies: [glib_dep, config_h_dep, lib_mu_dep]))
+test('test-cmd-query',
+     executable('test-cmd-query',
+               'test-mu-query.cc',
+               install: false,
+               dependencies: [glib_dep, config_h_dep, lib_mu_dep]))
+
+gmime_test = executable(
+  'gmime-test', [
+  'gmime-test.c'
+],
+  dependencies: [ glib_dep, gmime_dep ],
+  install: false)
diff --git a/mu/tests/test-mu-cmd-cfind.cc b/mu/tests/test-mu-cmd-cfind.cc
new file mode 100644 (file)
index 0000000..8aec0c2
--- /dev/null
@@ -0,0 +1,345 @@
+/*
+**
+** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-store.hh"
+#include "mu-query.hh"
+#include "utils/mu-utils.hh"
+
+static std::string CONTACTS_CACHE;
+
+using namespace Mu;
+
+static std::string
+fill_contacts_cache(const std::string& path)
+{
+       auto cmdline = format("/bin/sh -c '"
+                             "%s init  --muhome=%s --maildir=%s --quiet; "
+                             "%s index --muhome=%s  --quiet'",
+                             MU_PROGRAM,
+                             path.c_str(),
+                             MU_TESTMAILDIR,
+                             MU_PROGRAM,
+                             path.c_str());
+
+       if (g_test_verbose())
+               g_print("%s\n", cmdline.c_str());
+
+       GError *err{};
+       if (!g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, NULL, &err)) {
+               g_printerr("Error: %s\n", err ? err->message : "?");
+               g_assert(0);
+       }
+
+       return path;
+}
+
+static void
+test_mu_cfind_plain(void)
+{
+       gchar *cmdline, *output, *erroutput;
+
+       cmdline = g_strdup_printf("%s cfind --muhome=%s --format=plain "
+                                 "'testmu\\.xxx?'",
+                                 MU_PROGRAM,
+                                 CONTACTS_CACHE.c_str());
+       if (g_test_verbose())
+               g_print("%s\n", cmdline);
+
+       output = erroutput = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+       /* note, output order is unspecified */
+       g_assert(output);
+       if (output[0] == 'H')
+               g_assert_cmpstr(output,
+                               ==,
+                               "Helmut Kröger hk@testmu.xxx\n"
+                               "Mü testmu@testmu.xx\n");
+       else
+               g_assert_cmpstr(output,
+                               ==,
+                               "Mü testmu@testmu.xx\n"
+                               "Helmut Kröger hk@testmu.xxx\n");
+       g_free(cmdline);
+       g_free(output);
+       g_free(erroutput);
+}
+
+static void
+test_mu_cfind_bbdb(void)
+{
+       gchar *     cmdline, *output, *erroutput, *expected;
+       gchar       today[12];
+       struct tm*  tmtoday;
+       time_t      now;
+       const char* old_tz;
+
+       old_tz = set_tz("Europe/Helsinki");
+
+       cmdline = g_strdup_printf("%s cfind --muhome=%s --format=bbdb "
+                                 "'testmu\\.xxx?'",
+                                 MU_PROGRAM,
+                                 CONTACTS_CACHE.c_str());
+
+       output = erroutput = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+#define frm1                                                                                       \
+       ";; -*-coding: utf-8-emacs;-*-\n"                                                          \
+       ";;; file-version: 6\n"                                                                    \
+       "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") "                             \
+       "((creation-date . \"%s\") "                                                               \
+       "(time-stamp . \"1970-01-01\")) nil]\n"                                                    \
+       "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") "                                    \
+       "((creation-date . \"%s\") "                                                               \
+       "(time-stamp . \"1970-01-01\")) nil]\n"
+
+#define frm2                                                                                       \
+       ";; -*-coding: utf-8-emacs;-*-\n"                                                          \
+       ";;; file-version: 6\n"                                                                    \
+       "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") "                                    \
+       "((creation-date . \"%s\") "                                                               \
+       "(time-stamp . \"1970-01-01\")) nil]\n"                                                    \
+       "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") "                             \
+       "((creation-date . \"%s\") "                                                               \
+       "(time-stamp . \"1970-01-01\")) nil]\n"
+
+       g_assert(output);
+
+       now     = time(NULL);
+       tmtoday = localtime(&now);
+       strftime(today, sizeof(today), "%Y-%m-%d", tmtoday);
+
+       expected = g_strdup_printf(output[52] == 'H' ? frm1 : frm2, today, today);
+
+       /* g_print ("\n%s\n", output); */
+
+       g_assert_cmpstr(output, ==, expected);
+
+       g_free(cmdline);
+       g_free(output);
+       g_free(erroutput);
+       g_free(expected);
+
+       set_tz(old_tz);
+}
+
+static void
+test_mu_cfind_wl(void)
+{
+       gchar *cmdline, *output, *erroutput;
+
+       cmdline = g_strdup_printf("%s cfind --muhome=%s --format=wl "
+                                 "'testmu\\.xxx?'",
+                                 MU_PROGRAM,
+                                 CONTACTS_CACHE.c_str());
+
+       output = erroutput = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+       g_assert(output);
+       if (output[0] == 'h')
+               g_assert_cmpstr(output,
+                               ==,
+                               "hk@testmu.xxx \"HelmutK\" \"Helmut Kröger\"\n"
+                               "testmu@testmu.xx \"Mü\" \"Mü\"\n");
+       else
+               g_assert_cmpstr(output,
+                               ==,
+                               "testmu@testmu.xx \"Mü\" \"Mü\"\n"
+                               "hk@testmu.xxx \"HelmutK\" \"Helmut Kröger\"\n");
+
+       g_free(cmdline);
+       g_free(output);
+       g_free(erroutput);
+}
+
+static void
+test_mu_cfind_mutt_alias(void)
+{
+       gchar *cmdline, *output, *erroutput;
+
+       cmdline = g_strdup_printf("%s cfind --muhome=%s --format=mutt-alias "
+                                 "'testmu\\.xxx?'",
+                                 MU_PROGRAM,
+                                 CONTACTS_CACHE.c_str());
+
+       output = erroutput = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+       /* both orders are possible... */
+       g_assert(output);
+
+       if (output[6] == 'H')
+               g_assert_cmpstr(output,
+                               ==,
+                               "alias HelmutK Helmut Kröger <hk@testmu.xxx>\n"
+                               "alias Mü Mü <testmu@testmu.xx>\n");
+       else
+               g_assert_cmpstr(output,
+                               ==,
+                               "alias Mü Mü <testmu@testmu.xx>\n"
+                               "alias HelmutK Helmut Kröger <hk@testmu.xxx>\n");
+
+       g_free(cmdline);
+       g_free(output);
+       g_free(erroutput);
+}
+
+static void
+test_mu_cfind_mutt_ab(void)
+{
+       gchar *cmdline, *output, *erroutput;
+
+       cmdline = g_strdup_printf("%s cfind --muhome=%s --format=mutt-ab "
+                                 "'testmu\\.xxx?'",
+                                 MU_PROGRAM,
+                                 CONTACTS_CACHE.c_str());
+
+       if (g_test_verbose())
+               g_print("%s\n", cmdline);
+
+       output = erroutput = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       g_assert(output);
+
+       if (output[39] == 'h')
+               g_assert_cmpstr(output,
+                               ==,
+                               "Matching addresses in the mu database:\n"
+                               "hk@testmu.xxx\tHelmut Kröger\t\n"
+                               "testmu@testmu.xx\tMü\t\n");
+       else
+               g_assert_cmpstr(output,
+                               ==,
+                               "Matching addresses in the mu database:\n"
+                               "testmu@testmu.xx\tMü\t\n"
+                               "hk@testmu.xxx\tHelmut Kröger\t\n");
+
+       g_free(cmdline);
+       g_free(output);
+       g_free(erroutput);
+}
+
+static void
+test_mu_cfind_org_contact(void)
+{
+       gchar *cmdline, *output, *erroutput;
+
+       cmdline = g_strdup_printf("%s cfind --muhome=%s --format=org-contact "
+                                 "'testmu\\.xxx?'",
+                                 MU_PROGRAM,
+                                 CONTACTS_CACHE.c_str());
+
+       output = erroutput = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+       g_assert(output);
+
+       if (output[2] == 'H')
+               g_assert_cmpstr(output,
+                               ==,
+                               "* Helmut Kröger\n"
+                               ":PROPERTIES:\n"
+                               ":EMAIL: hk@testmu.xxx\n"
+                               ":END:\n\n"
+                               "* Mü\n"
+                               ":PROPERTIES:\n"
+                               ":EMAIL: testmu@testmu.xx\n"
+                               ":END:\n\n");
+       else
+               g_assert_cmpstr(output,
+                               ==,
+                               "* Mü\n"
+                               ":PROPERTIES:\n"
+                               ":EMAIL: testmu@testmu.xx\n"
+                               ":END:\n\n"
+                               "* Helmut Kröger\n"
+                               ":PROPERTIES:\n"
+                               ":EMAIL: hk@testmu.xxx\n"
+                               ":END:\n\n");
+
+       g_free(cmdline);
+       g_free(output);
+       g_free(erroutput);
+}
+
+static void
+test_mu_cfind_csv(void)
+{
+       gchar *cmdline, *output, *erroutput;
+
+       cmdline = g_strdup_printf("%s cfind --muhome=%s --format=csv "
+                                 "'testmu\\.xxx?'",
+                                 MU_PROGRAM,
+                                 CONTACTS_CACHE.c_str());
+
+       if (g_test_verbose())
+               g_print("%s\n", cmdline);
+
+       output = erroutput = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       g_assert(output);
+       if (output[1] == 'H')
+               g_assert_cmpstr(output,
+                               ==,
+                               "\"Helmut Kröger\",\"hk@testmu.xxx\"\n"
+                               "\"Mü\",\"testmu@testmu.xx\"\n");
+       else
+               g_assert_cmpstr(output,
+                               ==,
+                               "\"Mü\",\"testmu@testmu.xx\"\n"
+                               "\"Helmut Kröger\",\"hk@testmu.xxx\"\n");
+       g_free(cmdline);
+       g_free(output);
+       g_free(erroutput);
+}
+
+int
+main(int argc, char* argv[])
+{
+       mu_test_init(&argc, &argv);
+
+       if (!set_en_us_utf8_locale())
+               return 0; /* don't error out... */
+
+       TempDir tmpdir{};
+       CONTACTS_CACHE = fill_contacts_cache(tmpdir.path());
+
+       g_test_add_func("/mu-cmd-cfind/test-mu-cfind-plain", test_mu_cfind_plain);
+       g_test_add_func("/mu-cmd-cfind/test-mu-cfind-bbdb", test_mu_cfind_bbdb);
+       g_test_add_func("/mu-cmd-cfind/test-mu-cfind-wl", test_mu_cfind_wl);
+       g_test_add_func("/mu-cmd-cfind/test-mu-cfind-mutt-alias", test_mu_cfind_mutt_alias);
+       g_test_add_func("/mu-cmd-cfind/test-mu-cfind-mutt-ab", test_mu_cfind_mutt_ab);
+       g_test_add_func("/mu-cmd-cfind/test-mu-cfind-org-contact", test_mu_cfind_org_contact);
+       g_test_add_func("/mu-cmd-cfind/test-mu-cfind-csv", test_mu_cfind_csv);
+
+       return g_test_run();
+}
diff --git a/mu/tests/test-mu-cmd.cc b/mu/tests/test-mu-cmd.cc
new file mode 100644 (file)
index 0000000..4933c66
--- /dev/null
@@ -0,0 +1,879 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <string.h>
+#include <errno.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-store.hh"
+#include "mu-query.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+/* tests for the command line interface, uses testdir2 */
+
+static std::string DBPATH; /* global */
+
+static void
+fill_database(void)
+{
+       gchar * cmdline;
+       GError* err;
+
+       cmdline = g_strdup_printf("/bin/sh -c '"
+                                 "%s init  --muhome=%s --maildir=%s --quiet; "
+                                 "%s index --muhome=%s  --quiet'",
+                                 MU_PROGRAM,
+                                 DBPATH.c_str(),
+                                 MU_TESTMAILDIR2,
+                                 MU_PROGRAM,
+                                 DBPATH.c_str());
+       if (g_test_verbose())
+               g_print("%s\n", cmdline);
+
+       err = NULL;
+       if (!g_spawn_command_line_sync(cmdline, NULL, NULL, NULL, &err)) {
+               g_printerr("Error: %s\n", err ? err->message : "?");
+               g_assert(0);
+       }
+
+       g_free(cmdline);
+}
+
+static unsigned
+newlines_in_output(const char* str)
+{
+       int count;
+
+       count = 0;
+
+       while (str && *str) {
+               if (*str == '\n')
+                       ++count;
+               ++str;
+       }
+
+       return count;
+}
+
+static size_t
+search_func(const char* query, unsigned expected)
+{
+       size_t lines;
+       gchar *cmdline, *output, *erroutput;
+
+       cmdline = g_strdup_printf("%s find --muhome=%s %s", MU_PROGRAM,
+                                 DBPATH.c_str(), query);
+
+       g_message("[%u] %s", expected, query);
+
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       if (g_test_verbose())
+               g_print("\nOutput:\n%s", output);
+
+       lines = newlines_in_output(output);
+
+       /* we expect zero lines of error output if there is a match;
+        * otherwise there should be one line 'No matches found' */
+       /* g_assert_cmpuint (newlines_in_output(erroutput),==, */
+       /*                expected == 0 ? 1 : 0); */
+
+       g_free(output);
+       g_free(erroutput);
+       g_free(cmdline);
+
+       return lines;
+}
+
+#define search(Q,EXP) do {                     \
+       unsigned lines = search_func(Q, EXP);   \
+       g_assert_cmpuint(lines, ==, EXP);       \
+} while(0)
+
+
+/* index testdir2, and make sure it adds two documents */
+static void
+test_mu_index(void)
+{
+       auto store = Store::make(DBPATH + "/xapian");
+       assert_valid_result(store);
+       g_assert_cmpuint(store->size(), ==, 13);
+}
+
+static void
+test_mu_find_empty_query(void)
+{
+       search("\"\"", 13);
+}
+
+static void
+test_mu_find_01(void)
+{
+       search("f:john fruit", 1);
+       search("f:soc@example.com", 1);
+       search("t:alki@example.com", 1);
+       search("t:alcibiades", 1);
+       search("http emacs", 1);
+       search("f:soc@example.com OR f:john", 2);
+       search("f:soc@example.com OR f:john OR t:edmond", 3);
+       search("t:julius", 1);
+       search("s:dude", 1);
+       search("t:dantès", 1);
+}
+
+/* index testdir2, and make sure it adds two documents */
+static void
+test_mu_find_02(void)
+{
+       search("bull", 1);
+       search("bull m:foo", 0);
+       search("bull m:/foo", 1);
+       search("bull m:/Foo", 1);
+       search("bull flag:attach", 1);
+       search("bull flag:a", 1);
+       search("g:x", 0);
+       search("flag:encrypted", 0);
+       search("flag:attach", 1);
+}
+
+static void
+test_mu_find_file(void)
+{
+       search("file:sittingbull.jpg", 1);
+       search("file:custer.jpg", 1);
+       search("file:custer.*", 1);
+       search("j:sit*", 1);
+}
+
+static void
+test_mu_find_mime(void)
+{
+       search("mime:image/jpeg", 1);
+       search("mime:text/plain", 13);
+       search("y:text*", 13);
+       search("y:image*", 1);
+       search("mime:message/rfc822", 2);
+}
+
+static void
+test_mu_find_text_in_rfc822(void)
+{
+       search("embed:dancing", 1);
+       search("e:curious", 1);
+       search("embed:with", 2);
+       search("e:karjala", 0);
+       search("embed:navigation", 1);
+}
+
+/* some more tests */
+static void
+test_mu_find_03(void)
+{
+       search("bull", 1);
+       search("bull m:foo", 0);
+       search("bull m:/foo", 1);
+       search("i:3BE9E6535E0D852173@emss35m06.us.lmco.com", 1);
+}
+
+static void /* error cases */
+test_mu_find_04(void)
+{
+       gchar *cmdline, *erroutput;
+
+       cmdline = g_strdup_printf("find %s --muhome=%cfoo%cbar%cnonexistent "
+                                 "f:socrates",
+                                 MU_PROGRAM,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+
+       g_assert(g_spawn_command_line_sync(cmdline, NULL, &erroutput, NULL, NULL));
+
+       /* we expect multiple lines of error output */
+       g_assert_cmpuint(newlines_in_output(erroutput), >=, 1);
+
+       g_free(erroutput);
+       g_free(cmdline);
+}
+
+G_GNUC_UNUSED static void
+test_mu_find_links(void)
+{
+       gchar *cmdline, *output, *erroutput, *tmpdir;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+
+       cmdline = g_strdup_printf("%s find --muhome=%s --format=links --linksdir=%s "
+                                 "mime:message/rfc822",
+                                 MU_PROGRAM,
+                                 DBPATH.c_str(),
+                                 tmpdir);
+
+       if (g_test_verbose())
+               g_print("cmdline: %s\n", cmdline);
+
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       /* there should be no errors */
+       g_assert_cmpuint(newlines_in_output(output), ==, 0);
+       g_assert_cmpuint(newlines_in_output(erroutput), ==, 0);
+       g_free(output);
+       g_free(erroutput);
+
+       /* now we try again, we should get a line of error output,
+        * when we find the first target file already exists */
+
+       if (g_test_verbose())
+               g_print("cmdline: %s\n", cmdline);
+
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       g_assert_cmpuint(newlines_in_output(output), ==, 0);
+       g_assert_cmpuint(newlines_in_output(erroutput), ==, 1);
+       g_free(output);
+       g_free(erroutput);
+
+       /* now we try again with --clearlinks, and the we should be
+        * back to 0 errors */
+       g_free(cmdline);
+       cmdline = g_strdup_printf("%s find --muhome=%s --format=links --linksdir=%s --clearlinks "
+                                 "mime:message/rfc822",
+                                 MU_PROGRAM,
+                                 DBPATH.c_str(),
+                                 tmpdir);
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       if (g_test_verbose())
+               g_print("cmdline: %s\n", cmdline);
+       g_assert_cmpuint(newlines_in_output(output), ==, 0);
+       g_assert_cmpuint(newlines_in_output(erroutput), ==, 0);
+       g_free(output);
+       g_free(erroutput);
+
+       g_free(cmdline);
+       g_free(tmpdir);
+}
+
+/* some more tests */
+static void
+test_mu_find_maildir_special(void)
+{
+       search("\"maildir:/wOm_bàT\"", 3);
+       search("\"maildir:/wOm*\"", 3);
+       search("\"maildir:/wOm_*\"", 3);
+       search("\"maildir:wom_bat\"", 0);
+       search("\"maildir:/wombat\"", 0);
+       search("subject:atoms", 1);
+       search("\"maildir:/wom_bat\" subject:atoms", 1);
+}
+
+/* static void */
+/* test_mu_find_mime_types (void) */
+/* { */
+/*     /\* ensure that maldirs with spaces in their names work... *\/ */
+/*     search ("\"maildir:/wom bat\" subject:atoms", 1); */
+/*     search ("\"maildir:/wOm_bàT\"", 3); */
+/*     search ("subject:atoms", 1); */
+/* } */
+
+static void
+test_mu_extract_01(void)
+{
+       gchar *cmdline, *output, *erroutput, *tmpdir;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s extract --muhome=%s %s%cFoo%ccur%cmail5",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+
+       if (g_test_verbose())
+               g_print("cmd: %s\n", cmdline);
+
+       output = erroutput = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+
+       // ERROR:test-mu-cmd.cc:347:void test_mu_extract_01(): assertion failed (output ==
+       // "MIME-parts in this message:\n" " 1 <none> text/plain [<none>] (27 bytes)\n" " 2
+       // sittingbull.jpg image/jpeg [inline] (23.9\302\240kB)\n" " 3 custer.jpg image/jpeg
+       // [inline] (21.6\302\240kB)\n"):
+       // ("MIME-parts in this message:\n 1 <none> text/plain [<none>] (27 bytes)\n 2
+       // sittingbull.jpg image/jpeg [inline] (23.9 kB)\n 3 custer.jpg image/jpeg [inline] (21.6
+       // kB)\n" == "MIME-parts in this message:\n 1 <none> text/plain [<none>] (27 bytes)\n 2
+       // sittingbull.jpg image/jpeg [inline] (23.9\302\240kB)\n 3 custer.jpg image/jpeg [inline]
+       // (21.6\302\240kB)\n")
+       /* g_assert_cmpstr (output, */
+       /*               ==, */
+       /*               "MIME-parts in this message:\n" */
+       /*               "  1 <none> text/plain [<none>] (27 bytes)\n" */
+       /*               "  2 sittingbull.jpg image/jpeg [inline] (23.9\302\240kB)\n" */
+       /*               "  3 custer.jpg image/jpeg [inline] (21.6\302\240kB)\n"); */
+
+       /* we expect zero lines of error output */
+       g_assert_cmpuint(newlines_in_output(erroutput), ==, 0);
+
+       g_free(output);
+       g_free(erroutput);
+       g_free(cmdline);
+       g_free(tmpdir);
+}
+
+static gint64
+get_file_size(const char* path)
+{
+       int         rv;
+       struct stat statbuf;
+
+       rv = stat(path, &statbuf);
+       if (rv != 0) {
+               /* g_warning ("error: %s", g_strerror (errno)); */
+               return -1;
+       }
+
+       return (gint64)statbuf.st_size;
+}
+
+static void
+test_mu_extract_02(void)
+{
+       gchar *cmdline, *output, *tmpdir;
+       gchar *att1, *att2;
+       size_t size;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s extract --muhome=%s -a "
+                                 "--target-dir=%s %s%cFoo%ccur%cmail5",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+
+       if (g_test_verbose())
+               g_print("$ %s\n", cmdline);
+
+       output = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+       g_assert_cmpstr(output, ==, "");
+
+       att1 = g_strdup_printf("%s%ccuster.jpg", tmpdir, G_DIR_SEPARATOR);
+       att2 = g_strdup_printf("%s%csittingbull.jpg", tmpdir, G_DIR_SEPARATOR);
+
+       size = get_file_size(att1);
+       g_assert_cmpuint(size, >=, 15955);
+       g_assert_cmpuint(size, <=, 15960);
+
+       size = get_file_size(att2);
+       g_assert_cmpint(size, ==, 17674);
+
+
+       g_free(output);
+       g_free(tmpdir);
+       g_free(cmdline);
+       g_free(att1);
+       g_free(att2);
+}
+
+static void
+test_mu_extract_03(void)
+{
+       gchar *cmdline, *output, *tmpdir;
+       gchar *att1, *att2;
+       size_t size;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s extract --muhome=%s --parts 3 "
+                                 "--target-dir=%s %s%cFoo%ccur%cmail5",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+       output  = NULL;
+
+       if (g_test_verbose())
+               g_print("$ %s\n", cmdline);
+
+       g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+       g_assert_cmpstr(output, ==, "");
+
+       att1 = g_strdup_printf("%s%ccuster.jpg", tmpdir, G_DIR_SEPARATOR);
+       att2 = g_strdup_printf("%s%csittingbull.jpg", tmpdir, G_DIR_SEPARATOR);
+
+       size = get_file_size(att1);
+       g_assert_cmpuint(size, >=, 15955);
+       g_assert_cmpuint(size, <=, 15960);
+       g_assert_cmpint(get_file_size(att2), ==, -1);
+
+       g_free(output);
+       g_free(tmpdir);
+       g_free(cmdline);
+       g_free(att1);
+       g_free(att2);
+}
+
+static void
+test_mu_extract_overwrite(void)
+{
+       gchar *cmdline, *output, *erroutput, *tmpdir;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s extract --muhome=%s -a "
+                                 "--target-dir=%s %s%cFoo%ccur%cmail5",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+
+       if (g_test_verbose())
+               g_print("$ %s\n", cmdline);
+
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       g_assert_cmpstr(output, ==, "");
+       g_assert_cmpstr(erroutput, ==, "");
+       g_free(erroutput);
+       g_free(output);
+
+       if (g_test_verbose())
+               g_print("$ %s\n", cmdline);
+
+       /* now, it should fail, because we don't allow overwrites
+        * without --overwrite */
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       g_assert_cmpstr(output, ==, "");
+       g_assert_cmpstr(erroutput, !=, "");
+       g_free(erroutput);
+       g_free(output);
+
+       g_free(cmdline);
+       /* this should work now, because we have specified --overwrite */
+       cmdline = g_strdup_printf("%s extract --muhome=%s -a --overwrite "
+                                 "--target-dir=%s %s%cFoo%ccur%cmail5",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+       if (g_test_verbose())
+               g_print("$ %s\n", cmdline);
+
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       g_assert_cmpstr(output, ==, "");
+       g_assert_cmpstr(erroutput, ==, "");
+       g_free(erroutput);
+       g_free(output);
+
+       g_free(tmpdir);
+       g_free(cmdline);
+}
+
+static void
+test_mu_extract_by_name(void)
+{
+       gchar *cmdline, *output, *erroutput, *tmpdir, *path;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s extract --muhome=%s "
+                                 "--target-dir=%s %s%cFoo%ccur%cmail5 "
+                                 "sittingbull.jpg",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+
+       if (g_test_verbose())
+               g_print("$ %s\n", cmdline);
+
+       g_assert(g_spawn_command_line_sync(cmdline, &output, &erroutput, NULL, NULL));
+       g_assert_cmpstr(output, ==, "");
+       g_assert_cmpstr(erroutput, ==, "");
+       path = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "sittingbull.jpg");
+       g_assert(access(path, F_OK) == 0);
+       g_free(path);
+
+       g_free(erroutput);
+       g_free(output);
+
+       g_free(tmpdir);
+       g_free(cmdline);
+}
+
+static void
+test_mu_view_01(void)
+{
+       gchar *cmdline, *output, *tmpdir;
+       int    len;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s view --muhome=%s %s%cbar%ccur%cmail4",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+       output  = NULL;
+
+       if (g_test_verbose())
+               g_print("$ %s\n", cmdline);
+
+       g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+       g_assert_cmpstr(output, !=, NULL);
+
+       /*
+        * note: there are two possibilities here; older versions of
+        * GMime will produce:
+        *
+        *    From: "=?iso-8859-1?Q? =F6tzi ?=" <oetzi@web.de>
+        *
+        * while newer ones return something like:
+        *
+        *    From:  ?tzi  <oetzi@web.de>
+        *
+        * or even
+        *
+        *    From:  \xc3\xb6tzi  <oetzi@web.de>
+        *
+        * both are 'okay' from mu's perspective; it'd be even better
+        * to have some #ifdefs for the GMime versions, but this
+        * should work for now
+        *
+        * Added 350 as 'okay', which comes with gmime 2.4.24 (ubuntu 10.04)
+        */
+       len = strlen(output);
+       if (len < 339) {
+               g_print ("\n[%s] (%d)\n", output, len);
+       }
+       g_assert_cmpuint(len, >=,  339);
+       g_free(output);
+       g_free(cmdline);
+       g_free(tmpdir);
+}
+
+static void
+test_mu_view_multi(void)
+{
+       gchar *cmdline, *output, *tmpdir;
+       int    len;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s view --muhome=%s "
+                                 "%s%cbar%ccur%cmail5 "
+                                 "%s%cbar%ccur%cmail5",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+       output  = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+       g_assert_cmpstr(output, !=, NULL);
+
+       len = strlen(output);
+       if (g_test_verbose())
+               g_print ("\n[%s](%u)\n", output, len);
+       g_assert_cmpuint(len, >=, 112);
+
+       g_free(output);
+       g_free(cmdline);
+       g_free(tmpdir);
+}
+
+static void
+test_mu_view_multi_separate(void)
+{
+       gchar *cmdline, *output, *tmpdir;
+       int    len;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s view --terminate --muhome=%s "
+                                 "%s%cbar%ccur%cmail5 "
+                                 "%s%cbar%ccur%cmail5",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+       output  = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+       g_assert_cmpstr(output, !=, NULL);
+
+       len = strlen(output);
+       if (g_test_verbose())
+               g_print ("\n[%s](%u)\n", output, len);
+       g_print ("\n[%s](%u)\n", output, len);
+       //g_assert_cmpuint(len, >=, 112);
+
+       g_free(output);
+       g_free(cmdline);
+       g_free(tmpdir);
+}
+
+static void
+test_mu_view_attach(void)
+{
+       gchar *cmdline, *output, *tmpdir;
+       int    len;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s view --muhome=%s %s%cFoo%ccur%cmail5",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 MU_TESTMAILDIR2,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR,
+                                 G_DIR_SEPARATOR);
+       output  = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+       g_assert_cmpstr(output, !=, NULL);
+
+       len = strlen(output);
+       if (g_test_verbose())
+               g_print ("\n[%s](%u)\n", output, len);
+       //g_assert(len == 168 || len == 166);
+
+       g_free(output);
+       g_free(cmdline);
+       g_free(tmpdir);
+}
+
+static void
+test_mu_mkdir_01(void)
+{
+       gchar *cmdline, *output, *tmpdir;
+       gchar* dir;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert(g_mkdir_with_parents(tmpdir, 0700) == 0);
+
+       cmdline = g_strdup_printf("%s mkdir --muhome=%s %s%ctest1 %s%ctest2",
+                                 MU_PROGRAM,
+                                 tmpdir,
+                                 tmpdir,
+                                 G_DIR_SEPARATOR,
+                                 tmpdir,
+                                 G_DIR_SEPARATOR);
+
+       output = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, NULL, NULL));
+       g_assert_cmpstr(output, ==, "");
+
+       dir = g_strdup_printf("%s%ctest1%ccur", tmpdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR);
+       g_assert(access(dir, F_OK) == 0);
+       g_free(dir);
+
+       dir = g_strdup_printf("%s%ctest2%ctmp", tmpdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR);
+       g_assert(access(dir, F_OK) == 0);
+       g_free(dir);
+
+       dir = g_strdup_printf("%s%ctest1%cnew", tmpdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR);
+       g_assert(access(dir, F_OK) == 0);
+       g_free(dir);
+
+       g_free(output);
+       g_free(tmpdir);
+       g_free(cmdline);
+}
+
+/* we can only test 'verify' if gpg is installed, and has
+ * djcb@djcbsoftware's key in the keyring */
+G_GNUC_UNUSED static gboolean
+verify_is_testable(void)
+{
+       gchar *  gpg, *cmdline;
+       gchar *  output, *erroutput;
+       int      retval;
+       gboolean rv;
+
+       /* find GPG or return FALSE */
+       if ((gpg = (char*)g_getenv("MU_GPG_PATH"))) {
+               if (access(gpg, X_OK) != 0)
+                       return FALSE;
+               else
+                       gpg = g_strdup(gpg);
+
+       } else if (!(gpg = g_find_program_in_path("gpg2")))
+               return FALSE;
+
+       cmdline = g_strdup_printf("%s --list-keys DCC4A036", gpg);
+       g_free(gpg);
+
+       output = erroutput = NULL;
+       rv     = g_spawn_command_line_sync(cmdline, &output, &erroutput, &retval, NULL);
+       g_free(output);
+       g_free(erroutput);
+       g_free(cmdline);
+
+       return (rv && retval == 0) ? TRUE : FALSE;
+}
+
+G_GNUC_UNUSED static void
+test_mu_verify_good(void)
+{
+       gchar *cmdline, *output;
+       int    retval;
+
+       if (!verify_is_testable())
+               return;
+
+       cmdline = g_strdup_printf("%s verify '%s/signed!2,S'", MU_PROGRAM, MU_TESTMAILDIR4);
+
+       if (g_test_verbose())
+               g_print("$ %s\n", cmdline);
+
+       output = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, &retval, NULL));
+       g_free(output);
+       g_assert_cmpuint(retval, ==, 0);
+       g_free(cmdline);
+}
+
+G_GNUC_UNUSED static void
+test_mu_verify_bad(void)
+{
+       gchar *cmdline, *output;
+       int    retval;
+
+       if (!verify_is_testable())
+               return;
+
+       cmdline = g_strdup_printf("%s verify '%s/signed-bad!2,S'", MU_PROGRAM, MU_TESTMAILDIR4);
+
+       if (g_test_verbose())
+               g_print("$ %s\n", cmdline);
+
+       output = NULL;
+       g_assert(g_spawn_command_line_sync(cmdline, &output, NULL, &retval, NULL));
+       g_free(output);
+       g_assert_cmpuint(retval, !=, 0);
+       g_free(cmdline);
+}
+
+int
+main(int argc, char* argv[])
+{
+       int rv;
+
+       /* currently, something is broken on Ubuntu CI (but not elsewhere);
+        * selectively turn this test off */
+       if (!g_getenv("RUN_TEST_MU_CMD"))
+               return 0;
+
+       mu_test_init(&argc, &argv);
+
+       if (!set_en_us_utf8_locale())
+               return 0; /* don't error out... */
+
+       g_test_add_func("/mu-cmd/test-mu-index", test_mu_index);
+
+       g_test_add_func("/mu-cmd/test-mu-find-empty-query", test_mu_find_empty_query);
+       g_test_add_func("/mu-cmd/test-mu-find-01", test_mu_find_01);
+       g_test_add_func("/mu-cmd/test-mu-find-02", test_mu_find_02);
+
+       g_test_add_func("/mu-cmd/test-mu-find-file", test_mu_find_file);
+       g_test_add_func("/mu-cmd/test-mu-find-mime", test_mu_find_mime);
+
+       /* recently, this test breaks _sometimes_ when run on Travis; but it
+        * seems related to the setup there, as nothing has changed in the code.
+        * turn off for now. */
+       /* g_test_add_func ("/mu-cmd/test-mu-find-links",
+        * test_mu_find_links); */
+
+       g_test_add_func("/mu-cmd/test-mu-find-text-in-rfc822", test_mu_find_text_in_rfc822);
+
+       g_test_add_func("/mu-cmd/test-mu-find-03", test_mu_find_03);
+       g_test_add_func("/mu-cmd/test-mu-find-04", test_mu_find_04);
+       g_test_add_func("/mu-cmd/test-mu-find-maildir-special", test_mu_find_maildir_special);
+       g_test_add_func("/mu-cmd/test-mu-extract-01", test_mu_extract_01);
+       g_test_add_func("/mu-cmd/test-mu-extract-02", test_mu_extract_02);
+       g_test_add_func("/mu-cmd/test-mu-extract-03", test_mu_extract_03);
+       g_test_add_func("/mu-cmd/test-mu-extract-overwrite", test_mu_extract_overwrite);
+       g_test_add_func("/mu-cmd/test-mu-extract-by-name", test_mu_extract_by_name);
+
+       g_test_add_func("/mu-cmd/test-mu-view-01", test_mu_view_01);
+       g_test_add_func("/mu-cmd/test-mu-view-multi", test_mu_view_multi);
+       g_test_add_func("/mu-cmd/test-mu-view-multi-separate", test_mu_view_multi_separate);
+       g_test_add_func("/mu-cmd/test-mu-view-attach", test_mu_view_attach);
+       g_test_add_func("/mu-cmd/test-mu-mkdir-01", test_mu_mkdir_01);
+
+       g_test_add_func("/mu-cmd/test-mu-verify-good", test_mu_verify_good);
+       g_test_add_func("/mu-cmd/test-mu-verify-bad", test_mu_verify_bad);
+
+       TempDir tempdir;
+       DBPATH = tempdir.path();
+       fill_database();
+
+       rv     = g_test_run();
+
+       return rv;
+}
diff --git a/mu/tests/test-mu-query.cc b/mu/tests/test-mu-query.cc
new file mode 100644 (file)
index 0000000..283f2e9
--- /dev/null
@@ -0,0 +1,674 @@
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify it
+** under the terms of the GNU General Public License as published by the
+** Free Software Foundation; either version 3, or (at your option) any
+** later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <unordered_set>
+#include <string>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-query.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+#include "mu-store.hh"
+
+using namespace Mu;
+
+static std::string DB_PATH1;
+static std::string DB_PATH2;
+
+static std::string
+make_database(const std::string& testdir)
+{
+       char*      tmpdir{test_mu_common_get_random_tmpdir()};
+
+       /* use the env var rather than `--muhome` */
+
+       g_setenv("MUHOME", tmpdir, 1);
+       const auto cmdline{format("/bin/sh -c '"
+                                 "%s init --maildir=%s --quiet ; "
+                                 "%s index --quiet'",
+                                 MU_PROGRAM,
+                                 testdir.c_str(),
+                                 MU_PROGRAM)};
+
+       if (g_test_verbose())
+               g_printerr("\n%s\n", cmdline.c_str());
+
+       g_assert(g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, NULL, NULL));
+       auto xpath = g_strdup_printf("%s%c%s", tmpdir, G_DIR_SEPARATOR, "xapian");
+       g_free(tmpdir);
+
+       /* ensure MUHOME worked */
+       g_assert_cmpuint(::access(xpath, F_OK), ==, 0);
+
+       std::string dbpath{xpath};
+       g_free(xpath);
+
+       return dbpath;
+}
+
+static void
+assert_no_dups(const QueryResults& qres)
+{
+       std::unordered_set<std::string> msgid_set, path_set;
+
+       for (auto&& mi : qres) {
+               g_assert_true(msgid_set.find(mi.message_id().value()) == msgid_set.end());
+               g_assert_true(path_set.find(mi.path().value()) == path_set.end());
+
+               path_set.emplace(*mi.path());
+               msgid_set.emplace(*mi.message_id());
+
+               g_assert_false(msgid_set.find(mi.message_id().value()) == msgid_set.end());
+               g_assert_false(path_set.find(mi.path().value()) == path_set.end());
+       }
+}
+
+/* note: this also *moves the iter* */
+static size_t
+run_and_count_matches(const std::string& xpath,
+                     const std::string& expr,
+                     Mu::QueryFlags     flags = Mu::QueryFlags::None)
+{
+       auto store{Store::make(xpath)};
+       assert_valid_result(store);
+
+       // if (g_test_verbose()) {
+       //      std::cout << "==> mquery: " << store.parse_query(expr, false) << "\n";
+       //      std::cout << "==> xquery: " << store.parse_query(expr, true) << "\n";
+       // }
+
+       Mu::allow_warnings();
+
+       auto qres{store->run_query(expr, {}, flags)};
+       g_assert_true(!!qres);
+       assert_no_dups(*qres);
+
+
+       if (g_test_verbose())
+               g_print("'%s' => %zu\n", expr.c_str(), qres->size());
+
+       return qres->size();
+}
+
+typedef struct {
+       const char* query;
+       size_t      count; /* expected number of matches */
+} QResults;
+
+static void
+test_mu_query_01(void)
+{
+       int      i;
+       QResults queries[] = {
+           {"basic", 3},
+           {"question", 5},
+           {"thanks", 2},
+           {"html", 4},
+           {"subject:exception", 1},
+           {"exception", 1},
+           {"subject:A&B", 1},
+           {"A&B", 1},
+           {"subject:elisp", 1},
+           {"html AND contains", 1},
+           {"html and contains", 1},
+           {"from:pepernoot", 0},
+           {"foo:pepernoot", 0},
+           {"funky", 1},
+           {"fünkÿ", 1},
+           { "", 19 },
+           {"msgid:abcd$efgh@example.com", 1},
+           {"i:abcd$efgh@example.com", 1},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_02(void)
+{
+       const char* q;
+       q = "i:f7ccd24b0808061357t453f5962w8b61f9a453b684d0@mail.gmail.com";
+       g_assert_cmpuint(run_and_count_matches(DB_PATH1, q), ==, 1);
+}
+
+static void
+test_mu_query_03(void)
+{
+       int      i;
+       QResults queries[] = {{"ploughed", 1},
+                             {"i:3BE9E6535E3029448670913581E7A1A20D852173@"
+                              "emss35m06.us.lmco.com",
+                              1},
+                             {"i:!&!AAAAAAAAAYAAAAAAAAAOH1+8mkk+lLn7Gg5fke7"
+                              "FbCgAAAEAAAAJ7eBDgcactKhXL6r8cEnJ8BAAAAAA==@"
+                              "example.com",
+                              1},
+
+                             /* subsets of the words in the subject should match */
+                             {"s:gcc include search order", 1},
+                             {"s:gcc include search", 1},
+                             {"s:search order", 1},
+                             {"s:include", 1},
+
+                             {"s:lisp", 1},
+                             {"s:LISP", 1},
+
+                             // { "s:\"Re: Learning LISP; Scheme vs elisp.\"", 1},
+                             // { "subject:Re: Learning LISP; Scheme vs elisp.", 1},
+                             // { "subject:\"Re: Learning LISP; Scheme vs elisp.\"", 1},
+                             {"to:help-gnu-emacs@gnu.org", 4},
+                             //{"t:help-gnu-emacs", 4},
+                             {"flag:flagged", 1}};
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_04(void)
+{
+       int i;
+
+       QResults queries[] = {
+           {"frodo@example.com", 1},
+           {"f:frodo@example.com", 1},
+           {"f:Frodo Baggins", 1},
+           {"bilbo@anotherexample.com", 1},
+           {"t:bilbo@anotherexample.com", 1},
+           {"t:bilbo", 1},
+           {"f:bilbo", 0},
+           {"baggins", 1},
+           {"prio:h", 1},
+           {"prio:high", 1},
+           {"prio:normal", 11},
+           {"prio:l", 7},
+           {"not prio:l", 12},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_logic(void)
+{
+       int      i;
+       QResults queries[] = {{"subject:gcc", 1},
+                             {"subject:lisp", 1},
+                             {"subject:gcc OR subject:lisp", 2},
+                             {"subject:gcc or subject:lisp", 2},
+                             {"subject:gcc AND subject:lisp", 0},
+                             {"subject:gcc OR (subject:scheme AND subject:elisp)", 2},
+                             {"(subject:gcc OR subject:scheme) AND subject:elisp", 1}};
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_accented_chars_01(void)
+{
+       auto store = Store::make(DB_PATH1);
+       assert_valid_result(store);
+
+       auto qres{store->run_query("fünkÿ")};
+       g_assert_true(!!qres);
+       g_assert_false(qres->empty());
+
+       const auto msg{qres->begin().message()};
+       if (!msg) {
+               g_warning("error getting message");
+               g_assert_not_reached();
+       }
+
+       assert_equal(msg->subject(), "Greetings from Lothlórien");
+}
+
+static void
+test_mu_query_accented_chars_02(void)
+{
+       int i;
+
+       QResults queries[] = {{"f:mü", 1},
+               { "s:motörhead", 1},
+               {"t:Helmut", 1},
+               {"t:Kröger", 1},
+               {"s:MotorHeäD", 1},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+               auto count = run_and_count_matches(DB_PATH1, queries[i].query);
+               if (count != queries[i].count)
+                       g_warning("query '%s'; expect %zu but got %zu",
+                                 queries[i].query, queries[i].count, count);
+               g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+                                ==,
+                                queries[i].count);
+       }
+}
+
+static void
+test_mu_query_accented_chars_fraiche(void)
+{
+       int i;
+
+       QResults queries[] = {{"crème fraîche", 1},
+                             {"creme fraiche", 1},
+                             {"fraîche crème", 1},
+                             {"будланула", 1},
+                             {"БУДЛАНУЛА", 1},
+                             {"CRÈME FRAÎCHE", 1},
+                             {"CREME FRAICHE", 1}};
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+               if (g_test_verbose())
+                       g_print("'%s'\n", queries[i].query);
+
+               g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+                                ==,
+                                queries[i].count);
+       }
+}
+
+static void
+test_mu_query_wildcards(void)
+{
+       int i;
+
+       QResults queries[] = {
+           {"f:mü", 1},
+           {"s:mo*", 1},
+           {"t:Helm*", 1},
+           {"queensryche", 1},
+           {"Queen*", 1},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_dates_helsinki(void)
+{
+       const auto hki = "Europe/Helsinki";
+       if (!timezone_available(hki)) {
+               g_test_skip("timezone not available");
+               return;
+       }
+
+       int         i;
+       const char* old_tz;
+
+       QResults queries[] = {{"date:20080731..20080804", 5},
+                             {"date:20080731..20080804 s:gcc", 1},
+                             {"date:200808110803..now", 7},
+                             {"date:200808110803..today", 7},
+                             {"date:200808110801..now", 7}};
+
+       old_tz = set_tz(hki);
+
+       const auto xpath{make_database(MU_TESTMAILDIR)};
+       g_assert_false(xpath.empty());
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(xpath, queries[i].query),
+                                ==,
+                                queries[i].count);
+
+       set_tz(old_tz);
+}
+
+static void
+test_mu_query_dates_sydney(void)
+{
+       const auto syd = "Australia/Sydney";
+       if (!timezone_available(syd)) {
+               g_test_skip("timezone not available");
+               return;
+       }
+
+       int         i;
+       const char* old_tz;
+       QResults    queries[] = {{"date:20080731..20080804", 5},
+                             {"date:20080731..20080804 s:gcc", 1},
+                             {"date:200808110803..now", 7},
+                             {"date:200808110803..today", 7},
+                             {"date:200808110801..now", 7}};
+       old_tz = set_tz(syd);
+
+       const auto xpath{make_database(MU_TESTMAILDIR)};
+       g_assert_false(xpath.empty());
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(xpath, queries[i].query),
+                                ==,
+                                queries[i].count);
+       set_tz(old_tz);
+}
+
+static void
+test_mu_query_dates_la(void)
+{
+       const auto la = "America/Los_Angeles";
+       if (!timezone_available(la)) {
+               g_test_skip("timezone not available");
+               return;
+       }
+
+       int         i;
+       const char* old_tz;
+
+       QResults queries[] = {{"date:20080731..20080804", 5},
+                             {"date:2008-07-31..2008-08-04", 5},
+                             {"date:20080804..20080731", 5},
+                             {"date:20080731..20080804 s:gcc", 1},
+                             {"date:200808110803..now", 6},
+                             {"date:200808110803..today", 6},
+                             {"date:200808110801..now", 6}};
+       old_tz = set_tz(la);
+
+       const auto xpath = make_database(MU_TESTMAILDIR);
+       g_assert_false(xpath.empty());
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+               /* g_print ("%s\n", queries[i].query); */
+               g_assert_cmpuint(run_and_count_matches(xpath, queries[i].query),
+                                ==,
+                                queries[i].count);
+       }
+
+       set_tz(old_tz);
+}
+
+static void
+test_mu_query_sizes(void)
+{
+       int      i;
+       QResults queries[] = {
+           {"size:0b..2m", 19},
+           {"size:3b..2m", 19},
+           {"size:2k..4k", 4},
+
+           {"size:0b..2m", 19},
+           {"size:2m..0b", 19},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_attach(void)
+{
+       int      i;
+       QResults queries[] = {{"j:sittingbull.jpg", 1}, {"file:custer", 0}, {"file:custer.jpg", 1}};
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+               if (g_test_verbose())
+                       g_print("query: %s\n", queries[i].query);
+               g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+                                ==,
+                                queries[i].count);
+       }
+}
+
+static void
+test_mu_query_msgid(void)
+{
+       int      i;
+       QResults queries[] = {
+           {"i:CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz"
+            "=bfogtmbGGs5A@mail.gmail.com",
+            1},
+           {"msgid:CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz="
+            "bfogtmbGGs5A@mail.gmail.com",
+            1},
+
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+               if (g_test_verbose())
+                       g_print("query: %s\n", queries[i].query);
+               g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+                                ==,
+                                queries[i].count);
+       }
+}
+
+static void
+test_mu_query_tags(void)
+{
+       int      i;
+       QResults queries[] = {
+           {"x:paradise", 1},
+           {"tag:lost", 1},
+           {"tag:lost tag:paradise", 1},
+           {"tag:lost tag:horizon", 0},
+           {"tag:lost OR tag:horizon", 1},
+           {"tag:queensryche", 1},
+           {"tag:Queensrÿche", 1},
+           {"x:paradise,lost", 0},
+           {"x:paradise AND x:lost", 1},
+           {"x:\\\\backslash", 1},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_wom_bat(void)
+{
+       int      i;
+       QResults queries[] = {
+           {"maildir:/wom_bat", 3},
+           //{ "\"maildir:/wom bat\"", 3},
+           // as expected, no longer works with new parser
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_signed_encrypted(void)
+{
+       int      i;
+       QResults queries[] = {
+           {"flag:encrypted", 2},
+           {"flag:signed", 2},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_multi_to_cc(void)
+{
+       int      i;
+       QResults queries[] = {
+           {"to:a@example.com", 1},
+           {"cc:d@example.com", 1},
+           {"to:b@example.com", 1},
+           {"cc:e@example.com", 1},
+           {"cc:e@example.com AND cc:d@example.com", 1},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+                                ==,
+                                queries[i].count);
+}
+
+static void
+test_mu_query_tags_02(void)
+{
+       int      i;
+       QResults queries[] = {
+           {"x:paradise", 1},
+           {"tag:@NextActions", 1},
+           {"x:queensrÿche", 1},
+           {"tag:lost OR tag:operation*", 2},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+               g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+                                ==,
+                                queries[i].count);
+       }
+}
+
+/* Tests for https://github.com/djcb/mu/issues/380
+
+   On certain platforms, something goes wrong during compilation and the
+   --related option doesn't work.
+*/
+static void
+test_mu_query_threads_compilation_error(void)
+{
+       const auto xpath = make_database(MU_TESTMAILDIR);
+
+       g_assert_cmpuint(run_and_count_matches(xpath, "msgid:uwsireh25.fsf@one.dot.net"), ==, 1);
+
+       g_assert_cmpuint(run_and_count_matches(xpath,
+                                              "msgid:uwsireh25.fsf@one.dot.net",
+                                              QueryFlags::IncludeRelated),
+                        ==,
+                        3);
+}
+
+/* https://github.com/djcb/mu/issues/1428 */
+static void
+test_mu_query_cjk(void)
+{
+       /* XXX: this doesn't pass yet; return for now */
+       g_test_skip("skip CJK tests");
+       return;
+
+       {
+               g_unsetenv("XAPIAN_CJK_NGRAM");
+               const auto xpath = make_database(MU_TESTMAILDIR_CJK);
+               g_assert_cmpuint(run_and_count_matches(xpath,
+                                                      "サーバがダウンしました",
+                                                      QueryFlags::None),
+                                ==, 1);
+               g_assert_cmpuint(run_and_count_matches(xpath,
+                                                      "サーバ",
+                                                      QueryFlags::None),
+                                ==, 0);
+       }
+
+       {
+               g_setenv("XAPIAN_CJK_NGRAM", "1", TRUE);
+               const auto xpath = make_database(MU_TESTMAILDIR_CJK);
+               g_assert_cmpuint(run_and_count_matches(xpath,
+                                                      "サーバがダウンしました",
+                                                      QueryFlags::None),
+                                ==, 0);
+               g_assert_cmpuint(run_and_count_matches(xpath,
+                                                      "サーバ",
+                                                      QueryFlags::None),
+                                ==, 0);
+       }
+}
+
+int
+main(int argc, char* argv[])
+{
+       int rv;
+
+       setlocale(LC_ALL, "");
+
+       mu_test_init(&argc, &argv);
+       DB_PATH1 = make_database(MU_TESTMAILDIR);
+       g_assert_false(DB_PATH1.empty());
+
+       DB_PATH2 = make_database(MU_TESTMAILDIR2);
+       g_assert_false(DB_PATH2.empty());
+
+       g_test_add_func("/mu-query/test-mu-query-01", test_mu_query_01);
+       g_test_add_func("/mu-query/test-mu-query-02", test_mu_query_02);
+       g_test_add_func("/mu-query/test-mu-query-03", test_mu_query_03);
+       g_test_add_func("/mu-query/test-mu-query-04", test_mu_query_04);
+
+       g_test_add_func("/mu-query/test-mu-query-signed-encrypted", test_mu_query_signed_encrypted);
+       g_test_add_func("/mu-query/test-mu-query-multi-to-cc", test_mu_query_multi_to_cc);
+       g_test_add_func("/mu-query/test-mu-query-logic", test_mu_query_logic);
+
+       g_test_add_func("/mu-query/test-mu-query-accented-chars-1",
+                       test_mu_query_accented_chars_01);
+       g_test_add_func("/mu-query/test-mu-query-accented-chars-2",
+                       test_mu_query_accented_chars_02);
+       g_test_add_func("/mu-query/test-mu-query-accented-chars-fraiche",
+                       test_mu_query_accented_chars_fraiche);
+
+       g_test_add_func("/mu-query/test-mu-query-msgid", test_mu_query_msgid);
+
+       g_test_add_func("/mu-query/test-mu-query-wom-bat", test_mu_query_wom_bat);
+
+       g_test_add_func("/mu-query/test-mu-query-wildcards", test_mu_query_wildcards);
+       g_test_add_func("/mu-query/test-mu-query-sizes", test_mu_query_sizes);
+
+       g_test_add_func("/mu-query/test-mu-query-dates-helsinki", test_mu_query_dates_helsinki);
+       g_test_add_func("/mu-query/test-mu-query-dates-sydney", test_mu_query_dates_sydney);
+       g_test_add_func("/mu-query/test-mu-query-dates-la", test_mu_query_dates_la);
+
+       g_test_add_func("/mu-query/test-mu-query-attach", test_mu_query_attach);
+       g_test_add_func("/mu-query/test-mu-query-tags", test_mu_query_tags);
+       g_test_add_func("/mu-query/test-mu-query-tags_02", test_mu_query_tags_02);
+
+       g_test_add_func("/mu-query/test-mu-query-threads-compilation-error",
+                       test_mu_query_threads_compilation_error);
+
+       g_test_add_func("/mu-query/test-mu-query-cjk",
+                       test_mu_query_cjk);
+       rv = g_test_run();
+
+       return rv;
+}
diff --git a/mu4e/Makefile.am b/mu4e/Makefile.am
new file mode 100644 (file)
index 0000000..92bf39d
--- /dev/null
@@ -0,0 +1,68 @@
+## Copyright (C) 2008-2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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 $(top_srcdir)/gtest.mk
+
+SUBDIRS=
+
+info_TEXINFOS=mu4e.texi
+mu4e_TEXINFOS=fdl.texi
+
+dist_lisp_LISP=                        \
+       mu4e-actions.el         \
+       mu4e-bookmarks.el       \
+       mu4e-compose.el         \
+       mu4e-contacts.el        \
+       mu4e-context.el         \
+       mu4e-contrib.el         \
+       mu4e-draft.el           \
+       mu4e-folders.el         \
+       mu4e-headers.el         \
+       mu4e-icalendar.el       \
+       mu4e-lists.el           \
+       mu4e-helpers.el         \
+       mu4e-main.el            \
+       mu4e-mark.el            \
+       mu4e-message.el         \
+       mu4e-config.el          \
+       mu4e-org.el             \
+       mu4e-search.el          \
+       mu4e-server.el          \
+       mu4e-speedbar.el        \
+       mu4e-update.el          \
+       mu4e-vars.el            \
+       mu4e-view.el            \
+       mu4e.el                 \
+       obsolete/org-mu4e.el
+
+
+EXTRA_DIST=                    \
+       mu4e-about.org
+
+CLEANFILES=                    \
+       *.elc
+
+doc_DATA =                     \
+       mu4e-about.org
+
+##
+## Change as needed.
+##
+BUILDPATH=mu4e
+TEXINFO_CSS_PATH=~/Sources/ext/texinfo-css
+fancyhtml:
+       mkdir -p $(BUILDPATH) ; cp -R $(TEXINFO_CSS_PATH)/static $(BUILDPATH)
+       makeinfo --html --css-ref=static/css/texinfo-klare.css -o $(BUILDPATH) mu4e.texi
diff --git a/mu4e/TODO b/mu4e/TODO
new file mode 100644 (file)
index 0000000..327fc1b
--- /dev/null
+++ b/mu4e/TODO
@@ -0,0 +1,19 @@
+* TODO
+
+*** mu4e-get-sub-maildirs
+*** extract mailing list name
+*** mark thread
+*** bounce support
+*** sorting
+*** tool bars
+*** refiling-by-pattern
+*** inspect message (muile)
+*** message statistics
+*** include exchange handling / org integration
+*** integrate bbdb
+*** identity support
+
+
+# Local Variables:
+# mode: org; org-startup-folded: nil
+# End:
diff --git a/mu4e/fdl.texi b/mu4e/fdl.texi
new file mode 100644 (file)
index 0000000..96ce74e
--- /dev/null
@@ -0,0 +1,451 @@
+@c The GNU Free Documentation License.
+@center Version 1.2, November 2002
+
+@c This file is intended to be included within another document,
+@c hence no sectioning command or @node.
+
+@display
+Copyright @copyright{} 2000,2001,2002 Free Software Foundation, Inc.
+51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+@end display
+
+@enumerate 0
+@item
+PREAMBLE
+
+The purpose of this License is to make a manual, textbook, or other
+functional and useful document @dfn{free} in the sense of freedom: to
+assure everyone the effective freedom to copy and redistribute it,
+with or without modifying it, either commercially or noncommercially.
+Secondarily, this License preserves for the author and publisher a way
+to get credit for their work, while not being considered responsible
+for modifications made by others.
+
+This License is a kind of ``copyleft'', which means that derivative
+works of the document must themselves be free in the same sense.  It
+complements the GNU General Public License, which is a copyleft
+license designed for free software.
+
+We have designed this License in order to use it for manuals for free
+software, because free software needs free documentation: a free
+program should come with manuals providing the same freedoms that the
+software does.  But this License is not limited to software manuals;
+it can be used for any textual work, regardless of subject matter or
+whether it is published as a printed book.  We recommend this License
+principally for works whose purpose is instruction or reference.
+
+@item
+APPLICABILITY AND DEFINITIONS
+
+This License applies to any manual or other work, in any medium, that
+contains a notice placed by the copyright holder saying it can be
+distributed under the terms of this License.  Such a notice grants a
+world-wide, royalty-free license, unlimited in duration, to use that
+work under the conditions stated herein.  The ``Document'', below,
+refers to any such manual or work.  Any member of the public is a
+licensee, and is addressed as ``you''.  You accept the license if you
+copy, modify or distribute the work in a way requiring permission
+under copyright law.
+
+A ``Modified Version'' of the Document means any work containing the
+Document or a portion of it, either copied verbatim, or with
+modifications and/or translated into another language.
+
+A ``Secondary Section'' is a named appendix or a front-matter section
+of the Document that deals exclusively with the relationship of the
+publishers or authors of the Document to the Document's overall
+subject (or to related matters) and contains nothing that could fall
+directly within that overall subject.  (Thus, if the Document is in
+part a textbook of mathematics, a Secondary Section may not explain
+any mathematics.)  The relationship could be a matter of historical
+connection with the subject or with related matters, or of legal,
+commercial, philosophical, ethical or political position regarding
+them.
+
+The ``Invariant Sections'' are certain Secondary Sections whose titles
+are designated, as being those of Invariant Sections, in the notice
+that says that the Document is released under this License.  If a
+section does not fit the above definition of Secondary then it is not
+allowed to be designated as Invariant.  The Document may contain zero
+Invariant Sections.  If the Document does not identify any Invariant
+Sections then there are none.
+
+The ``Cover Texts'' are certain short passages of text that are listed,
+as Front-Cover Texts or Back-Cover Texts, in the notice that says that
+the Document is released under this License.  A Front-Cover Text may
+be at most 5 words, and a Back-Cover Text may be at most 25 words.
+
+A ``Transparent'' copy of the Document means a machine-readable copy,
+represented in a format whose specification is available to the
+general public, that is suitable for revising the document
+straightforwardly with generic text editors or (for images composed of
+pixels) generic paint programs or (for drawings) some widely available
+drawing editor, and that is suitable for input to text formatters or
+for automatic translation to a variety of formats suitable for input
+to text formatters.  A copy made in an otherwise Transparent file
+format whose markup, or absence of markup, has been arranged to thwart
+or discourage subsequent modification by readers is not Transparent.
+An image format is not Transparent if used for any substantial amount
+of text.  A copy that is not ``Transparent'' is called ``Opaque''.
+
+Examples of suitable formats for Transparent copies include plain
+@sc{ascii} without markup, Texinfo input format, La@TeX{} input
+format, @acronym{SGML} or @acronym{XML} using a publicly available
+@acronym{DTD}, and standard-conforming simple @acronym{HTML},
+PostScript or @acronym{PDF} designed for human modification.  Examples
+of transparent image formats include @acronym{PNG}, @acronym{XCF} and
+@acronym{JPG}.  Opaque formats include proprietary formats that can be
+read and edited only by proprietary word processors, @acronym{SGML} or
+@acronym{XML} for which the @acronym{DTD} and/or processing tools are
+not generally available, and the machine-generated @acronym{HTML},
+PostScript or @acronym{PDF} produced by some word processors for
+output purposes only.
+
+The ``Title Page'' means, for a printed book, the title page itself,
+plus such following pages as are needed to hold, legibly, the material
+this License requires to appear in the title page.  For works in
+formats which do not have any title page as such, ``Title Page'' means
+the text near the most prominent appearance of the work's title,
+preceding the beginning of the body of the text.
+
+A section ``Entitled XYZ'' means a named subunit of the Document whose
+title either is precisely XYZ or contains XYZ in parentheses following
+text that translates XYZ in another language.  (Here XYZ stands for a
+specific section name mentioned below, such as ``Acknowledgements'',
+``Dedications'', ``Endorsements'', or ``History''.)  To ``Preserve the Title''
+of such a section when you modify the Document means that it remains a
+section ``Entitled XYZ'' according to this definition.
+
+The Document may include Warranty Disclaimers next to the notice which
+states that this License applies to the Document.  These Warranty
+Disclaimers are considered to be included by reference in this
+License, but only as regards disclaiming warranties: any other
+implication that these Warranty Disclaimers may have is void and has
+no effect on the meaning of this License.
+
+@item
+VERBATIM COPYING
+
+You may copy and distribute the Document in any medium, either
+commercially or noncommercially, provided that this License, the
+copyright notices, and the license notice saying this License applies
+to the Document are reproduced in all copies, and that you add no other
+conditions whatsoever to those of this License.  You may not use
+technical measures to obstruct or control the reading or further
+copying of the copies you make or distribute.  However, you may accept
+compensation in exchange for copies.  If you distribute a large enough
+number of copies you must also follow the conditions in section 3.
+
+You may also lend copies, under the same conditions stated above, and
+you may publicly display copies.
+
+@item
+COPYING IN QUANTITY
+
+If you publish printed copies (or copies in media that commonly have
+printed covers) of the Document, numbering more than 100, and the
+Document's license notice requires Cover Texts, you must enclose the
+copies in covers that carry, clearly and legibly, all these Cover
+Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
+the back cover.  Both covers must also clearly and legibly identify
+you as the publisher of these copies.  The front cover must present
+the full title with all words of the title equally prominent and
+visible.  You may add other material on the covers in addition.
+Copying with changes limited to the covers, as long as they preserve
+the title of the Document and satisfy these conditions, can be treated
+as verbatim copying in other respects.
+
+If the required texts for either cover are too voluminous to fit
+legibly, you should put the first ones listed (as many as fit
+reasonably) on the actual cover, and continue the rest onto adjacent
+pages.
+
+If you publish or distribute Opaque copies of the Document numbering
+more than 100, you must either include a machine-readable Transparent
+copy along with each Opaque copy, or state in or with each Opaque copy
+a computer-network location from which the general network-using
+public has access to download using public-standard network protocols
+a complete Transparent copy of the Document, free of added material.
+If you use the latter option, you must take reasonably prudent steps,
+when you begin distribution of Opaque copies in quantity, to ensure
+that this Transparent copy will remain thus accessible at the stated
+location until at least one year after the last time you distribute an
+Opaque copy (directly or through your agents or retailers) of that
+edition to the public.
+
+It is requested, but not required, that you contact the authors of the
+Document well before redistributing any large number of copies, to give
+them a chance to provide you with an updated version of the Document.
+
+@item
+MODIFICATIONS
+
+You may copy and distribute a Modified Version of the Document under
+the conditions of sections 2 and 3 above, provided that you release
+the Modified Version under precisely this License, with the Modified
+Version filling the role of the Document, thus licensing distribution
+and modification of the Modified Version to whoever possesses a copy
+of it.  In addition, you must do these things in the Modified Version:
+
+@enumerate A
+@item
+Use in the Title Page (and on the covers, if any) a title distinct
+from that of the Document, and from those of previous versions
+(which should, if there were any, be listed in the History section
+of the Document).  You may use the same title as a previous version
+if the original publisher of that version gives permission.
+
+@item
+List on the Title Page, as authors, one or more persons or entities
+responsible for authorship of the modifications in the Modified
+Version, together with at least five of the principal authors of the
+Document (all of its principal authors, if it has fewer than five),
+unless they release you from this requirement.
+
+@item
+State on the Title page the name of the publisher of the
+Modified Version, as the publisher.
+
+@item
+Preserve all the copyright notices of the Document.
+
+@item
+Add an appropriate copyright notice for your modifications
+adjacent to the other copyright notices.
+
+@item
+Include, immediately after the copyright notices, a license notice
+giving the public permission to use the Modified Version under the
+terms of this License, in the form shown in the Addendum below.
+
+@item
+Preserve in that license notice the full lists of Invariant Sections
+and required Cover Texts given in the Document's license notice.
+
+@item
+Include an unaltered copy of this License.
+
+@item
+Preserve the section Entitled ``History'', Preserve its Title, and add
+to it an item stating at least the title, year, new authors, and
+publisher of the Modified Version as given on the Title Page.  If
+there is no section Entitled ``History'' in the Document, create one
+stating the title, year, authors, and publisher of the Document as
+given on its Title Page, then add an item describing the Modified
+Version as stated in the previous sentence.
+
+@item
+Preserve the network location, if any, given in the Document for
+public access to a Transparent copy of the Document, and likewise
+the network locations given in the Document for previous versions
+it was based on.  These may be placed in the ``History'' section.
+You may omit a network location for a work that was published at
+least four years before the Document itself, or if the original
+publisher of the version it refers to gives permission.
+
+@item
+For any section Entitled ``Acknowledgements'' or ``Dedications'', Preserve
+the Title of the section, and preserve in the section all the
+substance and tone of each of the contributor acknowledgements and/or
+dedications given therein.
+
+@item
+Preserve all the Invariant Sections of the Document,
+unaltered in their text and in their titles.  Section numbers
+or the equivalent are not considered part of the section titles.
+
+@item
+Delete any section Entitled ``Endorsements''.  Such a section
+may not be included in the Modified Version.
+
+@item
+Do not retitle any existing section to be Entitled ``Endorsements'' or
+to conflict in title with any Invariant Section.
+
+@item
+Preserve any Warranty Disclaimers.
+@end enumerate
+
+If the Modified Version includes new front-matter sections or
+appendices that qualify as Secondary Sections and contain no material
+copied from the Document, you may at your option designate some or all
+of these sections as invariant.  To do this, add their titles to the
+list of Invariant Sections in the Modified Version's license notice.
+These titles must be distinct from any other section titles.
+
+You may add a section Entitled ``Endorsements'', provided it contains
+nothing but endorsements of your Modified Version by various
+parties---for example, statements of peer review or that the text has
+been approved by an organization as the authoritative definition of a
+standard.
+
+You may add a passage of up to five words as a Front-Cover Text, and a
+passage of up to 25 words as a Back-Cover Text, to the end of the list
+of Cover Texts in the Modified Version.  Only one passage of
+Front-Cover Text and one of Back-Cover Text may be added by (or
+through arrangements made by) any one entity.  If the Document already
+includes a cover text for the same cover, previously added by you or
+by arrangement made by the same entity you are acting on behalf of,
+you may not add another; but you may replace the old one, on explicit
+permission from the previous publisher that added the old one.
+
+The author(s) and publisher(s) of the Document do not by this License
+give permission to use their names for publicity for or to assert or
+imply endorsement of any Modified Version.
+
+@item
+COMBINING DOCUMENTS
+
+You may combine the Document with other documents released under this
+License, under the terms defined in section 4 above for modified
+versions, provided that you include in the combination all of the
+Invariant Sections of all of the original documents, unmodified, and
+list them all as Invariant Sections of your combined work in its
+license notice, and that you preserve all their Warranty Disclaimers.
+
+The combined work need only contain one copy of this License, and
+multiple identical Invariant Sections may be replaced with a single
+copy.  If there are multiple Invariant Sections with the same name but
+different contents, make the title of each such section unique by
+adding at the end of it, in parentheses, the name of the original
+author or publisher of that section if known, or else a unique number.
+Make the same adjustment to the section titles in the list of
+Invariant Sections in the license notice of the combined work.
+
+In the combination, you must combine any sections Entitled ``History''
+in the various original documents, forming one section Entitled
+``History''; likewise combine any sections Entitled ``Acknowledgements'',
+and any sections Entitled ``Dedications''.  You must delete all
+sections Entitled ``Endorsements.''
+
+@item
+COLLECTIONS OF DOCUMENTS
+
+You may make a collection consisting of the Document and other documents
+released under this License, and replace the individual copies of this
+License in the various documents with a single copy that is included in
+the collection, provided that you follow the rules of this License for
+verbatim copying of each of the documents in all other respects.
+
+You may extract a single document from such a collection, and distribute
+it individually under this License, provided you insert a copy of this
+License into the extracted document, and follow this License in all
+other respects regarding verbatim copying of that document.
+
+@item
+AGGREGATION WITH INDEPENDENT WORKS
+
+A compilation of the Document or its derivatives with other separate
+and independent documents or works, in or on a volume of a storage or
+distribution medium, is called an ``aggregate'' if the copyright
+resulting from the compilation is not used to limit the legal rights
+of the compilation's users beyond what the individual works permit.
+When the Document is included in an aggregate, this License does not
+apply to the other works in the aggregate which are not themselves
+derivative works of the Document.
+
+If the Cover Text requirement of section 3 is applicable to these
+copies of the Document, then if the Document is less than one half of
+the entire aggregate, the Document's Cover Texts may be placed on
+covers that bracket the Document within the aggregate, or the
+electronic equivalent of covers if the Document is in electronic form.
+Otherwise they must appear on printed covers that bracket the whole
+aggregate.
+
+@item
+TRANSLATION
+
+Translation is considered a kind of modification, so you may
+distribute translations of the Document under the terms of section 4.
+Replacing Invariant Sections with translations requires special
+permission from their copyright holders, but you may include
+translations of some or all Invariant Sections in addition to the
+original versions of these Invariant Sections.  You may include a
+translation of this License, and all the license notices in the
+Document, and any Warranty Disclaimers, provided that you also include
+the original English version of this License and the original versions
+of those notices and disclaimers.  In case of a disagreement between
+the translation and the original version of this License or a notice
+or disclaimer, the original version will prevail.
+
+If a section in the Document is Entitled ``Acknowledgements'',
+``Dedications'', or ``History'', the requirement (section 4) to Preserve
+its Title (section 1) will typically require changing the actual
+title.
+
+@item
+TERMINATION
+
+You may not copy, modify, sublicense, or distribute the Document except
+as expressly provided for under this License.  Any other attempt to
+copy, modify, sublicense or distribute the Document is void, and will
+automatically terminate your rights under this License.  However,
+parties who have received copies, or rights, from you under this
+License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+@item
+FUTURE REVISIONS OF THIS LICENSE
+
+The Free Software Foundation may publish new, revised versions
+of the GNU Free Documentation License from time to time.  Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.  See
+@uref{http://www.gnu.org/copyleft/}.
+
+Each version of the License is given a distinguishing version number.
+If the Document specifies that a particular numbered version of this
+License ``or any later version'' applies to it, you have the option of
+following the terms and conditions either of that specified version or
+of any later version that has been published (not as a draft) by the
+Free Software Foundation.  If the Document does not specify a version
+number of this License, you may choose any version ever published (not
+as a draft) by the Free Software Foundation.
+@end enumerate
+
+@page
+@heading ADDENDUM: How to use this License for your documents
+
+To use this License in a document you have written, include a copy of
+the License in the document and put the following copyright and
+license notices just after the title page:
+
+@smallexample
+@group
+  Copyright (C)  @var{year}  @var{your name}.
+  Permission is granted to copy, distribute and/or modify this document
+  under the terms of the GNU Free Documentation License, Version 1.2
+  or any later version published by the Free Software Foundation;
+  with no Invariant Sections, no Front-Cover Texts, and no Back-Cover
+  Texts.  A copy of the license is included in the section entitled ``GNU
+  Free Documentation License''.
+@end group
+@end smallexample
+
+If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
+replace the ``with@dots{}Texts.'' line with this:
+
+@smallexample
+@group
+    with the Invariant Sections being @var{list their titles}, with
+    the Front-Cover Texts being @var{list}, and with the Back-Cover Texts
+    being @var{list}.
+@end group
+@end smallexample
+
+If you have Invariant Sections without Cover Texts, or some other
+combination of the three, merge those two alternatives to suit the
+situation.
+
+If your document contains nontrivial examples of program code, we
+recommend releasing these examples in parallel under your choice of
+free software license, such as the GNU General Public License,
+to permit their use in free software.
+
+@c Local Variables:
+@c ispell-local-pdict: "ispell-dict"
+@c End:
+
diff --git a/mu4e/meson.build b/mu4e/meson.build
new file mode 100644 (file)
index 0000000..27b676b
--- /dev/null
@@ -0,0 +1,124 @@
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 3 of the License, 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.
+
+
+# generate some build data for use in mu4e
+mu4e_meta = configure_file(
+  input: 'mu4e-config.el.in',
+  output: 'mu4e-config.el',
+  install: true,
+  install_dir: mu4e_lispdir,
+  configuration: {
+      'VERSION'         : meson.project_version(),
+      # project_build_root() with meson >= 0.56
+      'abs_top_builddir': join_paths(meson.current_build_dir()),
+      'MU_DOC_DIR'      : join_paths(datadir, 'doc', 'mu'),
+    })
+
+mu4e_srcs=[
+  'mu4e-actions.el',
+  'mu4e-bookmarks.el',
+  'mu4e-compose.el',
+  'mu4e-contacts.el',
+  'mu4e-context.el',
+  'mu4e-contrib.el',
+  'mu4e-draft.el',
+  'mu4e-folders.el',
+  'mu4e.el',
+  'mu4e-headers.el',
+  'mu4e-helpers.el',
+  'mu4e-icalendar.el',
+  'mu4e-lists.el',
+  'mu4e-main.el',
+  'mu4e-mark.el',
+  'mu4e-message.el',
+  'mu4e-org.el',
+  'mu4e-search.el',
+  'mu4e-server.el',
+  'mu4e-speedbar.el',
+  'mu4e-update.el',
+  'mu4e-vars.el',
+  'mu4e-view.el',
+  'obsolete/org-mu4e.el',
+]
+
+# note, we cannot compile mu4e-config.el without incurring
+#  WARNING: Source item
+#   '[...]/build/mu4e/mu4e-meta.el' cannot be converted to File object, because
+#   it is a generated file. This will become a hard error in the future.
+#
+#... so let's not do that!
+
+# hack-around for native compile issue: copy sources to builddir.
+# see: https://debbugs.gnu.org/db/47/47987.html
+foreach src : mu4e_srcs
+  configure_file(input: src, output:'@BASENAME@.el', copy:true)
+endforeach
+
+foreach src : mu4e_srcs
+  target_name= '@BASENAME@.elc'
+  target_path = join_paths(meson.current_build_dir(), target_name)
+  target_func = '(setq byte-compile-dest-file-function(lambda(_) "' + target_path + '"))'
+
+  custom_target(src.underscorify() + '_el',
+                build_by_default: true,
+                input: src,
+                output: target_name,
+                install_dir: mu4e_lispdir,
+                install: true,
+                command: [emacs,
+                          '--no-init-file',
+                          '--batch',
+                          '--eval', '(setq load-prefer-newer t)',
+                          '--eval', target_func,
+                          '--directory', meson.current_build_dir(),
+                          '--directory', meson.current_source_dir(),
+                          '--funcall', 'batch-byte-compile', '@INPUT@'])
+endforeach
+
+# also install the sources and the config
+install_data(mu4e_srcs, install_dir: mu4e_lispdir)
+
+# install mu4e-about.org
+install_data('mu4e-about.org', install_dir : join_paths(datadir, 'doc', 'mu'))
+
+if makeinfo.found()
+
+  fulldate = run_command('date', '+%d %B %Y', check:true).stdout().strip()
+  monthdate = run_command('date', '+%B %Y', check:true).stdout().strip()
+  version_texi_data = configuration_data({
+    'fulldate'  : fulldate,
+    'monthdate' : monthdate,
+    'version'   : meson.project_version(),
+  })
+  version_texi = configure_file(
+    input:  'version.texi.in',
+    output: 'version.texi',
+    configuration: version_texi_data)
+
+  custom_target('mu4e_info',
+                input: 'mu4e.texi',
+                output: 'mu4e.info',
+                install_dir: infodir,
+                install: true,
+                command: [makeinfo,
+                          '-o', join_paths(meson.current_build_dir(), 'mu4e.info'),
+                          join_paths(meson.current_source_dir(), 'mu4e.texi'),
+                          '-I', join_paths(meson.current_build_dir(), '..')])
+  if install_info.found()
+    meson.add_install_script(install_info_script, 'share/info', 'mu4e.info')
+  endif
+endif
diff --git a/mu4e/mu4e-about.org b/mu4e/mu4e-about.org
new file mode 100644 (file)
index 0000000..5c015b3
--- /dev/null
@@ -0,0 +1,15 @@
+#+STARTUP:showall
+* About mu4e
+
+  *mu4e* is an emacs e-mail client based on the [[http://djcbsoftware.nl/code/mu][mu]] email search engine. It was
+  written & designed by /Dirk-Jan C. Binnema/, with contributions from others.
+
+  *mu4e* and *mu* are free software, licensed under the terms of the [[http://www.gnu.org/licenses/gpl-3.0.html][GNU GPLv3]].
+
+  You can get the code from [[https://github.com/djcb/mu][the git repository]]; there, you can also
+  [[https://github.com/djcb/mu/issues][file bugs and feature requests]].
+
+  *mu4e* has its own [[info:mu4e][manual]], which includes an [[info:mu4e#FAQ%20-%20Frequently%20Anticipated%20Questions][FAQ]]. If that is not enough,
+   there's also the [[http://groups.google.com/group/mu-discuss][mu mailing list]].
+
+   [Press *q* to quit this buffer]
diff --git a/mu4e/mu4e-actions.el b/mu4e/mu4e-actions.el
new file mode 100644 (file)
index 0000000..bd4b338
--- /dev/null
@@ -0,0 +1,252 @@
+;;; mu4e-actions.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Example actions for messages, attachments (see chapter 'Actions' in the
+;; manual)
+
+;;; Code:
+
+(require 'ido)
+
+(require 'mu4e-helpers)
+(require 'mu4e-message)
+(require 'mu4e-search)
+(require 'mu4e-contacts)
+\f
+;;; Count lines
+
+(defun mu4e-action-count-lines (msg)
+  "Count the number of lines in the e-mail MSG.
+Works for headers view and message-view."
+  (message "Number of lines: %s"
+           (shell-command-to-string
+            (concat "wc -l < "
+                   (shell-quote-argument (mu4e-message-field msg :path))))))
+
+;;; Org Helpers
+
+(defvar mu4e-captured-message nil
+  "The most recently captured message.")
+
+(defun mu4e-action-capture-message (msg)
+  "Remember MSG.
+Later, we can create an attachment based on this message with
+`mu4e-compose-attach-captured-message'."
+  (setq mu4e-captured-message msg)
+  (message "Message has been captured"))
+
+
+(defun mu4e-action-copy-message-file-path (msg)
+  "Save the full path for the current MSG to the kill ring."
+  (kill-new (mu4e-message-field msg :path)))
+
+(defvar mu4e-org-contacts-file nil
+  "File to store contact information for org-contacts.
+Needed by `mu4e-action-add-org-contact'.")
+
+(eval-when-compile ;; silence compiler warning about free variable
+  (unless (require 'org-capture nil 'noerror)
+    (defvar org-capture-templates nil)))
+
+(defun mu4e-action-add-org-contact (msg)
+  "Add an org-contact based on the sender ddress of the current MSG.
+You need to set `mu4e-org-contacts-file' to the full path to the
+file where you store your org-contacts."
+  (unless (require 'org-capture nil 'noerror)
+    (mu4e-error "Feature org-capture is not available"))
+  (unless mu4e-org-contacts-file
+    (mu4e-error "Variable `mu4e-org-contacts-file' is nil"))
+  (let* ((sender (car-safe (mu4e-message-field msg :from)))
+         (name (mu4e-contact-name sender))
+        (email (mu4e-contact-email sender))
+         (blurb
+          (format
+           (concat
+            "* %%?%s\n"
+            ":PROPERTIES:\n"
+            ":EMAIL: %s\n"
+            ":NICK:\n"
+            ":BIRTHDAY:\n"
+            ":END:\n\n")
+           (or name email "")
+           (or email "")))
+         (key "mu4e-add-org-contact-key")
+         (org-capture-templates
+          (append org-capture-templates
+                  (list (list key "contacts" 'entry
+                              (list 'file mu4e-org-contacts-file) blurb)))))
+    (when (fboundp 'org-capture)
+      (org-capture nil key))))
+
+;;; Patches
+
+(defvar mu4e--patch-directory-history nil
+  "History of directories we have applied patches to.")
+
+;; This essentially works around the fact that read-directory-name
+;; can't have custom history.
+(defun mu4e--read-patch-directory (&optional prompt)
+  "Read a `PROMPT'ed directory name via `completing-read' with history."
+  (unless prompt
+    (setq prompt "Target directory:"))
+  (file-truename
+   (completing-read prompt 'read-file-name-internal #'file-directory-p
+                    nil nil 'mu4e--patch-directory-history)))
+
+(defun mu4e-action-git-apply-patch (msg)
+  "Apply `MSG' as a git patch."
+  (let ((path (mu4e--read-patch-directory "Target directory: ")))
+    (let ((default-directory path))
+      (shell-command
+       (format "git apply %s"
+               (shell-quote-argument (mu4e-message-field msg :path)))))))
+
+(defun mu4e-action-git-apply-mbox (msg &optional signoff)
+  "Apply `MSG' a git patch with optional `SIGNOFF'.
+
+If the `default-directory' matches the most recent history entry don't
+bother asking for the git tree again (useful for bulk actions)."
+
+  (let ((cwd (substring-no-properties
+              (or (car mu4e--patch-directory-history)
+                  "not-a-dir"))))
+    (unless (and (stringp cwd) (string= default-directory cwd))
+      (setq cwd (mu4e--read-patch-directory "Target directory: ")))
+    (let ((default-directory cwd))
+      (shell-command
+       (format "git am %s %s"
+               (if signoff "--signoff" "")
+               (shell-quote-argument (mu4e-message-field msg :path)))))))
+
+;;; Tagging
+
+(defvar mu4e-action-tags-header "X-Keywords"
+  "Header where tags are stored.
+Used by `mu4e-action-retag-message'. Make sure it is one of the
+headers mu recognizes for storing tags: X-Keywords, X-Label,
+Keywords. Also note that changing this setting on already tagged
+messages can lead to messages with multiple tags headers.")
+
+(defvar mu4e-action-tags-completion-list '()
+  "List of tags for completion in `mu4e-action-retag-message'.")
+
+(defun mu4e--contains-line-matching (regexp path)
+  "Return non-nil if the file at PATH contain a line matching REGEXP.
+Otherwise return nil."
+  (with-temp-buffer
+    (insert-file-contents path)
+    (save-excursion
+      (goto-char (point-min))
+      (re-search-forward regexp nil t))))
+
+(defun mu4e--replace-first-line-matching (regexp to-string path)
+  "Replace first line matching REGEXP in PATH with TO-STRING."
+  (with-temp-file path
+    (insert-file-contents path)
+    (save-excursion
+      (goto-char (point-min))
+      (if (re-search-forward regexp nil t)
+          (replace-match to-string nil nil)))))
+
+(declare-function mu4e--server-add "mu4e-server")
+(defun mu4e--refresh-message (path)
+  "Re-parse message at PATH.
+if this works, we will
+receive (:info add :path <path> :docid <docid>) as well as (:update
+<msg-sexp>)."
+  (mu4e--server-add path))
+
+(defun mu4e-action-retag-message (msg &optional retag-arg)
+  "Change tags of MSG with RETAG-ARG.
+
+RETAG-ARG is a comma-separated list of additions and removals.
+
+Example: +tag,+long tag,-oldtag
+would add \"tag\" and \"long tag\", and remove \"oldtag\"."
+  (let* (
+         (path (mu4e-message-field msg :path))
+         (oldtags (mu4e-message-field msg :tags))
+         (tags-completion
+          (append
+           mu4e-action-tags-completion-list
+           (mapcar (lambda (tag) (format "+%s" tag))
+                   mu4e-action-tags-completion-list)
+           (mapcar (lambda (tag) (format "-%s" tag))
+                   oldtags)))
+         (retag (if retag-arg
+                    (split-string retag-arg ",")
+                  (completing-read-multiple "Tags: " tags-completion)))
+         (header  mu4e-action-tags-header)
+         (sep     (cond ((string= header "Keywords") ", ")
+                        ((string= header "X-Label") " ")
+                        ((string= header "X-Keywords") ", ")
+                        (t ", ")))
+         (taglist (if oldtags (copy-sequence oldtags) '()))
+         tagstr)
+    (dolist (tag retag taglist)
+      (cond
+       ((string-match "^\\+\\(.+\\)" tag)
+        (setq taglist (push (match-string 1 tag) taglist)))
+       ((string-match "^\\-\\(.+\\)" tag)
+        (setq taglist (delete (match-string 1 tag) taglist)))
+       (t
+        (setq taglist (push tag taglist)))))
+
+    (setq taglist (sort (delete-dups taglist) 'string<))
+    (setq tagstr (mapconcat 'identity taglist sep))
+
+    (setq tagstr (replace-regexp-in-string "[\\&]" "\\\\\\&" tagstr))
+    (setq tagstr (replace-regexp-in-string "[/]"   "\\&" tagstr))
+
+    (if (not (mu4e--contains-line-matching (concat header ":.*") path))
+        ;; Add tags header just before the content
+        (mu4e--replace-first-line-matching
+         "^$" (concat header ": " tagstr "\n") path)
+
+      ;; replaces keywords, restricted to the header
+      (mu4e--replace-first-line-matching
+       (concat header ":.*")
+       (concat header ": " tagstr)
+       path))
+
+    (mu4e-message (concat "tagging: " (mapconcat 'identity taglist ", ")))
+    (mu4e--refresh-message path)))
+
+(defun mu4e-action-show-thread (msg)
+  "Show thread for message at point with point remaining on MSG.
+I.e., point remains on the message with the message-id where the
+action was invoked. If invoked in view mode, continue to display
+the message."
+  (let ((msgid (mu4e-message-field msg :message-id)))
+    (when msgid
+      (let ((mu4e-search-threads t)
+            (mu4e-headers-include-related t))
+        (mu4e-search
+         (format "msgid:%s" msgid)
+         nil nil nil
+         msgid (and (eq major-mode 'mu4e-view-mode)
+                    (not (eq mu4e-split-view 'single-window))))))))
+;;; _
+(provide 'mu4e-actions)
+;;; mu4e-actions.el ends here
diff --git a/mu4e/mu4e-bookmarks.el b/mu4e/mu4e-bookmarks.el
new file mode 100644 (file)
index 0000000..89c6ffa
--- /dev/null
@@ -0,0 +1,133 @@
+;;; mu4e-bookmarks.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(require 'mu4e-helpers)
+
+\f
+;;; Configuration
+
+(defgroup mu4e-bookmarks nil
+  "Settings for bookmarks."
+  :group 'mu4e)
+
+(defcustom mu4e-bookmarks
+  '(( :name  "Unread messages"
+             :query "flag:unread AND NOT flag:trashed"
+             :key ?u)
+    ( :name "Today's messages"
+            :query "date:today..now"
+            :key ?t)
+    ( :name "Last 7 days"
+            :query "date:7d..now"
+            :hide-unread t
+            :key ?w)
+    ( :name "Messages with images"
+            :query "mime:image/*"
+            :key ?p))
+  "List of pre-defined queries that are shown on the main screen.
+
+Each of the list elements is a plist with at least:
+`:name'  - the name of the query
+`:query' - the query expression or function
+`:key'   - the shortcut key.
+
+Note that the :query parameter can be a function/lambda.
+
+Optionally, you can add the following: `:hide' - if t, the
+bookmark is hidden from the main-view and speedbar.
+`:hide-unread' - do not show the counts of unread/total number of
+matches for the query in the main-view. This can be useful if a
+bookmark uses a very slow query.
+
+`:hide-unread' is implied from `:hide'. Furthermore, it is
+implied when `:query' is a function.
+
+Note: for efficiency, queries used to determine the unread/all
+counts do not discard duplicate or unreadable messages. Thus, the
+numbers shown may differ from the number you get from a normal
+query."
+  :type '(repeat (plist))
+  :group 'mu4e-bookmarks)
+
+\f
+ (defun mu4e-ask-bookmark (prompt)
+  "Ask the user for a bookmark (using PROMPT) as defined in
+`mu4e-bookmarks', then return the corresponding query."
+  (unless (mu4e-bookmarks) (mu4e-error "No bookmarks defined"))
+  (let* ((prompt (mu4e-format "%s" prompt))
+         (bmarks
+          (mapconcat
+           (lambda (bm)
+             (concat
+              "[" (propertize (make-string 1 (plist-get bm :key))
+                              'face 'mu4e-highlight-face)
+              "]"
+              (plist-get bm :name))) (mu4e-bookmarks) ", "))
+         (kar (read-char (concat prompt bmarks))))
+    (mu4e-get-bookmark-query kar)))
+
+(defun mu4e-get-bookmark-query (kar)
+  "Get the corresponding bookmarked query for shortcut KAR.
+Raise an error if none is found."
+  (let* ((chosen-bm
+          (or (seq-find
+               (lambda (bm)
+                 (= kar (plist-get bm :key)))
+               (mu4e-bookmarks))
+              (mu4e-warn "Unknown shortcut '%c'" kar)))
+         (expr (plist-get chosen-bm :query))
+         (expr (if (not (functionp expr)) expr
+                 (funcall expr)))
+         (query (eval expr)))
+    (if (stringp query)
+        query
+      (mu4e-warn "Expression must evaluate to query string ('%S')" expr))))
+
+
+(defun mu4e-bookmark-define (query name key)
+  "Define a bookmark for QUERY with NAME and shortcut KEY.
+Append it to `mu4e-bookmarks'. Replaces any existing bookmark
+with KEY."
+  (setq mu4e-bookmarks
+        (seq-remove
+         (lambda (bm)
+           (= (plist-get bm :key) key))
+         (mu4e-bookmarks)))
+  (cl-pushnew `(:name  ,name
+                       :query ,query
+                       :key   ,key)
+              mu4e-bookmarks :test 'equal))
+
+(defun mu4e-bookmarks ()
+  "Get `mu4e-bookmarks' in the (new) format.
+Convert from the old format if needed."
+  (seq-map (lambda (item)
+             (if (and (listp item) (= (length item) 3))
+                 `(:name  ,(nth 1 item) :query ,(nth 0 item)
+                          :key   ,(nth 2 item))
+               item)) mu4e-bookmarks))
+\f
+(provide 'mu4e-bookmarks)
+;;; mu4e-bookmarks.el ends here
diff --git a/mu4e/mu4e-compose.el b/mu4e/mu4e-compose.el
new file mode 100644 (file)
index 0000000..eb9647b
--- /dev/null
@@ -0,0 +1,945 @@
+;;; mu4e-compose.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file, various functions to compose/send messages, piggybacking on
+;; gnus' message mode
+
+;; Magic / Rupe Goldberg
+
+;; 1) When we reply/forward a message, we get it from the backend, ie:
+;; we send to the backend (mu4e-compose):
+;;     compose type:reply docid:30935
+;; backend responds with:
+;;      (:compose reply :original ( .... <original message> ))
+
+;; 2) When we compose a message, message and headers are separated by
+;; `mail-header-separator', ie. '--text follows this line--. We use
+;; before-save-hook and after-save-hook to remove/re-add this special line, so
+;; it stays in the buffer, but never hits the disk.
+;; see:
+;;     mu4e~compose-insert-mail-header-separator
+;;     mu4e~compose-remove-mail-header-separator
+;;
+;; (maybe we can get away with remove it only just before sending? what does
+;; gnus do?)
+
+;; 3) When sending a message, we want to do a few things:
+;;   a) move the message from drafts to the sent folder (maybe; depends on
+;;      `mu4e-sent-messages-behavior')
+;;   b) if it's a reply, mark the replied-to message as "R", i.e. replied
+;;      if it's a forward, mark the forwarded message as "P", i.e.
+;;      passed (forwarded)
+;;   c) kill all buffers looking at the sent message
+
+;;  a) is dealt with by message-mode, but we need to tell it where to move the
+;;     sent message. We do this by adding an Fcc: header with the target folder,
+;;     see `mu4e~compose-setup-fcc-maybe'. Since message-mode does not natively
+;;     understand maildirs, we also need to tell it what to do, so we also set
+;;     `message-fcc-handler-function' there. Finally, we add the the message in
+;;     the sent-folder to the database.
+;;
+;;   b) this is handled in `mu4e~compose-set-parent-flag'
+;;
+;;   c) this is handled in our handler for the `sent'-message from the backend
+;;   (`mu4e-sent-handler')
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'message)
+(require 'mail-parse)
+(require 'smtpmail)
+
+(require 'mu4e-server)
+(require 'mu4e-actions)
+(require 'mu4e-message)
+(require 'mu4e-draft)
+(require 'mu4e-context)
+
+\f
+;;; Configuration
+;; see mu4e-drafts.el
+\f
+;;; Attachments
+(defun mu4e-compose-attach-message (msg)
+  "Insert message MSG as an attachment."
+  (let ((path (plist-get msg :path)))
+    (unless (file-exists-p path)
+      (mu4e-warn "Message file not found"))
+    (mml-attach-file
+     path
+     "message/rfc822"
+     (or (plist-get msg :subject) "No subject")
+     "attachment")))
+
+(defun mu4e-compose-attach-captured-message ()
+  "Insert the last captured message file as an attachment.
+Messages are captured with `mu4e-action-capture-message'."
+  (interactive)
+  (unless mu4e-captured-message
+    (mu4e-warn "No message has been captured"))
+  (mu4e-compose-attach-message mu4e-captured-message))
+
+;;; Misc
+
+;; 'fcc' refers to saving a copy of a sent message to a certain folder. that's
+
+;; what these 'Sent mail' folders are for!
+;;
+;; We let message mode take care of this by adding a field
+
+;;   Fcc: <full-path-to-message-in-target-folder>
+
+;; in the "message-send-hook" (ie., just before sending).  message mode will
+;; then take care of the saving when the message is actually sent.
+;;
+;; note, where and if you make this copy depends on the value of
+;; `mu4e-sent-messages-behavior'.
+
+(defun mu4e~compose-setup-fcc-maybe ()
+  "Maybe setup Fcc, based on `mu4e-sent-messages-behavior'.
+If needed, set the Fcc header, and register the handler function."
+  (let* ((sent-behavior
+          ;; Note; we cannot simply use functionp here, since at least
+          ;; delete is a function, too...
+          (if (member mu4e-sent-messages-behavior '(delete trash sent))
+              mu4e-sent-messages-behavior
+            (if (functionp mu4e-sent-messages-behavior)
+                (funcall mu4e-sent-messages-behavior)
+              mu4e-sent-messages-behavior)))
+         (mdir
+          (pcase sent-behavior
+            ('delete nil)
+            ('trash (mu4e-get-trash-folder mu4e-compose-parent-message))
+            ('sent (mu4e-get-sent-folder mu4e-compose-parent-message))
+            (_ (mu4e-error
+               "Unsupported value %S for `mu4e-sent-messages-behavior'"
+                mu4e-sent-messages-behavior))))
+         (fccfile (and mdir
+                       (concat (mu4e-root-maildir) mdir "/cur/"
+                               (mu4e~draft-message-filename-construct "S")))))
+    ;; if there's an fcc header, add it to the file
+    (when fccfile
+      (message-add-header (concat "Fcc: " fccfile "\n"))
+      ;; sadly, we cannot define as 'buffer-local'...  this will screw up gnus
+      ;; etc. if you run it after mu4e so, (hack hack) we reset it to the old
+      ;; handler after we've done our thing.
+      (setq message-fcc-handler-function
+            (let ((maildir mdir)
+                  (old-handler message-fcc-handler-function))
+              (lambda (file)
+                (setq message-fcc-handler-function old-handler)
+               ;; reset the fcc handler
+                (let ((mdir-path (concat (mu4e-root-maildir) maildir)))
+                  ;; Create the full maildir structure for the sent folder if it
+                  ;; doesn't exist. `mu4e--server-mkdir` runs asynchronously but
+                  ;; no matter whether it runs before or after `write-file`, the
+                  ;; sent maildir ends up in the correct state.
+                  (unless (file-exists-p mdir-path)
+                    (mu4e--server-mkdir mdir-path)))
+                (write-file file) ;; writing maildirs files is easy
+                (mu4e--server-add file))))))) ;; update the database
+
+(defvar mu4e-compose-hidden-headers
+  `("^References:" "^Face:" "^X-Face:"
+    "^X-Draft-From:" "^User-agent:")
+  "Hidden headers when composing.")
+
+(defun mu4e~compose-hide-headers ()
+  "Hide the headers as per `mu4e-compose-hidden-headers'."
+  (let ((message-hidden-headers mu4e-compose-hidden-headers))
+    (message-hide-headers)))
+
+(defconst mu4e~compose-address-fields-regexp
+  "^\\(To\\|B?Cc\\|Reply-To\\|From\\):")
+
+(defun mu4e~compose-register-message-save-hooks ()
+  "Just before saving, we remove the `mail-header-separator'.
+Just after saving we restore it; thus, the separator should never
+appear on disk. Also update the Date and ensure we have a
+Message-ID."
+  (add-hook 'before-save-hook
+            #'mu4e~compose-before-save-hook-fn
+            nil t)
+  (add-hook 'after-save-hook
+            #'mu4e~compose-after-save-hook-fn
+            nil t))
+
+(defvar-local mu4e~compose-undo nil
+  "Remember the undo-state.")
+
+(defun mu4e~compose-before-save-hook-fn ()
+  "Add the message-id if necessary and update the date."
+  (setq mu4e~compose-undo buffer-undo-list)
+  (save-excursion
+    (save-restriction
+      (message-narrow-to-headers)
+      (unless (message-fetch-field "Message-ID")
+        (message-generate-headers '(Message-ID)))
+      (message-generate-headers '(Date)))
+    (save-match-data
+      (mu4e~draft-remove-mail-header-separator))))
+
+(defun mu4e~compose-after-save-hook-fn ()
+  (save-match-data
+    (mu4e~compose-set-friendly-buffer-name)
+    (mu4e~draft-insert-mail-header-separator)
+    ;; hide some headers again
+    (widen)
+    (mu4e~compose-hide-headers)
+    (set-buffer-modified-p nil)
+    (mu4e-message "Saved (%d lines)" (count-lines (point-min) (point-max)))
+    ;; update the file on disk -- ie., without the separator
+    (mu4e--server-add (buffer-file-name)))
+  (setq buffer-undo-list mu4e~compose-undo))
+
+\f
+;;; address completion
+
+;; inspired by org-contacts.el and
+;; https://github.com/nordlow/elisp/blob/master/mine/completion-styles-cycle.el
+
+(defun mu4e~compose-complete-handler (str pred action)
+  "Complete address STR with predication PRED for ACTION."
+  (cond
+   ((eq action nil)
+    (try-completion str mu4e--contacts-set pred))
+   ((eq action t)
+    (all-completions str mu4e--contacts-set pred))
+   ((eq action 'metadata)
+    ;; our contacts are already sorted - just need to tell the
+    ;; completion machinery not to try to undo that...
+    '(metadata
+      (display-sort-function . identity)
+      (cycle-sort-function   . identity)))))
+
+(defun mu4e~compose-complete-contact (&optional start)
+  "Complete the text at START with a contact.
+Ie. either \"name <email>\" or \"email\")."
+  (interactive)
+  (let ((mail-abbrev-mode-regexp mu4e~compose-address-fields-regexp)
+        (eoh ;; end-of-headers
+         (save-excursion
+           (goto-char (point-min))
+           (search-forward-regexp mail-header-separator nil t))))
+    ;; try to complete only when we're in the headers area,
+    ;; looking  at an address field.
+    (when (and eoh (> eoh (point)) (mail-abbrev-in-expansion-header-p))
+      (let* ((end (point))
+             (start
+              (or start
+                  (save-excursion
+                    (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
+                    (goto-char (match-end 0))
+                    (point)))))
+        (list start end 'mu4e~compose-complete-handler)))))
+
+(defun mu4e~compose-setup-completion ()
+  "Set up auto-completion of addresses."
+  (set (make-local-variable 'completion-ignore-case) t)
+  (set (make-local-variable 'completion-cycle-threshold) 7)
+  (add-to-list (make-local-variable 'completion-styles) 'substring)
+  (add-hook 'completion-at-point-functions
+            'mu4e~compose-complete-contact nil t))
+
+(defun mu4e~remove-refs-maybe ()
+  "Remove References: if In-Reply-To: is missing.
+This allows the user to effectively start a new message-thread by
+removing the In-Reply-To header."
+  (unless (message-fetch-field "in-reply-to")
+    (message-remove-header "References")))
+
+;;; Compose Mode
+
+(defvar mu4e-compose-mode-map nil
+  "Keymap for \"*mu4e-compose*\" buffers.")
+(unless mu4e-compose-mode-map
+  (setq mu4e-compose-mode-map
+        (let ((map (make-sparse-keymap)))
+          (define-key map (kbd "C-S-u")   'mu4e-update-mail-and-index)
+          (define-key map (kbd "C-c C-u") 'mu4e-update-mail-and-index)
+          (define-key map (kbd "C-c C-k") 'mu4e-message-kill-buffer)
+         (define-key map (kbd "C-c ;")   'mu4e-compose-context-switch)
+          (define-key map (kbd "M-q")     'mu4e-fill-paragraph)
+          map)))
+
+(defun mu4e-fill-paragraph (&optional region)
+  "Re-layout either the whole message or REGION.
+If variable `use-hard-newlines', takes a multi-line paragraph and
+makes it into a single line of text. Assume paragraphs are
+separated by blank lines. If variable `use-hard-newlines' is not
+set, this simply executes `fill-paragraph'."
+  ;; Inspired by https://www.emacswiki.org/emacs/UnfillParagraph
+  (interactive (progn (barf-if-buffer-read-only) '(t)))
+  (ignore-errors
+    (if mu4e-compose-format-flowed
+        (let ((fill-column (point-max))
+              (use-hard-newlines nil)); rfill "across" hard newlines
+          (when (use-region-p)
+            (delete-trailing-whitespace (region-beginning) (region-end)))
+          (fill-paragraph nil region))
+      (when (use-region-p)
+        (delete-trailing-whitespace (region-beginning) (region-end)))
+      (fill-paragraph nil region))))
+
+(defun mu4e-toggle-use-hard-newlines ()
+  (interactive)
+  (setq use-hard-newlines (not use-hard-newlines))
+  (if use-hard-newlines
+      (turn-off-auto-fill)
+    (turn-on-auto-fill)))
+
+(defun mu4e~compose-remap-faces ()
+  "Remap `message-mode' faces to mu4e ones.
+Our parent `message-mode' uses font-locking for the compose
+buffers; lets remap its faces so it uses the ones for mu4e."
+  ;; normal headers
+  (face-remap-add-relative 'message-header-name
+                           '((:inherit mu4e-header-key-face)))
+  (face-remap-add-relative 'message-header-other
+                           '((:inherit mu4e-header-value-face)))
+  ;; special headers
+  (face-remap-add-relative 'message-header-from
+                           '((:inherit mu4e-contact-face)))
+  (face-remap-add-relative 'message-header-to
+                           '((:inherit mu4e-contact-face)))
+  (face-remap-add-relative 'message-header-cc
+                           '((:inherit mu4e-contact-face)))
+  (face-remap-add-relative 'message-header-bcc
+                           '((:inherit mu4e-contact-face)))
+  (face-remap-add-relative 'message-header-subject
+                           '((:inherit mu4e-special-header-value-face)))
+  ;; citation
+  (face-remap-add-relative 'message-cited-text
+                           '((:inherit mu4e-cited-1-face))))
+
+(define-derived-mode mu4e-compose-mode message-mode "mu4e:compose"
+  "Major mode for the mu4e message composition, derived from `message-mode'.
+\\{message-mode-map}."
+  (progn
+    (use-local-map mu4e-compose-mode-map)
+
+    (mu4e-context-minor-mode)
+    (define-key mu4e-context-minor-mode-map (kbd ";") nil)
+    (define-key mu4e-context-minor-mode-map (kbd "C-c C-;")
+      #'mu4e-compose-context-switch)
+
+    (set (make-local-variable 'message-signature) mu4e-compose-signature)
+    ;; set this to allow mu4e to work when gnus-agent is unplugged in gnus
+    (set (make-local-variable 'message-send-mail-real-function) nil)
+    ;; Set to nil to enable `electric-quote-local-mode' to work:
+    (make-local-variable 'comment-use-syntax)
+    (setq comment-use-syntax nil)
+    ;; message-mode has font-locking, but uses its own faces. Let's
+    ;; use the mu4e-specific ones instead
+    (mu4e~compose-remap-faces)
+    (mu4e~compose-register-message-save-hooks)
+    ;; offer completion for e-mail addresses
+    (when mu4e-compose-complete-addresses
+      (unless mu4e--contacts-set
+       ;; work-around for https://github.com/djcb/mu/issues/1016
+        (mu4e--request-contacts-maybe))
+      (mu4e~compose-setup-completion))
+    (if mu4e-compose-format-flowed
+        (progn
+          (turn-off-auto-fill)
+          (setq truncate-lines nil
+                word-wrap t
+                mml-enable-flowed t
+                use-hard-newlines t)
+          (visual-line-mode t))
+      (setq mml-enable-flowed nil))
+
+    ;; set the attachment dir to something more reasonable than the draft
+    ;; directory.
+    (setq default-directory (mu4e~get-attachment-dir))
+
+    (let ((keymap (lookup-key message-mode-map [menu-bar text])))
+      (when keymap
+        (define-key-after
+          keymap
+          [mu4e-hard-newlines]
+          '(menu-item "Format=flowed" mu4e-toggle-use-hard-newlines
+                      :button (:toggle . use-hard-newlines)
+                      :help "Toggle format=flowed"
+                      :visible (eq major-mode 'mu4e-compose-mode)
+                      :enable mu4e-compose-format-flowed)
+          'sep)
+
+        (define-key-after
+          keymap
+          [mu4e-electric-quote-mode]
+          '(menu-item "Electric quote" electric-quote-local-mode
+                      :button (:toggle . electric-quote-mode)
+                      :help "Toggle Electric quote mode"
+                      :visible (and (eq major-mode 'mu4e-compose-mode)
+                                    (functionp 'electric-quote-local-mode)))
+          'mu4e-hard-newlines)))
+
+    (when (lookup-key mml-mode-map [menu-bar Attachments])
+      (define-key-after
+        (lookup-key mml-mode-map [menu-bar Attachments])
+        [mu4e-compose-attach-captured-message]
+        '(menu-item "Attach captured message"
+                    mu4e-compose-attach-captured-message
+                    :help "Attach message captured in Headers View (with 'a c')"
+                    :visible (eq major-mode 'mu4e-compose-mode))
+        (quote Attach\ External...)))
+
+    ;; setup the fcc-stuff, if needed
+    (add-hook 'message-send-hook
+              #'mu4e~setup-fcc-message-sent-hook-fn
+               nil t)
+    ;; when the message has been sent.
+    (add-hook 'message-sent-hook
+              #'mu4e~set-sent-handler-message-sent-hook-fn
+              nil t))
+  ;; mark these two hooks as permanent-local, so they'll survive mode-changes
+  ;;  (put 'mu4e~compose-save-before-sending 'permanent-local-hook t)
+  (put 'mu4e~compose-mark-after-sending 'permanent-local-hook t))
+
+(defun mu4e~setup-fcc-message-sent-hook-fn ()
+  ;; mu4e~compose-save-before-sending
+  ;; when in-reply-to was removed, remove references as well.
+  (when (eq mu4e-compose-type 'reply)
+    (mu4e~remove-refs-maybe))
+  (when use-hard-newlines
+    (mu4e-send-harden-newlines))
+  ;; for safety, always save the draft before sending
+  (set-buffer-modified-p t)
+  (save-buffer)
+  (mu4e~compose-setup-fcc-maybe)
+  (widen))
+
+(defun mu4e~set-sent-handler-message-sent-hook-fn ()
+  ;;  mu4e~compose-mark-after-sending
+  (setq mu4e-sent-func 'mu4e-sent-handler)
+  (mu4e--server-sent (buffer-file-name)))
+
+(defun mu4e-send-harden-newlines ()
+  "Set the hard property to all newlines."
+  (save-excursion
+    (goto-char (point-min))
+    (while (search-forward "\n" nil t)
+      (put-text-property (1- (point)) (point) 'hard t))))
+
+(defconst mu4e~compose-buffer-max-name-length 30
+  "Maximum length of the mu4e-send-buffer-name.")
+
+(defun mu4e~compose-set-friendly-buffer-name (&optional compose-type)
+  "Set some user-friendly buffer name based on the COMPOSE-TYPE."
+  (let* ((subj (message-field-value "subject"))
+         (subj (unless (and subj (string-match "^[:blank:]*$" subj)) subj))
+         (str (or subj
+                  (pcase compose-type
+                    ('reply       "*reply*")
+                    ('forward     "*forward*")
+                    (_             "*draft*")))))
+    (rename-buffer (generate-new-buffer-name
+                    (truncate-string-to-width
+                    str mu4e~compose-buffer-max-name-length)
+                    (buffer-name)))))
+
+(defun mu4e-compose-crypto-message (parent compose-type)
+  "Possibly encrypt or sign a message based on PARENT and COMPOSE-TYPE.
+See `mu4e-compose-crypto-policy' for more details."
+  (let* ((encrypted-p
+          (and parent (memq 'encrypted (mu4e-message-field parent :flags))))
+         (encrypt
+          (or (memq 'encrypt-all-messages mu4e-compose-crypto-policy)
+              ;; new messages
+              (and (memq 'encrypt-new-messages mu4e-compose-crypto-policy)
+                   (eq compose-type 'new))
+              ;; forwarded messages
+              (and (eq compose-type 'forward)
+                   (memq 'encrypt-forwarded-messages mu4e-compose-crypto-policy))
+              ;; edited messages
+              (and (eq compose-type 'edit)
+                   (memq 'encrypt-edited-messages mu4e-compose-crypto-policy))
+              ;; all replies
+              (and (eq compose-type 'reply)
+                   (memq 'encrypt-all-replies mu4e-compose-crypto-policy))
+              ;; plain replies
+              (and (eq compose-type 'reply) (not encrypted-p)
+                   (memq 'encrypt-plain-replies mu4e-compose-crypto-policy))
+              ;; encrypted replies
+              (and (eq compose-type 'reply) encrypted-p
+                   (memq 'encrypt-encrypted-replies
+                        mu4e-compose-crypto-policy))))
+         (sign
+          (or (memq 'sign-all-messages mu4e-compose-crypto-policy)
+              ;; new messages
+              (and (eq compose-type 'new)
+                   (memq 'sign-new-messages mu4e-compose-crypto-policy))
+              ;; forwarded messages
+              (and (eq compose-type 'forward)
+                   (memq 'sign-forwarded-messages mu4e-compose-crypto-policy))
+              ;; edited messages
+              (and (eq compose-type 'edit)
+                   (memq 'sign-edited-messages mu4e-compose-crypto-policy))
+              ;; all replies
+              (and (eq compose-type 'reply)
+                   (memq 'sign-all-replies mu4e-compose-crypto-policy))
+              ;; plain replies
+              (and (eq compose-type 'reply) (not encrypted-p)
+                   (memq 'sign-plain-replies mu4e-compose-crypto-policy))
+              ;; encrypted replies
+              (and (eq compose-type 'reply) encrypted-p
+                   (memq 'sign-encrypted-replies mu4e-compose-crypto-policy)))))
+    (cond ((and sign encrypt)
+           (mml-secure-message-sign-encrypt))
+          (sign (mml-secure-message-sign))
+          (encrypt (mml-secure-message-encrypt)))))
+
+(cl-defun mu4e~compose-handler (compose-type &optional original-msg includes
+                                             switch-function)
+  "Create a new draft message, or open an existing one.
+
+COMPOSE-TYPE determines the kind of message to compose and is a
+symbol, either `reply', `forward', `edit', `resend' `new'. `edit'
+is for editing existing (draft) messages. When COMPOSE-TYPE is
+`reply' or `forward', MSG should be a message plist.  If
+COMPOSE-TYPE is `new', ORIGINAL-MSG should be nil.
+
+Optionally (when forwarding, replying) ORIGINAL-MSG is the original
+message we will forward / reply to.
+
+Optionally (when inline forwarding) INCLUDES contains a list of
+   (:file-name <filename> :mime-type <mime-type>
+    :description <description> :disposition <disposition>)
+or
+   (:buffer-name <filename> :mime-type <mime-type>
+    :description <description> :disposition <disposition>)
+for the attachments to include; file-name refers to
+a file which our backend has conveniently saved for us (as a
+tempfile).  The properties :mime-type, :description and :disposition
+are optional."
+
+  ;; Run the hooks defined for `mu4e-compose-pre-hook'. If compose-type is
+  ;; `reply', `forward' or `edit', `mu4e-compose-parent-message' points to the
+  ;; message being forwarded or replied to, otherwise it is nil.
+  (set (make-local-variable 'mu4e-compose-parent-message) original-msg)
+  (put 'mu4e-compose-parent-message 'permanent-local t)
+  ;; remember the compose-type
+  (set (make-local-variable 'mu4e-compose-type) compose-type)
+  (put 'mu4e-compose-type 'permanent-local t)
+  ;; maybe switch the context
+  (mu4e--context-autoswitch mu4e-compose-parent-message
+                            mu4e-compose-context-policy)
+  (run-hooks 'mu4e-compose-pre-hook)
+  ;; this opens (or re-opens) a message with all the basic headers set.
+  (let ((winconf (current-window-configuration)))
+    (condition-case nil
+        (mu4e-draft-open compose-type original-msg switch-function)
+      (quit (set-window-configuration winconf)
+            (mu4e-message "Operation aborted")
+            (cl-return-from mu4e~compose-handler))))
+  ;; insert mail-header-separator, which is needed by message mode to separate
+  ;; headers and body. will be removed before saving to disk
+  (mu4e~draft-insert-mail-header-separator)
+
+  ;; maybe encrypt/sign replies
+  (mu4e-compose-crypto-message original-msg compose-type)
+
+  ;; include files -- e.g. when inline forwarding a message with
+  ;; attachments, we take those from the original.
+  (save-excursion
+    (goto-char (point-max)) ;; put attachments at the end
+
+    (if (and (eq compose-type 'forward) mu4e-compose-forward-as-attachment)
+        (mu4e-compose-attach-message original-msg)
+      (dolist (att includes)
+        (let ((file-name (plist-get att :file-name))
+              (mime (plist-get att :mime-type))
+              (description (plist-get att :description))
+              (disposition (plist-get att :disposition)))
+          (if file-name
+              (mml-attach-file file-name mime description disposition)
+            (mml-attach-buffer (plist-get att :buffer-name)
+                               mime description disposition))))))
+
+  (mu4e~compose-set-friendly-buffer-name compose-type)
+
+  ;; bind to `mu4e-compose-parent-message' of compose buffer
+  (set (make-local-variable 'mu4e-compose-parent-message) original-msg)
+  (put 'mu4e-compose-parent-message 'permanent-local t)
+  ;; set mu4e-compose-type once more for this buffer,
+  (set (make-local-variable 'mu4e-compose-type) compose-type)
+  (put 'mu4e-compose-type 'permanent-local t)
+
+  ;; hide some headers
+  (mu4e~compose-hide-headers)
+  ;; switch on the mode
+  (mu4e-compose-mode)
+
+  ;; now jump to some useful positions, and start writing that mail!
+  (if (member compose-type '(new forward))
+      (message-goto-to)
+    ;; otherwise, it depends...
+    (pcase message-cite-reply-position
+      ((or 'above 'traditional) (message-goto-body))
+      (_ (when (message-goto-signature) (forward-line -2)))))
+
+  ;; don't allow undoing anything before this.
+  (setq buffer-undo-list nil)
+
+  (when mu4e-compose-in-new-frame
+    ;; make sure to close the frame when we're done with the message these are
+    ;; all buffer-local;
+    (push 'delete-frame message-exit-actions)
+    (push 'delete-frame message-postpone-actions))
+
+  ;; buffer is not user-modified yet
+  (set-buffer-modified-p nil))
+
+(defun mu4e~switch-back-to-mu4e-buffer ()
+  "Try to go back to some previous buffer, in the order view->headers->main."
+  (unless (eq mu4e-split-view 'single-window)
+    (if (buffer-live-p (mu4e-get-view-buffer))
+        (switch-to-buffer (mu4e-get-view-buffer))
+      (if (buffer-live-p (mu4e-get-headers-buffer))
+          (switch-to-buffer (mu4e-get-headers-buffer))
+        ;; if all else fails, back to the main view
+        (when (fboundp 'mu4e) (mu4e))))))
+
+(defun mu4e-compose-context-switch (&optional force name)
+  "Change the context for the current draft message.
+
+With NAME, switch to the context with NAME, and with FORCE non-nil,
+switch even if the switch is to the same context.
+
+Like `mu4e-context-switch' but with some changes after switching:
+1. Update the From and Organization headers as per the new context
+2. Update the message-signature as per the new context.
+
+Unlike some earlier version of this function, does _not_ update
+the draft folder for the messages, as that would require changing
+the file under our feet, which is a bit fragile."
+  (interactive "P")
+
+  (unless (derived-mode-p 'mu4e-compose-mode)
+    (mu4e-error "Only available in mu4e compose buffers"))
+
+  (let ((old-context (mu4e-context-current)))
+    (unless (and name (not force) (eq old-context name))
+      (unless (and (not force)
+                  (eq old-context (mu4e-context-switch nil name)))
+       (save-excursion
+         ;; Change From / Organization if needed.
+         (message-replace-header "Organization"
+                                 (or (message-make-organization) "")
+                                 '("Subject")) ;; keep in same place
+         (message-replace-header "From"
+                                 (or (mu4e~draft-from-construct) ""))
+         ;; Update signature.
+         (when (message-goto-signature) ;; delete old signature.
+           (if message-signature-insert-empty-line
+               (forward-line -2) (forward-line -1))
+           (delete-region (point) (point-max)))
+         (if (and mu4e-compose-signature-auto-include mu4e-compose-signature)
+             (let ((message-signature mu4e-compose-signature))
+               (save-excursion (message-insert-signature)))))))))
+
+(defun mu4e-sent-handler (docid path)
+  "Handler called with DOCID and PATH for the just-sent message.
+For Forwarded ('Passed') and Replied messages, try to set the
+appropriate flag at the message forwarded or replied-to."
+  (mu4e~compose-set-parent-flag path)
+  (when (file-exists-p path) ;; maybe the draft was not saved at all
+    (mu4e--server-remove docid))
+  ;; kill any remaining buffers for the draft file, or they will hang around...
+  ;; this seems a bit hamfisted...
+  (when message-kill-buffer-on-exit
+    (dolist (buf (buffer-list))
+      (and (buffer-file-name buf)
+           (string= (buffer-file-name buf) path)
+           (kill-buffer buf))))
+  (mu4e~switch-back-to-mu4e-buffer)
+  (mu4e-message "Message sent"))
+
+(defun mu4e-message-kill-buffer ()
+  "Wrapper around `message-kill-buffer'.
+It restores mu4e window layout after killing the compose-buffer."
+  (interactive)
+  (let ((current-buffer (current-buffer)))
+    (message-kill-buffer)
+    ;; Compose buffer killed
+    (when (not (equal current-buffer (current-buffer)))
+      ;; Restore mu4e
+      (if mu4e-compose-in-new-frame
+          (delete-frame)
+        (mu4e~switch-back-to-mu4e-buffer)))))
+
+(defun mu4e~compose-set-parent-flag (path)
+  "Set flags for replied-to and forwarded for the message at PATH.
+That is, set the `replied' \"R\" flag on messages we replied to,
+and the `passed' \"F\" flag on message we have forwarded.
+
+If a message has an \"In-Reply-To\" header, it is considered a
+reply to the message with the corresponding message id.
+Otherwise, if it does not have an \"In-Reply-To\" header, but
+does have a \"References:\" header, it is considered to be a
+forward message for the message corresponding with the /last/
+message-id in the references header.
+
+If the message has been determined to be either a forwarded
+message or a reply, we instruct the server to update that message
+with resp. the \"P\" (passed) flag for a forwarded message, or
+the \"R\" flag for a replied message. The original messages are
+also marked as Seen.
+
+Function assumes that it is executed in the context of the
+message buffer."
+  (let ((buf (find-file-noselect path)))
+    (when buf
+      (with-current-buffer buf
+        (message-narrow-to-headers-or-head)
+        (let ((in-reply-to (message-fetch-field "in-reply-to"))
+              (forwarded-from)
+              (references (message-fetch-field "references")))
+          (unless in-reply-to
+            (when references
+              (with-temp-buffer ;; inspired by `message-shorten-references'.
+                (insert references)
+                (goto-char (point-min))
+                (let ((refs))
+                  (while (re-search-forward "<[^ <]+@[^ <]+>" nil t)
+                    (push (match-string 0) refs))
+                  ;; the last will be the first
+                  (setq forwarded-from (car refs))))))
+          ;; remove the <>
+          (when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to))
+            (mu4e--server-move (match-string 1 in-reply-to) nil "+R-N"))
+          (when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from))
+            (mu4e--server-move (match-string 1 forwarded-from) nil "+P-N")))))))
+
+(defun mu4e-compose (compose-type)
+  "Start composing a message of COMPOSE-TYPE.
+COMPOSE-TYPE is a symbol, one of `reply', `forward', `edit',
+`resend' `new'. All but `new' take the message at point as input.
+Symbol `edit' is only allowed for draft messages."
+  (let ((msg (mu4e-message-at-point 'noerror)))
+    ;; some sanity checks
+    (unless (or msg (eq compose-type 'new))
+      (mu4e-warn "No message at point"))
+    (unless (member compose-type '(reply forward edit resend new))
+      (mu4e-error "Invalid compose type '%S'" compose-type))
+    (when (and (eq compose-type 'edit)
+               (not (member 'draft (mu4e-message-field msg :flags))))
+      (mu4e-warn "Editing is only allowed for draft messages"))
+
+    ;; 'new is special, since it takes no existing message as arg; therefore, we
+    ;; don't need to involve the backend, and call the handler *directly*
+    (if (eq compose-type 'new)
+        (mu4e~compose-handler 'new)
+      ;; otherwise, we need the doc-id
+      (let* ((docid (mu4e-message-field msg :docid))
+             ;; decrypt (or not), based on `mu4e-decryption-policy'.
+             (decrypt
+              (and (member 'encrypted (mu4e-message-field msg :flags))
+                   (if (eq mu4e-decryption-policy 'ask)
+                       (yes-or-no-p (mu4e-format "Decrypt message?"))
+                     mu4e-decryption-policy))))
+        ;; if there's a visible view window, select that before starting
+        ;; composing a new message, so that one will be replaced by the compose
+        ;; window. The 10-or-so line headers buffer is not a good place to write
+        ;; it...
+        (unless (eq mu4e-split-view 'single-window)
+          (let ((viewwin (get-buffer-window (mu4e-get-view-buffer))))
+            (when (window-live-p viewwin)
+              (select-window viewwin))))
+        ;; talk to the backend
+        (mu4e--server-compose compose-type decrypt docid)))))
+
+(defun mu4e-compose-reply ()
+  "Compose a reply for the message at point in the headers buffer."
+  (interactive)
+  (mu4e-compose 'reply))
+
+(defun mu4e-compose-forward ()
+  "Forward the message at point in the headers buffer."
+  (interactive)
+  (mu4e-compose 'forward))
+
+(defun mu4e-compose-edit ()
+  "Edit the draft message at point in the headers buffer.
+This is only possible if the message at point is, in fact, a
+draft message."
+  (interactive)
+  (mu4e-compose 'edit))
+
+(defun mu4e-compose-resend ()
+  "Resend the message at point in the headers buffer."
+  (interactive)
+  (mu4e-compose 'resend))
+
+(defun mu4e-compose-new ()
+  "Start writing a new message."
+  (interactive)
+  (mu4e-compose 'new))
+
+\f
+;;; Compose Mail
+;; mu4e-compose-func and mu4e-send-func are wrappers so we can set ourselves
+;; as default emacs mailer (define-mail-user-agent etc.)
+
+(declare-function mu4e "mu4e")
+
+;;;###autoload
+(defun mu4e~compose-mail (&optional to subject other-headers _continue
+                                    switch-function yank-action
+                                   _send-actions _return-action)
+  "This is mu4e's implementation of `compose-mail'.
+Quoting its docstring:
+
+Start composing a mail message to send. This uses the user's
+chosen mail composition package as selected with the variable
+`mail-user-agent'. The optional arguments TO and SUBJECT specify
+recipients and the initial Subject field, respectively.
+
+OTHER-HEADERS is an alist specifying additional
+header fields.  Elements look like (HEADER . VALUE) where both
+HEADER and VALUE are strings.
+
+CONTINUE, if non-nil, says to continue editing a message already
+being composed.  Interactively, CONTINUE is the prefix argument.
+
+SWITCH-FUNCTION, if non-nil, is a function to use to
+switch to and display the buffer used for mail composition.
+
+YANK-ACTION, if non-nil, is an action to perform, if and when
+necessary, to insert the raw text of the message being replied
+to. It has the form (FUNCTION . ARGS). The user agent will apply
+FUNCTION to ARGS, to insert the raw text of the original message.
+\(The user agent will also run `mail-citation-hook', *after* the
+original text has been inserted in this way.)
+
+SEND-ACTIONS is a list of actions to call when the message is sent.
+Each action has the form (FUNCTION . ARGS).
+
+RETURN-ACTION, if non-nil, is an action for returning to the
+caller.  It has the form (FUNCTION . ARGS).  The function is
+called after the mail has been sent or put aside, and the mail
+buffer buried."
+
+  (unless (mu4e-running-p)
+     (mu4e))
+
+  ;; create a new draft message 'resetting' (as below) is not actually needed in
+  ;; this case, but let's prepare for the re-edit case as well
+  (mu4e~compose-handler 'new nil nil switch-function)
+
+  (when (message-goto-to) ;; reset to-address, if needed
+    (message-delete-line))
+  (message-add-header (concat "To: " to "\n"))
+
+  (when (message-goto-subject) ;; reset subject, if needed
+    (message-delete-line))
+  (message-add-header (concat "Subject: " subject "\n"))
+
+  ;; add any other headers specified
+  (seq-each (lambda(hdr)
+             (let ((field (capitalize(car hdr))) (value (cdr hdr)))
+               ;; fix in-reply without <>
+               (when (and (string= field "In-Reply-To")
+                          (string-match-p "\\`[^ @]+@[^ @]+\\'" value)
+                          (not (string-match-p "\\`<.*>\\'" value)))
+                 (setq value (concat "<" value ">")))
+               (message-add-header (concat (capitalize field) ": " value "\n"))))
+           other-headers)
+
+  ;; yank message
+  (if (bufferp yank-action)
+      (list 'insert-buffer yank-action)
+    yank-action)
+
+  ;; try to put the user at some reasonable spot...
+  (if (not to)
+      (message-goto-to)
+    (if (not subject)
+        (message-goto-subject)
+      (message-goto-body))))
+
+;; happily, we can re-use most things from message mode
+;;;###autoload
+(define-mail-user-agent 'mu4e-user-agent
+  'mu4e~compose-mail
+  'message-send-and-exit
+  'message-kill-buffer
+  'message-send-hook)
+;; Without this `mail-user-agent' cannot be set to `mu4e-user-agent'
+;; through customize, as the custom type expects a function.  Not
+;; sure whether this function is actually ever used; if it is then
+;; returning the symbol is probably the correct thing to do, as other
+;; such functions suggest.
+(defun mu4e-user-agent ()
+  "Return the `mu4e-user-agent' symbol."
+  'mu4e-user-agent)
+
+;;; Go to bottom / top
+
+(defun mu4e-compose-goto-top (&optional arg)
+  "Go to the beginning of the message or buffer.
+Go to the beginning of the message or, if already there, go to the
+beginning of the buffer.
+
+Push mark at previous position, unless either a \\[universal-argument] prefix
+is supplied, or Transient Mark mode is enabled and the mark is active."
+  (interactive "P")
+  (or arg
+      (region-active-p)
+      (push-mark))
+  (let ((old-position (point)))
+    (message-goto-body)
+    (when (equal (point) old-position)
+      (goto-char (point-min)))))
+
+(define-key mu4e-compose-mode-map
+  (vector 'remap 'beginning-of-buffer) 'mu4e-compose-goto-top)
+
+(defun mu4e-compose-goto-bottom (&optional arg)
+  "Go to the end of the message or buffer.
+Go to the end of the message (before signature) or, if already there, go to the
+end of the buffer.
+
+Push mark at previous position, unless either a \\[universal-argument] prefix
+is supplied, or Transient Mark mode is enabled and the mark is active."
+  (interactive "P")
+  (or arg
+      (region-active-p)
+      (push-mark))
+  (let ((old-position (point))
+        (message-position (save-excursion (message-goto-body) (point))))
+    (goto-char (point-max))
+    (when (re-search-backward message-signature-separator message-position t)
+      (forward-line -1))
+    (when (equal (point) old-position)
+      (goto-char (point-max)))))
+
+(define-key mu4e-compose-mode-map
+  (vector 'remap 'end-of-buffer) 'mu4e-compose-goto-bottom)
+
+;;; _
+(provide 'mu4e-compose)
+;;; mu4e-compose.el ends here
diff --git a/mu4e/mu4e-config.el.in b/mu4e/mu4e-config.el.in
new file mode 100644 (file)
index 0000000..22a6263
--- /dev/null
@@ -0,0 +1,12 @@
+;; auto-generated
+
+(defconst mu4e-mu-version "@VERSION@"
+  "Required mu binary version; mu4e's version must agree with this.")
+
+(defconst mu4e-builddir "@abs_top_builddir@"
+  "Top-level build directory.")
+
+(defconst mu4e-doc-dir "@MU_DOC_DIR@"
+  "Mu4e's data-dir.")
+
+(provide 'mu4e-config)
diff --git a/mu4e/mu4e-contacts.el b/mu4e/mu4e-contacts.el
new file mode 100644 (file)
index 0000000..d207f33
--- /dev/null
@@ -0,0 +1,298 @@
+;;; mu4e-contacts.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Utility functions used in the mu4e
+
+;;; Code:
+(require 'cl-lib)
+(require 'mu4e-helpers)
+(require 'mu4e-update)
+\f
+
+;;; Configuration
+(defcustom mu4e-compose-complete-addresses t
+  "Whether to do auto-completion of e-mail addresses."
+  :type 'boolean
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-complete-only-personal nil
+  "Whether to consider only \"personal\" e-mail addresses for completion.
+That is, addresses from messages where user was explicitly in one
+of the address fields (this excludes mailing list messages).
+These addresses are the ones specified with \"mu init\"."
+  :type 'boolean
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-complete-only-after "2018-01-01"
+  "Consider only contacts last seen after this date.
+
+Date must be a string of the form YYYY-MM-DD.
+
+This is useful for limiting a potentially enormous set of
+contacts for auto-completion to just those that are present in
+the e-mail corpus in recent times. Set to nil to not have any
+time-based restriction."
+  :type 'string
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-complete-max nil
+  "Consider only the top-n contacts.
+After considering the other
+constraints (`mu4e-compose-complete-addresses' and
+`mu4e-compose-complete-only-after'), pick only the highest-ranked
+<n>.
+
+This reduces start-up time and memory usage. Set to nil for no
+limits."
+  :type 'string
+  :group 'mu4e-compose)
+
+;; names and mail-addresses can be mapped onto their canonical
+;; counterpart.  use the customizeable function
+;; mu4e-canonical-contact-function to do that.  below the identity
+;; function for mapping a contact onto the canonical one.
+(defun mu4e-contact-identity (contact)
+  "Return the name and the mail-address of a CONTACT.
+It is used as the identity function for converting contacts to
+their canonical counterpart; useful as an example."
+  (let ((name (plist-get contact :name))
+        (mail (plist-get contact :mail)))
+    (list :name name :mail mail)))
+
+(make-obsolete-variable 'mu4e-contact-rewrite-function
+                        "mu4e-contact-process-function (see docstring)"
+                       "mu4e 1.3.2")
+(make-obsolete-variable 'mu4e-compose-complete-ignore-address-regexp
+                        "mu4e-contact-process-function (see docstring)"
+                       "mu4e 1.3.2")
+
+(defcustom mu4e-contact-process-function
+  (lambda(addr)
+    (cond
+     ((string-match-p "reply" addr)
+      ;; no-reply adresses are not useful of course, but neither are are
+      ;; reply-xxxx addresses since they're autogenerated only useful for direct
+      ;; replies.
+      nil)
+     (t addr)))
+  "Function for processing contact information for use in auto-completion.
+
+The function receives the contact as a string, e.g \"Foo Bar
+   <foo.bar@example.com>\" \"cuux@example.com\"
+
+The function should return either:
+- nil: do not use this contact for completion
+- the (possibly rewritten) address, which must be
+an RFC-2822-compatible e-mail address."
+  :type 'function
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-reply-ignore-address
+  '("no-?reply")
+  "Addresses to prune when doing wide replies.
+
+This can be a regexp matching the address, a list of regexps or a
+predicate function. A value of nil keeps all the addresses."
+  :type '(choice
+          (const nil)
+          function
+          string
+          (repeat string))
+  :group 'mu4e-compose)
+
+\f
+;;; Internal variables
+(defvar mu4e--contacts-tstamp "0"
+  "Timestamp for the most recent contacts update." )
+
+(defvar mu4e--contacts-set nil
+  "Set with the full contact addresses for autocompletion.")
+\f
+;;; user mail address
+(defun mu4e-personal-addresses(&optional no-regexp)
+  "Get the list user's personal addresses, as passed to mu init.
+The address are either plain e-mail address or /regular
+ expressions/. When NO-REGEXP is non-nil, do not include regexp
+ address patterns (if any)."
+  (seq-remove
+   (lambda(addr) (and no-regexp (string-match-p "^/.*/" addr)))
+   (when (mu4e-server-properties)
+     (plist-get (mu4e-server-properties) :personal-addresses))))
+
+(defun mu4e-personal-address-p (addr)
+  "Is ADDR a personal address?
+Evaluate to nil if ADDR matches any of the personal addresses.
+Uses (mu4e-personal-addresses) for the addresses with both the plain
+addresses and /regular expressions/."
+  (when addr
+    (seq-find
+     (lambda (m)
+       (if (string-match "/\\(.*\\)/" m)
+           (let ((rx (match-string 1 m))
+                 (case-fold-search t))
+             (if (string-match rx addr) t nil))
+         (eq t (compare-strings addr nil nil m nil nil 'case-insensitive))))
+     (mu4e-personal-addresses))))
+
+(define-obsolete-function-alias 'mu4e-user-mail-address-p
+  'mu4e-personal-address-p "1.5.5")
+
+
+;; don't use the older vars anymore
+(make-obsolete-variable 'mu4e-user-mail-address-regexp
+                        'mu4e-user-mail-address-list "0.9.9.x")
+(make-obsolete-variable 'mu4e-my-email-addresses
+                        'mu4e-user-mail-address-list "0.9.9.x")
+(make-obsolete-variable 'mu4e-user-mail-address-list
+                        "determined by server; see `mu4e-personal-addresses'."
+                       "1.3.8")
+
+\f
+;; Helpers
+
+
+;;; RFC2822 handling of phrases in mail-addresses
+;;
+;; The optional display-name contains a phrase, it sits before the
+;; angle-addr as specified in RFC2822 for email-addresses in header
+;; fields.  Contributed by jhelberg.
+
+(defun mu4e--rfc822-phrase-type (ph)
+  "Return an atom or quoted-string for the phrase PH.
+This checks for empty string first. Then quotes around the phrase
+\(returning symbol `rfc822-quoted-string'). Then whether there is
+a quote inside the phrase (returning symbol
+`rfc822-containing-quote').
+
+The reverse of the RFC atext definition is then tested. If it
+matches, nil is returned, if not, it returns a symbol
+`rfc822-atom'."
+  (cond
+   ((= (length ph) 0) 'rfc822-empty)
+   ((= (aref ph 0) ?\")
+    (if (string-match "\"\\([^\"\\\n]\\|\\\\.\\|\\\\\n\\)*\"" ph)
+        'rfc822-quoted-string
+      'rfc822-containing-quote)) ; starts with quote, but doesn't end with one
+   ((string-match-p "[\"]" ph) 'rfc822-containing-quote)
+   ((string-match-p "[\000-\037()\*<>@,;:\\\.]+" ph) nil)
+   (t 'rfc822-atom)))
+
+(defun mu4e--rfc822-quote-phrase (ph)
+  "Quote an RFC822 phrase PH only if necessary.
+Atoms and quoted strings don't need quotes. The rest do.  In
+case a phrase contains a quote, it will be escaped."
+  (let ((type (mu4e--rfc822-phrase-type ph)))
+    (cond
+     ((eq type 'rfc822-atom) ph)
+     ((eq type 'rfc822-quoted-string) ph)
+     ((eq type 'rfc822-containing-quote)
+      (format "\"%s\""
+              (replace-regexp-in-string "\"" "\\\\\"" ph)))
+     (t (format "\"%s\"" ph)))))
+
+(defsubst mu4e-contact-name (contact)
+  "Get the name of this CONTACT, or nil."
+  (plist-get contact :name))
+
+(defsubst mu4e-contact-email (contact)
+  "Get the name of this CONTACT, or nil."
+  (plist-get contact :email))
+
+(defsubst mu4e-contact-cons (contact)
+  "Convert a CONTACT plist into a old-style (name . email)."
+    (cons
+     (mu4e-contact-name contact)
+     (mu4e-contact-email contact)))
+
+(defsubst mu4e-contact-make (name email)
+  "Creata contact plist from NAME and EMAIL."
+    `(:name ,name :email ,email))
+
+(defun mu4e-contact-full (contact)
+  "Get the full combination of name and email address from CONTACT."
+  (let* ((email (mu4e-contact-email contact))
+        (name (mu4e-contact-name contact)))
+    (if (and name (> (length name) 0))
+       (format "%s <%s>" (mu4e--rfc822-quote-phrase name) email)
+      email)))
+
+\f
+(defun mu4e--update-contacts (contacts &optional tstamp)
+  "Receive a sorted list of CONTACTS newer than TSTAMP.
+Update an internal set with it.
+
+This is used by the completion function in mu4e-compose."
+  (let ((n 0))
+    (unless mu4e--contacts-set
+      (setq mu4e--contacts-set (make-hash-table :test 'equal :weakness nil
+                                           :size (length contacts))))
+    (dolist (contact contacts)
+      (cl-incf n)
+      (when (functionp mu4e-contact-process-function)
+       (setq contact (funcall mu4e-contact-process-function contact)))
+      (when contact ;; note the explicit deccode; the strings we get are
+                     ;; utf-8, but emacs doesn't know yet.
+        (puthash (decode-coding-string contact 'utf-8) t mu4e--contacts-set)))
+    (setq mu4e--contacts-tstamp (or tstamp "0"))
+    (unless (zerop n)
+      (mu4e-index-message "Contacts updated: %d; total %d"
+                          n (hash-table-count mu4e--contacts-set)))))
+
+(defun mu4e-contacts-info ()
+  "Display information about the contacts-cache.
+For testing/debugging."
+  (interactive)
+  (with-current-buffer (get-buffer-create "*mu4e-contacts-info*")
+    (erase-buffer)
+    (insert (format "complete addresses:        %s\n"
+                   (if mu4e-compose-complete-addresses "yes" "no")))
+    (insert (format "only personal addresses:   %s\n"
+                    (if mu4e-compose-complete-only-personal "yes" "no")))
+    (insert (format "only addresses seen after: %s\n"
+                    (or mu4e-compose-complete-only-after "no restrictions")))
+
+    (when mu4e--contacts-set
+      (insert (format "number of contacts cached: %d\n\n"
+                      (hash-table-count mu4e--contacts-set)))
+      (maphash (lambda (contact _)
+                (insert (format "%s\n" contact))) mu4e--contacts-set))
+    (pop-to-buffer "*mu4e-contacts-info*")))
+
+(declare-function mu4e--server-contacts  "mu4e-server")
+
+(defun mu4e--request-contacts-maybe ()
+  "Maybe update the set of contacts for autocompletion.\0
+
+If `mu4e-compose-complete-addresses' is non-nil, get/update the
+list of contacts we use for autocompletion; otherwise, do
+nothing."
+  (when mu4e-compose-complete-addresses
+    (mu4e--server-contacts
+     mu4e-compose-complete-only-personal
+     mu4e-compose-complete-only-after
+     mu4e-compose-complete-max
+     mu4e--contacts-tstamp)))
+
+(provide 'mu4e-contacts)
+;;; mu4e-contacts.el ends here
diff --git a/mu4e/mu4e-context.el b/mu4e/mu4e-context.el
new file mode 100644 (file)
index 0000000..41fbb33
--- /dev/null
@@ -0,0 +1,238 @@
+;;; mu4e-context.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; A mu4e 'context' is a set of variable-settings and functions, which can be
+;; used e.g. to switch between accounts.
+
+;;; Code:
+
+(require 'mu4e-helpers)
+
+\f
+;;; Configuration
+(defcustom mu4e-context-policy 'ask-if-none
+  "The policy to determine the context when entering the mu4e main view.
+
+If the value is `always-ask', ask the user unconditionally.
+
+In all other cases, if any context matches (using its match
+function), this context is used. Otherwise, if none of the
+contexts match, we have the following choices:
+
+- `pick-first': pick the first of the contexts available (ie. the default)
+- `ask': ask the user
+- `ask-if-none': ask if there is no context yet, otherwise leave it as it is
+-  nil: return nil; leaves the current context as is.
+
+Also see `mu4e-compose-context-policy'."
+  :type '(choice
+          (const :tag "Always ask what context to use, even if one matches"
+                 always-ask)
+          (const :tag "Ask if none of the contexts match" ask)
+          (const :tag "Ask when there's no context yet" ask-if-none)
+          (const :tag "Pick the first context if none match" pick-first)
+          (const :tag "Don't change the context when none match" nil))
+  :group 'mu4e)
+
+
+(defvar mu4e-contexts nil
+  "The list of `mu4e-context' objects describing mu4e's contexts.")
+
+(defvar mu4e-context-changed-hook nil
+  "Hook run just *after* the context changed.")
+
+(defface mu4e-context-face
+  '((t :inherit mu4e-title-face :weight bold))
+  "Face for displaying the context in the modeline."
+  :group 'mu4e-faces)
+
+(defvar mu4e--context-current nil
+  "The current context.
+Internal; use `mu4e-context-switch' to change it.")
+\f
+(defun mu4e-context-current (&optional output)
+  "Get the currently active context, or nil if there is none.
+When OUTPUT is non-nil, echo the name of the current context or
+none."
+  (interactive "p")
+  (let ((ctx mu4e--context-current))
+    (when output
+      (mu4e-message "Current context: %s"
+                    (if ctx (mu4e-context-name ctx) "<none>")))
+    ctx))
+
+(defun mu4e-context-label ()
+  "Propertized string with the current context name.
+An empty string \"\" if there is none."
+  (if (mu4e-context-current)
+      (concat "[" (propertize (mu4e-quote-for-modeline
+                               (mu4e-context-name (mu4e-context-current)))
+                              'face 'mu4e-context-face) "]") ""))
+
+(cl-defstruct mu4e-context
+  "A mu4e context object with the following members:
+- `name': the name of the context, eg. \"Work\" or \"Private\".
+- `enter-func': a parameterless function invoked when entering
+  this context, or nil
+- `leave-func':a parameterless function invoked when leaving this
+  context, or nil
+- `match-func': a function called when composing a new message,
+  that takes a message plist for the message replied to or
+  forwarded, and nil otherwise. Before composing a new message,
+  `mu4e' switches to the first context for which `match-func'
+  returns t.
+- `vars': variables to set when entering context."
+  name                      ;; name of the context, e.g. "work"
+  (enter-func nil)          ;; function invoked when entering the context
+  (leave-func nil)          ;; function invoked when leaving the context
+  (match-func nil)          ;; function that takes a msg-proplist, and return t
+  ;; if it matches, nil otherwise
+  vars) ;; alist of variables.
+
+
+(defun mu4e--context-ask-user (prompt)
+  "Let user choose some context based on its name with PROMPT."
+  (when mu4e-contexts
+    (let* ((names (seq-map (lambda (context)
+                             (cons (mu4e-context-name context) context))
+                          mu4e-contexts))
+           (context (mu4e-read-option prompt names)))
+      (or context (mu4e-error "No such context")))))
+
+(defun mu4e-context-switch (&optional force name)
+  "Switch to a context with NAME.
+Context must be part of `mu4e-contexts'; if NAME is nil, query user.
+
+If the new context is the same and the current context, only
+switch (run associated functions) when prefix argument FORCE is
+non-nil."
+  (interactive "P")
+  (unless mu4e-contexts
+    (mu4e-error "No contexts defined"))
+  (let* ((names (seq-map (lambda (context)
+                           (cons (mu4e-context-name context) context))
+                        mu4e-contexts))
+         (context
+          (if name
+              (cdr-safe (assoc name names))
+            (mu4e--context-ask-user "Switch to context: "))))
+    (unless context (mu4e-error "No such context"))
+    ;; if new context is same as old one one switch with FORCE is set.
+    (when (or force (not (eq context (mu4e-context-current))))
+      (when (and (mu4e-context-current)
+                 (mu4e-context-leave-func mu4e--context-current))
+        (funcall (mu4e-context-leave-func mu4e--context-current)))
+      ;; enter the new context
+      (when (mu4e-context-enter-func context)
+        (funcall (mu4e-context-enter-func context)))
+      (when (mu4e-context-vars context)
+        (mapc (lambda (cell)
+                (set (car cell) (cdr cell)))
+              (mu4e-context-vars context)))
+      (setq mu4e--context-current context)
+
+      (run-hooks 'mu4e-context-changed-hook)
+      (mu4e-message "Switched context to %s" (mu4e-context-name context))
+      (force-mode-line-update))
+    context))
+
+(defun mu4e--context-autoswitch (&optional msg policy)
+  "Automatically switch to some context.
+
+When contexts are defined but there is no context yet, switch to
+the first whose :match-func return non-nil. If none of them
+match, return the first. For MSG and POLICY, see
+`mu4e-context-determine'."
+  (when mu4e-contexts
+    (let ((context (mu4e-context-determine msg policy)))
+      (when context (mu4e-context-switch
+                     nil (mu4e-context-name context))))))
+
+(defun mu4e-context-determine (msg &optional policy)
+  "Return the first context where match-func evaluate to non-nil.
+
+MSG points to the plist for the message replied to or forwarded,
+or nil if there is no such MSG; similar to what
+`mu4e-compose-pre-hook' does.
+
+POLICY specifies how to do the determination. If POLICY is
+`always-ask', we ask the user unconditionally.
+
+In all other cases, if any context matches (using its match
+function), this context is returned. If none of the contexts
+match, POLICY determines what to do:
+
+- `pick-first': pick the first of the contexts available
+- `ask': ask the user
+- `ask-if-none': ask if there is no context yet
+- otherwise, return nil. Effectively, this leaves the current context
+as it is."
+  (when mu4e-contexts
+    (if (eq policy 'always-ask)
+        (mu4e--context-ask-user "Select context: ")
+      (or ;; is there a matching one?
+       (seq-find (lambda (context)
+                   (when (mu4e-context-match-func context)
+                     (funcall (mu4e-context-match-func context) msg)))
+                 mu4e-contexts)
+       ;; no context found yet; consult policy
+       (pcase policy
+         ('pick-first (car mu4e-contexts))
+         ('ask (mu4e--context-ask-user "Select context: "))
+         ('ask-if-none (or (mu4e-context-current)
+                          (mu4e--context-ask-user "Select context: ")))
+         (_ nil))))))
+
+(defun mu4e-context-in-modeline ()
+  "Display the mu4e-context (if any) in a (buffer-specific)
+global-mode-line."
+  (add-to-list
+   (make-local-variable 'global-mode-string)
+   '(:eval (mu4e-context-label))))
+
+(defmacro with-mu4e-context-vars (context &rest body)
+  "Evaluate BODY, with variables let-bound for CONTEXT (if any).
+`funcall'."
+  (declare (indent 2))
+  `(let* ((vars (and ,context (mu4e-context-vars ,context))))
+     (cl-progv ;; XXX: perhaps use eval's lexical environment instead of progv?
+         (mapcar (lambda(cell) (car cell)) vars)
+         (mapcar (lambda(cell) (cdr cell)) vars)
+       (eval ,@body))))
+
+(define-minor-mode mu4e-context-minor-mode
+  "Mode for switching the mu4e context."
+  :global nil
+  :init-value nil ;; disabled by default
+  :group 'mu4e
+  :lighter ""
+  :keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map (kbd";") #'mu4e-context-switch)
+    map)
+  (mu4e-context-in-modeline))
+
+;;;
+(provide 'mu4e-context)
+;;; mu4e-context.el ends here
diff --git a/mu4e/mu4e-contrib.el b/mu4e/mu4e-contrib.el
new file mode 100644 (file)
index 0000000..28f29c2
--- /dev/null
@@ -0,0 +1,185 @@
+;;; mu4e-contrib.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013-2022 Dirk-Jan C. Binnema
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Some user-contributed functions for mu4e
+
+;;; Code:
+
+(require 'mu4e-headers)
+(require 'mu4e-view)
+(require 'bookmark)
+(require 'eshell)
+
+\f
+;;; Various simple commands
+(defun mu4e-headers-mark-all-unread-read ()
+  "Put a ! \(read) mark on all visible unread messages."
+  (interactive)
+  (mu4e-headers-mark-for-each-if
+   (cons 'read nil)
+   (lambda (msg _param)
+     (memq 'unread (mu4e-msg-field msg :flags)))))
+
+(defun mu4e-headers-flag-all-read ()
+  "Flag all visible messages as \"read\"."
+  (interactive)
+  (mu4e-headers-mark-all-unread-read)
+  (mu4e-mark-execute-all t))
+
+(defun mu4e-headers-mark-all ()
+  "Mark all headers for some action.
+Ask user what action to execute."
+  (interactive)
+  (mu4e-headers-mark-for-each-if
+   (cons 'something nil)
+   (lambda (_msg _param) t))
+  (mu4e-mark-execute-all))
+
+
+\f
+;;; Bogofilter/SpamAssassin
+;;
+;; Support for handling spam with Bogofilter with the possibility
+;; to define it for SpamAssassin, contributed by Gour.
+;;
+;; To add the actions to the menu, you can use something like:
+;;
+;; (add-to-list 'mu4e-headers-actions
+;;              '("sMark as spam" . mu4e-register-msg-as-spam) t)
+;; (add-to-list 'mu4e-headers-actions
+;;              '("hMark as ham" . mu4e-register-msg-as-ham) t)
+
+(defvar mu4e-register-as-spam-cmd nil
+  "Command for invoking spam processor to register message as spam.
+For example for bogofilter, use \"/usr/bin/bogofilter -Ns < %s\"")
+
+(defvar mu4e-register-as-ham-cmd nil
+  "Command for invoking spam processor to register message as ham.
+For example for bogofile, use \"/usr/bin/bogofilter -Sn < %s\"")
+
+(defun mu4e-register-msg-as-spam (msg)
+  "Register MSG  as spam."
+  (interactive)
+  (let* ((path (shell-quote-argument (mu4e-message-field msg :path)))
+         (command (format mu4e-register-as-spam-cmd path)))
+    (shell-command command))
+  (mu4e-mark-at-point 'delete nil))
+
+(defun mu4e-register-msg-as-ham (msg)
+  "Register MSG as ham."
+  (interactive)
+  (let* ((path (shell-quote-argument(mu4e-message-field msg :path)))
+         (command (format mu4e-register-as-ham-cmd path)))
+    (shell-command command))
+  (mu4e-mark-at-point 'something nil))
+
+;; (add-to-list 'mu4e-view-actions
+;;              '("sMark as spam" . mu4e-view-register-msg-as-spam) t)
+;; (add-to-list 'mu4e-view-actions
+;;              '("hMark as ham" . mu4e-view-register-msg-as-ham) t)
+
+(defun mu4e-view-register-msg-as-spam (msg)
+  "Register MSG as spam (view mode)."
+  (interactive)
+  (let* ((path (shell-quote-argument (mu4e-message-field msg :path)))
+         (command (format mu4e-register-as-spam-cmd path)))
+    (shell-command command))
+  (mu4e-view-mark-for-delete))
+
+(defun mu4e-view-register-msg-as-ham (msg)
+  "Mark MSG as ham (view mode)."
+  (interactive)
+  (let* ((path (shell-quote-argument(mu4e-message-field msg :path)))
+         (command (format mu4e-register-as-ham-cmd path)))
+    (shell-command command))
+  (mu4e-view-mark-for-something))
+
+\f
+;;; Eshell functions
+;;
+;; Code for `gnus-dired-attached' modified to run from eshell,
+;; allowing files to be attached to an email via mu4e using the
+;; eshell.  Does not depend on gnus.
+
+
+(defun mu4e--active-composition-buffers ()
+  "Return all active mu4e composition buffers."
+  (let (buffers)
+    (save-excursion
+      (dolist (buffer (buffer-list t))
+        (set-buffer buffer)
+        (when (eq major-mode 'mu4e-compose-mode)
+          (push (buffer-name buffer) buffers))))
+    (nreverse buffers)))
+
+(defun eshell/mu4e-attach (&rest args)
+  "Attach files to a mu4e message using eshell with ARGS.
+If no mu4e buffers found, compose a new message and then attach
+the file."
+  (let ((destination nil)
+        (files-str nil)
+        (bufs nil)
+        ;; Remove directories from the list
+        (files-to-attach
+         (delq nil (mapcar
+                    (lambda (f) (if (or (not (file-exists-p f))
+                                       (file-directory-p f))
+                               nil
+                             (expand-file-name f)))
+                    (eshell-flatten-list (reverse args))))))
+    ;; warn if user tries to attach without any files marked
+    (if (null files-to-attach)
+        (error "No files to attach")
+      (setq files-str
+            (mapconcat
+             (lambda (f) (file-name-nondirectory f))
+             files-to-attach ", "))
+      (setq bufs (mu4e--active-composition-buffers))
+      ;; set up destination mail composition buffer
+      (if (and bufs
+               (y-or-n-p "Attach files to existing mail composition buffer? "))
+          (setq destination
+                (if (= (length bufs) 1)
+                    (get-buffer (car bufs))
+                  (let ((prompt (mu4e-format "%s" "Attach to buffer")))
+                    (substring-no-properties
+                     (funcall mu4e-completing-read-function prompt
+                              bufs)))))
+        ;; setup a new mail composition buffer
+        (if (y-or-n-p "Compose new mail and attach this file? ")
+            (progn (mu4e-compose-new)
+                   (setq destination (current-buffer)))))
+      ;; if buffer was found, set buffer to destination buffer, and attach files
+      (if (not (eq destination 'nil))
+          (progn (set-buffer destination)
+                 (goto-char (point-max)) ; attach at end of buffer
+                 (while files-to-attach
+                   (mml-attach-file (car files-to-attach)
+                                    (or (mm-default-file-encoding
+                                        (car files-to-attach))
+                                        "application/octet-stream") nil)
+                   (setq files-to-attach (cdr files-to-attach)))
+                 (message "Attached file(s) %s" files-str))
+        (message "No buffer to attach file to.")))))
+
+;;; _
+(provide 'mu4e-contrib)
+;;; mu4e-contrib.el ends here
diff --git a/mu4e/mu4e-draft.el b/mu4e/mu4e-draft.el
new file mode 100644 (file)
index 0000000..46f7d8d
--- /dev/null
@@ -0,0 +1,771 @@
+;;; mu4e-draft.el -- part of mu4e, the mu mail user agent for emacs -*- lexical-binding: t -*-
+;;
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file, various functions to create draft messages
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'mu4e-message)
+(require 'mu4e-contacts)
+(require 'mu4e-folders)
+(require 'message) ;; mail-header-separator
+
+\f
+;;; Configuration
+(defgroup mu4e-compose nil
+  "Customizations for composing/sending messages."
+  :group 'mu4e)
+
+(defcustom mu4e-compose-reply-recipients 'ask
+  "Which recipients to use when replying to a message.
+May be a symbol `ask', `all', `sender'. Note that that only
+applies to non-mailing-list message; for those, mu4e always
+asks."
+  :type '(choice ask
+                 all
+                 sender)
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-reply-to-address nil
+  "The Reply-To address.
+Useful when this is not equal to the From: address."
+  :type 'string
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-forward-as-attachment nil
+  "Whether to forward messages as attachments instead of inline."
+  :type 'boolean
+  :group 'mu4e-compose)
+
+;; backward compatibility
+(make-obsolete-variable 'mu4e-reply-to-address
+                        'mu4e-compose-reply-to-address
+                        "v0.9.9")
+
+(defcustom mu4e-compose-keep-self-cc nil
+  "When non-nil. keep your e-mail address in Cc: when replying."
+  :type 'boolean
+  :group 'mu4e-compose)
+
+(defvar mu4e-compose-parent-message nil
+  "The parent message plist.
+This is the message being replied to, forwarded or edited; used
+in `mu4e-compose-pre-hook'. For new messages, it is nil.")
+
+(make-obsolete-variable 'mu4e-auto-retrieve-keys  "no longer used." "1.3.1")
+
+(defcustom mu4e-decryption-policy t
+  "Policy for dealing with replying/forwarding encrypted parts.
+The setting is a symbol:
+ * t:     try to decrypt automatically
+ * `ask': ask before decrypting anything
+ * nil:   don't try to decrypt anything."
+  :type '(choice (const :tag "Try to decrypt automatically" t)
+                 (const :tag "Ask before decrypting anything" ask)
+                 (const :tag "Don't try to decrypt anything" nil))
+  :group 'mu4e-compose)
+\f
+;;; Composing / Sending messages
+
+(defcustom mu4e-sent-messages-behavior 'sent
+  "Determines what mu4e does with sent messages.
+
+This is one of the symbols:
+* `sent'    move the sent message to the Sent-folder (`mu4e-sent-folder')
+* `trash'   move the sent message to the Trash-folder (`mu4e-trash-folder')
+* `delete'  delete the sent message.
+
+Note, when using GMail/IMAP, you should set this to either
+`trash' or `delete', since GMail already takes care of keeping
+copies in the sent folder.
+
+Alternatively, `mu4e-sent-messages-behavior' can be a function
+which takes no arguments, and which should return one of the mentioned
+symbols, for example:
+
+  (setq mu4e-sent-messages-behavior (lambda ()
+  (if (string= (message-sendmail-envelope-from) \"foo@example.com\")
+       \='delete \='sent)))
+
+The various `message-' functions from `message-mode' are available
+for querying the message information."
+  :type '(choice (const :tag "move message to mu4e-sent-folder" sent)
+                 (const :tag "move message to mu4e-trash-folder" trash)
+                 (const :tag "delete message" delete))
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-context-policy 'ask
+  "Policy for determining the context when composing a new message.
+
+If the value is `always-ask', ask the user unconditionally.
+
+In all other cases, if any context matches (using its match
+function), this context is used. Otherwise, if none of the
+contexts match, we have the following choices:
+
+- `pick-first': pick the first of the contexts available (ie. the default)
+- `ask': ask the user
+- `ask-if-none': ask if there is no context yet, otherwise leave it as it is
+-  nil: return nil; leaves the current context as is.
+
+Also see `mu4e-context-policy'."
+  :type '(choice
+          (const :tag "Always ask what context to use" always-ask)
+          (const :tag "Ask if none of the contexts match" ask)
+          (const :tag "Ask when there's no context yet" ask-if-none)
+          (const :tag "Pick the first context if none match" pick-first)
+          (const :tag "Don't change the context when none match" nil))
+  :safe 'symbolp
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-crypto-policy
+  '(encrypt-encrypted-replies sign-encrypted-replies)
+  "Policy to control when messages will be signed/encrypted.
+
+The value is a list, whose members determine the behaviour of
+`mu4e~compose-crypto-message'. Specifically, it might contain:
+
+- `sign-all-messages': Always add a signature.
+- `sign-new-messages': Add a signature to new message, ie.
+  messages that aren't responses to another message.
+- `sign-forwarded-messages': Add a signature when forwarding
+  a message
+- `sign-edited-messages': Add a signature to drafts
+- `sign-all-replies': Add a signature when responding to
+  another message.
+- `sign-plain-replies': Add a signature when responding to
+  non-encrypted messages.
+- `sign-encrypted-replies': Add a signature when responding
+  to encrypted messages.
+
+It should be noted that certain symbols have priorities over one
+another. So `sign-all-messages' implies `sign-all-replies', which
+in turn implies `sign-plain-replies'. Adding both to the set, is
+not a contradiction, but a redundant configuration.
+
+All `sign-*' options have a `encrypt-*' analogue."
+  :type '(set :greedy t
+              (const :tag "Sign all messages" sign-all-messages)
+              (const :tag "Encrypt all messages" encrypt-all-messages)
+              (const :tag "Sign new messages" sign-new-messages)
+              (const :tag "Encrypt new messages" encrypt-new-messages)
+              (const :tag "Sign forwarded messages" sign-forwarded-messages)
+              (const :tag "Encrypt forwarded messages" encrypt-forwarded-messages)
+              (const :tag "Sign edited messages" sign-edited-messages)
+              (const :tag "Encrypt edited messages" edited-forwarded-messages)
+              (const :tag "Sign all replies" sign-all-replies)
+              (const :tag "Encrypt all replies" encrypt-all-replies)
+              (const :tag "Sign replies to plain messages" sign-plain-replies)
+              (const :tag "Encrypt replies to plain messages" encrypt-plain-replies)
+              (const :tag "Sign replies to encrypted messages" sign-encrypted-replies)
+              (const :tag "Encrypt replies to encrypted messages" encrypt-encrypted-replies))
+  :group 'mu4e-compose)
+
+(make-obsolete-variable 'mu4e-compose-crypto-reply-encrypted-policy "The use of the
+ 'mu4e-compose-crypto-reply-encrypted-policy' variable is deprecated.
+ 'mu4e-compose-crypto-policy' should be used instead" "2020-03-06")
+
+(make-obsolete-variable 'mu4e-compose-crypto-reply-plain-policy "The use of the
+ 'mu4e-compose-crypto-reply-plain-policy' variable is deprecated.
+ 'mu4e-compose-crypto-policy' should be used instead"
+                        "2020-03-06")
+
+(make-obsolete-variable 'mu4e-compose-crypto-reply-policy "The use of the
+ 'mu4e-compose-crypto-reply-policy' variable is deprecated.
+ 'mu4e-compose-crypto-reply-plain-policy' and
+ 'mu4e-compose-crypto-reply-encrypted-policy' should be used instead"
+                        "2017-09-02")
+
+(defcustom mu4e-compose-format-flowed nil
+  "Whether to compose messages to be sent as format=flowed.
+\(Or with long lines if variable `use-hard-newlines' is set to
+nil). The variable `fill-flowed-encode-column' lets you customize
+the width beyond which format=flowed lines are wrapped."
+  :type 'boolean
+  :safe 'booleanp
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-pre-hook nil
+  "Hook run just *before* message composition starts.
+If the compose-type is a symbol, either `reply' or `forward', the
+variable `mu4e-compose-parent-message' points to the message
+replied to / being forwarded / edited, and `mu4e-compose-type'
+contains the type of message to be composed.
+
+Note that there is no draft message yet when this hook runs, it
+is meant for influencing the how mu4e constructs the draft
+message. If you want to do something with the draft messages after
+it has been constructed, `mu4e-compose-mode-hook' would be the
+place to do that."
+  :type 'hook
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-dont-reply-to-self nil
+  "If non-nil, do not include self.
+Selfness is decided by `mu4e-personal-address-p'"
+  :type 'boolean
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-cite-function
+  (or message-cite-function 'message-cite-original-without-signature)
+  "The function for citing message in replies and forwards.
+This is the mu4e-specific version of
+`message-cite-function'."
+  :type 'function
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-signature
+  (or message-signature "Sent with my mu4e")
+  "The message signature.
+\(i.e. the blob at the bottom of messages). This is the
+mu4e-specific version of `message-signature'."
+  :type '(choice string
+                 (const :tag "None" nil)
+                 (const :tag "Contents of signature file" t)
+                 function sexp)
+  :risky t
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-signature-auto-include t
+  "Whether to automatically include a message-signature."
+  :type 'boolean
+  :group 'mu4e-compose)
+
+(make-obsolete-variable 'mu4e-compose-auto-include-date
+                        "This is done unconditionally now" "1.3.5")
+
+(defcustom mu4e-compose-in-new-frame nil
+  "Whether to compose messages in a new frame."
+  :type 'boolean
+  :group 'mu4e-compose)
+
+(defvar mu4e-user-agent-string
+  (format "mu4e %s; emacs %s" mu4e-mu-version emacs-version)
+  "The User-Agent string for mu4e, or nil.")
+
+(defvar mu4e-view-date-format)
+
+(defvar mu4e-compose-type nil
+  "The compose-type for this buffer.
+This is a symbol, `new', `forward', `reply' or `edit'.")
+\f
+
+(defun mu4e~draft-cite-original (msg)
+  "Return a cited version of the original message MSG as a plist.
+This function uses `mu4e-compose-cite-function', and as such all
+its settings apply."
+  (with-temp-buffer
+    (when (fboundp 'mu4e-view-message-text) ;; keep bytecompiler happy
+      (let ((mu4e-view-date-format "%Y-%m-%dT%T%z"))
+        (insert (mu4e-view-message-text msg)))
+      (message-yank-original)
+      (goto-char (point-min))
+      (push-mark (point-max))
+      ;; set the the signature separator to 'loose', since in the real world,
+      ;; many message don't follow the standard...
+      (let ((message-signature-separator "^-- *$")
+            (message-signature-insert-empty-line t))
+        (funcall mu4e-compose-cite-function))
+      (pop-mark)
+      (goto-char (point-min))
+      (buffer-string))))
+
+(defun mu4e~draft-header (hdr val)
+  "Return a header line of the form \"HDR: VAL\".
+If VAL is nil, return nil."
+  ;; note: the propertize here is currently useless, since gnus sets its own
+  ;; later.
+  (when val (format "%s: %s\n"
+                    (propertize hdr 'face 'mu4e-header-key-face)
+                    (propertize val 'face 'mu4e-header-value-face))))
+
+(defconst mu4e~max-reference-num 21
+  "Specifies the maximum number of References:.
+As suggested by `message-shorten-references'.")
+
+(defun mu4e~shorten-1 (list cut surplus)
+  "Cut SURPLUS elements out of LIST.
+Beginning with CUTth
+one. Code borrowed from `message-shorten-1'."
+  (setcdr (nthcdr (- cut 2) list)
+          (nthcdr (+ (- cut 2) surplus 1) list)))
+
+
+(defun mu4e~fontify-signature ()
+  "Give the message signatures a distinctive color. This is used
+in the view and compose modes and will color each signature in
+digest messages adhering to RFC 1153."
+  (let ((inhibit-read-only t))
+    (save-excursion
+      ;; give the footer a different color...
+      (goto-char (point-min))
+      (while (re-search-forward "^-- *$" nil t)
+        (let ((p (point))
+              (end (or ;; 30 by RFC1153
+                    (re-search-forward "\\(^-\\{30\\}.*$\\)" nil t)
+                    (point-max))))
+          (add-text-properties p end '(face mu4e-footer-face)))))))
+
+(defun mu4e~draft-references-construct (msg)
+  "Construct the value of the References: header based on MSG.
+This assumes a comma-separated string. Normally, this the concatenation of the
+existing References + In-Reply-To (which may be empty, an note
+that :references includes the old in-reply-to as well) and the
+message-id. If the message-id is empty, returns the old
+References. If both are empty, return nil."
+  (let* ( ;; these are the ones from the message being replied to / forwarded
+         (refs (mu4e-message-field msg :references))
+         (msgid (mu4e-message-field msg :message-id))
+         ;; now, append in
+         (refs (if (and msgid (not (string= msgid "")))
+                   (append refs (list msgid)) refs))
+         ;; no doubles
+         (refs (cl-delete-duplicates refs :test #'equal))
+         (refnum (length refs))
+         (cut 2))
+    ;; remove some refs when there are too many
+    (when (> refnum mu4e~max-reference-num)
+      (let ((surplus (- refnum mu4e~max-reference-num)))
+        (mu4e~shorten-1 refs cut surplus)))
+    (mapconcat (lambda (id) (format "<%s>" id)) refs " ")))
+
+\f
+;;; Determine the recipient fields for new messages
+
+(defun mu4e~draft-recipients-list-to-string (lst)
+  "Convert a lst LST of address cells into a string.
+This is specified as a comma-separated list of e-mail addresses.
+If LST is nil, returns nil."
+  (when lst
+    (mapconcat
+     (lambda (contact) (mu4e-contact-full contact)) lst ", ")))
+
+(defun mu4e~draft-address-cell-equal (cell1 cell2)
+  "Return t if CELL1 and CELL2 have the same e-mail address.
+The comparison is done case-insensitively. If the cells done
+match return nil. CELL1 and CELL2 are cons cells of the
+form (NAME . EMAIL)."
+  (string=
+   (downcase (or (mu4e-contact-email cell1) ""))
+   (downcase (or (mu4e-contact-email cell2) ""))))
+
+
+(defun mu4e~draft-create-to-lst (origmsg)
+  "Create a list of address for the To: in a new message.
+This is based on the original message ORIGMSG. If the Reply-To
+address is set, use that, otherwise use the From address. Note,
+whatever was in the To: field before, goes to the Cc:-list (if
+we're doing a reply-to-all). Special case: if we were the sender
+of the original, we simple copy the list form the original."
+  (let ((reply-to
+         (or (plist-get origmsg :reply-to) (plist-get origmsg :from))))
+    (cl-delete-duplicates reply-to :test #'mu4e~draft-address-cell-equal)
+    (if mu4e-compose-dont-reply-to-self
+        (cl-delete-if
+         (lambda (to-cell)
+           (mu4e-personal-address-p (mu4e-contact-email to-cell)))
+         reply-to)
+      reply-to)))
+
+
+(defun mu4e~strip-ignored-addresses (addrs)
+  "Return all addresses that are not to be ignored.
+I.e. return all the addresses in ADDRS not matching
+`mu4e-compose-reply-ignore-address'."
+  (cond
+   ((null mu4e-compose-reply-ignore-address)
+    addrs)
+   ((functionp mu4e-compose-reply-ignore-address)
+    (cl-remove-if
+     (lambda (elt)
+       (funcall mu4e-compose-reply-ignore-address (mu4e-contact-email elt)))
+     addrs))
+   (t
+    ;; regexp or list of regexps
+    (let* ((regexp mu4e-compose-reply-ignore-address)
+           (regexp (if (listp regexp)
+                       (mapconcat (lambda (elt) (concat "\\(" elt "\\)"))
+                                  regexp "\\|")
+                     regexp)))
+      (cl-remove-if
+       (lambda (elt)
+         (string-match regexp (mu4e-contact-email elt)))
+       addrs)))))
+
+(defun mu4e~draft-create-cc-lst (origmsg &optional reply-all include-from)
+  "Create a list of address for the Cc: in a new message.
+This is based on the original message ORIGMSG, and whether it's a
+REPLY-ALL."
+  (when reply-all
+    (let* ((cc-lst ;; get the cc-field from the original, remove dups
+            (cl-delete-duplicates
+             (append
+              (plist-get origmsg :to)
+              (plist-get origmsg :cc)
+              (when include-from(plist-get origmsg :from))
+              (plist-get origmsg :list-post))
+             :test #'mu4e~draft-address-cell-equal))
+           ;; now we have the basic list, but we must remove
+           ;; addresses also in the To: list
+           (cc-lst
+            (cl-delete-if
+             (lambda (cc-cell)
+               (cl-find-if
+                (lambda (to-cell)
+                  (mu4e~draft-address-cell-equal cc-cell to-cell))
+                (mu4e~draft-create-to-lst origmsg)))
+             cc-lst))
+           ;; remove ignored addresses
+           (cc-lst (mu4e~strip-ignored-addresses cc-lst))
+           ;; finally, we need to remove ourselves from the cc-list
+           ;; unless mu4e-compose-keep-self-cc is non-nil
+           (cc-lst
+            (if (or mu4e-compose-keep-self-cc (null user-mail-address))
+                cc-lst
+              (cl-delete-if
+               (lambda (cc-cell)
+                 (mu4e-personal-address-p (mu4e-contact-email cc-cell)))
+               cc-lst))))
+      cc-lst)))
+
+(defun mu4e~draft-recipients-construct (field origmsg &optional reply-all include-from)
+  "Create value (a string) for the recipient FIELD.
+\(which is a symbol, :to or :cc), based on the original message ORIGMSG,
+and (optionally) REPLY-ALL which indicates this is a reply-to-all
+message. Return nil if there are no recipients for the particular field."
+  (mu4e~draft-recipients-list-to-string
+   (cl-case field
+     (:to
+      (mu4e~draft-create-to-lst origmsg))
+     (:cc
+      (mu4e~draft-create-cc-lst origmsg reply-all include-from))
+     (otherwise
+      (mu4e-error "Unsupported field")))))
+
+(defun mu4e~draft-from-construct ()
+  "Construct a value for the From:-field of the reply.
+This is based on the variable `user-full-name' and
+`user-mail-address'; if the latter is nil, function returns nil."
+  (when user-mail-address
+    (mu4e-contact-full (mu4e-contact-make
+                        user-full-name
+                       user-mail-address))))
+
+;;; Header separators
+
+(defun mu4e~draft-insert-mail-header-separator ()
+  "Insert `mail-header-separator' in the first empty line of the message.
+`message-mode' needs this line to know where the headers end and
+the body starts. Note, in `mu4e-compose-mode', we use
+`before-save-hook' and `after-save-hook' to ensure that this
+separator is never written to the message file. Also see
+`mu4e-remove-mail-header-separator'."
+    ;; we set this here explicitly, since (as it has happened) a wrong
+    ;; value for this (such as "") breaks address completion and other things
+    (set (make-local-variable 'mail-header-separator) "--text follows this line--")
+    (put 'mail-header-separator 'permanent-local t)
+    (save-excursion
+      ;; make sure there's not one already
+      (mu4e~draft-remove-mail-header-separator)
+      (let ((sepa (propertize mail-header-separator
+                              'intangible t
+                              ;; don't make this read-only, message-mode
+                              ;; seems to require it being writable in some cases
+                              ;;'read-only "Can't touch this"
+                              'rear-nonsticky t
+                              'font-lock-face 'mu4e-compose-separator-face)))
+       (widen)
+       ;; search for the first empty line
+       (goto-char (point-min))
+       (if (search-forward-regexp "^$" nil t)
+            (progn
+              (replace-match sepa)
+              ;; `message-narrow-to-headers` searches for a
+              ;; `mail-header-separator` followed by a new line. Therefore, we
+              ;; must insert a newline if on the last line of the buffer.
+              (when (= (point) (point-max))
+               (insert "\n")))
+          (progn ;; no empty line? then prepend one
+            (goto-char (point-max))
+            (insert "\n" sepa))))))
+
+(defun mu4e~draft-remove-mail-header-separator ()
+  "Remove `mail-header-separator'.
+We do this before saving a
+file (and restore it afterwards), to ensure that the separator
+never hits the disk. Also see
+`mu4e~draft-insert-mail-header-separator."
+  (save-excursion
+    (widen)
+    (goto-char (point-min))
+    ;; remove the --text follows this line-- separator
+    (when (search-forward-regexp (concat "^" mail-header-separator) nil t)
+      (let ((inhibit-read-only t))
+        (replace-match "")))))
+
+(defun mu4e~draft-reply-all-p (origmsg)
+  "Ask user whether she wants to reply to *all* recipients.
+If there is just one recipient of ORIGMSG do nothing."
+  (let* ((recipnum
+          (+ (length (mu4e~draft-create-to-lst origmsg))
+             (length (mu4e~draft-create-cc-lst origmsg t))))
+         (response
+          (if (< recipnum 2)
+              'all ;; with less than 2 recipients, we can reply to 'all'
+            (mu4e-read-option
+             "Reply to "
+             `( (,(format "all %d recipients" recipnum) . all)
+                ("sender only" . sender-only))))))
+    (eq response 'all)))
+
+(defun mu4e~draft-message-filename-construct (&optional flagstr)
+  "Construct a randomized name for a message file with flags FLAGSTR.
+It looks something like
+  <time>-<random>.<hostname>:2,
+You can append flags."
+  (let* ((sysname (if (fboundp 'system-name)
+                      (system-name)
+                    (with-no-warnings system-name)))
+         (sysname (if (string= sysname "") "localhost" sysname))
+         (hostname (downcase
+                    (save-match-data
+                      (substring sysname
+                                 (string-match "^[^.]+" sysname)
+                                 (match-end 0))))))
+    (format "%s.%04x%04x%04x%04x.%s%s2,%s"
+            (format-time-string "%s" (current-time))
+            (random 65535) (random 65535) (random 65535) (random 65535)
+            hostname mu4e-maildir-info-delimiter (or flagstr ""))))
+
+(defun mu4e~draft-common-construct ()
+  "Construct the common headers for each message."
+  (concat
+   (when-let ((organization (message-make-organization)))
+     (mu4e~draft-header "Organization" organization))
+   (when mu4e-user-agent-string
+     (mu4e~draft-header "User-agent" mu4e-user-agent-string))
+   (mu4e~draft-header "Date" (message-make-date))))
+
+(defconst mu4e~draft-reply-prefix "Re: "
+  "String to prefix replies with.")
+
+(defun mu4e~draft-reply-construct-recipients (origmsg)
+  "Determine the to/cc recipients for a reply message."
+  (let* ((reply-to-self (mu4e-message-contact-field-matches-me origmsg :from))
+         ;; reply-to-self implies reply-all
+         (reply-all (or reply-to-self
+                        (eq mu4e-compose-reply-recipients 'all)
+                        (and (not (eq mu4e-compose-reply-recipients 'sender))
+                             (mu4e~draft-reply-all-p origmsg)))))
+    (concat
+     (if reply-to-self
+         ;; When we're replying to ourselves, simply keep the same headers.
+         (concat
+          (mu4e~draft-header "To" (mu4e~draft-recipients-list-to-string
+                                   (mu4e-message-field origmsg :to)))
+          (mu4e~draft-header "Cc" (mu4e~draft-recipients-list-to-string
+                                   (mu4e-message-field origmsg :cc))))
+
+       ;; if there's no-one in To, copy the CC-list
+       (if (zerop (length (mu4e~draft-create-to-lst origmsg)))
+           (mu4e~draft-header "To" (mu4e~draft-recipients-construct
+                                    :cc origmsg reply-all))
+         ;; otherwise...
+         (concat
+          (mu4e~draft-header "To" (mu4e~draft-recipients-construct :to origmsg))
+          (mu4e~draft-header "Cc" (mu4e~draft-recipients-construct :cc origmsg reply-all))))))))
+
+(defun mu4e~draft-reply-construct-recipients-list (origmsg)
+  "Determine the to/cc recipients for a reply message to a
+mailing-list."
+  (let* ( ;; reply-to-self implies reply-all
+         (list-post (plist-get origmsg :list-post))
+         (from      (plist-get origmsg :from))
+         (recipnum
+          (+ (length (mu4e~draft-create-to-lst origmsg))
+             (length (mu4e~draft-create-cc-lst origmsg t t))))
+        (sender (mu4e-contact-full (car from)))
+         (reply-type
+          (mu4e-read-option
+           "Reply to mailing-list "
+           `( (,(format "all %d recipient(s)" recipnum)    . all)
+              (,(format "list-only (%s)" (cdar list-post)) . list-only)
+              (,(format "sender-only (%s)" sender)         . sender-only)))))
+    (cl-case reply-type
+      (all
+       (concat
+        (mu4e~draft-header "To" (mu4e~draft-recipients-construct :to origmsg))
+        (mu4e~draft-header "Cc" (mu4e~draft-recipients-construct :cc origmsg t t))))
+      (list-only
+       (mu4e~draft-header "To"
+                          (mu4e~draft-recipients-list-to-string list-post)))
+      (sender-only
+       (mu4e~draft-header "To"
+                          (mu4e~draft-recipients-list-to-string from))))))
+
+(defun mu4e~draft-reply-construct (origmsg)
+  "Create a draft message as a reply to ORIGMSG.
+Replying-to-self is special; in that case, the To and Cc fields
+will be the same as in the original."
+  (let* ((old-msgid (plist-get origmsg :message-id))
+         (subject (concat mu4e~draft-reply-prefix
+                          (message-strip-subject-re
+                           (or (plist-get origmsg :subject) ""))))
+         (list-post (plist-get origmsg :list-post)))
+    (concat
+     (mu4e~draft-header "From" (or (mu4e~draft-from-construct) ""))
+     (mu4e~draft-header "Reply-To" mu4e-compose-reply-to-address)
+
+     (if list-post ;; mailing-lists are a bit special.
+         (mu4e~draft-reply-construct-recipients-list origmsg)
+       (mu4e~draft-reply-construct-recipients origmsg))
+
+     (mu4e~draft-header "Subject" subject)
+     (mu4e~draft-header "References"
+                        (mu4e~draft-references-construct origmsg))
+     (mu4e~draft-common-construct)
+     (when old-msgid
+       (mu4e~draft-header "In-reply-to" (format "<%s>" old-msgid)))
+     "\n\n"
+     (mu4e~draft-cite-original origmsg))))
+
+(defconst mu4e~draft-forward-prefix "Fwd: "
+  "String to prefix replies with.")
+
+(defun mu4e~draft-forward-construct (origmsg)
+  "Create a draft forward message for original message ORIGMSG."
+  (let ((subject
+         (or (plist-get origmsg :subject) "")))
+    (concat
+     (mu4e~draft-header "From" (or (mu4e~draft-from-construct) ""))
+     (mu4e~draft-header "Reply-To" mu4e-compose-reply-to-address)
+     (mu4e~draft-header "To" "")
+     (mu4e~draft-common-construct)
+     (mu4e~draft-header "References"
+                        (mu4e~draft-references-construct origmsg))
+     (mu4e~draft-header "Subject"
+                        (concat
+                         ;; if there's no Fwd: yet, prepend it
+                         (if (string-match "^Fwd:" subject)
+                             ""
+                           mu4e~draft-forward-prefix)
+                         subject))
+     (unless mu4e-compose-forward-as-attachment
+       (concat
+        "\n\n"
+        (mu4e~draft-cite-original origmsg))))))
+
+(defun mu4e~draft-newmsg-construct ()
+  "Create a new message."
+  (concat
+   (mu4e~draft-header "From" (or (mu4e~draft-from-construct) ""))
+   (mu4e~draft-header "Reply-To" mu4e-compose-reply-to-address)
+   (mu4e~draft-header "To" "")
+   (mu4e~draft-header "Subject" "")
+   (mu4e~draft-common-construct)))
+
+(defvar mu4e~draft-drafts-folder nil
+  "The drafts-folder for this compose buffer.
+This is based on `mu4e-drafts-folder', which is evaluated once.")
+
+(defun mu4e~draft-open-file (path switch-function)
+  "Open the the draft file at PATH."
+  (let ((buf (find-file-noselect path)))
+    (funcall (or
+              switch-function
+              (and mu4e-compose-in-new-frame 'switch-to-buffer-other-frame)
+              'switch-to-buffer)
+             buf)))
+
+
+(defun mu4e~draft-determine-path (draft-dir)
+  "Determines the path for a new draft file in DRAFT-DIR."
+  (format "%s/%s/cur/%s"
+          (mu4e-root-maildir) draft-dir (mu4e~draft-message-filename-construct "DS")))
+
+
+(defun mu4e-draft-open (compose-type &optional msg switch-function)
+  "Open a draft file for a message MSG.
+In case of a new message (when COMPOSE-TYPE is `reply', `forward'
+ or `new'), open an existing draft (when COMPOSE-TYPE is `edit'),
+ or re-send an existing message (when COMPOSE-TYPE is `resend').
+
+The name of the draft folder is constructed from the
+concatenation of `(mu4e-root-maildir)' and `mu4e-drafts-folder' (the
+latter will be evaluated). The message file name is a unique name
+determined by `mu4e-send-draft-file-name'. The initial contents
+will be created from either `mu4e~draft-reply-construct', or
+`mu4e~draft-forward-construct' or `mu4e~draft-newmsg-construct'."
+  (let ((draft-dir nil))
+    (cl-case compose-type
+
+      (edit
+       ;; case-1: re-editing a draft messages. in this case, we do know the
+       ;; full path, but we cannot really know 'drafts folder'... we make a
+       ;; guess
+       (setq draft-dir (mu4e--guess-maildir (mu4e-message-field msg :path)))
+       (mu4e~draft-open-file (mu4e-message-field msg :path) switch-function))
+
+      (resend
+       ;; case-2: copy some exisisting message to a draft message, then edit
+       ;; that.
+       (setq draft-dir (mu4e--guess-maildir (mu4e-message-field msg :path)))
+       (let ((draft-path (mu4e~draft-determine-path draft-dir)))
+         (copy-file (mu4e-message-field msg :path) draft-path)
+         (mu4e~draft-open-file draft-path switch-function)))
+
+      ((reply forward new)
+       ;; case-3: creating a new message; in this case, we can determine
+       ;; mu4e-get-drafts-folder
+       (setq draft-dir (mu4e-get-drafts-folder msg))
+       (let ((draft-path (mu4e~draft-determine-path draft-dir))
+             (initial-contents
+              (cl-case compose-type
+                (reply   (mu4e~draft-reply-construct msg))
+                (forward (mu4e~draft-forward-construct msg))
+                (new     (mu4e~draft-newmsg-construct)))))
+         (mu4e~draft-open-file draft-path switch-function)
+         (insert initial-contents)
+         (newline)
+         ;; include the message signature (if it's set)
+         (if (and mu4e-compose-signature-auto-include mu4e-compose-signature)
+             (let ((message-signature mu4e-compose-signature))
+               (save-excursion
+                 (message-insert-signature)
+                 (mu4e~fontify-signature))))))
+      (t (mu4e-error "Unsupported compose-type %S" compose-type)))
+    ;; if we didn't find a draft folder yet, try some default
+    (unless draft-dir
+      (setq draft-dir (mu4e-get-drafts-folder msg)))
+    ;; evaluate mu4e~drafts-drafts-folder once, here, and use that value
+    ;; throughout.
+    (set (make-local-variable 'mu4e~draft-drafts-folder) draft-dir)
+    (put 'mu4e~draft-drafts-folder 'permanent-local t)
+    (unless mu4e~draft-drafts-folder
+      (mu4e-error "Failed to determine drafts folder"))))
+
+;;; _
+(provide 'mu4e-draft)
+;;; mu4e-draft.el ends here
diff --git a/mu4e/mu4e-folders.el b/mu4e/mu4e-folders.el
new file mode 100644 (file)
index 0000000..a01e17e
--- /dev/null
@@ -0,0 +1,341 @@
+;;; mu4e-folders.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Dealing with maildirs & folders
+
+;;; Code:
+(require 'mu4e-helpers)
+(require 'mu4e-context)
+(require 'mu4e-server)
+\f
+;;; Customization
+(defgroup mu4e-folders nil
+  "Special folders."
+  :group 'mu4e)
+
+(defcustom mu4e-drafts-folder "/drafts"
+  "Folder for draft messages, relative to the root maildir.
+For instance, \"/drafts\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. Note, the message
+parameter refers to the original message being replied to / being
+forwarded / re-edited and is nil otherwise. `mu4e-drafts-folder'
+is only evaluated once."
+  :type '(choice
+          (string :tag "Folder name")
+          (function :tag "Function return folder name"))
+  :group 'mu4e-folders)
+
+(defcustom mu4e-refile-folder "/archive"
+  "Folder for refiling messages, relative to the root maildir.
+For instance \"/Archive\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. Note that the
+message parameter refers to the message-at-point."
+  :type '(choice
+          (string :tag "Folder name")
+          (function :tag "Function return folder name"))
+  :group 'mu4e-folders)
+
+(defcustom mu4e-sent-folder "/sent"
+  "Folder for sent messages, relative to the root maildir.
+For instance, \"/Sent Items\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. Note that the
+message parameter refers to the original message being replied to
+/ being forwarded / re-edited, and is nil otherwise."
+  :type '(choice
+          (string :tag "Folder name")
+          (function :tag "Function return folder name"))
+  :group 'mu4e-folders)
+
+(defcustom mu4e-trash-folder "/trash"
+  "Folder for trashed messages, relative to the root maildir.
+For instance, \"/trash\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. When using
+`mu4e-trash-folder' in the headers view (when marking messages
+for trash). Note that the message parameter refers to the
+message-at-point. When using it when composing a message (see
+`mu4e-sent-messages-behavior'), this refers to the original
+message being replied to / being forwarded / re-edited, and is
+nil otherwise."
+  :type '(choice
+          (string :tag "Folder name")
+          (function :tag "Function return folder name"))
+  :group 'mu4e-folders)
+
+(defcustom mu4e-maildir-shortcuts nil
+  "A list of maildir shortcuts.
+This makes it possible to quickly go to a particular
+maildir (folder), or quickly moving messages to them (e.g., for
+archiving or refiling).
+
+Each of the list elements is a plist with at least:
+`:maildir'  - the maildir for the shortcut (e.g. \"/archive\")
+`:key'      - the shortcut key.
+
+Optionally, you can add the following:
+`:hide'  - if t, the shortcut is hidden from the main-view and
+speedbar.
+`:hide-unread' - do not show the counts of unread/total number
+ of matches for the maildir in the main-view, and is implied
+from `:hide'.
+
+For backward compatibility, an older form is recognized as well:
+
+   (maildir . key), where MAILDIR is a maildir (such as
+\"/archive/\"), and key is a single character.
+
+You can use these shortcuts in the headers and view buffers, for
+example with `mu4e-mark-for-move-quick' (or \"m\", by default) or
+`mu4e-jump-to-maildir' (or \"j\", by default), followed by the
+designated shortcut character for the maildir.
+
+Unlike in search queries, folder names with spaces in them must
+NOT be quoted, since mu4e does this for you."
+  :type '(repeat (cons (string :tag "Maildir") character))
+  :version "1.3.9"
+  :group 'mu4e-folders)
+
+(defcustom mu4e-maildir-info-delimiter
+  (if (member system-type '(ms-dos windows-nt cygwin))
+      ";" ":")
+  "Separator character between message identifier and flags.
+It defaults to ':' on most platforms, except on Windows, where it
+is not allowed and we use ';' for compatibility with mbsync,
+offlineimap and other programs."
+  :type 'string
+  :group 'mu4e-folders)
+
+(defcustom mu4e-attachment-dir (expand-file-name "~/")
+  "Default directory for attaching and saving attachments.
+
+This can be either a string (a file system path), or a function
+that takes a filename and the mime-type as arguments, and returns
+the attachment dir. See Info node `(mu4e) Attachments' for
+details.
+
+When this called for composing a message, both filename and
+mime-type are nill."
+  :type 'directory
+  :group 'mu4e-folders
+  :safe 'stringp)
+
+\f
+(defun mu4e-maildir-shortcuts ()
+  "Get `mu4e-maildir-shortcuts' in the (new) format.
+Converts from the old format if needed."
+  (seq-map (lambda (item) ;; convert from old format?
+             (if (and (consp item) (not (consp (cdr item))))
+                 `(:maildir  ,(car item) :key ,(cdr item))
+               item))
+           mu4e-maildir-shortcuts))
+
+(defun mu4e--maildirs-with-query ()
+  "Like `mu4e-maildir-shortcuts', but with :query populated.
+This is compatibile with `mu4e-bookmarks'."
+  (seq-map
+   (lambda (item)
+     (let* ((maildir (plist-get item :maildir))
+           (item (plist-put item :name maildir))
+           (item (plist-put item :query (format "maildir:\"%s\"" maildir))))
+       item)) ;; we don't need ":maildir", but it's harmless.
+   (mu4e-maildir-shortcuts)))
+
+;; the standard folders can be functions too
+(defun mu4e--get-folder (foldervar msg)
+  "Within the mu-context of MSG, get message folder FOLDERVAR.
+If FOLDER is a string, return it, if it is a function, evaluate
+this function with MSG as parameter which may be nil, and return
+the result."
+  (unless (member foldervar
+                  '(mu4e-sent-folder mu4e-drafts-folder
+                                     mu4e-trash-folder mu4e-refile-folder))
+    (mu4e-error "Folder must be one of mu4e-(sent|drafts|trash|refile)-folder"))
+  ;; get the value with the vars for the relevants context let-bound
+  (with-mu4e-context-vars (mu4e-context-determine msg nil)
+      (let* ((folder (symbol-value foldervar))
+             (val
+              (cond
+               ((stringp   folder) folder)
+               ((functionp folder) (funcall folder msg))
+               (t (mu4e-error "Unsupported type for %S" folder)))))
+        (or val (mu4e-error "%S evaluates to nil" foldervar)))))
+
+(defun mu4e-get-drafts-folder (&optional msg)
+  "Get the sent folder, optionallly based on MSG.
+See `mu4e-drafts-folder'." (mu4e--get-folder 'mu4e-drafts-folder msg))
+
+(defun mu4e-get-refile-folder (&optional msg)
+  "Get the folder for refiling, optionallly based on MSG.
+See `mu4e-refile-folder'." (mu4e--get-folder 'mu4e-refile-folder msg))
+
+(defun mu4e-get-sent-folder (&optional msg)
+  "Get the sent folder, optionallly based on MSG.
+See `mu4e-sent-folder'." (mu4e--get-folder 'mu4e-sent-folder msg))
+
+(defun mu4e-get-trash-folder (&optional msg)
+  "Get the sent folder, optionallly based on MSG.
+See `mu4e-trash-folder'." (mu4e--get-folder 'mu4e-trash-folder msg))
+
+
+;;; Maildirs
+(defun mu4e--guess-maildir (path)
+  "Guess the maildir for PATH, or nil if cannot find it."
+  (let ((idx (string-match (mu4e-root-maildir) path)))
+    (when (and idx (zerop idx))
+      (replace-regexp-in-string
+       (mu4e-root-maildir)
+       ""
+       (expand-file-name
+        (concat path "/../.."))))))
+
+(defun mu4e-create-maildir-maybe (dir)
+  "Offer to create maildir DIR if it does not exist yet.
+Return t if the dir already existed, or an attempt has been made to
+create it -- we cannot be sure creation succeeded here, since this
+is done asynchronously. Otherwise, return nil. NOte, DIR has to be
+an absolute path."
+  (if (and (file-exists-p dir) (not (file-directory-p dir)))
+      (mu4e-error "File %s exists, but is not a directory" dir))
+  (cond
+   ((file-directory-p dir) t)
+   ((yes-or-no-p (mu4e-format "%s does not exist yet. Create now?" dir))
+    (mu4e--server-mkdir dir) t)
+   (t nil)))
+
+(defun mu4e~get-maildirs-1 (path mdir)
+  "Get maildirs for MDIR under PATH.
+Do so recursively and produce a list of relative paths."
+  (let ((dirs)
+        (dentries
+         (ignore-errors
+           (directory-files-and-attributes
+            (concat path mdir) nil
+            "^[^.]\\|\\.[^.][^.]" t))))
+    (dolist (dentry dentries)
+      (when (and (booleanp (cadr dentry)) (cadr dentry))
+        (if (file-accessible-directory-p
+             (concat (mu4e-root-maildir) "/" mdir "/" (car dentry) "/cur"))
+            (setq dirs (cons (concat mdir (car dentry)) dirs)))
+        (unless (member (car dentry) '("cur" "new" "tmp"))
+          (setq dirs
+               (append dirs
+                       (mu4e~get-maildirs-1 path
+                                             (concat mdir
+                                                    (car dentry) "/")))))))
+    dirs))
+
+(defvar mu4e-cache-maildir-list nil
+  "Whether to cache the list of maildirs.
+Set it to t if you find that generating the list on the fly is
+too slow. If you do, you can set `mu4e-maildir-list' to nil to
+force regenerating the cache the next time `mu4e-get-maildirs'
+gets called.")
+
+(defvar mu4e-maildir-list nil
+  "Cached list of maildirs.")
+
+(defun mu4e-get-maildirs ()
+  "Get maildirs under `mu4e-maildir'.
+Do so recursively, and produce a list of relative paths (ie.,
+/archive, /sent etc.). Most of the work is done in
+`mu4e~get-maildirs-1'. Note, these results are /cached/ if
+`mu4e-cache-maildir-list' is customized to non-nil. In that case,
+the list of maildirs will not change until you restart mu4e."
+  (unless (and mu4e-maildir-list mu4e-cache-maildir-list)
+    (setq mu4e-maildir-list
+          (sort
+           (append
+            (when (file-accessible-directory-p
+                   (concat (mu4e-root-maildir) "/cur")) '("/"))
+            (mu4e~get-maildirs-1 (mu4e-root-maildir) "/"))
+           (lambda (s1 s2) (string< (downcase s1) (downcase s2))))))
+  mu4e-maildir-list)
+
+(defun mu4e-ask-maildir (prompt)
+  "Ask the user for a shortcut (using PROMPT).
+As per (mu4e-maildir-shortcuts), then return the corresponding
+folder name. If the special shortcut \"o\" (for _o_ther) is used,
+or if (mu4e-maildir-shortcuts) evaluates to nil, let user choose
+from all maildirs under `mu4e-maildir'."
+  (let ((prompt (mu4e-format "%s" prompt)))
+    (if (not (mu4e-maildir-shortcuts))
+        (substring-no-properties
+         (funcall mu4e-completing-read-function prompt (mu4e-get-maildirs)))
+      (let* ((mlist (append (mu4e-maildir-shortcuts)
+                            '((:maildir "ther"  :key ?o))))
+             (fnames
+              (mapconcat
+               (lambda (item)
+                 (concat
+                  "["
+                  (propertize (make-string 1 (plist-get item :key))
+                              'face 'mu4e-highlight-face)
+                  "]"
+                  (plist-get item :maildir)))
+               mlist ", "))
+             (kar (read-char (concat prompt fnames))))
+        (if (member kar '(?/ ?o)) ;; user chose 'other'?
+            (substring-no-properties
+             (funcall mu4e-completing-read-function prompt
+                      (mu4e-get-maildirs) nil nil "/"))
+          (or (plist-get
+               (seq-find (lambda (item) (= kar (plist-get item :key)))
+                         (mu4e-maildir-shortcuts)) :maildir)
+              (mu4e-warn "Unknown shortcut '%c'" kar)))))))
+
+(defun mu4e-ask-maildir-check-exists (prompt)
+  "Like `mu4e-ask-maildir', PROMPT for existence of the maildir.
+Offer to create it if it does not exist yet."
+  (let* ((mdir (mu4e-ask-maildir prompt))
+         (fullpath (concat (mu4e-root-maildir) mdir)))
+    (unless (file-directory-p fullpath)
+      (and (yes-or-no-p
+            (mu4e-format "%s does not exist. Create now?" fullpath))
+           (mu4e--server-mkdir fullpath)))
+    mdir))
+
+;; mu4e-attachment-dir is either a string or a function that takes a
+;; filename and the mime-type as argument, either (or both) which can
+;; be nil
+
+(defun mu4e~get-attachment-dir (&optional fname mimetype)
+  "Get the directory for saving attachments from `mu4e-attachment-dir'.
+This is optionally based on the file-name FNAME and its MIMETYPE."
+  (let ((dir
+         (cond
+          ((stringp mu4e-attachment-dir)
+           mu4e-attachment-dir)
+          ((functionp mu4e-attachment-dir)
+           (funcall mu4e-attachment-dir fname mimetype))
+          (t
+          (mu4e-error "Unsupported type for mu4e-attachment-dir" )))))
+    (if dir
+        (expand-file-name dir)
+      (mu4e-error "Mu4e-attachment-dir evaluates to nil"))))
+
+(provide 'mu4e-folders)
+;;; mu4e-folders.el ends here
diff --git a/mu4e/mu4e-headers.el b/mu4e/mu4e-headers.el
new file mode 100644 (file)
index 0000000..1eabd8c
--- /dev/null
@@ -0,0 +1,1871 @@
+;;; mu4e-headers.el -- part of mu4e -*- lexical-binding: t; coding:utf-8 -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file are function related mu4e-headers-mode, to creating the list of
+;; one-line descriptions of emails, aka 'headers' (not to be confused with
+;; headers like 'To:' or 'Subject:')
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'fringe)
+(require 'hl-line)
+(require 'mailcap)
+(require 'mule-util) ;; seems _some_ people need this for truncate-string-ellipsis
+
+(require 'mu4e-update)
+
+    ;; utility functions
+(require 'mu4e-server)
+(require 'mu4e-vars)
+(require 'mu4e-mark)
+(require 'mu4e-context)
+(require 'mu4e-contacts)
+(require 'mu4e-search)
+(require 'mu4e-compose)
+(require 'mu4e-actions)
+(require 'mu4e-message)
+(require 'mu4e-lists)
+(require 'mu4e-update)
+(require 'mu4e-folders)
+
+(declare-function mu4e-view       "mu4e-view")
+(declare-function mu4e--main-view  "mu4e-main")
+
+\f
+
+;;; Configuration
+
+(defgroup mu4e-headers nil
+  "Settings for the headers view."
+  :group 'mu4e)
+
+(defcustom mu4e-headers-fields
+  '( (:human-date    .   12)
+     (:flags         .    6)
+     (:mailing-list  .   10)
+     (:from          .   22)
+     (:subject       .   nil))
+  "A list of header fields to show in the headers buffer.
+Each element has the form (HEADER . WIDTH), where HEADER is one of
+the available headers (see `mu4e-header-info') and WIDTH is the
+respective width in characters.
+
+A width of nil means \"unrestricted\", and this is best reserved
+for the rightmost (last) field. Note that emacs may become very
+slow with excessively long lines (1000s of characters), so if you
+regularly get such messages, you want to avoid fields with nil
+altogether."
+  :type `(repeat (cons (choice ,@(mapcar (lambda (h)
+                                           (list 'const :tag
+                                                 (plist-get (cdr h) :help)
+                                                 (car h)))
+                                         mu4e-header-info))
+                       (choice (integer :tag "width")
+                               (const :tag "unrestricted width" nil))))
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-date-format "%x"
+  "Date format to use in the headers view.
+In the format of `format-time-string'."
+  :type  'string
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-time-format "%X"
+  "Time format to use in the headers view.
+In the format of `format-time-string'."
+  :type  'string
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-long-date-format "%c"
+  "Date format to use in the headers view tooltip.
+In the format of `format-time-string'."
+  :type  'string
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-visible-lines 10
+  "Number of lines to display in the header view when using the
+horizontal split-view. This includes the header-line at the top,
+and the mode-line."
+  :type 'integer
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-visible-columns 30
+  "Number of columns to display for the header view when using the
+vertical split-view."
+  :type 'integer
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-precise-alignment nil
+  "When set, use precise (but relatively slow) alignment for columns.
+By default, do it in a slightly inaccurate but faster way. To get
+an idea about the difference, In some tests, the rendering time
+was around 5.8 ms per messages for precise alignment, versus 3.3
+for non-precise aligment (for 445 messages)."
+  :type 'boolean
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-auto-update t
+  "Whether to automatically update the current headers buffer if an
+indexing operation showed changes."
+  :type 'boolean
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-advance-after-mark t
+  "With this option set to non-nil, automatically advance to the
+next mail after marking a message in header view."
+  :type 'boolean
+  :group 'mu4e-headers)
+
+
+(defvar mu4e-headers-hide-predicate nil
+  "Predicate function to hide matching heasders.
+If the function evaluates to non-nil when applied a a message
+plist, do not show the corresponding header. The function takes
+one parameter MSG, which is the message plist for the message to
+be hidden or not.
+
+Example that hides all trashed messages:
+
+  (setq mu4e-headers-hide-predicate
+     (lambda (msg)
+       (member \='trashed (mu4e-message-field msg :flags)))).")
+
+(defcustom mu4e-headers-visible-flags
+  '(draft flagged new passed replied trashed attach encrypted signed list personal)
+  "An ordered list of flags to show in the headers buffer.
+Each element is a symbol in the list.
+
+By default, we leave out `unread' and `seen', since those are
+mostly covered by `new', and the display gets cluttered otherwise."
+  :type '(set
+          (const :tag "Draft" draft)
+          (const :tag "Flagged" flagged)
+          (const :tag "New" new)
+          (const :tag "Passed" passed)
+          (const :tag "Replied" replied)
+          (const :tag "Seen" seen)
+          (const :tag "Trashed" trashed)
+          (const :tag "Attach" attach)
+          (const :tag "Encrypted" encrypted)
+          (const :tag "Signed" signed)
+         (const :tag "List" list)
+         (const :tag "Personal" personal)
+         (const :tag "Calendar" calendar))
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-found-hook nil
+  "Hook run just *after* all of the headers for the last search
+query have been received and are displayed."
+  :type 'hook
+  :group 'mu4e-headers)
+
+;;; Public variables
+
+(defvar mu4e-headers-sort-field :date
+  "Field to sort the headers by. A symbol:
+one of: `:date', `:subject', `:size', `:prio', `:from', `:to.',
+`:list'.
+
+Note that when threading is enabled (through
+`mu4e-search-threads'), the headers are exclusively sorted
+chronologically (`:date') by the newest message in the thread.")
+
+(defvar mu4e-headers-sort-direction 'descending
+  "Direction to sort by; a symbol either `descending' (sorting
+  Z->A) or `ascending' (sorting A->Z).")
+
+(defcustom mu4e-headers-from-or-to-prefix '("" . "To ")
+  "Prefix for the :from-or-to field when it is showing,
+  respectively, From: or To:.  It is a cons cell with the car
+  element being the From: prefix, the cdr element the To: prefix."
+  :type '(cons string string)
+  :group 'mu4e-headers)
+
+;;;; Fancy marks
+
+;; marks for headers of the form; each is a cons-cell (basic . fancy)
+;; each of which is basic ascii char and something fancy, respectively
+;; by default, we some conservative marks, even when 'fancy'
+;; so they're less likely to break if people don't have certain fonts.
+;; However, if you want to be really 'fancy', you could use something like
+;; the following; esp. with a newer Emacs with color-icon support.
+;; (setq
+;;  mu4e-headers-draft-mark     '("D" . "💈")
+;;  mu4e-headers-flagged-mark   '("F" . "📍")
+;;  mu4e-headers-new-mark       '("N" . "🔥")
+;;  mu4e-headers-passed-mark    '("P" . "❯")
+;;  mu4e-headers-replied-mark   '("R" . "❮")
+;;  mu4e-headers-seen-mark      '("S" . "☑")
+;;  mu4e-headers-trashed-mark   '("T" . "💀")
+;;  mu4e-headers-attach-mark    '("a" . "📎")
+;;  mu4e-headers-encrypted-mark '("x" . "🔒")
+;;  mu4e-headers-signed-mark    '("s" . "🔑")
+;;  mu4e-headers-unread-mark    '("u" . "⎕")
+;;  mu4e-headers-list-mark      '("s" . "🔈")
+;;  mu4e-headers-personal-mark  '("p" . "👨")
+;;  mu4e-headers-calendar-mark  '("c" . "📅"))
+
+
+(defvar mu4e-headers-draft-mark     '("D" . "⚒") "Draft.")
+(defvar mu4e-headers-flagged-mark   '("F" . "✚") "Flagged.")
+(defvar mu4e-headers-new-mark       '("N" . "✱") "New.")
+(defvar mu4e-headers-passed-mark    '("P" . "❯") "Passed (fwd).")
+(defvar mu4e-headers-replied-mark   '("R" . "❮") "Replied.")
+(defvar mu4e-headers-seen-mark      '("S" . "✔") "Seen.")
+(defvar mu4e-headers-trashed-mark   '("T" . "⏚") "Trashed.")
+(defvar mu4e-headers-attach-mark    '("a" . "⚓") "W/ attachments.")
+(defvar mu4e-headers-encrypted-mark '("x" . "⚴") "Encrypted.")
+(defvar mu4e-headers-signed-mark    '("s" . "☡") "Signed.")
+(defvar mu4e-headers-unread-mark    '("u" . "⎕") "Unread.")
+(defvar mu4e-headers-list-mark      '("s" . "Ⓛ") "Mailing list.")
+(defvar mu4e-headers-personal-mark  '("p" . "Ⓟ") "Personal.")
+(defvar mu4e-headers-calendar-mark  '("c" . "Ⓒ") "Calendar invitation.")
+
+
+;;;; Graph drawing
+
+(defvar mu4e-headers-thread-mark-as-orphan 'first
+  "Define which messages should be prefixed with the orphan mark.
+`all' marks all the messages without a parent as orphan, `first'
+only marks the first message in the thread.")
+
+(defvar mu4e-headers-thread-root-prefix '("* " . "□ ")
+  "Prefix for root messages.")
+(defvar mu4e-headers-thread-child-prefix '("|>" . "│ ")
+  "Prefix for messages in sub threads that do have a following sibling.")
+(defvar mu4e-headers-thread-first-child-prefix '("o " . "⚬ ")
+  "Prefix for the first child messages in a sub thread.")
+(defvar mu4e-headers-thread-last-child-prefix '("L" . "└ ")
+  "Prefix for messages in sub threads that do not have a following sibling.")
+(defvar mu4e-headers-thread-connection-prefix '("|" . "│ ")
+  "Prefix to connect sibling messages that do not follow each other.
+Must have the same length as `mu4e-headers-thread-blank-prefix'.")
+(defvar mu4e-headers-thread-blank-prefix '(" " . "  ")
+  "Prefix to separate non connected messages.
+Must have the same length as `mu4e-headers-thread-connection-prefix'.")
+(defvar mu4e-headers-thread-orphan-prefix '("<>" . "♢ ")
+  "Prefix for orphan messages with siblings.")
+(defvar mu4e-headers-thread-single-orphan-prefix '("<>" . "♢ ")
+  "Prefix for orphan messages with no siblings.")
+(defvar mu4e-headers-thread-duplicate-prefix '("=" . "≡ ")
+  "Prefix for duplicate messages.")
+
+(defvar mu4e-headers-threaded-label   '("T" . "Ⓣ")
+  "Non-fancy and fancy labels to indicate threaded search in the mode-line.")
+(defvar mu4e-headers-full-label       '("F" . "Ⓕ")
+  "Non-fancy and fancy labels to indicate full search in the mode-line.")
+(defvar mu4e-headers-related-label    '("R" . "Ⓡ")
+  "Non-fancy and fancy labels to indicate related search in the mode-line.")
+(defvar mu4e-headers-skip-duplicates-label '("U" . "Ⓤ") ;; 'U' for 'unique'
+  "Non-fancy and fancy labels for include-related search in the mode-line.")
+
+;;;; Various
+
+(defvar mu4e-headers-actions
+  '( ("capture message"  . mu4e-action-capture-message)
+     ("show this thread" . mu4e-action-show-thread))
+  "List of actions to perform on messages in the headers list.
+The actions are cons-cells of the form (NAME . FUNC) where:
+* NAME is the name of the action (e.g. \"Count lines\")
+* FUNC is a function which receives a message plist as an argument.
+
+The first character of NAME is used as the shortcut.")
+
+(defvar mu4e-headers-custom-markers
+  '(("Older than"
+     (lambda (msg date) (time-less-p (mu4e-msg-field msg :date) date))
+     (lambda () (mu4e-get-time-date "Match messages before: ")))
+    ("Newer than"
+     (lambda (msg date) (time-less-p date (mu4e-msg-field msg :date)))
+     (lambda () (mu4e-get-time-date "Match messages after: ")))
+    ("Bigger than"
+     (lambda (msg bytes) (> (mu4e-msg-field msg :size) (* 1024 bytes)))
+     (lambda () (read-number "Match messages bigger than (Kbytes): "))))
+  "List of custom markers -- functions to mark message that match
+some custom function. Each of the list members has the following format:
+  (NAME PREDICATE-FUNC PARAM-FUNC)
+* NAME is the name of the predicate function, and the first character
+is the shortcut (so keep those unique).
+* PREDICATE-FUNC is a function that takes two parameters, MSG
+and (optionally) PARAM, and should return non-nil when there's a
+match.
+* PARAM-FUNC is function that is evaluated once, and its value is then passed to
+PREDICATE-FUNC as PARAM. This is useful for getting user-input.")
+;;; Internal variables/constants
+
+;; docid cookies
+(defconst mu4e~headers-docid-pre "\376"
+  "Each header starts (invisibly) with the `mu4e~headers-docid-pre',
+followed by the docid, followed by `mu4e~headers-docid-post'.")
+(defconst mu4e~headers-docid-post "\377"
+  "Each header starts (invisibly) with the `mu4e~headers-docid-pre',
+followed by the docid, followed by `mu4e~headers-docid-post'.")
+
+(defvar mu4e~headers-sort-field-choices
+  '( ("date"    . :date)
+     ("from"    . :from)
+     ("list"    . :list)
+     ("maildir" . :maildir)
+     ("prio"    . :prio)
+     ("zsize"   . :size)
+     ("subject" . :subject)
+     ("to"      . :to))
+  "List of cells describing the various sort-options.
+In the format needed for `mu4e-read-option'.")
+
+
+(defvar mu4e~headers-search-start nil)
+(defvar mu4e~headers-render-start nil)
+(defvar mu4e~headers-render-time  nil)
+
+(defvar mu4e-headers-report-render-time nil
+  "If non-nil, report on the time it took to render the messages.
+This is mostly useful for profiling.")
+
+\f
+
+;;; Clear
+
+(defun mu4e~headers-clear (&optional text)
+  "Clear the headers buffer and related data structures.
+Optionally, show TEXT."
+  (when (buffer-live-p (mu4e-get-headers-buffer))
+    (setq mu4e~headers-render-start (float-time))
+    (let ((inhibit-read-only t))
+      (with-current-buffer (mu4e-get-headers-buffer)
+        (mu4e--mark-clear)
+        (erase-buffer)
+       (when text
+          (goto-char (point-min))
+          (insert (propertize text 'face 'mu4e-system-face 'intangible t)))))))
+
+\f
+;;; Misc
+
+(defun mu4e~headers-contact-str (contacts)
+  "Turn the list of contacts CONTACTS (with elements (NAME . EMAIL)
+into a string."
+  (mapconcat
+   (lambda (contact)
+     (let ((name (mu4e-contact-name contact))
+          (email (mu4e-contact-email contact)))
+       (or name email "?"))) contacts ", "))
+
+(defun mu4e~headers-thread-prefix-map (type)
+  "Return the thread prefix based on the symbol TYPE."
+  (let ((get-prefix
+         (lambda (cell)
+           (if mu4e-use-fancy-chars (cdr cell) (car cell)))))
+    (cl-case type
+      (child         (funcall get-prefix mu4e-headers-thread-child-prefix))
+      (first-child   (funcall get-prefix mu4e-headers-thread-first-child-prefix))
+      (last-child    (funcall get-prefix mu4e-headers-thread-last-child-prefix))
+      (connection    (funcall get-prefix mu4e-headers-thread-connection-prefix))
+      (blank         (funcall get-prefix mu4e-headers-thread-blank-prefix))
+      (orphan        (funcall get-prefix mu4e-headers-thread-orphan-prefix))
+      (single-orphan (funcall get-prefix mu4e-headers-thread-single-orphan-prefix))
+      (duplicate     (funcall get-prefix mu4e-headers-thread-duplicate-prefix))
+      (t              "?"))))
+
+
+;; headers in the buffer are prefixed by an invisible string with the docid
+;; followed by an EOT ('end-of-transmission', \004, ^D) non-printable ascii
+;; character. this string also has a text-property with the docid. the former
+;; is used for quickly finding a certain header, the latter for retrieving the
+;; docid at point without string matching etc.
+
+(defun mu4e~headers-docid-pos (docid)
+  "Return the pos of the beginning of the line with the header with
+docid DOCID, or nil if it cannot be found."
+  (let ((pos))
+    (save-excursion
+      (setq pos (mu4e~headers-goto-docid docid)))
+    pos))
+
+(defun mu4e~headers-docid-cookie (docid)
+  "Create an invisible string containing DOCID; this is to be used
+at the beginning of lines to identify headers."
+  (propertize (format "%s%d%s"
+                      mu4e~headers-docid-pre docid mu4e~headers-docid-post)
+              'docid docid 'invisible t));;
+
+(defun mu4e~headers-docid-at-point (&optional point)
+  "Get the docid for the header at POINT, or at current (point) if
+nil. Returns the docid, or nil if there is none."
+  (save-excursion
+    (when point
+      (goto-char point))
+    (get-text-property (line-beginning-position) 'docid)))
+
+
+
+(defun mu4e~headers-goto-docid (docid &optional to-mark)
+  "Go to the beginning of the line with the header with docid
+DOCID, or nil if it cannot be found. If the optional TO-MARK is
+non-nil, go to the point directly *after* the docid-cookie instead
+of the beginning of the line."
+  (let ((oldpoint (point)) (newpoint))
+    (goto-char (point-min))
+    (setq newpoint
+          (search-forward (mu4e~headers-docid-cookie docid) nil t))
+    (unless to-mark
+      (if (null newpoint)
+          (goto-char oldpoint) ;; not found; restore old pos
+        (progn
+          (beginning-of-line) ;; found, move to beginning of line
+          (setq newpoint (point)))))
+    newpoint)) ;; return the point, or nil if not found
+
+(defun mu4e~headers-field-for-docid (docid field)
+  "Get FIELD (a symbol, see `mu4e-headers-names') for the message
+with DOCID which must be present in the headers buffer."
+  (save-excursion
+    (when (mu4e~headers-goto-docid docid)
+      (mu4e-message-field (mu4e-message-at-point) field))))
+
+
+;; In order to print a thread tree with all the message connections,
+;; it's necessary to keep track of all sub levels that still have
+;; following messages. For each level, mu4e~headers-thread-state keeps
+;; the value t for a connection or nil otherwise.
+(defvar-local mu4e~headers-thread-state '())
+
+(defun mu4e~headers-thread-prefix (thread)
+  "Calculate the thread prefix based on thread info THREAD."
+  (when thread
+    (let* ((prefix       "")
+          (level        (plist-get thread :level))
+          (has-child    (plist-get thread :has-child))
+          (first-child  (plist-get thread :first-child))
+          (last-child   (plist-get thread :last-child))
+          (orphan       (plist-get thread :orphan))
+          (single-orphan(and orphan first-child last-child))
+          (duplicate    (plist-get thread :duplicate)))
+      ;; Do not prefix root messages.
+      (if (= level 0)
+          (setq mu4e~headers-thread-state '()))
+      (if (> level 0)
+          (let* ((length (length mu4e~headers-thread-state))
+                 (padding (make-list (max 0 (- level length)) nil)))
+            ;; Trim and pad the state to ensure a message will
+            ;; always be shown with the correct indentation, even if
+            ;; a broken thread is returned. It's trimmed to level-1
+            ;; because the current level has always an connection
+            ;; and it used a special formatting.
+            (setq mu4e~headers-thread-state
+                  (cl-subseq (append mu4e~headers-thread-state padding)
+                             0 (- level 1)))
+            ;; Prepare the thread prefix.
+            (setq prefix
+                  (concat
+                   ;; Current mu4e~headers-thread-state, composed by
+                   ;; connections or blanks.
+                   (mapconcat
+                    (lambda (s)
+                      (mu4e~headers-thread-prefix-map
+                       (if s 'connection 'blank)))
+                    mu4e~headers-thread-state "")
+                   ;; Current entry.
+                   (mu4e~headers-thread-prefix-map
+                    (if single-orphan 'single-orphan
+                      (if (and orphan
+                               (or first-child
+                                   (not (eq mu4e-headers-thread-mark-as-orphan 'first))))
+                               'orphan
+                        (if last-child 'last-child
+                          (if first-child 'first-child
+                            'child)))))))))
+      ;; If a new sub-thread will follow (has-child) and the current
+      ;; one is still not done (not last-child), then a new
+      ;; connection needs to be added to the tree-state.  It's not
+      ;; necessary to a blank (nil), because padding will handle
+      ;; that.
+      (if (and has-child (not last-child))
+          (setq mu4e~headers-thread-state
+                (append mu4e~headers-thread-state '(t))))
+      ;; Return the thread prefix.
+      (format "%s%s"
+              prefix
+              (if duplicate
+                  (mu4e~headers-thread-prefix-map 'duplicate) "")))))
+
+(defun mu4e~headers-flags-str (flags)
+  "Get a display string for FLAGS.
+Note that `mu4e-flags-to-string' is for internal use only; this
+function is for display. (This difference is significant, since
+internally, the Maildir spec determines what the flags look like,
+while our display may be different)."
+  (or  (mapconcat
+       (lambda (flag)
+         (when (member flag mu4e-headers-visible-flags)
+           (if-let* ((mark (intern-soft
+                            (format "mu4e-headers-%s-mark" (symbol-name flag))))
+                     (cell (symbol-value mark)))
+               (if mu4e-use-fancy-chars (cdr cell) (car cell))
+             "")))
+       flags "")
+       ""))
+
+;;; Special headers
+
+(defun mu4e~headers-from-or-to (msg)
+  "Get the From: address from MSG if not one of user's; otherwise get To:.
+When the from address for message MSG is one of the the user's
+addresses, (as per `mu4e-personal-address-p'), show the To
+address. Otherwise, show the From address, prefixed with the
+appropriate `mu4e-headers-from-or-to-prefix'."
+  (let* ((from1 (car-safe (mu4e-message-field msg :from)))
+        (from1-addr (and from1 (mu4e-contact-email from1)))
+        (is-user (and from1-addr (mu4e-personal-address-p from1-addr))))
+    (if is-user
+        (concat (cdr mu4e-headers-from-or-to-prefix)
+                (mu4e~headers-contact-str (mu4e-message-field msg :to)))
+      (concat (car mu4e-headers-from-or-to-prefix)
+              (mu4e~headers-contact-str (mu4e-message-field msg :from))))))
+
+(defun mu4e~headers-human-date (msg)
+  "Show a \"human\" date for MSG.
+If the date is today, show the time, otherwise, show the date.
+The formats used for date and time are `mu4e-headers-date-format'
+and `mu4e-headers-time-format'."
+  (let ((date (mu4e-msg-field msg :date)))
+    (if (equal date '(0 0 0))
+        "None"
+      (let ((day1 (decode-time date))
+            (day2 (decode-time (current-time))))
+        (if (and
+             (eq (nth 3 day1) (nth 3 day2))     ;; day
+             (eq (nth 4 day1) (nth 4 day2))     ;; month
+             (eq (nth 5 day1) (nth 5 day2)))    ;; year
+            (format-time-string mu4e-headers-time-format date)
+          (format-time-string mu4e-headers-date-format date))))))
+
+(defun mu4e~headers-thread-subject (msg)
+  "Get the subject for MSG if it is the first one in a thread.
+Otherwise, return the thread-prefix without the subject-text. In
+other words, show the subject of a thread only once, similar to
+e.g. \"mutt\"."
+  (let* ((tinfo (mu4e-message-field msg :meta))
+         (subj (mu4e-msg-field msg :subject)))
+    (concat ;; prefix subject with a thread indicator
+     (mu4e~headers-thread-prefix tinfo)
+     (if (plist-get tinfo :thread-subject)
+         (truncate-string-to-width subj 600) ""))))
+
+(defun mu4e~headers-mailing-list (list)
+  "Get some identifier for the mailing list."
+  (if list
+      (propertize (mu4e-get-mailing-list-shortname list) 'help-echo list)
+    ""))
+
+(defsubst mu4e~headers-custom-field-value (msg field)
+  "Show some custom header field, or raise an error if it is not
+found."
+  (let* ((item (or (assoc field mu4e-header-info-custom)
+                   (mu4e-error "field %S not found" field)))
+         (func (or (plist-get (cdr-safe item) :function)
+                   (mu4e-error "no :function defined for field %S %S"
+                               field (cdr item)))))
+    (funcall func msg)))
+
+(defun mu4e~headers-field-value (msg field)
+  (let ((val (mu4e-message-field msg field)))
+    (cl-case field
+      (:subject
+       (concat ;; prefix subject with a thread indicator
+        (mu4e~headers-thread-prefix (mu4e-message-field msg :meta))
+        ;;  "["(plist-get (mu4e-message-field msg :meta) :path) "] "
+        ;; work-around: emacs' display gets really slow when lines are too long;
+        ;; so limit subject length to 600
+        (truncate-string-to-width val 600)))
+      (:thread-subject ;; if not searching threads, fall back to :subject
+       (if mu4e-search-threads
+          (mu4e~headers-thread-subject msg)
+        (mu4e~headers-field-value msg :subject)))
+      ((:maildir :path :message-id) val)
+      ((:to :from :cc :bcc) (mu4e~headers-contact-str val))
+      ;; if we (ie. `user-mail-address' is the 'From', show
+      ;; 'To', otherwise show From
+      (:from-or-to (mu4e~headers-from-or-to msg))
+      (:date (format-time-string mu4e-headers-date-format val))
+      (:list (or val ""))
+      (:mailing-list (mu4e~headers-mailing-list (mu4e-msg-field msg :list)))
+      (:human-date (propertize (mu4e~headers-human-date msg)
+                               'help-echo (format-time-string
+                                           mu4e-headers-long-date-format
+                                           (mu4e-msg-field msg :date))))
+      (:flags (propertize (mu4e~headers-flags-str val)
+                          'help-echo (format "%S" val)))
+      (:tags (propertize (mapconcat 'identity val ", ")))
+      (:size (mu4e-display-size val))
+      (t (mu4e~headers-custom-field-value msg field)))))
+
+(defsubst mu4e~headers-truncate-field-fast (val width)
+  "Truncate VAL to WIDTH. Fast and somewhat inaccurate."
+  (if width
+      (truncate-string-to-width val width 0 ?\s truncate-string-ellipsis)
+    val))
+
+(defun mu4e~headers-truncate-field-precise (field val width)
+  "Return VAL truncated to one less than WIDTH, with a trailing
+space propertized with a `display' text property which expands to
+ the correct column for display."
+  (when width
+    (let ((end-col (cl-loop for (f . w) in mu4e-headers-fields
+                            sum w
+                            until (equal f field))))
+      (setq val (string-trim-right val))
+      (if (> width (length val))
+          (setq val (concat val " "))
+        (setq val
+              (concat
+               (truncate-string-to-width val (1- width) 0 ?\s t)
+               " ")))
+      (put-text-property (1- (length val))
+                         (length val)
+                         'display
+                         `(space . (:align-to ,end-col))
+                         val)))
+  val)
+
+(defsubst mu4e~headers-truncate-field (field val width)
+  "Truncate VAL to WIDTH."
+  (if mu4e-headers-precise-alignment
+      (mu4e~headers-truncate-field-precise field val width)
+    (mu4e~headers-truncate-field-fast val width)))
+
+(make-obsolete-variable 'mu4e-headers-field-properties-function
+                        "not used" "1.6.1")
+
+(defsubst mu4e~headers-field-handler (f-w msg)
+  "Create a description of the field of MSG described by F-W."
+  (let* ((field (car f-w))
+         (width (cdr f-w))
+         (val (mu4e~headers-field-value msg field))
+         (val (and val (if width (mu4e~headers-truncate-field field val width) val))))
+    val))
+
+(defsubst mu4e~headers-apply-flags (msg fieldval)
+  "Adjust FIELDVAL's face property based on flags in MSG."
+  (let* ((flags (plist-get msg :flags))
+        (meta (plist-get msg :meta))
+        (face (cond
+               ((memq 'trashed flags) 'mu4e-trashed-face)
+                ((memq 'draft flags)   'mu4e-draft-face)
+                ((or (memq 'unread flags) (memq 'new flags))
+                 'mu4e-unread-face)
+                ((memq 'flagged flags) 'mu4e-flagged-face)
+               ((plist-get meta :related) 'mu4e-related-face)
+                ((memq 'replied flags) 'mu4e-replied-face)
+                ((memq 'passed flags)  'mu4e-forwarded-face)
+               (t                     'mu4e-header-face))))
+    (add-face-text-property 0 (length fieldval) face t fieldval)
+    fieldval))
+
+(defsubst mu4e~message-header-line (msg)
+  "Return a propertized description of MSG suitable for
+displaying in the header view."
+  (unless (and mu4e-headers-hide-predicate
+               (funcall mu4e-headers-hide-predicate msg))
+    (mu4e~headers-apply-flags
+     msg
+     (mapconcat (lambda (f-w) (mu4e~headers-field-handler f-w msg))
+                mu4e-headers-fields " "))))
+
+
+(defsubst mu4e~headers-insert-header (msg pos)
+  "Insert a header for MSG at point POS."
+  (when-let ((line (mu4e~message-header-line msg))
+            (docid (plist-get msg :docid)))
+    (goto-char pos)
+    (insert
+     (propertize
+      (concat
+       (mu4e~headers-docid-cookie docid)
+       mu4e--mark-fringe line "\n")
+      'docid docid 'msg msg))))
+
+(defun mu4e~headers-remove-header (docid &optional ignore-missing)
+  "Remove header with DOCID at point.
+When IGNORE-MISSING is non-nill, don't raise an error when the
+docid is not found."
+  (with-current-buffer (mu4e-get-headers-buffer)
+    (if (mu4e~headers-goto-docid docid)
+        (let ((inhibit-read-only t))
+          (delete-region (line-beginning-position) (line-beginning-position 2)))
+      (unless ignore-missing
+        (mu4e-error "Cannot find message with docid %S" docid)))))
+
+\f
+;;; Handler functions
+
+;; next are a bunch of handler functions; those will be called from mu4e~proc in
+;; response to output from the server process
+
+(defun mu4e~headers-view-handler (msg)
+  "Handler function for displaying a message."
+  (mu4e-view msg))
+
+(defun mu4e~headers-view-this-message-p (docid)
+  "Is DOCID currently being viewed?"
+  (when (buffer-live-p (mu4e-get-view-buffer))
+    (with-current-buffer (mu4e-get-view-buffer)
+      (eq docid (plist-get mu4e~view-message :docid)))))
+
+;; note: this function is very performance-sensitive
+(defun mu4e~headers-append-handler (msglst)
+  "Append one-line descriptions of messages in MSGLIST.
+Do this at the end of the headers-buffer."
+  (when (buffer-live-p (mu4e-get-headers-buffer))
+    (with-current-buffer (mu4e-get-headers-buffer)
+      (save-excursion
+       (let ((inhibit-read-only t))
+         (seq-do
+          (lambda (msg)
+            (mu4e~headers-insert-header msg (point-max)))
+          msglst))))))
+
+
+(defun mu4e~headers-update-handler (msg is-move maybe-view)
+  "Update handler, will be called when a message has been updated
+in the database. This function will update the current list of
+headers."
+  (when (buffer-live-p (mu4e-get-headers-buffer))
+    (with-current-buffer (mu4e-get-headers-buffer)
+      (let* ((docid (mu4e-message-field msg :docid))
+             (initial-message-at-point (mu4e~headers-docid-at-point))
+             (initial-column (current-column))
+            (inhibit-read-only t)
+             (point (mu4e~headers-docid-pos docid))
+             (markinfo (gethash docid mu4e--mark-map)))
+        (when point ;; is the message present in this list?
+
+          ;; if it's marked, unmark it now
+          (when (mu4e-mark-docid-marked-p docid)
+            (mu4e-mark-set 'unmark))
+
+          ;; re-use the thread info from the old one; this is needed because
+          ;; *update* messages don't have thread info by themselves (unlike
+          ;; search results)
+          ;; since we still have the search results, re-use
+          ;; those
+          (plist-put msg :meta
+                     (mu4e~headers-field-for-docid docid :meta))
+
+          ;; first, remove the old one (otherwise, we'd have two headers with
+          ;; the same docid...
+          (mu4e~headers-remove-header docid t)
+
+          ;; if we're actually viewing this message (in mu4e-view mode), we
+          ;; update it; that way, the flags can be updated, as well as the path
+          ;; (which is useful for viewing the raw message)
+          (when (and maybe-view (mu4e~headers-view-this-message-p docid))
+            (save-excursion (mu4e-view msg)))
+          ;; now, if this update was about *moving* a message, we don't show it
+          ;; anymore (of course, we cannot be sure if the message really no
+          ;; longer matches the query, but this seem a good heuristic.  if it
+          ;; was only a flag-change, show the message with its updated flags.
+          (unless is-move
+           (save-excursion
+             (mu4e~headers-insert-header msg point)))
+
+          ;; restore the mark, if any. See #2076.
+          (when (and markinfo (mu4e~headers-goto-docid docid))
+            (mu4e-mark-at-point (car markinfo) (cdr markinfo)))
+
+          (if (and initial-message-at-point
+                   (mu4e~headers-goto-docid initial-message-at-point))
+              (progn
+                (move-to-column initial-column)
+                (mu4e~headers-highlight initial-message-at-point))
+            ;; attempt to highlight the corresponding line and make it visible
+            (mu4e~headers-highlight docid))
+          (run-hooks 'mu4e-message-changed-hook))))))
+
+(defun mu4e~headers-remove-handler (docid)
+  "Remove handler, will be called when a message with DOCID has
+been removed from the database. This function will hide the removed
+message from the current list of headers. If the message is not
+present, don't do anything."
+  (when (buffer-live-p (mu4e-get-headers-buffer))
+    (mu4e~headers-remove-header docid t))
+  ;; if we were viewing this message, close it now.
+  (when (and (mu4e~headers-view-this-message-p docid)
+             (buffer-live-p (mu4e-get-view-buffer)))
+    (unless (eq mu4e-split-view 'single-window)
+      (mapc #'delete-window (get-buffer-window-list
+                             (mu4e-get-view-buffer) nil t)))
+    (kill-buffer (mu4e-get-view-buffer))))
+
+
+\f
+;;; Performing queries (internal)
+(defconst mu4e~search-message "Searching...")
+(defconst mu4e~no-matches     "No matching messages found")
+(defconst mu4e~end-of-results "End of search results")
+
+(defun mu4e--search-execute (expr ignore-history)
+  "Search for query EXPR.
+
+Switch to the output buffer for the results. If IGNORE-HISTORY is
+true, do *not* update the query history stack."
+  (let* ((buf (get-buffer-create mu4e-headers-buffer-name))
+         (inhibit-read-only t)
+         (rewritten-expr (funcall mu4e-query-rewrite-function expr))
+         (maxnum (unless mu4e-search-full mu4e-search-results-limit)))
+    (with-current-buffer buf
+      (mu4e-headers-mode)
+      (unless ignore-history
+        ;; save the old present query to the history list
+        (when mu4e--search-last-query
+          (mu4e--search-push-query mu4e--search-last-query 'past)))
+      (setq mu4e--search-last-query rewritten-expr)
+      (setq list-buffers-directory rewritten-expr)
+      (mu4e~headers-update-mode-line))
+
+    ;; when the buffer is already visible, select it; otherwise,
+    ;; switch to it.
+    (unless (get-buffer-window buf 0)
+      (switch-to-buffer buf))
+    (run-hook-with-args 'mu4e-search-hook expr)
+    (mu4e~headers-clear mu4e~search-message)
+    (setq mu4e~headers-search-start (float-time))
+    (mu4e--server-find
+     rewritten-expr
+     mu4e-search-threads
+     mu4e-headers-sort-field
+     mu4e-headers-sort-direction
+     maxnum
+     mu4e-headers-skip-duplicates
+     mu4e-headers-include-related)))
+
+(defun mu4e~headers-benchmark-message (count)
+  "Get some report message for messaging search and rendering speed."
+  (if (and mu4e-headers-report-render-time
+          mu4e~headers-search-start
+          mu4e~headers-render-start
+          (> count 0))
+      (let ((render-time-ms (* 1000(- (float-time) mu4e~headers-render-start)))
+           (search-time-ms (* 1000(- (float-time) mu4e~headers-search-start))))
+       (format (concat
+                "; search: %0.1f ms (%0.2f ms/msg)"
+                "; render: %0.1f ms (%0.2f ms/msg)")
+               search-time-ms (/ search-time-ms count)
+               render-time-ms (/ render-time-ms count)))
+    ""))
+
+(defun mu4e~headers-found-handler (count)
+  "Create a one line description of the number of headers found
+after the end of the search results."
+  (when (buffer-live-p (mu4e-get-headers-buffer))
+    (with-current-buffer (mu4e-get-headers-buffer)
+      (save-excursion
+        (goto-char (point-max))
+        (let ((inhibit-read-only t)
+              (str (if (zerop count) mu4e~no-matches mu4e~end-of-results))
+              (msg (format "Found %d matching message%s%s"
+                           count (if (= 1 count) "" "s")
+                          (mu4e~headers-benchmark-message count))))
+
+          (insert (propertize str 'face 'mu4e-system-face 'intangible t))
+          (unless (zerop count)
+            (mu4e-message "%s" msg))))
+
+          ;; if we need to jump to some specific message, do so now
+          (goto-char (point-min))
+          (when mu4e--search-msgid-target
+            (if (eq (current-buffer) (window-buffer))
+                (mu4e-headers-goto-message-id mu4e--search-msgid-target)
+              (let* ((pos (mu4e-headers-goto-message-id mu4e--search-msgid-target)))
+                (when pos
+                  (set-window-point (get-buffer-window nil t) pos)))))
+          (when (and mu4e--search-view-target (mu4e-message-at-point 'noerror))
+            ;; view the message at point when there is one.
+            (mu4e-headers-view-message))
+          (setq mu4e--search-view-target nil
+                mu4e--search-msgid-target nil)
+          (when (mu4e~headers-docid-at-point)
+            (mu4e~headers-highlight (mu4e~headers-docid-at-point)))))
+    ;; run-hooks
+    (run-hooks 'mu4e-headers-found-hook))
+
+\f
+;;; Marking
+
+(defmacro mu4e~headers-defun-mark-for (mark)
+  "Define a function mu4e~headers-mark-MARK."
+  (let ((funcname (intern (format "mu4e-headers-mark-for-%s" mark)))
+        (docstring (format "Mark header at point with %s." mark)))
+    `(progn
+       (defun ,funcname () ,docstring
+              (interactive)
+              (mu4e-headers-mark-and-next ',mark))
+       (put ',funcname 'definition-name ',mark))))
+
+(mu4e~headers-defun-mark-for refile)
+(mu4e~headers-defun-mark-for something)
+(mu4e~headers-defun-mark-for delete)
+(mu4e~headers-defun-mark-for trash)
+(mu4e~headers-defun-mark-for flag)
+(mu4e~headers-defun-mark-for move)
+(mu4e~headers-defun-mark-for read)
+(mu4e~headers-defun-mark-for unflag)
+(mu4e~headers-defun-mark-for untrash)
+(mu4e~headers-defun-mark-for unmark)
+(mu4e~headers-defun-mark-for unread)
+(mu4e~headers-defun-mark-for action)
+
+;;; Headers-mode and mode-map
+
+(defvar mu4e-headers-mode-map nil
+  "Keymap for *mu4e-headers* buffers.")
+(unless mu4e-headers-mode-map
+  (setq mu4e-headers-mode-map
+        (let ((map (make-sparse-keymap)))
+
+          (define-key map "j" 'mu4e~headers-jump-to-maildir)
+          (define-key map "O" 'mu4e-headers-change-sorting)
+         (define-key map "M" 'mu4e-headers-toggle-setting)
+
+         ;; these are impossible to remember; use mu4e-headers-toggle-setting
+         ;; instead :)
+          (define-key map "P" 'mu4e-headers-toggle-threading)
+          (define-key map "Q" 'mu4e-headers-toggle-full-search)
+          (define-key map "W" 'mu4e-headers-toggle-include-related)
+          (define-key map "V" 'mu4e-headers-toggle-skip-duplicates)
+
+          (define-key map "q" 'mu4e~headers-quit-buffer)
+          (define-key map "g" 'mu4e-search-rerun) ;; for compatibility
+
+          (define-key map "%" 'mu4e-headers-mark-pattern)
+          (define-key map "t" 'mu4e-headers-mark-subthread)
+          (define-key map "T" 'mu4e-headers-mark-thread)
+
+         (define-key map "," #'mu4e-sexp-at-point)
+
+          ;; navigation between messages
+          (define-key map "p" 'mu4e-headers-prev)
+          (define-key map "n" 'mu4e-headers-next)
+          (define-key map (kbd "<M-up>") 'mu4e-headers-prev)
+          (define-key map (kbd "<M-down>") 'mu4e-headers-next)
+
+          (define-key map (kbd "[") 'mu4e-headers-prev-unread)
+          (define-key map (kbd "]") 'mu4e-headers-next-unread)
+
+          ;; change the number of headers
+          (define-key map (kbd "C-+") 'mu4e-headers-split-view-grow)
+          (define-key map (kbd "C--") 'mu4e-headers-split-view-shrink)
+          (define-key map (kbd "<C-kp-add>") 'mu4e-headers-split-view-grow)
+          (define-key map (kbd "<C-kp-subtract>") 'mu4e-headers-split-view-shrink)
+
+          ;; switching to view mode (if it's visible)
+          (define-key map "y" 'mu4e-select-other-view)
+
+          ;; marking/unmarking ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+          (define-key map (kbd "<backspace>")  'mu4e-headers-mark-for-trash)
+          (define-key map (kbd "d")            'mu4e-headers-mark-for-trash)
+          (define-key map (kbd "<delete>")     'mu4e-headers-mark-for-delete)
+          (define-key map (kbd "<deletechar>") 'mu4e-headers-mark-for-delete)
+          (define-key map (kbd "D")            'mu4e-headers-mark-for-delete)
+          (define-key map (kbd "m")            'mu4e-headers-mark-for-move)
+          (define-key map (kbd "r")            'mu4e-headers-mark-for-refile)
+
+          (define-key map (kbd "?")            'mu4e-headers-mark-for-unread)
+          (define-key map (kbd "!")            'mu4e-headers-mark-for-read)
+          (define-key map (kbd "A")            'mu4e-headers-mark-for-action)
+
+          (define-key map (kbd "u")            'mu4e-headers-mark-for-unmark)
+          (define-key map (kbd "+")            'mu4e-headers-mark-for-flag)
+          (define-key map (kbd "-")            'mu4e-headers-mark-for-unflag)
+          (define-key map (kbd "=")            'mu4e-headers-mark-for-untrash)
+          (define-key map (kbd "&")            'mu4e-headers-mark-custom)
+
+          (define-key map (kbd "*")              'mu4e-headers-mark-for-something)
+          (define-key map (kbd "<kp-multiply>")  'mu4e-headers-mark-for-something)
+          (define-key map (kbd "<insertchar>")   'mu4e-headers-mark-for-something)
+          (define-key map (kbd "<insert>")       'mu4e-headers-mark-for-something)
+
+          (define-key map (kbd "#")   'mu4e-mark-resolve-deferred-marks)
+
+          (define-key map "U" 'mu4e-mark-unmark-all)
+          (define-key map "x" 'mu4e-mark-execute-all)
+
+          (define-key map "a" 'mu4e-headers-action)
+
+          ;; message composition
+          (define-key map "R" 'mu4e-compose-reply)
+          (define-key map "F" 'mu4e-compose-forward)
+          (define-key map "C" 'mu4e-compose-new)
+          (define-key map "E" 'mu4e-compose-edit)
+
+          (define-key map (kbd "RET") 'mu4e-headers-view-message)
+          (define-key map [mouse-2]   'mu4e-headers-view-message)
+
+          (define-key map "$" 'mu4e-show-log)
+          (define-key map "H" 'mu4e-display-manual)
+
+          (define-key map "|" 'mu4e-view-pipe)
+
+          ;; menu
+          ;;(define-key map [menu-bar] (make-sparse-keymap))
+          (let ((menumap (make-sparse-keymap)))
+            (define-key map [menu-bar headers] (cons "Mu4e" menumap))
+
+            (define-key menumap [mu4e~headers-quit-buffer]
+              '("Quit view" . mu4e~headers-quit-buffer))
+            (define-key menumap [display-help] '("Help" . mu4e-display-manual))
+
+            (define-key menumap [sepa0] '("--"))
+
+            (define-key menumap [toggle-include-related]
+              '(menu-item "Toggle related messages"
+                          mu4e-headers-toggle-include-related
+                          :button (:toggle .
+                                           (and (boundp 'mu4e-headers-include-related)
+                                                mu4e-headers-include-related))))
+            (define-key menumap [toggle-threading]
+              '(menu-item "Toggle threading" mu4e-headers-toggle-threading
+                          :button (:toggle .
+                                           (and (boundp 'mu4e-search-threads)
+                                                mu4e-search-threads))))
+
+            (define-key menumap "|" '("Pipe through shell" . mu4e-view-pipe))
+            (define-key menumap [sepa1] '("--"))
+
+            (define-key menumap [execute-marks]  '("Execute marks"
+                                                   . mu4e-mark-execute-all))
+            (define-key menumap [unmark-all]  '("Unmark all" . mu4e-mark-unmark-all))
+            (define-key menumap [unmark]
+              '("Unmark" . mu4e-headers-mark-for-unmark))
+
+            (define-key menumap [mark-pattern]  '("Mark pattern" .
+                                                  mu4e-headers-mark-pattern))
+            (define-key menumap [mark-as-read]  '("Mark as read" .
+                                                  mu4e-headers-mark-for-read))
+            (define-key menumap [mark-as-unread]
+              '("Mark as unread" .  mu4e-headers-mark-for-unread))
+
+            (define-key menumap [mark-delete]
+              '("Mark for deletion" . mu4e-headers-mark-for-delete))
+            (define-key menumap [mark-untrash]
+              '("Mark for untrash" .  mu4e-headers-mark-for-untrash))
+            (define-key menumap [mark-trash]
+              '("Mark for trash" .  mu4e-headers-mark-for-trash))
+            (define-key menumap [mark-move]
+              '("Mark for move" . mu4e-headers-mark-for-move))
+            (define-key menumap [sepa2] '("--"))
+
+            (define-key menumap [resend]  '("Resend" . mu4e-compose-resend))
+            (define-key menumap [forward]  '("Forward" . mu4e-compose-forward))
+            (define-key menumap [reply]  '("Reply" . mu4e-compose-reply))
+            (define-key menumap [compose-new]  '("Compose new" . mu4e-compose-new))
+
+            (define-key menumap [sepa3] '("--"))
+
+            (define-key menumap [query-next]
+              '("Next query" . mu4e-headers-query-next))
+            (define-key menumap [query-prev]  '("Previous query" .
+                                                mu4e-headers-query-prev))
+            (define-key menumap [narrow-search] '("Narrow search" .
+                                                  mu4e-headers-search-narrow))
+            (define-key menumap [bookmark]  '("Search bookmark" .
+                                              mu4e-headers-search-bookmark))
+            (define-key menumap [jump]  '("Jump to maildir" .
+                                          mu4e~headers-jump-to-maildir))
+            (define-key menumap [refresh]  '("Refresh" . mu4e-search-rerun))
+            (define-key menumap [search]  '("Search" . mu4e-headers-search))
+
+            (define-key menumap [sepa4] '("--"))
+
+            (define-key menumap [view]  '("View" . mu4e-headers-view-message))
+            (define-key menumap [next]  '("Next" . mu4e-headers-next))
+            (define-key menumap [previous]  '("Previous" . mu4e-headers-prev))
+            (define-key menumap [sepa5] '("--")))
+          map)))
+(fset 'mu4e-headers-mode-map mu4e-headers-mode-map)
+
+(defun mu4e~header-line-format ()
+  "Get the format for the header line."
+  (let ((uparrow   (if mu4e-use-fancy-chars " ▲" " ^"))
+        (downarrow (if mu4e-use-fancy-chars " ▼" " V")))
+    (cons
+     (make-string
+      (+ mu4e--mark-fringe-len (floor (fringe-columns 'left t))) ?\s)
+     (mapcar
+      (lambda (item)
+        (let* ( ;; with threading enabled, we're necessarily sorting by date.
+               (sort-field (if mu4e-search-threads :date mu4e-headers-sort-field))
+               (field (car item)) (width (cdr item))
+               (info (cdr (assoc field
+                                 (append mu4e-header-info mu4e-header-info-custom))))
+              (sortable (plist-get info :sortable))
+               ;; if sortable, it is either t (when field is sortable itself)
+               ;; or a symbol (if another field is used for sorting)
+               (this-field (when sortable (if (booleanp sortable) field sortable)))
+               (help (plist-get info :help))
+               ;; triangle to mark the sorted-by column
+               (arrow
+                (when (and sortable (eq this-field sort-field))
+                  (if (eq mu4e-headers-sort-direction 'descending) downarrow uparrow)))
+               (name (concat (plist-get info :shortname) arrow))
+               (map (make-sparse-keymap)))
+          (when sortable
+            (define-key map [header-line mouse-1]
+              (lambda (&optional e)
+                ;; getting the field, inspired by `tabulated-list-col-sort'
+                (interactive "e")
+                (let* ((obj (posn-object (event-start e)))
+                       (field
+                        (and obj (get-text-property 0 'field (car obj)))))
+                  ;; "t": if we're already sorted by field, the sort-order is
+                  ;; changed
+                  (mu4e-headers-change-sorting field t)))))
+          (concat
+           (propertize
+            (if width
+                (truncate-string-to-width name width 0 ?\s truncate-string-ellipsis)
+              name)
+            'face (when arrow 'bold)
+            'help-echo help
+            'mouse-face (when sortable 'highlight)
+            'keymap (when sortable map)
+            'field field) " ")))
+      mu4e-headers-fields))))
+
+(defun mu4e~headers-maybe-auto-update ()
+  "Update the current headers buffer after indexing has brought
+some changes, `mu4e-headers-auto-update' is non-nil and there is
+no user-interaction ongoing."
+  (when (and mu4e-headers-auto-update          ;; must be set
+            mu4e-index-update-status
+            (not (zerop (plist-get mu4e-index-update-status :updated)))
+             (zerop (mu4e-mark-marks-num))     ;; non active marks
+             (not (active-minibuffer-window))) ;; no user input only
+    ;; rerun search if there's a live window with search results;
+    ;; otherwise we'd trigger a headers view from out of nowhere.
+    (when (and (buffer-live-p (mu4e-get-headers-buffer))
+               (window-live-p (get-buffer-window (mu4e-get-headers-buffer) t)))
+      (mu4e-search-rerun))))
+
+(defcustom mu4e-headers-eldoc-format "“%s” from %f on %d"
+  "Format for the `eldoc' string for the current message in the headers buffer.
+The following specs are supported:
+- %s: the message Subject
+- %f: the message From
+- %t: the message To
+- %c: the message Cc
+- %d: the message Date
+- %p: the message priority
+- %m: the maildir containing the message
+- %F: the message’s flags
+- %M: the Message-Id"
+  :type 'string
+  :group 'mu4e-headers)
+
+(defun mu4e-headers-eldoc-function (&rest _args)
+  (let ((msg (get-text-property (point) 'msg)))
+    (when msg
+      (format-spec
+       mu4e-headers-eldoc-format
+       `((?s . ,(mu4e-message-field msg :subject))
+         (?f . ,(mu4e~headers-contact-str (mu4e-message-field msg :from)))
+         (?t . ,(mu4e~headers-contact-str (mu4e-message-field msg :to)))
+         (?c . ,(mu4e~headers-contact-str (mu4e-message-field msg :cc)))
+         (?d . ,(mu4e~headers-human-date msg))
+         (?p . ,(mu4e-message-field msg :priority))
+         (?m . ,(mu4e-message-field msg :maildir))
+         (?F . ,(mu4e-message-field msg :flags))
+         (?M . ,(mu4e-message-field msg :message-id)))))))
+
+(define-derived-mode mu4e-headers-mode special-mode
+  "mu4e:headers"
+  "Major mode for displaying mu4e search results.
+\\{mu4e-headers-mode-map}."
+  (use-local-map mu4e-headers-mode-map)
+  (make-local-variable 'mu4e~headers-proc)
+  (make-local-variable 'mu4e~highlighted-docid)
+  (set (make-local-variable 'hl-line-face) 'mu4e-header-highlight-face)
+
+  ;; Eldoc support
+  (when (featurep 'eldoc)
+    (if (boundp 'eldoc-documentation-functions)
+        ;; Emacs 28 or newer
+        (add-hook 'eldoc-documentation-functions
+                  #'mu4e-headers-eldoc-function nil t)
+      ;; Emacs 27 or older
+      (when (fboundp 'add-function) ;; add-function was added in 24.4.
+        (add-function :before-until (local 'eldoc-documentation-function)
+                      #'mu4e-headers-eldoc-function))))
+
+  ;; support bookmarks.
+  (set (make-local-variable 'bookmark-make-record-function)
+       'mu4e--make-bookmark-record)
+  ;; maybe update the current headers upon indexing changes
+  (add-hook 'mu4e-index-updated-hook #'mu4e~headers-maybe-auto-update)
+  (setq
+   truncate-lines t
+   buffer-undo-list t ;; don't record undo information
+   overwrite-mode nil
+   header-line-format (mu4e~header-line-format))
+
+  (mu4e--mark-initialize) ;; initialize the marking subsystem
+  (mu4e-context-minor-mode)
+  (mu4e-update-minor-mode)
+  (mu4e-search-minor-mode)
+  (hl-line-mode 1))
+
+;;; Highlighting
+
+(defvar mu4e~highlighted-docid nil
+  "The highlighted docid")
+
+(defun mu4e~headers-highlight (docid)
+  "Highlight the header with DOCID, or do nothing if it's not found.
+Also, unhighlight any previously highlighted headers."
+  (with-current-buffer (mu4e-get-headers-buffer)
+    (save-excursion
+      ;; first, unhighlight the previously highlighted docid, if any
+      (when (and docid mu4e~highlighted-docid
+                 (mu4e~headers-goto-docid mu4e~highlighted-docid))
+        (hl-line-unhighlight))
+      ;; now, highlight the new one
+      (when (mu4e~headers-goto-docid docid)
+        (hl-line-highlight)))
+    (setq mu4e~highlighted-docid docid)))
+
+;;; Misc 2
+
+(defun mu4e~headers-select-window ()
+  "When there is a visible window for the headers buffer, make sure
+to select it. This is needed when adding new headers, otherwise
+adding a lot of new headers looks really choppy."
+  (let ((win (get-buffer-window (mu4e-get-headers-buffer))))
+    (when win (select-window win))))
+
+(defun mu4e-headers-goto-message-id (msgid)
+  "Go to the next message with message-id MSGID. Return the
+message plist, or nil if not found."
+  (mu4e-headers-find-if
+   (lambda (msg)
+     (let ((this-msgid (mu4e-message-field msg :message-id)))
+       (when (and this-msgid (string= msgid this-msgid))
+         msg)))))
+
+;;; Marking 2
+
+(defun mu4e~headers-mark (docid mark)
+  "(Visually) mark the header for DOCID with character MARK."
+  (with-current-buffer (mu4e-get-headers-buffer)
+    (let ((inhibit-read-only t) (oldpoint (point)))
+      (unless (mu4e~headers-goto-docid docid)
+        (mu4e-error "Cannot find message with docid %S" docid))
+      ;; now, we're at the beginning of the header, looking at
+      ;; <docid>\004
+      ;; (which is invisible). jump past that…
+      (unless (re-search-forward mu4e~headers-docid-post nil t)
+        (mu4e-error "Cannot find the `mu4e~headers-docid-post' separator"))
+
+      ;; clear old marks, and add the new ones.
+      (let ((msg (get-text-property (point) 'msg)))
+        (delete-char mu4e--mark-fringe-len)
+        (insert (propertize
+                 (format mu4e--mark-fringe-format mark)
+                 'face 'mu4e-header-marks-face
+                 'docid docid
+                 'msg msg)))
+      (goto-char oldpoint))))
+
+
+;;; Queries & searching
+(defvar mu4e~headers-mode-line-label "")
+(defun mu4e~headers-update-mode-line ()
+  "Update mode-line settings."
+    (let* ((flagstr
+            (mapconcat
+            (lambda (flag-cell)
+               (if (car flag-cell)
+                   (if mu4e-use-fancy-chars
+                       (cddr flag-cell) (cadr flag-cell) ) ""))
+             `((,mu4e-search-full             . ,mu4e-headers-full-label)
+               (,mu4e-headers-include-related . ,mu4e-headers-related-label)
+               (,mu4e-search-threads          . ,mu4e-headers-threaded-label)
+              (,mu4e-headers-skip-duplicates . ,mu4e-headers-skip-duplicates-label))
+             ""))
+           (name "mu4e-headers"))
+
+      (setq mode-name name)
+      (setq mu4e~headers-mode-line-label (concat flagstr " " mu4e--search-last-query))
+
+      (make-local-variable 'global-mode-string)
+
+      (add-to-list 'global-mode-string
+                   `(:eval
+                     (concat
+                      (propertize
+                       (mu4e-quote-for-modeline ,mu4e~headers-mode-line-label)
+                       'face 'mu4e-modeline-face)
+                      " "
+                      (if (and mu4e-display-update-status-in-modeline
+                               (buffer-live-p mu4e--update-buffer)
+                               (process-live-p (get-buffer-process
+                                                mu4e--update-buffer)))
+                          (propertize " (updating)" 'face 'mu4e-modeline-face)
+                        ""))))))
+
+
+(defun mu4e~headers-redraw-get-view-window ()
+  "Close all windows, redraw the headers buffer based on the value
+of `mu4e-split-view', and return a window for the message view."
+  (if (eq mu4e-split-view 'single-window)
+      (or (and (buffer-live-p (mu4e-get-view-buffer))
+               (get-buffer-window (mu4e-get-view-buffer)))
+          (selected-window))
+    (mu4e-hide-other-mu4e-buffers)
+    (unless (buffer-live-p (mu4e-get-headers-buffer))
+      (mu4e-error "No headers buffer available"))
+    (switch-to-buffer (mu4e-get-headers-buffer))
+    ;; kill the existing view buffer
+    (when (buffer-live-p (mu4e-get-view-buffer))
+      (kill-buffer (mu4e-get-view-buffer)))
+    ;; get a new view window
+    (setq mu4e~headers-view-win
+          (with-demoted-errors "Unable to split window: %S"
+            (cond
+             ((eq mu4e-split-view 'horizontal) ;; split horizontally
+              (split-window-vertically mu4e-headers-visible-lines))
+             ((eq mu4e-split-view 'vertical) ;; split vertically
+              (split-window-horizontally mu4e-headers-visible-columns))
+             ((functionp mu4e-split-view)
+              (funcall mu4e-split-view))
+             (t ;; no splitting; just use the currently selected one
+              (selected-window)))))))
+
+;;; Search-based marking
+
+(defun mu4e-headers-for-each (func)
+  "Call FUNC for each header, moving point to the header.
+FUNC receives one argument, the message s-expression for the
+corresponding header."
+  (save-excursion
+    (goto-char (point-min))
+    (while (search-forward mu4e~headers-docid-pre nil t)
+      ;; not really sure why we need to jump to bol; we do need to, otherwise we
+      ;; miss lines sometimes...
+      (let ((msg (get-text-property (line-beginning-position) 'msg)))
+        (when msg
+          (funcall func msg))))))
+
+(defun mu4e-headers-find-if (func &optional backward)
+  "Move to the next header for which FUNC returns non-`nil',
+starting from the current position. FUNC receives one argument, the
+message s-expression for the corresponding header. If BACKWARD is
+non-`nil', search backwards. Returns the new position, or `nil' if
+nothing was found. If you want to exclude matches for the current
+message, you can use `mu4e-headers-find-if-next'."
+  (let ((pos)
+        (search-func (if backward 'search-backward 'search-forward)))
+    (save-excursion
+      (while (and (null pos)
+                  (funcall search-func mu4e~headers-docid-pre nil t))
+        ;; not really sure why we need to jump to bol; we do need to, otherwise
+        ;; we miss lines sometimes...
+        (let ((msg (get-text-property (line-beginning-position) 'msg)))
+          (when (and msg (funcall func msg))
+            (setq pos (point))))))
+    (when pos
+      (goto-char pos))))
+
+(defun mu4e-headers-find-if-next (func &optional backwards)
+  "Like `mu4e-headers-find-if', but do not match the current header.
+Move to the next or (if BACKWARDS is non-`nil') header for which FUNC
+returns non-`nil', starting from the current position."
+  (let ((pos))
+    (save-excursion
+      (if backwards
+          (beginning-of-line)
+        (end-of-line))
+      (setq pos (mu4e-headers-find-if func backwards)))
+    (when pos (goto-char pos))))
+
+(defvar mu4e~headers-regexp-hist nil
+  "History list of regexps used.")
+
+(defun mu4e-headers-mark-for-each-if (markpair mark-pred &optional param)
+  "Mark all headers for which predicate function MARK-PRED returns
+non-nil with MARKPAIR. MARK-PRED is function that receives two
+arguments, MSG (the message at point) and PARAM (a user-specified
+parameter). MARKPAIR is a cell (MARK . TARGET); see
+`mu4e-mark-at-point' for details about marks."
+  (mu4e-headers-for-each
+   (lambda (msg)
+     (when (funcall mark-pred msg param)
+       (mu4e-mark-at-point (car markpair) (cdr markpair))))))
+
+(defun mu4e-headers-mark-pattern ()
+  "Ask user for a kind of mark (move, delete etc.), a field to
+match and a regular expression to match with. Then, mark all
+matching messages with that mark."
+  (interactive)
+  (let ((markpair (mu4e--mark-get-markpair "Mark matched messages with: " t))
+        (field (mu4e-read-option "Field to match: "
+                                 '( ("subject" . :subject)
+                                    ("from"    . :from)
+                                    ("to"      . :to)
+                                    ("cc"      . :cc)
+                                    ("bcc"     . :bcc)
+                                    ("list"    . :list))))
+        (pattern (read-string
+                  (mu4e-format "Regexp:")
+                  nil 'mu4e~headers-regexp-hist)))
+    (mu4e-headers-mark-for-each-if
+     markpair
+     (lambda (msg _param)
+       (let* ((value (mu4e-msg-field msg field)))
+         (if (member field '(:to :from :cc :bcc :reply-to))
+             (cl-find-if (lambda (contact)
+                           (let ((name (mu4e-contact-name contact))
+                                (email (mu4e-contact-email contact)))
+                             (or (and name (string-match pattern name))
+                                 (and email (string-match pattern email))))) value)
+           (string-match pattern (or value ""))))))))
+
+(defun mu4e-headers-mark-custom ()
+  "Mark messages based on a user-provided predicate function."
+  (interactive)
+  (let* ((pred (mu4e-read-option "Match function: "
+                                 mu4e-headers-custom-markers))
+         (param (when (cdr pred) (eval (cdr pred))))
+         (markpair (mu4e--mark-get-markpair "Mark matched messages with: " t)))
+    (mu4e-headers-mark-for-each-if markpair (car pred) param)))
+
+(defun mu4e~headers-get-thread-info (msg what)
+  "Get WHAT (a symbol, either path or thread-id) for MSG."
+  (let* ((meta (or (mu4e-message-field msg :meta)
+                     (mu4e-error "No thread info found")))
+         (path  (or (plist-get meta :path)
+                    (mu4e-error "No threadpath found"))))
+    (cl-case what
+      (path path)
+      (thread-id
+       (save-match-data
+         ;; the thread id is the first segment of the thread path
+         (when (string-match "^\\([[:xdigit:]]+\\):?" path)
+           (match-string 1 path))))
+      (otherwise (mu4e-error "Not supported")))))
+
+(defun mu4e-headers-mark-thread-using-markpair (markpair &optional subthread)
+  "Mark the thread at point using the given markpair. If SUBTHREAD is
+non-nil, marking is limited to the message at point and its
+descendants."
+  (let* ((mark (car markpair))
+         (allowed-marks (mapcar 'car mu4e-marks)))
+    (unless (memq mark allowed-marks)
+      (mu4e-error "The mark (%s) has to be one of: %s"
+                  mark allowed-marks)))
+  ;; note: the thread id is shared by all messages in a thread
+  (let* ((msg (mu4e-message-at-point))
+         (thread-id (mu4e~headers-get-thread-info msg 'thread-id))
+         (path      (mu4e~headers-get-thread-info msg 'path))
+         ;; the thread path may have a ':z' suffix for sorting;
+         ;; remove it for subthread matching.
+         (match-path (replace-regexp-in-string ":z$" "" path))
+         (last-marked-point))
+    (mu4e-headers-for-each
+     (lambda (cur-msg)
+       (let ((cur-thread-id   (mu4e~headers-get-thread-info cur-msg 'thread-id))
+             (cur-thread-path (mu4e~headers-get-thread-info cur-msg 'path)))
+         (if subthread
+             ;; subthread matching; mymsg's thread path should have path as its
+             ;; prefix
+             (when (string-match (concat "^" match-path) cur-thread-path)
+               (mu4e-mark-at-point (car markpair) (cdr markpair))
+               (setq last-marked-point (point)))
+           ;; nope; not looking for the subthread; looking for the whole thread
+           (when (string= thread-id cur-thread-id)
+             (mu4e-mark-at-point (car markpair) (cdr markpair))
+             (setq last-marked-point (point)))))))
+    (when last-marked-point
+      (goto-char last-marked-point)
+      (mu4e-headers-next))))
+
+(defun mu4e-headers-mark-thread (&optional subthread markpair)
+  "Like `mu4e-headers-mark-thread-using-markpair' but prompt for the markpair."
+  (interactive
+   (let* ((subthread current-prefix-arg))
+     (list current-prefix-arg
+           ;; FIXME: e.g., for refiling we should evaluate this
+           ;; for each line separately
+           (mu4e--mark-get-markpair
+            (if subthread "Mark subthread with: " "Mark whole thread with: ")
+            t))))
+  (mu4e-headers-mark-thread-using-markpair markpair subthread))
+
+(defun mu4e-headers-mark-subthread (&optional markpair)
+  "Like `mu4e-mark-thread', but only for a sub-thread."
+  (interactive)
+  (if markpair (mu4e-headers-mark-thread t markpair)
+    (let ((current-prefix-arg t))
+      (call-interactively 'mu4e-headers-mark-thread))))
+
+\f
+;;; Interactive functions
+(defun mu4e-headers-change-sorting (&optional field dir)
+  "Change the sorting/threading parameters.
+FIELD is the field to sort by; DIR is a symbol: either
+`ascending', `descending', t (meaning: if FIELD is the same as
+the current sortfield, change the sort-order) or nil (ask the
+user)."
+  (interactive)
+  (let* ((field
+          (or field
+              (mu4e-read-option "Sortfield: " mu4e~headers-sort-field-choices)))
+         ;; note: 'sortable' is either a boolean (meaning: if non-nil, this is
+         ;; sortable field), _or_ another field (meaning: sort by this other field).
+         (sortable (plist-get (cdr (assoc field mu4e-header-info)) :sortable))
+         ;; error check
+         (sortable
+          (if sortable
+              sortable
+            (mu4e-error "Not a sortable field")))
+         (sortfield (if (booleanp sortable) field sortable))
+         (dir
+          (cl-case dir
+            ((ascending descending) dir)
+            ;; change the sort order if field = curfield
+            (t
+             (if (eq sortfield mu4e-headers-sort-field)
+                 (if (eq mu4e-headers-sort-direction 'ascending)
+                     'descending 'ascending)
+               'descending)))))
+    (setq
+     mu4e-headers-sort-field sortfield
+     mu4e-headers-sort-direction dir)
+    (mu4e-message "Sorting by %s (%s)"
+                  (symbol-name sortfield)
+                  (symbol-name mu4e-headers-sort-direction))
+    (mu4e-search-rerun)))
+
+
+(defun mu4e-headers-toggle-setting (&optional dont-refresh)
+  "Toggle some aspect of headers display.
+When prefix-argument DONT-REFRESH is non-nill, do not refresh the
+last search with the new setting."
+  (interactive "P")
+  (let* ((toggles '(("fFull-search"      . mu4e-search-full)
+                   ("rInclude-related"  . mu4e-headers-include-related)
+                   ("tShow threads"     . mu4e-search-threads)
+                   ("uSkip duplicates"  . mu4e-headers-skip-duplicates)))
+        (toggles (seq-map
+                  (lambda (cell)
+                    (cons
+                     (concat (car cell) (format" (%s)"
+                                               (if (symbol-value (cdr cell)) "on" "off")))
+                     (cdr cell))) toggles))
+        (choice (mu4e-read-option "Toggle setting " toggles)))
+    (when choice
+      (set choice (not (symbol-value choice)))
+      (mu4e-message "Set `%s' to %s" (symbol-name choice) (symbol-value choice))
+      (unless dont-refresh
+       (mu4e-search-rerun)))))
+
+
+(defun mu4e~headers-toggle (name togglevar dont-refresh)
+  "Toggle variable TOGGLEVAR for feature NAME. Unless DONT-REFRESH is non-nil,
+re-run the last search."
+  (set togglevar (not (symbol-value togglevar)))
+  (mu4e-message "%s turned %s%s"
+                name
+                (if (symbol-value togglevar) "on" "off")
+                (if dont-refresh
+                    " (press 'g' to refresh)" ""))
+  (unless dont-refresh
+    (mu4e-search-rerun)))
+
+(defun mu4e-headers-toggle-threading (&optional dont-refresh)
+  "Toggle `mu4e-search-threads'. With prefix-argument, do
+_not_ refresh the last search with the new setting for threading."
+  (interactive "P")
+  (mu4e~headers-toggle "Threading" 'mu4e-search-threads dont-refresh))
+
+(defun mu4e-headers-toggle-full-search (&optional dont-refresh)
+  "Toggle `mu4e-search-full'. With prefix-argument, do
+_not_ refresh the last search with the new setting for threading."
+  (interactive "P")
+  (mu4e~headers-toggle "Full-search"
+                       'mu4e-search-full dont-refresh))
+
+(defun mu4e-headers-toggle-include-related (&optional dont-refresh)
+  "Toggle `mu4e-headers-include-related'. With prefix-argument, do
+_not_ refresh the last search with the new setting for threading."
+  (interactive "P")
+  (mu4e~headers-toggle "Include-related"
+                       'mu4e-headers-include-related dont-refresh))
+
+(defun mu4e-headers-toggle-skip-duplicates (&optional dont-refresh)
+  "Toggle `mu4e-headers-skip-duplicates'. With prefix-argument, do
+_not_ refresh the last search with the new setting for threading."
+  (interactive "P")
+  (mu4e~headers-toggle "Skip-duplicates"
+                       'mu4e-headers-skip-duplicates dont-refresh))
+
+(defvar mu4e~headers-loading-buf nil
+  "A buffer for loading a message view.")
+
+(defun mu4e-headers-view-message ()
+  "View message at point                                    .
+If there's an existing window for the view, re-use that one . If
+not, create a new one, depending on the value of
+`mu4e-split-view': if it's a symbol `horizontal' or `vertical',
+split the window accordingly; if it is nil, replace the current
+window                                                      . "
+  (interactive)
+  (unless (eq major-mode 'mu4e-headers-mode)
+    (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode))
+  (let* ((msg (mu4e-message-at-point))
+        (path (mu4e-message-field msg :path))
+        (_exists (or (file-readable-p  path)
+                    (mu4e-warn "No message at %s" path)))
+         (docid (or (mu4e-message-field msg :docid)
+                    (mu4e-warn "No message at point")))
+         (mark-as-read
+          (if (functionp mu4e-view-auto-mark-as-read)
+              (funcall mu4e-view-auto-mark-as-read msg)
+            mu4e-view-auto-mark-as-read))
+         (viewwin (mu4e~headers-redraw-get-view-window)))
+    (unless (window-live-p viewwin)
+      (mu4e-error "Cannot get a message view"))
+    (select-window viewwin)
+
+    ;; show some 'loading...' buffer
+    (unless (buffer-live-p mu4e~headers-loading-buf)
+      (setq mu4e~headers-loading-buf (get-buffer-create " *mu4e-loading*"))
+      (with-current-buffer mu4e~headers-loading-buf
+        (mu4e-loading-mode)))
+
+    (switch-to-buffer mu4e~headers-loading-buf)
+    (mu4e--server-view docid mark-as-read)))
+
+
+(defun mu4e~headers-move (lines)
+  "Move point LINES lines.
+Move foward if LINES is positive or backwards if LINES is
+negative. If this succeeds, return the new docid. Otherwise,
+return nil."
+  (unless (eq major-mode 'mu4e-headers-mode)
+    (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode))
+  (cl-flet ((goto-next-line
+              (arg)
+              (condition-case _err
+                  (prog1
+                      (let (line-move-visual)
+                       (and (line-move arg) 0))
+                    ;; Skip invisible text at BOL possibly hidden by
+                    ;; the end of another invisible overlay covering
+                    ;; previous EOL.
+                    (move-to-column 2))
+               ((beginning-of-buffer end-of-buffer)
+                 1))))
+    (let* ((succeeded (zerop (goto-next-line lines)))
+           (docid (mu4e~headers-docid-at-point)))
+      ;; move point, even if this function is called when this window is not
+      ;; visible
+      (when docid
+        ;; update all windows showing the headers buffer
+        (walk-windows
+         (lambda (win)
+           (when (eq (window-buffer win) (mu4e-get-headers-buffer))
+             (set-window-point win (point))))
+         nil t)
+        (if (eq mu4e-split-view 'single-window)
+            (when (eq (window-buffer) (mu4e-get-view-buffer))
+              (mu4e-headers-view-message))
+          ;; update message view if it was already showing
+          (when (and mu4e-split-view (window-live-p mu4e~headers-view-win))
+            (mu4e-headers-view-message)))
+        ;; attempt to highlight the new line, display the message
+        (mu4e~headers-highlight docid)
+        (if succeeded
+           docid
+         nil)))))
+
+(defun mu4e-headers-next (&optional n)
+  "Move point to the next message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth next
+header."
+  (interactive "P")
+  (mu4e~headers-move (or n 1)))
+
+(defun mu4e-headers-prev (&optional n)
+  "Move point to the previous message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth
+previous header."
+  (interactive "P")
+  (mu4e~headers-move (- (or n 1))))
+
+(defun mu4e~headers-prev-or-next-unread (backwards)
+  "Move point to the next message that is unread (and
+untrashed). If BACKWARDS is non-`nil', move backwards."
+  (interactive)
+  (or (mu4e-headers-find-if-next
+       (lambda (msg)
+         (let ((flags (mu4e-message-field msg :flags)))
+           (and (member 'unread flags) (not (member 'trashed flags)))))
+       backwards)
+      (mu4e-message (format "No %s unread message found"
+                            (if backwards "previous" "next")))))
+
+(defun mu4e-headers-prev-unread ()
+  "Move point to the previous message that is unread (and
+untrashed)."
+  (interactive)
+  (mu4e~headers-prev-or-next-unread t))
+
+(defun mu4e-headers-next-unread ()
+  "Move point to the next message that is unread (and
+untrashed)."
+  (interactive)
+  (mu4e~headers-prev-or-next-unread nil))
+
+(defun mu4e~headers-jump-to-maildir (maildir &optional edit)
+  "Show the messages in maildir.
+The user is prompted to ask what maildir.  If prefix arg EDIT is
+given, offer to edit the search query before executing it."
+  (interactive
+   (let ((maildir (mu4e-ask-maildir "Jump to maildir: ")))
+     (list maildir current-prefix-arg)))
+  (when maildir
+    (let* ((query (format "maildir:\"%s\"" maildir))
+           (query (if edit (mu4e-search-read-query "Refine query: " query) query)))
+      (mu4e-mark-handle-when-leaving)
+      (mu4e-search query))))
+
+(defun mu4e-headers-split-view-grow (&optional n)
+  "In split-view, grow the headers window.
+In horizontal split-view, increase the number of lines shown by N.
+In vertical split-view, increase the number of columns shown by N.
+If N is negative shrink the headers window.  When not in split-view
+do nothing."
+  (interactive "P")
+  (let ((n (or n 1))
+        (hwin (get-buffer-window (mu4e-get-headers-buffer))))
+    (when (and (buffer-live-p (mu4e-get-view-buffer)) (window-live-p hwin))
+      (let ((n (or n 1)))
+        (cl-case mu4e-split-view
+          ;; emacs has weird ideas about what horizontal, vertical means...
+          (horizontal
+           (window-resize hwin n nil)
+           (cl-incf mu4e-headers-visible-lines n))
+          (vertical
+           (window-resize hwin n t)
+           (cl-incf mu4e-headers-visible-columns n)))))))
+
+(defun mu4e-headers-split-view-shrink (&optional n)
+  "In split-view, shrink the headers window.
+In horizontal split-view, decrease the number of lines shown by N.
+In vertical split-view, decrease the number of columns shown by N.
+If N is negative grow the headers window.  When not in split-view
+do nothing."
+  (interactive "P")
+  (mu4e-headers-split-view-grow (- (or n 1))))
+
+(defun mu4e-headers-action (&optional actionfunc)
+  "Ask user what to do with message-at-point, then do it.
+The actions are specified in `mu4e-headers-actions'. Optionally,
+pass ACTIONFUNC, which is a function that takes a msg-plist
+argument."
+  (interactive)
+  (let ((msg (mu4e-message-at-point))
+        (afunc (or actionfunc (mu4e-read-option "Action: " mu4e-headers-actions))))
+    (funcall afunc msg)))
+
+(defun mu4e-headers-mark-and-next (mark)
+  "Set mark MARK on the message at point or on all messages in the
+region if there is a region, then move to the next message."
+  (interactive)
+  (mu4e-mark-set mark)
+  (when mu4e-headers-advance-after-mark (mu4e-headers-next)))
+
+(defun mu4e~headers-quit-buffer ()
+  "Quit the mu4e-headers buffer.
+This is a rather complex function, to ensure we don't disturb
+other windows."
+  (interactive)
+  (if (eq mu4e-split-view 'single-window)
+      (progn (mu4e-mark-handle-when-leaving)
+             (kill-buffer))
+    (unless (eq major-mode 'mu4e-headers-mode)
+      (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode))
+    (mu4e-mark-handle-when-leaving)
+    (let ((curbuf (current-buffer))
+          (curwin (selected-window)))
+      (walk-windows
+       (lambda (win)
+         (with-selected-window win
+           ;; if we the view window connected to this one, kill it
+           (when (and (not (one-window-p win)) (eq mu4e~headers-view-win win))
+             (delete-window win)
+             (setq mu4e~headers-view-win nil)))
+         ;; and kill any _other_ (non-selected) window that shows the current
+         ;; buffer
+         (when (and
+                (eq curbuf (window-buffer win)) ;; does win show curbuf?
+                (not (eq curwin win))             ;; it's not the curwin?
+                (not (one-window-p)))           ;; and not the last one?
+           (delete-window win))))  ;; delete it!
+      ;; now, all *other* windows should be gone. kill ourselves, and return
+      ;; to the main view
+      (kill-buffer)
+      (mu4e--main-view 'refresh))))
+
+\f
+;;; Loading messages
+;;
+(defvar mu4e-loading-mode-map
+  (let ((map (make-sparse-keymap)))
+          (define-key map "n" #'ignore)
+          (define-key map "p" #'ignore)
+          (define-key map "q" #'bury-buffer)
+          map)
+  "Keymap for *mu4e-loading* buffers.")
+
+(define-derived-mode mu4e-loading-mode special-mode
+  "mu4e:loading"
+  (use-local-map mu4e-loading-mode-map)
+  (let ((inhibit-read-only t))
+    (erase-buffer)
+    (insert (propertize "Loading message..."
+                        'face 'mu4e-system-face 'intangible t))))
+
+(defun mu4e~loading-close ()
+  "Bury the mu4e Loading... buffer, if any."
+  (let* ((buf mu4e~headers-loading-buf)
+        (win (and (buffer-live-p buf) (get-buffer-window buf t))))
+    (when (window-live-p win)
+      (delete-window win))))
+
+(provide 'mu4e-headers)
+;;; mu4e-headers.el ends here
diff --git a/mu4e/mu4e-helpers.el b/mu4e/mu4e-helpers.el
new file mode 100644 (file)
index 0000000..18c973d
--- /dev/null
@@ -0,0 +1,567 @@
+;;; mu4e-helpers.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Helper functions used in the mu4e. This is slowly usurp all the code from
+;; mu4e-utils.el that does not depend on other parts of mu4e.
+
+;;; Code:
+
+(require 'seq)
+(require 'ido)
+(require 'cl-lib)
+(require 'bookmark)
+
+(require 'mu4e-config)
+\f
+;;; Customization
+
+(defcustom mu4e-debug nil
+  "When set to non-nil, log debug information to the mu4e log  buffer."
+  :type 'boolean
+  :group 'mu4e)
+
+(defcustom mu4e-modeline-max-width 42
+  "Determines the maximum length of the modeline string.
+If the string exceeds this limit, it will be truncated to fit."
+  :type 'integer
+  :group 'mu4e)
+
+(defcustom mu4e-completing-read-function 'ido-completing-read
+  "Function to be used to receive user-input during completion.
+Suggested possible values are:
+ * `completing-read':      built-in completion method
+ * `ido-completing-read':  dynamic completion within the minibuffer."
+  :type 'function
+  :options '(completing-read ido-completing-read)
+  :group 'mu4e)
+
+(defcustom mu4e-use-fancy-chars nil
+  "When set, allow fancy (Unicode) characters for marks/threads.
+You can customize the exact fancy characters used with
+`mu4e-marks' and various `mu4e-headers-..-mark' and
+`mu4e-headers..-prefix' variables."
+  :type 'boolean
+  :group 'mu4e)
+
+(defcustom mu4e-display-update-status-in-modeline nil
+  "Non-nil value will display the update status in the modeline."
+  :group 'mu4e
+  :type 'boolean)
+
+;; maybe move the next ones... but they're convenient
+;; here because they're needed in multiple buffers.
+
+(defcustom mu4e-view-auto-mark-as-read t
+  "Automatically mark messages as read when you read them.
+This is the default behavior, but can be turned off, for example
+when using a read-only file-system.
+
+This can also be set to a function; if so, receives a message
+plist which should evaluate to nil if the message should *not* be
+marked as read-only, or non-nil otherwise."
+  :type '(choice
+          boolean
+          function)
+  :group 'mu4e-view)
+
+
+(defcustom mu4e-split-view 'horizontal
+  "How to show messages / headers.
+A symbol which is either:
+ * `horizontal':    split horizontally (headers on top)
+ * `vertical':      split vertically (headers on the left).
+ * `single-window': view and headers in one window (mu4e will try not to
+        touch your window layout), main view in minibuffer
+ * a function:      the function is responsible to return some window for
+        the view.
+ * anything else:   don't split (show either headers or messages,
+        not both).
+Also see `mu4e-headers-visible-lines'
+and `mu4e-headers-visible-columns'."
+  :type '(choice (const :tag "Split horizontally" horizontal)
+                 (const :tag "Split vertically" vertical)
+                 (const :tag "Single window" single-window)
+                 (const :tag "Don't split" nil))
+  :group 'mu4e-headers)
+\f
+;;; Buffers
+
+(defconst mu4e-main-buffer-name " *mu4e-main*"
+  "Name of the mu4e main buffer.
+The default name starts with SPC and therefore is not visible in
+buffer list.")
+(defconst mu4e-headers-buffer-name "*mu4e-headers*"
+  "Name of the buffer for message headers.")
+(defconst mu4e-embedded-buffer-name " *mu4e-embedded*"
+  "Name for the embedded message view buffer.")
+(defconst mu4e-view-buffer-name "*Article*"
+  "Name of the view buffer.")
+
+(defun mu4e-get-headers-buffer ()
+  "Get the buffer object from `mu4e-headers-buffer-name'."
+  (get-buffer mu4e-headers-buffer-name))
+
+(defun mu4e-get-view-buffer ()
+  "Get the buffer object from `mu4e-view-buffer-name'."
+  (get-buffer mu4e-view-buffer-name))
+
+(defun mu4e-select-other-view ()
+  "Switch between headers view and message view."
+  (interactive)
+  (let* ((other-buf
+          (cond
+           ((eq major-mode 'mu4e-headers-mode)
+            (mu4e-get-view-buffer))
+           ((eq major-mode 'mu4e-view-mode)
+            (mu4e-get-headers-buffer))))
+         (other-win (and other-buf (get-buffer-window other-buf))))
+    (if (window-live-p other-win)
+        (select-window other-win)
+      (mu4e-message "No window to switch to"))))
+
+\f
+;;; Windows
+(defun mu4e-hide-other-mu4e-buffers ()
+  "Bury mu4e buffers.
+Hide (main, headers, view) (and delete all windows displaying
+it). Do _not_ bury the current buffer, though."
+  (interactive)
+  (unless (eq mu4e-split-view 'single-window)
+    (let ((curbuf (current-buffer)))
+      ;; note: 'walk-windows' does not seem to work correctly when modifying
+      ;; windows; therefore, the doloops here
+      (dolist (frame (frame-list))
+        (dolist (win (window-list frame nil))
+          (with-current-buffer (window-buffer win)
+            (unless (eq curbuf (current-buffer))
+              (when (member major-mode '(mu4e-headers-mode mu4e-view-mode))
+                (when (eq t (window-deletable-p win))
+                  (delete-window win))))))) t)))
+\f
+;;; Modeline
+
+(defun mu4e-quote-for-modeline (str)
+  "Quote STR to be used literally in the modeline.
+The string will be shortened to fit if its length exceeds
+`mu4e-modeline-max-width'."
+  (replace-regexp-in-string
+   "%" "%%"
+   (truncate-string-to-width str mu4e-modeline-max-width 0 nil t)))
+
+
+\f
+;;; Messages, warnings and errors
+(defun mu4e-format (frm &rest args)
+  "Create [mu4e]-prefixed string based on format FRM and ARGS."
+  (concat
+   "[" (propertize "mu4e" 'face 'mu4e-title-face) "] "
+   (apply 'format frm
+          (mapcar (lambda (x)
+                    (if (stringp x)
+                        (decode-coding-string x 'utf-8)
+                      x))
+                  args))))
+
+(defun mu4e-message (frm &rest args)
+  "Display FRM with ARGS like `message' in mu4e style.
+If we're waiting for user-input or if there's some message in the
+echo area, don't show anything."
+  (unless (or (active-minibuffer-window))
+    (message "%s" (apply 'mu4e-format frm args))))
+
+(declare-function mu4e~loading-close "mu4e-headers")
+
+(defun mu4e-error (frm &rest args)
+  "Display an error with FRM and ARGS like `mu4e-message'.
+
+Create [mu4e]-prefixed error based on format FRM and ARGS. Does a
+local-exit and does not return, and raises a
+debuggable (backtrace) error."
+  (mu4e-log 'error (apply 'mu4e-format frm args))
+  ;; opportunistically close the "loading" window.
+  (mu4e~loading-close)
+  (error "%s" (apply 'mu4e-format frm args)))
+
+(defun mu4e-warn (frm &rest args)
+  "Create [mu4e]-prefixed warning based on format FRM and ARGS.
+Does a local-exit and does not return."
+  (mu4e-log 'error (apply 'mu4e-format frm args))
+  (user-error "%s" (apply 'mu4e-format frm args)))
+
+;;; Reading user input
+
+(defun mu4e--read-char-choice (prompt choices)
+  "Read and return one of CHOICES, prompting for PROMPT.
+Any input that is not one of CHOICES is ignored. This is mu4e's
+version of `read-char-choice' which becomes case-insentive after
+trying an exact match."
+  (let ((choice) (chosen) (inhibit-quit nil))
+    (while (not chosen)
+      (message nil);; this seems needed...
+      (setq choice (read-char-exclusive prompt))
+      (if (eq choice 27) (keyboard-quit)) ;; quit if ESC is pressed
+      (setq chosen (or (member choice choices)
+                       (member (downcase choice) choices)
+                       (member (upcase choice) choices))))
+    (car chosen)))
+
+(defun mu4e-read-option (prompt options)
+  "Ask user for an option from a list on the input area.
+PROMPT describes a multiple-choice question to the user. OPTIONS
+describe the options, and is a list of cells describing
+particular options. Cells have the following structure:
+
+   (OPTIONSTRING . RESULT)
+
+where OPTIONSTRING is a non-empty string describing the
+option. The first character of OPTIONSTRING is used as the
+shortcut, and obviously all shortcuts must be different, so you
+can prefix the string with an uniquifying character.
+
+The options are provided as a list for the user to choose from;
+user can then choose by typing CHAR.  Example:
+  (mu4e-read-option \"Choose an animal: \"
+              \='((\"Monkey\" . monkey) (\"Gnu\" . gnu) (\"xMoose\" . moose)))
+
+User now will be presented with a list: \"Choose an animal:
+   [M]onkey, [G]nu, [x]Moose\".
+
+Function returns the cdr of the list element."
+  (let* ((prompt (mu4e-format "%s" prompt))
+         (optionsstr
+          (mapconcat
+           (lambda (option)
+             ;; try to detect old-style options, and warn
+             (when (characterp (car-safe (cdr-safe option)))
+               (mu4e-error
+                (concat "Please use the new format for options/actions; "
+                        "see the manual")))
+             (let ((kar (substring (car option) 0 1)))
+               (concat
+                "[" (propertize kar 'face 'mu4e-highlight-face) "]"
+                (substring (car option) 1))))
+           options ", "))
+         (response
+          (mu4e--read-char-choice
+           (concat prompt optionsstr
+                   " [" (propertize "C-g" 'face 'mu4e-highlight-face)
+                   " to cancel]")
+           ;; the allowable chars
+           (seq-map (lambda(elm) (string-to-char (car elm))) options)))
+         (chosen
+          (seq-find
+           (lambda (option) (eq response (string-to-char (car option))))
+           options)))
+    (if chosen
+        (cdr chosen)
+      (mu4e-warn "Unknown shortcut '%c'" response))))
+
+
+\f
+;;; Server properties
+(defvar mu4e--server-props nil
+  "Metadata we receive from the mu4e server.")
+
+(defun mu4e-server-properties ()
+  "Get the server metadata plist."
+  mu4e--server-props)
+
+(defun mu4e-root-maildir()
+  "Get the root maildir."
+  (or (and mu4e--server-props
+           (plist-get mu4e--server-props :root-maildir))
+      (mu4e-error "Root maildir unknown; did you start mu4e?")))
+
+(defun mu4e-database-path()
+  "Get the root maildir."
+  (or (and mu4e--server-props
+           (plist-get mu4e--server-props :database-path))
+      (mu4e-error "Root maildir unknown; did you start mu4e?")))
+
+(defun mu4e-server-version()
+  "Get the root maildir."
+  (or (and mu4e--server-props
+           (plist-get mu4e--server-props :version))
+      (mu4e-error "Version unknown; did you start mu4e?")))
+
+(defun mu4e-last-query-results ()
+  "Get the results (counts) of the last cached queries.
+
+The cached queries are the bookmark / maildir queries that are
+used to populated the read/unread counts in the main view. They
+are refreshed when calling `(mu4e)', i.e., when going to the main
+view.
+
+The results are a list of elements of the form
+   (:query \"query string\"
+            :count  <total number matching count>
+            :unread <number of unread messages in count>)"
+  (plist-get mu4e--server-props :queries))
+
+(defun mu4e-last-query-result (query)
+  "Get the last result for some QUERY or nil if not found."
+  (seq-find
+   (lambda (elm) (string= (plist-get elm :query) query))
+   (mu4e-last-query-results)))
+
+\f
+;;; Logging / debugging
+
+(defconst mu4e--log-max-size 1000000
+  "Max number of characters to keep around in the log buffer.")
+(defconst mu4e--log-buffer-name "*mu4e-log*"
+  "Name of the logging buffer.")
+
+(defun mu4e--get-log-buffer ()
+  "Fetch (and maybe create) the log buffer."
+  (unless (get-buffer mu4e--log-buffer-name)
+    (with-current-buffer (get-buffer-create mu4e--log-buffer-name)
+      (view-mode)
+      (when (fboundp 'so-long-mode)
+        (unless (eq major-mode 'so-long-mode)
+          (eval '(so-long-mode))))
+      (setq buffer-undo-list t)))
+  mu4e--log-buffer-name)
+
+(defun mu4e-log (type frm &rest args)
+  "Log a message of TYPE with format-string FRM and ARGS.
+Use the mu4e log buffer for this. If the variable mu4e-debug is
+non-nil. Type is a symbol, either `to-server', `from-server' or
+`misc'.
+
+This function is meant for debugging."
+  (when mu4e-debug
+    (with-current-buffer (mu4e--get-log-buffer)
+      (let* ((inhibit-read-only t)
+             (tstamp (propertize (format-time-string "%Y-%m-%d %T.%3N"
+                                                     (current-time))
+                                 'face 'font-lock-string-face))
+             (msg-face
+              (pcase type
+                ('from-server 'font-lock-type-face)
+                ('to-server   'font-lock-function-name-face)
+                ('misc        'font-lock-variable-name-face)
+                ('error       'font-lock-warning-face)
+                (_            (mu4e-error "Unsupported log type"))))
+             (msg (propertize (apply 'format frm args) 'face msg-face)))
+        (save-excursion
+          (goto-char (point-max))
+          (insert tstamp
+                  (pcase type
+                    ('from-server " <- ")
+                    ('to-server   " -> ")
+                    ('error       " !! ")
+                    (_            " "))
+                  msg "\n")
+          ;; if `mu4e-log-max-lines is specified and exceeded, clearest the
+          ;; oldest lines
+          (when (> (buffer-size) mu4e--log-max-size)
+            (goto-char (- (buffer-size) mu4e--log-max-size))
+            (beginning-of-line)
+            (delete-region (point-min) (point))))))))
+
+(defun mu4e-toggle-logging ()
+  "Toggle `mu4e-debug'.
+In debug-mode, mu4e logs some of its internal workings to a
+log-buffer. See `mu4e-show-log'."
+  (interactive)
+  (mu4e-log 'misc "logging disabled")
+  (setq mu4e-debug (not mu4e-debug))
+  (mu4e-message "debug logging has been %s"
+                (if mu4e-debug "enabled" "disabled"))
+  (mu4e-log 'misc "logging enabled"))
+
+(defun mu4e-show-log ()
+  "Visit the mu4e debug log."
+  (interactive)
+  (unless mu4e-debug (mu4e-toggle-logging))
+  (let ((buf (get-buffer mu4e--log-buffer-name)))
+    (unless (buffer-live-p buf)
+      (mu4e-warn "No debug log available"))
+    (switch-to-buffer buf)))
+
+
+\f
+;;; Flags
+;; Converting flags->string and vice-versa
+
+(defun mu4e-flags-to-string (flags)
+  "Convert a list of Maildir[1] FLAGS into a string.
+
+See `mu4e-string-to-flags'. \[1\]:
+http://cr.yp.to/proto/maildir.html."
+  (seq-sort
+   '<
+   (seq-mapcat
+    (lambda (flag)
+      (pcase flag
+       (`draft     "D")
+       (`flagged   "F")
+       (`new       "N")
+       (`passed    "P")
+       (`replied   "R")
+       (`seen      "S")
+       (`trashed   "T")
+       (`attach    "a")
+       (`encrypted "x")
+       (`signed    "s")
+       (`unread    "u")
+       (_          "")))
+    (seq-uniq flags) 'string)))
+
+(defun mu4e-string-to-flags (str)
+  "Convert a STR with Maildir[1] flags into a list of flags.
+
+See `mu4e-string-to-flags'. \[1\]:
+http://cr.yp.to/proto/maildir.html."
+  (seq-uniq
+   (seq-filter
+    'identity
+    (seq-mapcat
+     (lambda (kar)
+       (list
+       (pcase kar
+         ('?D   'draft)
+         ('?F   'flagged)
+         ('?P   'passed)
+         ('?R   'replied)
+         ('?S   'seen)
+         ('?T   'trashed)
+         (_     nil))))
+     str))))
+
+\f
+;;; Misc
+(defun mu4e-copy-thing-at-point ()
+  "Copy e-mail address or URL at point to the kill ring.
+If there is not e-mail address at point, do nothing."
+  (interactive)
+  (let* ((thing (and (thing-at-point 'email)
+                    (string-trim (thing-at-point 'email 'no-props) "<" ">")))
+        (thing (or thing (thing-at-point 'url 'no-props))))
+    (when thing
+      (kill-new thing)
+      (mu4e-message "Copied '%s' to kill-ring" thing))))
+
+(defun mu4e-display-size (size)
+  "Get a human-friendly string representation of SIZE (in bytes)."
+  (cond
+   ((>= size 1000000)
+    (format "%2.1fM" (/ size 1000000.0)))
+   ((and (>= size 1000) (< size 1000000))
+    (format "%2.1fK" (/ size 1000.0)))
+   ((< size 1000)
+    (format "%d" size))
+   (t "?")))
+
+
+(defun mu4e-split-ranges-to-numbers (str n)
+  "Convert STR containing attachment numbers into a list of numbers.
+
+STR is a string; N is the highest possible number in the list.
+This includes expanding e.g. 3-5 into 3,4,5. If the letter
+\"a\" ('all')) is given, that is expanded to a list with numbers
+[1..n]."
+  (let ((str-split (split-string str))
+        beg end list)
+    (dolist (elem str-split list)
+      ;; special number "a" converts into all attachments 1-N.
+      (when (equal elem "a")
+        (setq elem (concat "1-" (int-to-string n))))
+      (if (string-match "\\([0-9]+\\)-\\([0-9]+\\)" elem)
+          ;; we have found a range A-B, which needs converting
+          ;; into the numbers A, A+1, A+2, ... B.
+          (progn
+            (setq beg (string-to-number (match-string 1 elem))
+                  end (string-to-number (match-string 2 elem)))
+            (while (<= beg end)
+              (cl-pushnew beg list :test 'equal)
+              (setq beg (1+ beg))))
+        ;; else just a number
+        (cl-pushnew (string-to-number elem) list :test 'equal)))
+    ;; Check that all numbers are valid.
+    (mapc
+     (lambda (x)
+       (cond
+        ((> x n)
+         (mu4e-warn "Attachment %d bigger than maximum (%d)" x n))
+        ((< x 1)
+         (mu4e-warn "Attachment number must be greater than 0 (%d)" x))))
+     list)))
+
+(defun mu4e-make-temp-file (ext)
+  "Create a self-destructing temporary file with extension EXT.
+The file will self-destruct in a short while, enough to open it
+in an external program."
+  (let ((tmpfile (make-temp-file "mu4e-" nil (concat "." ext))))
+    (run-at-time "30 sec" nil
+                (lambda () (ignore-errors (delete-file tmpfile))))
+    tmpfile))
+
+(defsubst mu4e-is-mode-or-derived-p (mode)
+  "Is the current mode equal to MODE or derived from it?"
+  (or (eq major-mode mode) (derived-mode-p mode)))
+
+(defun mu4e-display-manual ()
+  "Display the mu4e manual page for the current mode.
+Or go to the top level if there is none."
+  (interactive)
+  (info (pcase major-mode
+          ('mu4e-main-mode    "(mu4e)Main view")
+          ('mu4e-headers-mode "(mu4e)Headers view")
+          ('mu4e-view-mode    "(mu4e)Message view")
+          (_                  "mu4e"))))
+
+\f
+;;; bookmarks
+(defun mu4e--make-bookmark-record ()
+  "Create a bookmark for the message at point."
+  (let* ((msg     (mu4e-message-at-point))
+        (subject (or (plist-get msg :subject) "No subject"))
+        (date    (plist-get msg :date))
+        (date    (if date (format-time-string "%F: " date) ""))
+        (title   (format "%s%s" date subject))
+        (msgid   (or (plist-get msg :message-id)
+                     (mu4e-error "Cannot bookmark message without message-id"))))
+    `(,title
+      ,@(bookmark-make-record-default 'no-file 'no-context)
+      (message-id . ,msgid)
+      (handler    . mu4e--jump-to-bookmark))))
+
+(declare-function mu4e-view-message-with-message-id "mu4e-view")
+(declare-function mu4e-message-at-point             "mu4e-message")
+
+(defun mu4e--jump-to-bookmark (bookmark)
+  "View the message referred to by BOOKMARK."
+  (when-let ((msgid (bookmark-prop-get bookmark 'message-id)))
+    (mu4e-view-message-with-message-id msgid)))
+
+\f;;; Macros
+
+(defmacro mu4e-setq-if-nil (var val)
+  "Set VAR to VAL if VAR is nil."
+  `(unless ,var (setq ,var ,val)))
+
+(provide 'mu4e-helpers)
+;;; mu4e-helpers.el ends here
diff --git a/mu4e/mu4e-icalendar.el b/mu4e/mu4e-icalendar.el
new file mode 100644 (file)
index 0000000..929d188
--- /dev/null
@@ -0,0 +1,229 @@
+;;; mu4e-icalendar.el --- reply to iCalendar meeting requests (part of mu4e)  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2019- Christophe Troestler
+
+;; Author: Christophe Troestler <Christophe.Troestler@umons.ac.be>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: email icalendar
+;; Version: 0.0
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; To install:
+;; (require 'mu4e-icalendar)
+;; (mu4e-icalendar-setup)
+;; Optional
+;; (setq mu4e-icalendar-trash-after-reply t)
+
+;; By default, the original message is not cited.  However, if you
+;; would like to reply to it, the citation is in the kill-ring (paste
+;; it with `yank').
+
+;; To add the event to a diary file of your choice:
+;; (setq mu4e-icalendar-diary-file "/path/to/your/diary")
+;; If the file specified is not your main diary file, add
+;; #include "/path/to/your/diary"
+;; to you main diary file to display the events.
+
+;; To enable optional iCalendar->Org sync functionality
+;; NOTE: both the capture file and the headline(s) inside must already exist
+;; (require 'org-agenda)
+;; (setq gnus-icalendar-org-capture-file "~/org/notes.org")
+;; (setq gnus-icalendar-org-capture-headline '("Calendar"))
+;; (gnus-icalendar-org-setup)
+
+;;; Code:
+
+(require 'gnus-icalendar)
+(require 'cl-lib)
+
+(require 'mu4e-mark)
+(require 'mu4e-helpers)
+(require 'mu4e-contacts)
+(require 'mu4e-headers)
+(require 'mu4e-view)
+
+\f
+;;; Configuration
+;;;; Calendar
+
+(defgroup mu4e-icalendar nil
+  "Icalendar related settings."
+  :group 'mu4e)
+
+(defcustom mu4e-icalendar-trash-after-reply nil
+  "If non-nil, trash the icalendar invitation after replying."
+  :type 'boolean
+  :group 'mu4e-icalendar)
+
+(defcustom mu4e-icalendar-diary-file nil
+  "If non-nil, the file in which to add events upon reply."
+  :type '(choice (const :tag "Do not insert a diary entry" nil)
+                 (string :tag "Insert a diary entry in this file"))
+  :group 'mu4e-icalendar)
+
+\f
+;;;###autoload
+(defun mu4e-icalendar-setup ()
+  "Perform the necessary initialization to use mu4e-icalendar."
+  (gnus-icalendar-setup)
+  (cl-defmethod gnus-icalendar-event:inline-reply-buttons :around
+    ((event gnus-icalendar-event) handle)
+    (if (and (boundp 'mu4e~view-rendering)
+             (gnus-icalendar-event:rsvp event))
+        (let ((method (gnus-icalendar-event:method event)))
+          (when (or (string= method "REQUEST") (string= method "PUBLISH"))
+            `(("Accept" mu4e-icalendar-reply (,handle accepted ,event))
+              ("Tentative" mu4e-icalendar-reply (,handle tentative ,event))
+              ("Decline" mu4e-icalendar-reply (,handle declined ,event)))))
+      (cl-call-next-method event handle))))
+
+(defun mu4e~icalendar-has-email (email list)
+  "Check that EMAIL is in LIST."
+  (let ((email (downcase email)))
+    (cl-find-if (lambda (c) (let ((e (mu4e-contact-email c)))
+                              (and (stringp e) (string= email (downcase e)))))
+                list)))
+
+(defun mu4e-icalendar-reply (data)
+  "Reply to the text/calendar event present in DATA."
+  ;; Based on `gnus-icalendar-reply'.
+  (let* ((handle (car data))
+         (status (cadr data))
+         (event (caddr data))
+         (gnus-icalendar-additional-identities (mu4e-personal-addresses 'no-regexp))
+         (reply (gnus-icalendar-with-decoded-handle
+                 handle
+                 (gnus-icalendar-event-reply-from-buffer
+                  (current-buffer) status (gnus-icalendar-identities))))
+         (msg (mu4e-message-at-point 'noerror))
+         (charset (cdr (assoc 'charset (mm-handle-type handle)))))
+    (when reply
+      (cl-labels
+          ((fold-icalendar-buffer
+            ()
+            (goto-char (point-min))
+            (while (re-search-forward "^\\(.\\{72\\}\\)\\(.+\\)$" nil t)
+              (replace-match "\\1\n \\2")
+              (goto-char (line-beginning-position)))))
+
+        (let ((ical-name  gnus-icalendar-reply-bufname))
+          (with-current-buffer (get-buffer-create ical-name)
+            (delete-region (point-min) (point-max))
+            (insert reply)
+            (fold-icalendar-buffer)
+            (when (and charset (string= (downcase charset) "utf-8"))
+              (decode-coding-region (point-min) (point-max) 'utf-8)))
+          ;; Compose the reply message.
+          (save-excursion
+            (let ((message-signature nil)
+                  (mu4e-compose-cite-function #'mu4e~icalendar-delete-citation)
+                  (mu4e-sent-messages-behavior 'delete)
+                  (mu4e-compose-reply-recipients 'sender)
+                  (ical-msg (cl-copy-list msg)))
+              ;; Make sure the reply is sent to email of the organiser with proper name.
+              (let* ((organizer (gnus-icalendar-event:organizer event))
+                     (reply-to (car (plist-get msg :reply-to)))
+                     (from     (car (plist-get msg :from)))
+                     (name (or  (plist-get reply-to :name)
+                                (plist-get from :name))))
+                ;; Add :reply-to field when incomplete or absent
+                (unless (or (string= organizer "")
+                            (mu4e~icalendar-has-email organizer reply-to))
+                  (plist-put ical-msg :reply-to `((:name ,name :email ,organizer))))
+                (plist-put ical-msg :subject
+                           (concat (capitalize (symbol-name status))
+                                   ": " (gnus-icalendar-event:summary event))))
+              (mu4e~compose-handler
+               'reply ical-msg
+               `((:buffer-name ,ical-name
+                  :mime-type "text/calendar; method=REPLY; charset=utf-8")))
+              (message-goto-body)
+              (set-buffer-modified-p nil); not yet modified by user
+              (when mu4e-icalendar-trash-after-reply
+                ;; Override `mu4e-sent-handler' set by `mu4e-compose-mode' to
+                ;; also trash the message (thus must be appended to hooks).
+                (add-hook 'message-sent-hook
+                          (mu4e~icalendar-trash-message-hook msg)
+                          90 t)))))
+
+        ;; Back in article buffer
+        (setq-local gnus-icalendar-reply-status status)
+
+        (when gnus-icalendar-org-enabled-p
+          (if (gnus-icalendar-find-org-event-file event)
+              (gnus-icalendar--update-org-event event status)
+              (gnus-icalendar:org-event-save event status)))
+        (when mu4e-icalendar-diary-file
+          (mu4e~icalendar-insert-diary event status
+                                       mu4e-icalendar-diary-file))))))
+
+(defun mu4e~icalendar-delete-citation ()
+  "Function passed to `mu4e-compose-cite-function' to remove the citation."
+  (message-cite-original-without-signature)
+  (kill-region (point-min) (point-max)))
+
+(defun mu4e~icalendar-trash-message (original-msg)
+  "Trash the message ORIGINAL-MSG and move to the next one."
+  (lambda (docid path)
+    "See `mu4e-sent-handler' for DOCID and PATH."
+    (mu4e-sent-handler docid path)
+    (let* ((docid (mu4e-message-field original-msg :docid))
+           (markdescr (assq 'trash mu4e-marks))
+           (action (plist-get (cdr markdescr) :action))
+           (target (mu4e-get-trash-folder original-msg)))
+      (with-current-buffer (mu4e-get-headers-buffer)
+        (run-hook-with-args 'mu4e-mark-execute-pre-hook 'trash original-msg)
+        (funcall action docid original-msg target))
+      (when (and (mu4e~headers-view-this-message-p docid)
+                 (buffer-live-p (mu4e-get-view-buffer)))
+        (switch-to-buffer (mu4e-get-view-buffer))
+        (or (mu4e-view-headers-next)
+            (kill-buffer-and-window))))))
+
+(defun mu4e~icalendar-trash-message-hook (original-msg)
+  (lambda () (setq mu4e-sent-func
+                   (mu4e~icalendar-trash-message original-msg))))
+
+(defun mu4e~icalendar-insert-diary (event reply-status filename)
+  "Insert a diary entry for the EVENT in file named FILENAME.
+REPLY-STATUS is the status of the reply.  The possible values are
+given in the doc of `gnus-icalendar-event-reply-from-buffer'."
+  ;; FIXME: handle recurring events
+  (let* ((beg (gnus-icalendar-event:start-time event))
+         (beg-date (format-time-string "%d/%m/%Y" beg))
+         (beg-time (format-time-string "%H:%M" beg))
+         (end (gnus-icalendar-event:end-time event))
+         (end-date (format-time-string "%d/%m/%Y" end))
+         (end-time (format-time-string "%H:%M" end))
+         (summary (gnus-icalendar-event:summary event))
+         (location (gnus-icalendar-event:location event))
+         (status (capitalize (symbol-name reply-status)))
+         (txt (if location
+                  (format "%s (%s)\n %s " summary status location)
+                (format "%s (%s)" summary status))))
+    (with-temp-buffer
+      (if (string= beg-date end-date)
+          (insert beg-date " " beg-time "-" end-time " " txt "\n")
+        (insert beg-date " " beg-time " Start of: " txt "\n")
+        (insert beg-date " " end-time " End of: " txt "\n"))
+      (write-region (point-min) (point-max) filename t))))
+
+;;; _
+(provide 'mu4e-icalendar)
+;;; mu4e-icalendar.el ends here
diff --git a/mu4e/mu4e-lists.el b/mu4e/mu4e-lists.el
new file mode 100644 (file)
index 0000000..30d9667
--- /dev/null
@@ -0,0 +1,131 @@
+;;; mu4e-lists.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file, we create a table of list-id -> shortname for mailing lists.
+;; The shortname (friendly) should a at most 8 characters, camel-case
+
+;;; Code:
+\f
+;;; Configuration
+(defvar mu4e-mailing-lists
+  '( ("bbdb-info.lists.sourceforge.net"                       . "BBDB")
+     ("boost-announce.lists.boost.org"                        . "BoostA")
+     ("boost-interest.lists.boost.org"                        . "BoostI")
+     ("conkeror.mozdev.org"                                   . "Conkeror")
+     ("curl-library.cool.haxx.se"                             . "LibCurl")
+     ("crypto-gram-list.schneier.com "                        . "CryptoGr")
+     ("dbus.lists.freedesktop.org"                            . "DBus")
+     ("desktop-devel-list.gnome.org"                          . "GnomeDT")
+     ("discuss-webrtc.googlegroups.com"                       . "WebRTC")
+     ("emacs-devel.gnu.org"                                   . "EmacsDev")
+     ("emacs-orgmode.gnu.org"                                 . "Orgmode")
+     ("emms-help.gnu.org"                                     . "Emms")
+     ("enlightenment-devel.lists.sourceforge.net"             . "E-Dev")
+     ("erlang-questions.erlang.org"                           . "Erlang")
+     ("evolution-hackers.lists.ximian.com"                    . "EvoDev")
+     ("farsight-devel.lists.sourceforge.net"                  . "Farsight")
+     ("mailman.lists.freedesktop.org"                         . "FDeskTop")
+     ("gcc-help.gcc.gnu.org"                                  . "Gcc")
+     ("gmime-devel-list.gnome.org"                            . "GMimeDev")
+     ("gnome-shell-list.gnome.org"                            . "GnomeSh")
+     ("gnu-emacs-sources.gnu.org"                             . "EmacsSrc")
+     ("gnupg-users.gnupg.org"                                 . "GnupgU")
+     ("gpe.handhelds.org"                                     . "GPE")
+     ("gstreamer-devel.lists.freedesktop.org"                 . "GstDev")
+     ("gstreamer-devel.lists.sourceforge.net"                 . "GstDev")
+     ("gstreamer-openmax.lists.sourceforge.net"               . "GstOmx")
+     ("gtk-devel-list.gnome.org"                              . "GtkDev")
+     ("gtkmm-list.gnome.org"                                  . "GtkmmDev")
+     ("guile-devel.gnu.org"                                   . "GuileDev")
+     ("guile-user.gnu.org"                                    . "GuileUsr")
+     ("help-gnu-emacs.gnu.org"                                . "EmacsUsr")
+     ("lggdh-algemeen.vvtp.tudelft.nl"                        . "LGGDH")
+     ("linux-bluetooth.vger.kernel.org"                       . "Bluez")
+     ("maemo-developers.maemo.org"                            . "MaemoDev")
+     ("maemo-users.maemo.org"                                 . "MaemoUsr")
+     ("monit-general.nongnu.org"                              . "Monit")
+     ("mu-discuss.googlegroups.com"                           . "Mu")
+     ("nautilus-list.gnome.org"                               . "Nautilus")
+     ("notmuch.notmuchmail.org"                               . "Notmuch")
+     ("orbit-list.gnome.org"                                  . "ORBit")
+     ("pulseaudio-discuss.lists.freedesktop.org"              . "PulseA")
+     ("sqlite-announce.sqlite.org"                            . "SQliteAnn")
+     ("sqlite-dev.sqlite.org"                                 . "SQLiteDev")
+     ("sup-talk.rubyforge.org"                                . "Sup")
+     ("sylpheed-claws-users.lists.sourceforge.net"            . "Sylpheed")
+     ("tinymail-devel-list.gnome.org"                         . "Tinymail")
+     ("unicode.sarasvati.unicode.org"                         . "Unicode")
+     ("xapian-discuss.lists.xapian.org"                       . "Xapian")
+     ("xdg.lists.freedesktop.org"                             . "XDG")
+     ("wl-en.lists.airs.net"                                  . "Wdrlust")
+     ("wl-en.ml.gentei.org"                                   . "WdrLust")
+     ("xapian-devel.lists.xapian.org"                         . "Xapian")
+     ("zsh-users.zsh.org"                                     . "ZshUsr"))
+  "AList of cells (MAILING-LIST-ID . SHORTNAME).")
+
+(defcustom mu4e-user-mailing-lists nil
+  "An alist with cells (MAILING-LIST-ID . SHORTNAME).
+These are used in addition to the built-in list `mu4e-mailing-lists'."
+  :group 'mu4e-headers
+  :type '(repeat (cons string string)))
+
+(defcustom mu4e-mailing-list-patterns nil
+  "A list of regexps to capture a shortname out of a list-id.
+For the first regex that matches, its first matchgroup will be
+used as the shortname."
+  :group 'mu4e-headers
+  :type '(repeat (regexp)))
+\f
+
+(defvar mu4e--lists-hash nil
+  "Hashtable of mailing-list-id => shortname.
+Based on `mu4e-mailing-lists' and `mu4e-user-mailing-lists'.")
+
+\f
+(defun mu4e-get-mailing-list-shortname (list-id)
+  "Get the shortname for a mailing-list with list-id LIST-ID.
+Based on `mu4e-mailing-lists', `mu4e-user-mailing-lists', and
+`mu4e-mailing-list-patterns'."
+  (unless mu4e--lists-hash
+    (setq mu4e--lists-hash (make-hash-table :test 'equal))
+    (dolist (cell mu4e-mailing-lists)
+      (puthash (car cell) (cdr cell) mu4e--lists-hash))
+    (dolist (cell mu4e-user-mailing-lists)
+      (puthash (car cell) (cdr cell) mu4e--lists-hash)))
+  (or
+   (gethash list-id mu4e--lists-hash)
+   (and (boundp 'mu4e-mailing-list-patterns)
+        (seq-drop-while
+         (lambda (pattern)
+           (not (string-match pattern list-id)))
+         mu4e-mailing-list-patterns)
+        (match-string 1 list-id))
+   ;; if it's not in the db, take the part until the first dot if there is one;
+   ;; otherwise just return the whole thing
+   (if (string-match "\\([^.]*\\)\\." list-id)
+       (match-string 1 list-id)
+     list-id)))
+;;; _
+(provide 'mu4e-lists)
+;;; mu4e-lists.el ends here
diff --git a/mu4e/mu4e-main.el b/mu4e/mu4e-main.el
new file mode 100644 (file)
index 0000000..f1b5e92
--- /dev/null
@@ -0,0 +1,425 @@
+;;; mu4e-main.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'smtpmail)      ;; the queueing stuff (silence elint)
+(require 'mu4e-helpers)    ;; utility functions
+(require 'mu4e-context)  ;; the context
+(require 'mu4e-bookmarks)
+(require 'mu4e-folders)
+(require 'mu4e-update)
+(require 'mu4e-contacts)
+(require 'mu4e-search)
+(require 'mu4e-vars)     ;; mu-wide variables
+
+(declare-function mu4e-compose-new  "mu4e-compose")
+(declare-function mu4e~headers-jump-to-maildir  "mu4e-headers")
+(declare-function mu4e-quit "mu4e")
+
+(require 'cl-lib)
+
+\f
+;; Configuration
+
+(define-obsolete-variable-alias
+  'mu4e-main-buffer-hide-personal-addresses
+  'mu4e-main-hide-personal-addresses "1.5.7")
+
+(defvar mu4e-main-hide-personal-addresses nil
+  "Whether to hide the personal address in the main view.
+
+This can be useful to avoid the noise when there are many, and
+also hides the warning if your `user-mail-address' is not part of
+the personal addresses.")
+
+(defvar mu4e-main-hide-fully-read nil
+  "Whether to hide bookmarks or maildirs without unread messages.")
+
+\f
+;;; Mode
+(define-derived-mode mu4e-org-mode org-mode "mu4e:org"
+  "Major mode for mu4e documents.")
+
+(defun mu4e-info (path)
+  "Show a buffer with the information (an org-file) at PATH."
+  (unless (file-exists-p path)
+    (mu4e-error "Cannot find %s" path))
+  (let ((curbuf (current-buffer)))
+    (find-file path)
+    (mu4e-org-mode)
+    (setq buffer-read-only t)
+    (define-key mu4e-org-mode-map (kbd "q")
+      `(lambda ()
+         (interactive)
+         (bury-buffer)
+         (switch-to-buffer ,curbuf)))))
+
+(defun mu4e-about ()
+  "Show the mu4e \"About\" page."
+  (interactive)
+  (mu4e-info (concat mu4e-doc-dir "/mu4e-about.org")))
+
+(defun mu4e-news ()
+  "Show page with news for the current version of mu4e."
+  (interactive)
+  (mu4e-info (concat mu4e-doc-dir "/NEWS.org")))
+
+
+(defvar mu4e-main-mode-map
+  (let ((map (make-sparse-keymap)))
+
+    (define-key map "q" #'mu4e-quit)
+    (define-key map "j" #'mu4e~headers-jump-to-maildir)
+    (define-key map "C" #'mu4e-compose-new)
+
+    (define-key map "m" #'mu4e--main-toggle-mail-sending-mode)
+    (define-key map "f" #'smtpmail-send-queued-mail)
+    ;;
+    (define-key map "U" #'mu4e-update-mail-and-index)
+    (define-key map  (kbd "C-S-u")   #'mu4e-update-mail-and-index)
+    ;; for terminal users
+    (define-key map  (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+
+    (define-key map "S" #'mu4e-kill-update-mail)
+    (define-key map  (kbd "C-S-u") #'mu4e-update-mail-and-index)
+    (define-key map ";"
+      (lambda()(interactive)(mu4e-context-switch)(revert-buffer)))
+
+    (define-key map "$" #'mu4e-show-log)
+    (define-key map "A" #'mu4e-about)
+    (define-key map "N" #'mu4e-news)
+    (define-key map "H" #'mu4e-display-manual)
+    map)
+  "Keymap for the *mu4e-main* buffer.")
+
+(define-derived-mode mu4e-main-mode special-mode "mu4e:main"
+  "Major mode for the mu4e main screen.
+\\{mu4e-main-mode-map}."
+  (setq truncate-lines t
+        overwrite-mode 'overwrite-mode-binary)
+  (mu4e-context-minor-mode)
+  (mu4e-search-minor-mode)
+  (mu4e-update-minor-mode)
+  (set (make-local-variable 'revert-buffer-function) #'mu4e--main-view-real))
+
+
+(defun mu4e--main-action-str (str &optional func-or-shortcut)
+  "Highlight the first occurrence of [.] in STR.
+If FUNC-OR-SHORTCUT is non-nil and if it is a function, call it
+when STR is clicked (using RET or mouse-2); if FUNC-OR-SHORTCUT is
+a string, execute the corresponding keyboard action when it is
+clicked."
+  (let ((newstr
+         (replace-regexp-in-string
+          "\\[\\(..?\\)\\]"
+          (lambda(m)
+            (format "[%s]"
+                    (propertize (match-string 1 m) 'face 'mu4e-highlight-face)))
+          str))
+        (map (make-sparse-keymap))
+        (func (if (functionp func-or-shortcut)
+                  func-or-shortcut
+                (if (stringp func-or-shortcut)
+                    (lambda()(interactive)
+                      (execute-kbd-macro func-or-shortcut))))))
+    (define-key map [mouse-2] func)
+    (define-key map (kbd "RET") func)
+    (put-text-property 0 (length newstr) 'keymap map newstr)
+    (put-text-property (string-match "\\[.+$" newstr)
+                       ;; only subtract one from length of newstr if we're
+                       ;; actually consuming the first letter (e.g.
+                       ;; `func-or-shortcut' is a function, meaning we put
+                       ;; braces around the first letter of `str')
+                       (if (stringp func-or-shortcut)
+                           (length newstr)
+                         (- (length newstr) 1))
+                       'mouse-face 'highlight newstr)
+    newstr))
+
+
+
+(defun mu4e--longest-of-maildirs-and-bookmarks ()
+  "Return the length of longest name of bookmarks and maildirs."
+  (cl-loop for b in (append (mu4e-bookmarks)
+                            (mu4e--maildirs-with-query))
+           maximize (string-width (plist-get b :name))))
+
+(defun mu4e--main-bookmarks ()
+  "Return the entries for the bookmarks menu."
+  ;; TODO: it's a bit uncool to hard-code the "b" shortcut...
+  (cl-loop with bmks = (mu4e-bookmarks)
+           with longest = (mu4e--longest-of-maildirs-and-bookmarks)
+           with queries = (mu4e-last-query-results)
+           for bm in bmks
+           for key = (string (plist-get bm :key))
+           for name = (plist-get bm :name)
+           for query = (funcall (or mu4e-query-rewrite-function #'identity)
+                                (plist-get bm :query))
+           for qcounts = (and (stringp query)
+                              (cl-loop for q in queries
+                                       when (string=
+                                             (decode-coding-string
+                                              (plist-get q :query) 'utf-8 t)
+                                             query)
+                                       collect q))
+           for unread = (and qcounts (plist-get (car qcounts) :unread))
+           when (not (plist-get bm :hide))
+           when (not (and mu4e-main-hide-fully-read (eq unread 0)))
+           concat (concat
+                   ;; menu entry
+                   (mu4e--main-action-str
+                    (concat "\t* [b" key "] " name)
+                    (concat "b" key))
+                   ;; append all/unread numbers, if available.
+                   (if qcounts
+                       (let ((unread (plist-get (car qcounts) :unread))
+                             (count  (plist-get (car qcounts) :count)))
+                         (format
+                          "%s (%s/%s)"
+                          (make-string (- longest (string-width name)) ? )
+                          (propertize (number-to-string unread)
+                                      'face 'mu4e-header-key-face)
+                          count))
+                     "")
+                   "\n")))
+
+
+(defun mu4e--main-maildirs ()
+  "Return a string of maildirs with their counts."
+  (cl-loop with mds = (mu4e--maildirs-with-query)
+           with longest = (mu4e--longest-of-maildirs-and-bookmarks)
+           with queries = (plist-get mu4e--server-props :queries)
+           for m in mds
+           for key = (string (plist-get m :key))
+           for name = (plist-get m :name)
+           for query = (plist-get m :query)
+           for qcounts = (and (stringp query)
+                              (cl-loop for q in queries
+                                       when (string=
+                                             (decode-coding-string
+                                              (plist-get q :query)
+                                              'utf-8 t)
+                                             query)
+                                       collect q))
+           for unread = (and qcounts (plist-get (car qcounts) :unread))
+           when (not (plist-get m :hide))
+           when (not (and mu4e-main-hide-fully-read (eq unread 0)))
+           concat (concat
+                   ;; menu entry
+                   (mu4e--main-action-str
+                    (concat "\t* [j" key "] " name)
+                    (concat "j" key))
+                   ;; append all/unread numbers, if available.
+                   (if qcounts
+                       (let ((unread (plist-get (car qcounts) :unread))
+                             (count  (plist-get (car qcounts) :count)))
+                         (format
+                          "%s (%s/%s)"
+                          (make-string (- longest (string-width name)) ? )
+                          (propertize (number-to-string unread)
+                                      'face 'mu4e-header-key-face)
+                          count))
+                     "")
+                   "\n")))
+
+
+(defun mu4e--key-val (key val &optional unit)
+  "Show a KEY / VAL pair, with optional UNIT."
+  (concat
+   "\t* "
+   (propertize (format "%-20s" key) 'face 'mu4e-header-title-face)
+   ": "
+   (propertize val 'face 'mu4e-header-key-face)
+   (if unit
+       (propertize (concat " " unit) 'face 'mu4e-header-title-face)
+     "")
+   "\n"))
+
+;; NEW This is the old `mu4e--main-view' function but without
+;; buffer switching at the end.
+(defun mu4e--main-view-real (_ignore-auto _noconfirm)
+  "The revert buffer function for `mu4e-main-mode'."
+  (mu4e--main-view-real-1 'refresh))
+
+(declare-function mu4e--start "mu4e")
+
+(defun mu4e--main-view-real-1 (&optional refresh)
+  "Create `mu4e-main-buffer-name' and set it up.
+When REFRESH is non nil refresh infos from server."
+  (let ((inhibit-read-only t))
+    ;; Maybe refresh infos from server.
+    (if refresh
+        (mu4e--start 'mu4e--main-redraw-buffer)
+      (mu4e--main-redraw-buffer))))
+
+(defun mu4e--main-redraw-buffer ()
+  "Redraw the main buffer."
+  (with-current-buffer mu4e-main-buffer-name
+    (let ((inhibit-read-only t)
+          (pos (point))
+          (addrs (mu4e-personal-addresses)))
+      (erase-buffer)
+      (insert
+       "* "
+       (propertize "mu4e" 'face 'mu4e-header-key-face)
+       (propertize " - mu for emacs version " 'face 'mu4e-title-face)
+       (propertize  mu4e-mu-version 'face 'mu4e-header-key-face)
+        "\n\n"
+       (propertize "  Basics\n\n" 'face 'mu4e-title-face)
+       (mu4e--main-action-str
+        "\t* [j]ump to some maildir\n" #'mu4e~headers-jump-to-maildir)
+       (mu4e--main-action-str
+        "\t* enter a [s]earch query\n" #'mu4e-search)
+       (mu4e--main-action-str
+        "\t* [C]ompose a new message\n" #'mu4e-compose-new)
+       "\n"
+       (propertize "  Bookmarks\n\n" 'face 'mu4e-title-face)
+       (mu4e--main-bookmarks)
+       "\n"
+       (propertize "  Maildirs\n\n" 'face 'mu4e-title-face)
+       (mu4e--main-maildirs)
+       "\n"
+       (propertize "  Misc\n\n" 'face 'mu4e-title-face)
+
+       (mu4e--main-action-str "\t* [;]Switch context\n"
+                             (lambda()(interactive)
+                              (mu4e-context-switch)(revert-buffer)))
+
+       (mu4e--main-action-str "\t* [U]pdate email & database\n"
+                             'mu4e-update-mail-and-index)
+
+       ;; show the queue functions if `smtpmail-queue-dir' is defined
+       (if (file-directory-p smtpmail-queue-dir)
+           (mu4e--main-view-queue)
+         "")
+       "\n"
+       (mu4e--main-action-str "\t* [N]ews\n" #'mu4e-news)
+       (mu4e--main-action-str "\t* [A]bout mu4e\n" #'mu4e-about)
+       (mu4e--main-action-str "\t* [H]elp\n" #'mu4e-display-manual)
+       (mu4e--main-action-str "\t* [q]uit\n" #'mu4e-quit)
+
+       "\n"
+       (propertize "  Info\n\n" 'face 'mu4e-title-face)
+       (mu4e--key-val "last updated" (current-time-string (plist-get mu4e-index-update-status :tstamp)))
+       (mu4e--key-val "database-path" (mu4e-database-path))
+       (mu4e--key-val "maildir" (mu4e-root-maildir))
+       (mu4e--key-val "in store"
+                     (format "%d" (plist-get mu4e--server-props :doccount))
+                    "messages")
+       (if mu4e-main-hide-personal-addresses ""
+         (mu4e--key-val "personal addresses"
+                      (if addrs (mapconcat #'identity addrs ", "  ) "none"))))
+
+      (if mu4e-main-hide-personal-addresses ""
+        (unless (mu4e-personal-address-p user-mail-address)
+          (mu4e-message (concat
+                         "Tip: `user-mail-address' ('%s') is not part "
+                         "of mu's addresses; add it with 'mu init
+                        --my-address='") user-mail-address)))
+      (mu4e-main-mode)
+      (goto-char pos))))
+
+(defun mu4e--main-view-queue ()
+  "Display queue-related actions in the main view."
+  (concat
+   (mu4e--main-action-str "\t* toggle [m]ail sending mode "
+                         'mu4e--main-toggle-mail-sending-mode)
+   "(currently "
+   (propertize (if smtpmail-queue-mail "queued" "direct")
+               'face 'mu4e-header-key-face)
+   ")\n"
+   (let ((queue-size (mu4e--main-queue-size)))
+     (if (zerop queue-size)
+         ""
+       (mu4e--main-action-str
+        (format "\t* [f]lush %s queued %s\n"
+                (propertize (int-to-string queue-size)
+                            'face 'mu4e-header-key-face)
+                (if (> queue-size 1) "mails" "mail"))
+        'smtpmail-send-queued-mail)))))
+
+(defun mu4e--main-queue-size ()
+  "Return, as an int, the number of emails in the queue."
+  (condition-case nil
+      (with-temp-buffer
+        (insert-file-contents (expand-file-name smtpmail-queue-index-file
+                                                smtpmail-queue-dir))
+        (count-lines (point-min) (point-max)))
+    (error 0)))
+
+(defun mu4e--main-view (&optional refresh)
+  "Create the mu4e main-view, and switch to it.
+
+When REFRESH is non nil refresh infos from server."
+  (let ((buf (get-buffer-create mu4e-main-buffer-name)))
+    (if (eq mu4e-split-view 'single-window)
+        (if (buffer-live-p (mu4e-get-headers-buffer))
+            (switch-to-buffer (mu4e-get-headers-buffer))
+          (mu4e--main-menu))
+      ;; `mu4e--main-view' is called from `mu4e--start', so don't call it
+      ;; a second time here i.e. do not refresh unless specified
+      ;; explicitly with REFRESH arg.
+      (switch-to-buffer buf)
+      (with-current-buffer buf
+        (mu4e--main-view-real-1 refresh))
+      (goto-char (point-min)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Interactive functions
+;; NEW
+;; Toggle mail sending mode without switching
+(defun mu4e--main-toggle-mail-sending-mode ()
+  "Toggle sending mail mode, either queued or direct."
+  (interactive)
+  (unless (file-directory-p smtpmail-queue-dir)
+    (mu4e-error "`smtpmail-queue-dir' does not exist"))
+  (setq smtpmail-queue-mail (not smtpmail-queue-mail))
+  (message (concat "Outgoing mail will now be "
+                   (if smtpmail-queue-mail "queued" "sent directly")))
+  (unless (or (eq mu4e-split-view 'single-window)
+              (not (buffer-live-p (get-buffer mu4e-main-buffer-name))))
+    (with-current-buffer mu4e-main-buffer-name
+      (revert-buffer))))
+
+(defun mu4e--main-menu ()
+  "The mu4e main menu in the mini-buffer."
+  (let ((func (mu4e-read-option
+              "Do: "
+              '(("jump"            . mu4e~headers-jump-to-maildir)
+                ("search"          . mu4e-search)
+                ("Compose"         . mu4e-compose-new)
+                ("bookmarks"       . mu4e-headers-search-bookmark)
+                (";Switch context" . mu4e-context-switch)
+                ("Update"          . mu4e-update-mail-and-index)
+                ("News"            . mu4e-news)
+                ("About"           . mu4e-about)
+                ("Help "           . mu4e-display-manual)))))
+    (call-interactively func)
+    (when (eq func 'mu4e-context-switch)
+      (sit-for 1)
+      (mu4e--main-menu))))
+
+(provide 'mu4e-main)
+;;; mu4e-main.el ends here
diff --git a/mu4e/mu4e-mark.el b/mu4e/mu4e-mark.el
new file mode 100644 (file)
index 0000000..e575ea4
--- /dev/null
@@ -0,0 +1,471 @@
+;;; mu4e-mark.el -- part of mu4e  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file are function related to marking messages; they assume we are
+;; currently in the headers buffer.
+
+;;; Code:
+
+(require 'mu4e-server)
+(require 'mu4e-message)
+(require 'mu4e-folders)
+
+;; keep byte-compiler happy
+(declare-function mu4e~headers-mark "mu4e-headers")
+(declare-function mu4e~headers-goto-docid "mu4e-headers")
+(declare-function mu4e-headers-next "mu4e-headers")
+
+;;; Variables & constants
+
+(defcustom mu4e-headers-leave-behavior 'ask
+  "What to do when user leaves the headers view.
+That is when he e.g. quits, refreshes or does a new search.
+Value is one of the following symbols:
+- `ask'     ask user whether to ignore the marks
+- `apply'   automatically apply the marks before doing anything else
+- `ignore'  automatically ignore the marks without asking"
+  :type '(choice (const ask    :tag "ask user whether to ignore marks")
+                 (const apply  :tag "apply marks without asking")
+                 (const ignore :tag "ignore marks without asking"))
+  :group 'mu4e-headers)
+
+(defcustom mu4e-mark-execute-pre-hook nil
+  "Hook run just *before* a mark is applied to a message.
+The hook function is called with two arguments, the mark being
+executed and the message itself."
+  :type 'hook
+  :group 'mu4e-headers)
+
+(defvar mu4e-headers-show-target t
+  "Whether to show targets (such as \"-> delete\", \"-> /archive\")
+when marking message. Normally, this is useful information for
+the user, however, when you often mark large numbers (thousands)
+of message, showing the target makes this quite a bit
+slower (showing the target uses Emacs overlays, which can be slow
+when overused).")
+
+;;; Insert stuff
+
+(defvar mu4e--mark-map nil
+  "Contains a mapping of docid->markinfo.
+When a message is marked, the information is added here. markinfo
+is a cons cell consisting of the following: (mark . target) where
+MARK is the type of mark (move, trash, delete) TARGET (optional)
+is the target directory (for \"move\")")
+
+;; the mark-map is specific for the current header buffer
+;; currently, there can't be more than one, but we never know what will
+;; happen in the future
+
+;; the fringe is the space on the left of headers, where we put marks below some
+;; handy definitions; only `mu4e-mark-fringe-len' should be change (if ever),
+;; the others follow from that.
+(defconst mu4e--mark-fringe-len 2
+  "Width of the fringe for marks on the left.")
+(defconst mu4e--mark-fringe (make-string mu4e--mark-fringe-len ?\s)
+  "The space on the left of message headers to put marks.")
+(defconst mu4e--mark-fringe-format (format "%%-%ds" mu4e--mark-fringe-len)
+  "Format string to set a mark and leave remaining space.")
+
+(defun mu4e--mark-initialize ()
+  "Initialize the marks-subsystem."
+  (set (make-local-variable 'mu4e--mark-map) (make-hash-table)))
+
+(defun mu4e--mark-clear ()
+  "Clear the marks-subsystem."
+  (clrhash mu4e--mark-map))
+
+(defun mu4e--mark-find-headers-buffer ()
+  "Find the headers buffer, if any."
+  (seq-find (lambda (b)
+             (with-current-buffer b
+               (eq major-mode 'mu4e-headers-mode)))
+           (buffer-list)))
+
+(defmacro mu4e--mark-in-context (&rest body)
+  "Evaluate BODY in the context of the headers buffer.
+The current buffer must be either a headers or view buffer."
+  `(cond
+    ((eq major-mode 'mu4e-headers-mode) ,@body)
+    ((eq major-mode 'mu4e-view-mode)
+     (when (buffer-live-p (mu4e-get-headers-buffer))
+       (let* ((msg (mu4e-message-at-point))
+              (docid (mu4e-message-field msg :docid)))
+         (with-current-buffer (mu4e-get-headers-buffer)
+           (if (mu4e~headers-goto-docid docid)
+               ,@body
+             (mu4e-error "Cannot find message in headers buffer"))))))
+    (t
+     ;; even in other modes (e.g. mu4e-main-mode we try to find
+     ;; the headers buffer
+     (let ((hbuf (mu4e--mark-find-headers-buffer)))
+       (if (buffer-live-p hbuf)
+           (with-current-buffer hbuf ,@body)
+         ,@body)))))
+
+(defconst mu4e-marks
+  '((refile
+     :char ("r" . "▶")
+     :prompt "refile"
+     :dyn-target (lambda (target msg) (mu4e-get-refile-folder msg))
+     :action (lambda (docid msg target)
+               (mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
+    (delete
+     :char ("D" . "x")
+     :prompt "Delete"
+     :show-target (lambda (target) "delete")
+     :action (lambda (docid msg target) (mu4e--server-remove docid)))
+    (flag
+     :char ("+" . "✚")
+     :prompt "+flag"
+     :show-target (lambda (target) "flag")
+     :action (lambda (docid msg target)
+               (mu4e--server-move docid nil "+F-u-N")))
+    (move
+     :char ("m" . "▷")
+     :prompt "move"
+     :ask-target  mu4e--mark-get-move-target
+     :action (lambda (docid msg target)
+               (mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
+    (read
+     :char    ("!" . "◼")
+     :prompt "!read"
+     :show-target (lambda (target) "read")
+     :action (lambda (docid msg target) (mu4e--server-move docid nil "+S-u-N")))
+    (trash
+     :char ("d" . "▼")
+     :prompt "dtrash"
+     :dyn-target (lambda (target msg) (mu4e-get-trash-folder msg))
+     :action (lambda (docid msg target)
+              (mu4e--server-move docid
+                                  (mu4e--mark-check-target target) "+T-N")))
+    (unflag
+     :char    ("-" . "➖")
+     :prompt "-unflag"
+     :show-target (lambda (target) "unflag")
+     :action (lambda (docid msg target) (mu4e--server-move docid nil "-F-N")))
+    (untrash
+     :char   ("=" . "▲")
+     :prompt "=untrash"
+     :show-target (lambda (target) "untrash")
+     :action (lambda (docid msg target) (mu4e--server-move docid nil "-T")))
+    (unread
+     :char    ("?" . "◻")
+     :prompt "?unread"
+     :show-target (lambda (target) "unread")
+     :action (lambda (docid msg target) (mu4e--server-move docid nil "-S+u-N")))
+    (unmark
+     :char  " "
+     :prompt "unmark"
+     :action (mu4e-error "No action for unmarking"))
+    (action
+     :char ( "a" . "◯")
+     :prompt "action"
+     :ask-target  (lambda () (mu4e-read-option "Action: " mu4e-headers-actions))
+     :action  (lambda (docid msg actionfunc)
+                (save-excursion
+                  (when (mu4e~headers-goto-docid docid)
+                    (mu4e-headers-action actionfunc)))))
+    (something
+     :char  ("*" . "✱")
+     :prompt "*something"
+     :action (mu4e-error "No action for deferred mark")))
+
+  "The list of all the possible marks.
+This is an alist mapping mark symbols to their properties.  The
+properties are:
+  :char (string) or (basic . fancy) The character to display in
+    the headers view. Either a single-character string, or a
+    dotted-pair cons cell where the second item will be used if
+    `mu4e-use-fancy-chars' is t, otherwise we'll use
+    the first one. It can also be a plain string for backwards
+    compatibility since we didn't always support
+    `mu4e-use-fancy-chars' here.
+  :prompt (string) The prompt to use when asking for marks (used for
+     example when marking a whole thread)
+  :ask-target (function returning a string) Get the target.  This
+     function run once per bulk-operation, and thus is suitable
+     for user-interaction.  If nil, the target is nil.
+  :dyn-target (function from (TARGET MSG) to string).  Compute
+     the dynamic target.  This is run once per message, which is
+     passed as MSG.  The default is to just return the target.
+  :show-target (function from TARGET to string) How to display
+     the target.
+  :action (function taking (DOCID MSG TARGET)).  The action to
+     apply on the message.")
+
+(defun mu4e-mark-at-point (mark target)
+  "Mark (or unmark) message at point.
+MARK specifies the mark-type. For `move'-marks and `trash'-marks
+the TARGET argument is non-nil and specifies to which maildir the
+message is to be moved/trashed. The function works in both
+headers buffers and message buffers.
+
+The following marks are available, and the corresponding props:
+
+   MARK       TARGET    description
+   ----------------------------------------------------------
+   `refile'    y        mark this message for archiving
+   `something' n        mark this message for *something* (decided later)
+   `delete'    n        remove the message
+   `flag'      n        mark this message for flagging
+   `move'      y        move the message to some folder
+   `read'      n        mark the message as read
+   `trash'     y        trash the message to some folder
+   `unflag'    n        mark this message for unflagging
+   `untrash'   n        remove the `trashed' flag from a message
+   `unmark'    n        unmark this message
+   `unread'    n        mark the message as unread
+   `action'    y        mark the message for some action."
+  (interactive)
+  (let* ((msg (mu4e-message-at-point))
+         (docid (mu4e-message-field msg :docid))
+         ;; get a cell with the mark char and the "move" already has a target
+         ;; (the target folder) the other ones get a pseudo "target", as info
+         ;; for the user.
+         (markdesc (cdr (or (assq mark mu4e-marks)
+                            (mu4e-error "Invalid mark %S" mark))))
+         (get-markkar
+          (lambda (char)
+            (if (listp char)
+                (if mu4e-use-fancy-chars (cdr char) (car char))
+              char)))
+         (markkar (funcall get-markkar (plist-get markdesc :char)))
+         (target (mu4e--mark-get-dyn-target mark target))
+         (show-fct (plist-get markdesc :show-target))
+         (shown-target (if show-fct
+                           (funcall show-fct target)
+                         (if target (format "%S" target)))))
+    (unless docid (mu4e-warn "No message on this line"))
+    (unless (eq major-mode 'mu4e-headers-mode)
+      (mu4e-error "Not in headers-mode"))
+    (save-excursion
+      (when (mu4e~headers-mark docid markkar)
+        ;; update the hash -- remove everything current, and if add the new
+        ;; stuff, unless we're unmarking
+        (remhash docid mu4e--mark-map)
+        ;; remove possible mark overlays
+        (remove-overlays (line-beginning-position) (line-end-position)
+                        'mu4e-mark t)
+        ;; now, let's set a mark (unless we were unmarking)
+        (unless (eql mark 'unmark)
+          (puthash docid (cons mark target) mu4e--mark-map)
+          ;; when we have a target (ie., when moving), show the target folder in
+          ;; an overlay
+          (when (and shown-target mu4e-headers-show-target)
+            (let* ((targetstr (propertize (concat "-> " shown-target " ")
+                                          'face 'mu4e-system-face))
+                   ;; mu4e~headers-goto-docid docid t \will take us just after
+                   ;; the docid cookie and then we skip the mu4e--mark-fringe
+                   (start (+ (length mu4e--mark-fringe)
+                             (mu4e~headers-goto-docid docid t)))
+                   (overlay (make-overlay start (+ start (length targetstr)))))
+              (overlay-put overlay 'display targetstr)
+              (overlay-put overlay 'mu4e-mark t)
+              (overlay-put overlay 'evaporate t)
+              docid)))))))
+
+(defun mu4e--mark-get-move-target ()
+  "Ask for a move target, and propose to create it if it does not exist."
+  (interactive)
+  ;;  (mu4e-message-at-point) ;; raises error if there is none
+  (let* ((target (mu4e-ask-maildir "Move message to: "))
+         (target (if (string= (substring target 0 1) "/")
+                     target
+                   (concat "/" target)))
+         (fulltarget (concat (mu4e-root-maildir) target)))
+    (when (or (file-directory-p fulltarget)
+              (and (yes-or-no-p
+                    (format "%s does not exist.  Create now?" fulltarget))
+                   (mu4e--server-mkdir fulltarget)))
+      target)))
+
+(defun mu4e--mark-ask-target (mark)
+  "Ask the target for MARK, if the user should be asked the target."
+  (let ((getter (plist-get (cdr (assq mark mu4e-marks)) :ask-target)))
+    (and getter (funcall getter))))
+
+(defun mu4e--mark-get-dyn-target (mark target)
+  "Get the dynamic TARGET for MARK.
+The result may depend on the message at point."
+  (let ((getter (plist-get (cdr (assq mark mu4e-marks)) :dyn-target)))
+    (if getter
+        (funcall getter target (mu4e-message-at-point))
+      target)))
+
+(defun mu4e-mark-set (mark &optional target)
+  "Mark the header at point with MARK or all in the region.
+Optionally, provide TARGET (for moves)."
+  (unless target
+    (setq target (mu4e--mark-ask-target mark)))
+  (if (not (use-region-p))
+      ;; single message
+      (mu4e-mark-at-point mark target)
+    ;; mark all messages in the region.
+    (save-excursion
+      (let ((cant-go-further) (eor (region-end)))
+        (goto-char (region-beginning))
+        (while (and (< (point) eor) (not cant-go-further))
+          (mu4e-mark-at-point mark target)
+          (setq cant-go-further (not (mu4e-headers-next))))))))
+
+(defun mu4e-mark-restore (docid)
+  "Restore the visual mark for the message with DOCID."
+  (let ((markcell (gethash docid mu4e--mark-map)))
+    (when markcell
+      (save-excursion
+        (when (mu4e~headers-goto-docid docid)
+          (mu4e-mark-at-point (car markcell) (cdr markcell)))))))
+
+(defun mu4e--mark-get-markpair (prompt &optional allow-something)
+  "Ask user with PROMPT for a mark and return (MARK . TARGET).
+If ALLOW-SOMETHING is non-nil, allow the `something' pseudo mark
+as well."
+  (let* ((marks (mapcar (lambda (markdescr)
+                          (cons (plist-get (cdr markdescr) :prompt)
+                                (car markdescr)))
+                        mu4e-marks))
+         (marks
+          (if allow-something
+              marks (seq-remove (lambda (m) (eq 'something (cdr m))) marks)))
+         (mark (mu4e-read-option prompt marks))
+         (target (mu4e--mark-ask-target mark)))
+    (cons mark target)))
+
+(defun mu4e-mark-resolve-deferred-marks ()
+  "Check if there are any deferred ('something') mark-instances.
+If there are such marks, replace them with a _real_ mark (ask the
+user which one)."
+  (interactive)
+  (mu4e--mark-in-context
+   (let ((markpair))
+     (maphash
+      (lambda (docid val)
+        (let ((mark (car val)))
+          (when (eql mark 'something)
+            (unless markpair
+              (setq markpair
+                    (mu4e--mark-get-markpair "Set deferred mark(s) to: " nil)))
+            (save-excursion
+              (when (mu4e~headers-goto-docid docid)
+                (mu4e-mark-set (car markpair) (cdr markpair)))))))
+      mu4e--mark-map))))
+
+(defun mu4e--mark-check-target (target)
+  "Check if TARGET exists; if not, offer to create it."
+  (let ((fulltarget (concat (mu4e-root-maildir) target)))
+    (if (not (mu4e-create-maildir-maybe fulltarget))
+        (mu4e-error "Target dir %s does not exist " fulltarget)
+      target)))
+
+(defun mu4e-mark-execute-all (&optional no-confirmation)
+  "Execute the actions for all marked messages in this buffer.
+After the actions have been executed successfully, the affected
+messages are *hidden* from the current header list. Since the
+headers are the result of a search, we cannot be certain that the
+messages no longer match the current one - to get that
+certainty, we need to rerun the search, but we don't want to do
+that automatically, as it may be too slow and/or break the user's
+flow. Therefore, we hide the message, which in practice seems to
+work well.
+
+If NO-CONFIRMATION is non-nil, don't ask user for confirmation."
+  (interactive "P")
+  (mu4e--mark-in-context
+   (let* ((marknum (hash-table-count mu4e--mark-map))
+          (prompt (format "Are you sure you want to execute %d mark%s?"
+                          marknum (if (> marknum 1) "s" ""))))
+     (if (zerop marknum)
+         (mu4e-warn "Nothing is marked")
+       (mu4e-mark-resolve-deferred-marks)
+       (when (or no-confirmation (y-or-n-p prompt))
+         (maphash
+          (lambda (docid val)
+            (let* ((mark (car val)) (target (cdr val))
+                   (markdescr (assq mark mu4e-marks))
+                   (msg (save-excursion
+                          (mu4e~headers-goto-docid docid)
+                          (mu4e-message-at-point))))
+              ;; note: whenever you do something with the message,
+              ;; it looses its N (new) flag
+              (if markdescr
+                  (progn
+                    (run-hook-with-args
+                     'mu4e-mark-execute-pre-hook mark msg)
+                    (funcall (plist-get (cdr markdescr) :action)
+                             docid msg target))
+                (mu4e-error "Unrecognized mark %S" mark))))
+          mu4e--mark-map))
+       (mu4e-mark-unmark-all)
+       (message nil)))))
+
+(defun mu4e-mark-unmark-all ()
+  "Unmark all marked messages."
+  (interactive)
+  (mu4e--mark-in-context
+   (when (or (null mu4e--mark-map) (zerop (hash-table-count mu4e--mark-map)))
+     (mu4e-warn "Nothing is marked"))
+   (maphash
+    (lambda (docid _val)
+      (save-excursion
+        (when (mu4e~headers-goto-docid docid)
+          (mu4e-mark-set 'unmark))))
+    mu4e--mark-map)
+   ;; in any case, clear the marks map
+   (mu4e--mark-clear)))
+
+(defun mu4e-mark-docid-marked-p (docid)
+  "Is the given DOCID marked?"
+  (when (gethash docid mu4e--mark-map) t))
+
+
+(defun mu4e-mark-marks-num ()
+  "Return the number of mark-instances in the current buffer."
+  (mu4e--mark-in-context
+   (if mu4e--mark-map (hash-table-count mu4e--mark-map) 0)))
+
+(defun mu4e-mark-handle-when-leaving ()
+  "Handle any mark-instances in the current buffer when leaving.
+This is done according to the value of
+`mu4e-headers-leave-behavior'. This function is to be called
+before any further action (like searching, quitting the buffer)
+is taken; returning t means \"take the following action\", return
+nil means \"don't do anything\"."
+  (mu4e--mark-in-context
+   (let ((marknum (mu4e-mark-marks-num))
+         (what mu4e-headers-leave-behavior))
+     (unless (zerop marknum) ;; nothing to do?
+       (when (eq what 'ask)
+         (setq what (mu4e-read-option
+                     (format  "There are %d existing mark(s); should we: "
+                              marknum)
+                     '( ("apply marks"   . apply)
+                        ("ignore marks?" . ignore)))))
+       ;; we determined what to do... now do it
+       (when (eq what 'apply)
+         (mu4e-mark-execute-all t))))))
+
+;;; _
+(provide 'mu4e-mark)
+;;; mu4e-mark.el ends here
diff --git a/mu4e/mu4e-message.el b/mu4e/mu4e-message.el
new file mode 100644 (file)
index 0000000..ccf6eb3
--- /dev/null
@@ -0,0 +1,234 @@
+;;; mu4e-message.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Functions to get data from mu4e-message plist structure
+
+;;; Code:
+
+(require 'mu4e-vars)
+(require 'mu4e-contacts)
+(require 'flow-fill)
+(require 'shr)
+(require 'pp)
+
+(declare-function mu4e-error "mu4e-helpers")
+(declare-function mu4e-warn  "mu4e-helpers")
+(declare-function mu4e-personal-address-p "mu4e-contacts")
+(declare-function mu4e-make-temp-file  "mu4e-helpers")
+
+(make-obsolete-variable 'mu4e-html2text-command "No longer in use" "1.7.0")
+(make-obsolete-variable 'mu4e-view-prefer-html "No longer in use" "1.7.0")
+(make-obsolete-variable 'mu4e-view-html-plaintext-ratio-heuristic
+                       "No longer in use" "1.7.0")
+(make-obsolete-variable 'mu4e-message-body-rewrite-functions
+                       "No longer in use" "1.7.0")
+
+;;; Message fields
+
+(defsubst mu4e-message-field-raw (msg field)
+  "Retrieve FIELD from message plist MSG.
+
+See \"mu fields\" for the full list of field, in particular the
+\"sexp\" column.
+
+Returns nil if the field does not exist.
+
+A message plist looks something like:
+\(:docid 32461
+ :from ((:name \"Nikola Tesla\" :email \"niko@example.com\"))
+ :to ((:name \"Thomas Edison\" :email \"tom@example.com\"))
+ :cc ((:name \"Rupert The Monkey\" :email \"rupert@example.com\"))
+ :subject \"RE: what about the 50K?\"
+ :date (20369 17624 0)
+ :size 4337
+ :message-id \"238C8233AB82D81EE81AF0114E4E74@123213.mail.example.com\"
+ :path  \"/home/tom/Maildir/INBOX/cur/133443243973_1.10027.atlas:2,S\"
+ :maildir \"/INBOX\"
+ :priority normal
+ :flags (seen)
+\)).
+Some notes on the format:
+- The address fields are lists of plist (:name NAME :email EMAIL),
+  where the :name part can be absent. The `mu4e-contact-name' and
+  `mu4e-contact-email' accessors can be useful for this.
+- The date is in format emacs uses in `current-time'
+- Attachments are a list of elements with fields :index (the number of
+  the MIME-part), :name (the file name, if any), :mime-type (the
+  MIME-type, if any) and :size (the size in bytes, if any).
+- Messages in the Headers view come from the database and do not have
+  :attachments, :body-txt or :body-html fields. Message in the
+  Message view use the actual message file, and do include these fields."
+  ;; after all this documentation, the spectacular implementation
+  (if msg
+      (plist-get msg field)
+    (mu4e-error "Message must be non-nil")))
+
+(defsubst mu4e-message-field (msg field)
+  "Retrieve FIELD from message plist MSG.
+Like `mu4e-message-field-nil', but will sanitize nil values:
+- all string field except body-txt/body-html: nil -> \"\"
+- numeric fields + dates                    : nil -> 0
+- all others                                : return the value
+Thus, function will return nil for empty lists, non-existing body-txt
+or body-html."
+  (let ((val (mu4e-message-field-raw msg field)))
+    (cond
+     (val
+      val)   ;; non-nil -> just return it
+     ((member field '(:subject :message-id :path :maildir :in-reply-to))
+      "")    ;; string fields except body-txt, body-html: nil -> ""
+     ((member field '(:body-html :body-txt))
+      val)
+     ((member field '(:docid :size))
+      0)     ;; numeric type: nil -> 0
+     (t
+      val)))) ;; otherwise, just return nil
+
+(defsubst mu4e-message-has-field (msg field)
+  "If MSG has a FIELD return t, nil otherwise."
+  (plist-member msg field))
+
+(defsubst mu4e-message-at-point (&optional noerror)
+  "Get the message s-expression for the message at point.
+Either the headers buffer or the view buffer, or nil if there is
+no such message. If optional NOERROR is non-nil, do not raise an
+error when there is no message at point."
+  (or (cond
+       ((eq major-mode 'mu4e-headers-mode) (get-text-property (point) 'msg))
+       ((eq major-mode 'mu4e-view-mode) mu4e~view-message))
+      (unless noerror (mu4e-warn "No message at point"))))
+
+(defsubst mu4e-message-field-at-point (field)
+  "Get the field FIELD from the message at point.
+This is equivalent to:
+  (mu4e-message-field (mu4e-message-at-point) FIELD)."
+  (mu4e-message-field (mu4e-message-at-point) field))
+
+(defun mu4e-message-contact-field-matches (msg cfield rx)
+  "Does MSG's contact-field CFIELD match regexp RX?
+Check if any of the of the CFIELD in MSG matches RX. I.e.
+anything in field CFIELD (either :to, :from, :cc or :bcc, or a
+list of those) of msg MSG matches (with their name or e-mail
+address) regular expressions RX. If there is a match, return
+non-nil; otherwise return nil. RX can also be a list of regular
+expressions, in which case any of those are tried for a match."
+  (cond
+   ((null cfield))
+   ((listp cfield)
+    (seq-find (lambda (cf) (mu4e-message-contact-field-matches msg cf rx))
+              cfield))
+   ((listp rx)
+    ;; if rx is a list, try each one of them for a match
+    (seq-find
+     (lambda (a-rx) (mu4e-message-contact-field-matches msg cfield a-rx))
+     rx))
+   (t
+    ;; not a list, check the rx
+    (seq-find
+     (lambda (ct)
+       (let ((name (mu4e-contact-name ct))
+            (email (mu4e-contact-email ct))
+             ;; the 'rx' may be some `/rx/` from mu4e-personal-addresses;
+             ;; so let's detect and extract in that case.
+             (rx (if (string-match-p  "^\\(.*\\)/$" rx)
+                     (substring rx  1 -1) rx)))
+         (or
+          (and name  (string-match rx name))
+          (and email (string-match rx email)))))
+     (mu4e-message-field msg cfield)))))
+
+(defun mu4e-message-contact-field-matches-me (msg cfield)
+  "Does contact-field CFIELD in MSG match me?
+Checks whether any
+of the of the contacts in field CFIELD (either :to, :from, :cc or
+:bcc) of msg MSG matches *me*, that is, any of the addresses for
+which `mu4e-personal-address-p' return t. Returns the contact
+cell that matched, or nil."
+  (seq-find (lambda (cell)
+             (mu4e-personal-address-p (mu4e-contact-email cell)))
+            (mu4e-message-field msg cfield)))
+
+(defun mu4e-message-sent-by-me (msg)
+  "Is this MSG (to be) sent by me?
+Checks if the from field matches user's personal addresses."
+  (mu4e-message-contact-field-matches-me msg :from))
+
+(defun mu4e-message-personal-p (msg)
+  "Does MSG have user's personal address?
+In any of the contact
+ fields?"
+  (seq-some
+   (lambda (field)
+     (mu4e-message-contact-field-matches-me msg field))
+   '(:from :to :cc :bcc)))
+
+(defsubst mu4e-message-part-field (msgpart field)
+  "Get some FIELD from MSGPART.
+A part would look something like:
+  (:index 2 :name \"photo.jpg\" :mime-type \"image/jpeg\" :size 147331)."
+  (plist-get msgpart field))
+
+;; backward compatibility ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(defalias 'mu4e-msg-field 'mu4e-message-field)
+
+(defun mu4e-field-at-point (field)
+  "Get FIELD for the message at point.
+Either in the headers buffer or the view buffer. Field is a
+symbol, see `mu4e-header-info'."
+  (plist-get (mu4e-message-at-point) field))
+
+;;; Html2Text
+(make-obsolete 'mu4e-shr2text "No longer in use" "1.7.0")
+
+(defun mu4e-message-readable-path (&optional msg)
+  "Get a readable path to MSG or raise an error.
+If MSG is nil, use `mu4e-message-at-point'."
+  (let ((path (plist-get (or msg (mu4e-message-at-point)) :path)))
+    (unless (file-readable-p path)
+      (mu4e-error "No readable message at %s; database outdated?" path))
+    path))
+
+(defun mu4e-copy-message-path ()
+  "Copy the message-path of message at point to the kill ring."
+  (interactive)
+  (let ((path (mu4e-message-field-at-point :path)))
+    (kill-new path)
+    (mu4e-message "Saved '%s' to kill-ring" path)))
+
+(defconst mu4e--sexp-buffer-name " *mu4e-sexp-at-point"
+  "Buffer name for sexp buffers.")
+
+(defun mu4e-sexp-at-point ()
+  "Show or hide the s-expression for the message-at-point, if any."
+  (interactive)
+  (if-let ((win (get-buffer-window mu4e--sexp-buffer-name)))
+      (delete-window win)
+    (when-let ((msg (mu4e-message-at-point 'noerror)))
+      (with-current-buffer-window mu4e--sexp-buffer-name nil nil
+       ;; the "pretty-printing" is not very pretty...
+       (insert (pp-to-string msg))))))
+
+;;;
+(provide 'mu4e-message)
+;;; mu4e-message.el ends here
diff --git a/mu4e/mu4e-org.el b/mu4e/mu4e-org.el
new file mode 100644 (file)
index 0000000..f3fc090
--- /dev/null
@@ -0,0 +1,143 @@
+;;; mu4e-org -- Org-links to mu4e messages/queries -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: outlines, hypermedia, calendar, mail
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of 1the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The expect version here is org 9.x.
+
+;;; Code:
+
+(require 'org)
+(require 'mu4e-view)
+(require 'mu4e-contacts)
+
+
+(defgroup mu4e-org nil
+  "Settings for the Org mode related functionality in mu4e."
+  :group 'mu4e
+  :group 'org)
+
+(defcustom mu4e-org-link-desc-func
+  (lambda (msg) (or (plist-get msg :subject) "No subject"))
+  "Function that takes a msg and returns a description.
+This can be used in org capture templates and storing links.
+
+Example usage:
+
+  (defun my-link-descr (msg)
+    (let ((subject (or (plist-get msg :subject)
+                       \"No subject\"))
+          (date (or (format-time-string mu4e-headers-date-format
+                    (mu4e-msg-field msg :date))
+                    \"No date\")))
+      (concat subject \" \" date)))
+
+  (setq org-mu4e-link-desc-func \='my-link-descr)"
+  :type '(function)
+  :group 'mu4e-org)
+
+(defvar mu4e-org-link-query-in-headers-mode nil
+  "Prefer linking to the query rather than to the message.
+If non-nil, `org-store-link' in `mu4e-headers-mode' links to the
+the current query; otherwise, it links to the message at point.")
+
+(defun mu4e--org-store-link-query ()
+  "Store a link to a mu4e query."
+  (setq org-store-link-plist nil) ; reset
+  (org-store-link-props
+   :type        "mu4e"
+   :query       (mu4e-last-query)
+   :date        (format-time-string "%FT%T") ;; avoid error
+   :link        (concat "mu4e:query:" (mu4e-last-query))
+   :description (format "[%s]" (mu4e-last-query))))
+
+(defun mu4e--org-store-link-message ()
+  "Store a link to a mu4e message."
+  (setq org-store-link-plist nil)
+  (let* ((msg      (mu4e-message-at-point))
+         (from     (car-safe (plist-get msg :from)))
+         (to       (car-safe (plist-get msg :to)))
+         (date     (format-time-string "%FT%T" (plist-get msg :date)))
+         (msgid    (or (plist-get msg :message-id)
+                       (mu4e-error "Cannot link message without message-id")))
+        (props `(:type  "mu4e"
+                 :date              ,date
+                 :from              ,(mu4e-contact-full from)
+                 :fromname          ,(mu4e-contact-name from)
+                 :fromnameoraddress ,(or (mu4e-contact-name from)
+                                       (mu4e-contact-email from)) ;; mu4e-specific
+                 :maildir           ,(plist-get msg :maildir)
+                 :message-id        ,msgid
+                 :path              ,(plist-get msg :path)
+                 :subject           ,(plist-get msg :subject)
+                 :to                ,(mu4e-contact-full to)
+                 :tonameoraddress   ,(or (mu4e-contact-name to)
+                                       (mu4e-contact-email to)) ;; mu4e-specific
+                 :link              ,(concat "mu4e:msgid:" msgid)
+                 :description       ,(funcall mu4e-org-link-desc-func msg))))
+    (apply #'org-store-link-props props)))
+
+(defun mu4e-org-store-link ()
+  "Store a link to a mu4e message or query.
+It links to the last known query when in `mu4e-headers-mode' with
+`mu4e-org-link-query-in-headers-mode' set; otherwise it links to
+a specific message, based on its message-id, so that links stay
+valid even after moving the message around."
+  (cond
+   ((mu4e-is-mode-or-derived-p 'mu4e-view-mode) (mu4e--org-store-link-message))
+   ((mu4e-is-mode-or-derived-p 'mu4e-headers-mode)
+    (if mu4e-org-link-query-in-headers-mode
+        (mu4e--org-store-link-query)
+      (mu4e--org-store-link-message)))))
+
+(defun mu4e-org-open (link)
+  "Open the org LINK.
+Open the mu4e message (for links starting with \"msgid:\") or run
+the query (for links starting with \"query:\")."
+  (require 'mu4e)
+  (cond
+   ((string-match "^msgid:\\(.+\\)" link)
+    (mu4e-view-message-with-message-id (match-string 1 link)))
+   ((string-match "^query:\\(.+\\)" link)
+    (mu4e-search (match-string 1 link) current-prefix-arg))
+   (t (mu4e-error "Unrecognized link type '%s'" link))))
+
+(make-obsolete 'org-mu4e-open 'mu4e-org-open "1.3.6")
+
+(defun mu4e-org-store-and-capture ()
+  "Store a link to the current message or query.
+\(depending on `mu4e-org-link-query-in-headers-mode', and capture
+it with org)."
+  (interactive)
+  (call-interactively 'org-store-link)
+  (org-capture))
+
+(make-obsolete 'org-mu4e-store-and-capture
+               'mu4e-org-store-and-capture "1.3.6")
+
+;; install mu4e-link support.
+(org-link-set-parameters "mu4e"
+                         :follow #'mu4e-org-open
+                         :store  #'mu4e-org-store-link)
+(provide 'mu4e-org)
+;;; mu4e-org.el ends here
diff --git a/mu4e/mu4e-search.el b/mu4e/mu4e-search.el
new file mode 100644 (file)
index 0000000..20f98d1
--- /dev/null
@@ -0,0 +1,473 @@
+;;; mu4e-search.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021,2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Search-related functions and a minor-mode.
+
+;;; Code:
+
+(require 'seq)
+(require 'mu4e-helpers)
+(require 'mu4e-message)
+(require 'mu4e-bookmarks)
+(require 'mu4e-contacts)
+(require 'mu4e-lists)
+(require 'mu4e-mark)
+
+\f
+;;; Configuration
+(defgroup mu4e-search nil
+  "Search-related settings."
+  :group 'mu4e)
+
+(define-obsolete-variable-alias 'mu4e-headers-results-limit
+  'mu4e-search-results-limit "1.7.0")
+(defcustom mu4e-search-results-limit 500
+  "Maximum number of results to show.
+This affects performance, especially when
+`mu4e-summary-include-related' is non-nil.
+Set to -1 for no limits."
+  :type '(choice (const :tag "Unlimited" -1)
+                (integer :tag "Limit"))
+  :group 'mu4e-search)
+
+(define-obsolete-variable-alias 'mu4e-headers-full-search
+  'mu4e-search-full "1.7.0")
+(defvar mu4e-search-full nil
+  "Whether to search for all results.
+If this is nil, search for up to `mu4e-search-results-limit')")
+
+
+(define-obsolete-variable-alias 'mu4e-headers-show-threads
+  'mu4e-search-threads "1.7.0")
+(defvar mu4e-search-threads t
+  "Whether to calculate threads for the search results.")
+
+(defcustom mu4e-query-rewrite-function 'identity
+  "Function to rewrite a query.
+
+It takes a search expression string, and returns a possibly
+  changed search expression string.
+
+This function is applied on the search expression just before
+searching, and allows users to modify the query.
+
+For instance, we could change and of workmail into
+\"maildir:/long-path-to-work-related-emails\", by setting the function
+
+(setq mu4e-query-rewrite-function
+  (lambda(expr)
+     (replace-regexp-in-string \"workmail\"
+                   \"maildir:/long-path-to-work-related-emails\" expr)))
+
+It is good to remember that the replacement does not understand
+anything about the query, it just does text replacement."
+  :type 'function
+  :group 'mu4e-search)
+
+(define-obsolete-variable-alias
+  'mu4e-headers-search-bookmark-hook
+  'mu4e-search-bookmark-hook "1.7.0")
+(defcustom mu4e-search-bookmark-hook nil
+  "Hook run just after invoking a bookmarked search.
+
+This function receives the query as its parameter, before any
+rewriting as per `mu4e-query-rewrite-function' has taken place.
+
+The reason to use this instead of `mu4e-headers-search-hook' is
+if you only want to execute a hook when a search is entered via a
+bookmark, e.g. if you'd like to treat the bookmarks as a custom
+folder and change the options for the search."
+  :type 'hook
+  :group 'mu4e-search)
+
+(define-obsolete-variable-alias 'mu4e-headers-search-hook
+  'mu4e-search-hook "1.7.0")
+(defcustom mu4e-search-hook nil
+  "Hook run just before executing a new search operation.
+This function receives the query as its parameter, before any
+rewriting as per `mu4e-query-rewrite-function' has taken place
+
+This is a more general hook facility than the
+`mu4e-search-bookmark-hook'. It gets called on every
+executed search, not just those that are invoked via bookmarks,
+but also manually invoked searches."
+  :type 'hook
+  :group 'mu4e-search)
+\f
+;; Internals
+
+;;; History
+(defvar mu4e--search-query-past nil
+  "Stack of queries before the present one.")
+(defvar mu4e--search-query-future nil
+  "Stack of queries after the present one.")
+(defvar mu4e--search-query-stack-size 20
+  "Maximum size for the query stacks.")
+(defvar mu4e--search-last-query nil
+  "The present (most recent) query.")
+
+
+\f
+;;; Interactive functions
+(declare-function mu4e--search-execute "mu4e-headers")
+
+(defvar mu4e--search-view-target nil
+  "Whether to automatically view (open) the target message.")
+(defvar mu4e--search-msgid-target nil
+  "Message-id to jump to after the search has finished.")
+
+
+(defun mu4e-search (&optional expr prompt edit ignore-history msgid show)
+  "Search for query EXPR.
+
+Switch to the output buffer for the results. This is an
+interactive function which ask user for EXPR. PROMPT, if non-nil,
+is the prompt used by this function (default is \"Search for:\").
+If EDIT is non-nil, instead of executing the query for EXPR, let
+the user edit the query before executing it.
+
+If IGNORE-HISTORY is true, do *not* update the query history
+stack. If MSGID is non-nil, attempt to move point to the first
+message with that message-id after searching. If SHOW is non-nil,
+show the message with MSGID."
+  (interactive)
+  (let* ((prompt (mu4e-format (or prompt "Search for: ")))
+        (expr
+         (if (or (null expr) edit)
+             (mu4e-read-query prompt expr)
+           expr)))
+    (mu4e-mark-handle-when-leaving)
+    (mu4e--search-execute expr ignore-history)
+    (setq mu4e--search-msgid-target msgid
+         mu4e--search-view-target show)))
+
+(define-obsolete-function-alias 'mu4e-headers-search 'mu4e-search "1.7.0")
+
+(defun mu4e-search-edit ()
+  "Edit the last search expression."
+  (interactive)
+  (mu4e-search mu4e--search-last-query nil t))
+
+(define-obsolete-function-alias 'mu4e-headers-search-edit
+  'mu4e-search-edit "1.7.0")
+
+(defun mu4e-search-bookmark (&optional expr edit)
+  "Search using some bookmarked query EXPR.
+If EDIT is non-nil, let the user edit the bookmark before starting
+the search."
+  (interactive)
+  (let ((expr
+        (or expr
+            (mu4e-ask-bookmark (if edit "Select bookmark: " "Bookmark: ")))))
+    (run-hook-with-args 'mu4e-search-bookmark-hook expr)
+    (mu4e-search expr (when edit "Edit bookmark: ") edit)))
+
+(define-obsolete-function-alias 'mu4e-headers-search-bookmark
+  'mu4e-search-bookmark "1.7.0")
+
+(defun mu4e-search-bookmark-edit ()
+  "Edit an existing bookmark before executing it."
+  (interactive)
+  (mu4e-search-bookmark nil t))
+
+(define-obsolete-function-alias 'mu4e-headers-search-bookmark-edit
+  'mu4e-search-bookmark-edit "1.7.0")
+
+(defun mu4e-search-narrow(&optional filter)
+  "Narrow the last search.
+Do so by appending search expression FILTER to the last search
+expression. Note that you can go back to the previous
+query (effectively, \"widen\" it), with `mu4e-search-prev'."
+  (interactive
+   (let ((filter
+         (read-string (mu4e-format "Narrow down to: ")
+                      nil 'mu4e~headers-search-hist nil t)))
+     (list filter)))
+  (unless mu4e--search-last-query
+    (mu4e-warn "There's nothing to filter"))
+  (mu4e-search (format "(%s) AND (%s)" mu4e--search-last-query filter)))
+
+(define-obsolete-function-alias 'mu4e-headers-search-narrow
+  'mu4e-search-narrow "1.7.0")
+
+;; (defun mu4e-headers-change-sorting (&optional field dir)
+;;   "Change the sorting/threading parameters.
+;; FIELD is the field to sort by; DIR is a symbol: either 'ascending,
+;; 'descending, 't (meaning: if FIELD is the same as the current
+;; sortfield, change the sort-order) or nil (ask the user)."
+;;   (interactive)
+;;   (let* ((field
+;;           (or field
+;;               (mu4e-read-option "Sortfield: " mu4e~headers-sort-field-choices)))
+;;          ;; note: 'sortable' is either a boolean (meaning: if non-nil, this is
+;;          ;; sortable field), _or_ another field (meaning: sort by this other field).
+;;          (sortable (plist-get (cdr (assoc field mu4e-header-info)) :sortable))
+;;          ;; error check
+;;          (sortable
+;;           (if sortable
+;;               sortable
+;;             (mu4e-error "Not a sortable field")))
+;;          (sortfield (if (booleanp sortable) field sortable))
+;;          (dir
+;;           (cl-case dir
+;;             ((ascending descending) dir)
+;;             ;; change the sort order if field = curfield
+;;             (t
+;;              (if (eq sortfield mu4e-headers-sort-field)
+;;                  (if (eq mu4e-headers-sort-direction 'ascending)
+;;                      'descending 'ascending)
+;;                'descending))
+;;             (mu4e-read-option "Direction: "
+;;                               '(("ascending" . 'ascending) ("descending" . 'descending))))))
+;;     (setq
+;;      mu4e-headers-sort-field sortfield
+;;      mu4e-headers-sort-direction dir)
+;;     (mu4e-message "Sorting by %s (%s)"
+;;                   (symbol-name sortfield)
+;;                   (symbol-name mu4e-headers-sort-direction))
+;;     (mu4e-headers-rerun-search)))
+
+;; (defun mu4e~headers-toggle (name togglevar dont-refresh)
+;;   "Toggle variable TOGGLEVAR for feature NAME. Unless DONT-REFRESH is non-nil,
+;; re-run the last search."
+;;   (set togglevar (not (symbol-value togglevar)))
+;;   (mu4e-message "%s turned %s%s"
+;;                 name
+;;                 (if (symbol-value togglevar) "on" "off")
+;;                 (if dont-refresh
+;;                     " (press 'g' to refresh)" ""))
+;;   (unless dont-refresh
+;;     (mu4e-headers-rerun-search)))
+
+;; (defun mu4e-headers-toggle-threading (&optional dont-refresh)
+;;   "Toggle `mu4e-headers-show-threads'. With prefix-argument, do
+;; _not_ refresh the last search with the new setting for threading."
+;;   (interactive "P")
+;;   (mu4e~headers-toggle "Threading" 'mu4e-headers-show-threads dont-refresh))
+
+;; (defun mu4e-headers-toggle-full-search (&optional dont-refresh)
+;;   "Toggle `mu4e-headers-full-search'. With prefix-argument, do
+;; _not_ refresh the last search with the new setting for threading."
+;;   (interactive "P")
+;;   (mu4e~headers-toggle "Full-search"
+;;                        'mu4e-headers-full-search dont-refresh))
+
+;; (defun mu4e-headers-toggle-include-related (&optional dont-refresh)
+;;   "Toggle `mu4e-headers-include-related'. With prefix-argument, do
+;; _not_ refresh the last search with the new setting for threading."
+;;   (interactive "P")
+;;   (mu4e~headers-toggle "Include-related"
+;;                        'mu4e-headers-include-related dont-refresh))
+
+;; (defun mu4e-headers-toggle-skip-duplicates (&optional dont-refresh)
+;;   "Toggle `mu4e-headers-skip-duplicates'. With prefix-argument, do
+;; _not_ refresh the last search with the new setting for threading."
+;;   (interactive "P")
+;;   (mu4e~headers-toggle "Skip-duplicates"
+;;                        'mu4e-headers-skip-duplicates dont-refresh))
+
+\f
+
+(defun mu4e--search-push-query (query where)
+  "Push QUERY to one of the query stacks.
+WHERE is a symbol telling us where to push; it's a symbol, either
+`future' or `past'. Also removes duplicates and truncates to
+limit the stack size."
+  (let ((stack
+        (pcase where
+          ('past   mu4e--search-query-past)
+          ('future mu4e--search-query-future))))
+    ;; only add if not the same item
+    (unless (and stack (string= (car stack) query))
+      (push query stack)
+      ;; limit the stack to `mu4e--search-query-stack-size' elements
+      (when (> (length stack) mu4e--search-query-stack-size)
+       (setq stack (cl-subseq stack 0 mu4e--search-query-stack-size)))
+      ;; remove all duplicates of the new element
+      (seq-remove (lambda (elm) (string= elm (car stack))) (cdr stack))
+      ;; update the stacks
+      (pcase where
+       ('past   (setq mu4e--search-query-past   stack))
+       ('future (setq mu4e--search-query-future stack))))))
+
+(defun mu4e--search-pop-query (whence)
+  "Pop a query from the stack.
+WHENCE is a symbol telling us where to get it from, either `future'
+or `past'."
+  (pcase whence
+    ('past
+     (unless mu4e--search-query-past
+       (mu4e-warn "No more previous queries"))
+     (pop mu4e--search-query-past))
+    ('future
+     (unless mu4e--search-query-future
+       (mu4e-warn "No more next queries"))
+     (pop mu4e--search-query-future))))
+
+(defun mu4e-search-rerun ()
+  "Re-run the search for the last search expression."
+  (interactive)
+  ;; if possible, try to return to the same message
+  (let* ((msg (mu4e-message-at-point t))
+        (msgid (and msg (mu4e-message-field msg :message-id))))
+    (mu4e-search mu4e--search-last-query nil nil t msgid)))
+
+(define-obsolete-function-alias 'mu4e-headers-rerun-search
+  'mu4e-search-rerun "1.7.0")
+
+(defun mu4e--search-query-navigate (whence)
+  "Execute the previous query from the query stacks.
+WHENCE determines where the query is taken from and is a symbol,
+either `future' or `past'."
+  (let ((query (mu4e--search-pop-query whence))
+       (where (if (eq whence 'future) 'past 'future)))
+    (when query
+      (mu4e--search-push-query mu4e--search-last-query where)
+      (mu4e-search query nil nil t))))
+
+(defun mu4e-search-next ()
+  "Execute the next query from the query stack."
+  (interactive)
+  (mu4e--search-query-navigate 'future))
+
+(define-obsolete-function-alias 'mu4e-headers-query-next
+  'mu4e-search-next "1.7.0")
+
+(defun mu4e-search-prev ()
+  "Execute the previous query from the query stacks."
+  (interactive)
+  (mu4e--search-query-navigate 'past))
+
+(define-obsolete-function-alias 'mu4e-headers-query-prev
+  'mu4e-search-prev "1.7.0")
+
+;; forget the past so we don't repeat it :/
+(defun mu4e-search-forget ()
+  "Forget the search history."
+  (interactive)
+  (setq mu4e--search-query-past nil
+       mu4e--search-query-future nil)
+  (mu4e-message "Query history cleared"))
+
+(define-obsolete-function-alias 'mu4e-headers-forget-queries
+  'mu4e-search-forget "1.7.0")
+
+(defun mu4e-last-query ()
+  "Get the most recent query or nil if there is none."
+  mu4e--search-last-query)
+\f
+;;; Completion for queries
+
+(defvar mu4e--search-hist nil "History list of searches.")
+(defvar mu4e-minibuffer-search-query-map
+  (let ((map (copy-keymap minibuffer-local-map)))
+    (define-key map (kbd "TAB") #'completion-at-point)
+    map)
+  "The keymap for reading a search query.")
+
+(defun mu4e-search-read-query (prompt &optional initial-input)
+  "Read a query with completion using PROMPT and INITIAL-INPUT."
+  (minibuffer-with-setup-hook
+      (lambda ()
+       (setq-local completion-at-point-functions
+                   #'mu4e--search-query-completion-at-point)
+       (use-local-map mu4e-minibuffer-search-query-map))
+    (read-string prompt initial-input 'mu4e--search-hist)))
+
+(define-obsolete-function-alias 'mu4e-read-query
+  'mu4e-search-read-query "1.7.0")
+
+(defconst mu4e--search-query-keywords
+  '("and" "or" "not"
+    "from:" "to:" "cc:" "bcc:" "contact:" "recip:" "date:" "subject:" "body:"
+    "list:" "maildir:" "flag:" "mime:" "file:" "prio:" "tag:" "msgid:"
+    "size:" "embed:"))
+
+(defun mu4e--search-completion-contacts-action (match _status)
+  "Delete contact alias from contact autocompletion, leaving just email address.
+Implements the `completion-extra-properties' :exit-function' which
+requires a function with arguments string MATCH and completion
+status, STATUS."
+  (let ((contact-email (replace-regexp-in-string "^.*<\\|>$" "" match)))
+    (delete-char (- (length match)))
+    (insert contact-email)))
+
+(defun mu4e--search-query-completion-at-point ()
+  "Provide completion when entering search expressions."
+  (cond
+   ((not (looking-back "[:\"][^ \t]*" nil))
+    (let ((bounds (bounds-of-thing-at-point 'word)))
+      (list (or (car bounds) (point))
+           (or (cdr bounds) (point))
+           mu4e--search-query-keywords)))
+   ((looking-back "flag:\\(\\w*\\)" nil)
+    (list (match-beginning 1)
+         (match-end 1)
+         '("attach" "draft" "flagged" "list" "new" "passed" "replied"
+           "seen" "trashed" "unread" "encrypted" "signed" "personal")))
+   ((looking-back "maildir:\\([a-zA-Z0-9/.]*\\)" nil)
+    (list (match-beginning 1)
+          (match-end 1)
+          (mu4e-get-maildirs)))
+   ((looking-back "prio:\\(\\w*\\)" nil)
+    (list (match-beginning 1)
+         (match-end 1)
+         (list "high" "normal" "low")))
+   ((looking-back "mime:\\([a-zA-Z0-9/-]*\\)" nil)
+    (list (match-beginning 1)
+         (match-end 1)
+          (mailcap-mime-types)))
+   ((looking-back "\\(from\\|to\\|cc\\|bcc\\|contact\\|recip\\):\\([a-zA-Z0-9/.@]*\\)" nil)
+    (list (match-beginning 2)
+          (match-end 2)
+          mu4e--contacts-set
+          :exit-function
+          #'mu4e--search-completion-contacts-action))
+   ((looking-back "list:\\([a-zA-Z0-9/.@]*\\)" nil)
+    (list (match-beginning 1)
+          (match-end 1)
+          mu4e--lists-hash))))
+
+(define-minor-mode mu4e-search-minor-mode
+  "Mode for searching for messages."
+  :global nil
+  :init-value nil ;; disabled by default
+  :group 'mu4e
+  :lighter ""
+  :keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map "s" 'mu4e-search)
+    (define-key map "S" 'mu4e-search-edit)
+    (define-key map "/" 'mu4e-search-narrow)
+    ;;(define-key map "j" 'mu4e~headers-jump-to-maildir)
+    (define-key map (kbd "<M-left>")  'mu4e-search-prev)
+    (define-key map (kbd "\\")        'mu4e-search-prev)
+    (define-key map (kbd "<M-right>") 'mu4e-search-next)
+
+    (define-key map "b" 'mu4e-search-bookmark)
+    (define-key map "B" 'mu4e-search-bookmark-edit)
+    map))
+
+(provide 'mu4e-search)
+;;; mu4e-search.el ends here
diff --git a/mu4e/mu4e-server.el b/mu4e/mu4e-server.el
new file mode 100644 (file)
index 0000000..ace5612
--- /dev/null
@@ -0,0 +1,602 @@
+;;; mu4e-server.el -- part of mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'mu4e-helpers)
+
+\f
+;;; Configuration
+(defcustom mu4e-mu-home nil
+  "Location of an alternate mu home dir.
+If not set, use the defaults, based on the XDG Base Directory
+Specification.
+
+Changes to this value only take effect after (re)starting the mu
+session."
+  :group 'mu4e
+  :type '(choice (const :tag "Default location" nil)
+                 (directory :tag "Specify location"))
+  :safe 'stringp)
+
+(defcustom mu4e-mu-binary (executable-find "mu")
+  "Path to the mu-binary to use.
+
+Changes to this value only take effect after (re)starting the mu
+session."
+  :type 'file
+  :group 'mu4e
+  :safe 'stringp)
+
+(defcustom mu4e-mu-debug nil
+  "Whether to run the mu binary in debug-mode.
+Setting this to t increases the amount of information in the log.
+
+Changes to this value only take effect after (re)starting the mu
+session."
+  :type 'boolean
+  :group 'mu4e)
+
+(make-obsolete-variable
+ 'mu4e-maildir
+ "determined by server; see `mu4e-root-maildir'." "1.3.8")
+
+(defcustom mu4e-change-filenames-when-moving nil
+  "Change message file names when moving them.
+
+When moving messages to different folders, normally mu/mu4e keep
+the base filename the same (the flags-part of the filename may
+change still). With this option set to non-nil, mu4e instead
+changes the filename.
+
+This latter behavior works better with some
+IMAP-synchronization programs such as mbsync; the default works
+better with e.g. offlineimap."
+  :type 'boolean
+  :group 'mu4e
+  :safe 'booleanp)
+
+\f
+;; Handlers are not strictly internal, but are not meant
+;; for overriding outside mu4e. The are mainly for breaking
+;; dependency cycles.
+
+(defvar mu4e-error-func nil
+  "Function called for each error received.
+The function is passed an error plist as argument. See
+`mu4e--server-filter' for the format.")
+
+(defvar mu4e-update-func nil
+  "Function called for each :update sexp returned.
+The function is passed a msg sexp as argument.
+See `mu4e--server-filter' for the format.")
+
+(defvar mu4e-remove-func nil
+  "Function called for each :remove sexp returned.
+This happens when some message has been deleted. The function is
+passed the docid of the removed message.")
+
+(defvar mu4e-sent-func  nil
+  "Function called for each :sent sexp received.
+This happens when some message has been sent. The function is
+passed the docid and the draft-path of the sent message.")
+
+(defvar mu4e-view-func  nil
+  "Function called for each single-message sexp.
+The function is passed a message sexp as argument. See
+`mu4e--server-filter' for the format.")
+
+(make-obsolete-variable 'mu4e-header-func "mu4e-headers-append-func" "1.7.4")
+
+(defvar mu4e-headers-append-func nil
+  "Function called with a list of headers to append.
+The function is passed a list of message plists as argument. See
+See `mu4e--server-filter' for the details.")
+
+(defvar mu4e-found-func nil
+  "Function called for when we received a :found sexp.
+This happens after the headers have been returned, to report on
+the number of matches. See `mu4e--server-filter' for the format.")
+
+(defvar mu4e-erase-func nil
+  "Function called we receive an :erase sexp.
+This before new headers are displayed, to clear the current
+headers buffer. See `mu4e--server-filter' for the format.")
+
+(defvar mu4e-compose-func nil
+  "Function called for each compose message received.
+I.e., the original message that is used as basis for composing a
+new message (i.e., either a reply or a forward); the function is
+passed msg and a symbol (either reply or forward). See
+`mu4e--server-filter' for the format of <msg-plist>.")
+
+(defvar mu4e-info-func nil
+  "Function called for each (:info type ....) sexp received.
+from the server process.")
+
+(defvar mu4e-pong-func nil
+  "Function called for each (:pong type ....) sexp received.")
+
+(defvar mu4e-contacts-func nil
+  "A function called for each (:contacts (<list-of-contacts>)
+sexp received from the server process.")
+
+(make-obsolete-variable 'mu4e-temp-func "No longer used" "1.7.0")
+\f
+;;; Internal vars
+
+(defvar mu4e--server-buf nil
+  "Buffer (string) for data received from the backend.")
+(defconst mu4e--server-name " *mu4e-server*"
+  "Name of the server process, buffer.")
+(defvar mu4e--server-process nil
+  "The mu-server process.")
+
+;; dealing with the length cookie that precedes expressions
+(defconst mu4e--server-cookie-pre "\376"
+  "Each expression starts with a length cookie:
+<`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>.")
+(defconst mu4e--server-cookie-post "\377"
+  "Each expression starts with a length cookie:
+<`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>.")
+(defconst mu4e--server-cookie-matcher-rx
+  (concat mu4e--server-cookie-pre "\\([[:xdigit:]]+\\)"
+         mu4e--server-cookie-post)
+  "Regular expression matching the length cookie.
+Match 1 will be the length (in hex).")
+\f
+(defun mu4e-running-p ()
+  "Whether mu4e is running.
+Checks whether the server process is live."
+  (and mu4e--server-process
+       (memq (process-status mu4e--server-process)
+             '(run open listen connect stop))
+       t))
+
+(defsubst mu4e--server-eat-sexp-from-buf ()
+  "'Eat' the next s-expression from `mu4e--server-buf'.
+Note: this is a string, not an emacs-buffer. `mu4e--server-buf gets
+its contents from the mu-servers in the following form:
+   <`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>
+Function returns this sexp, or nil if there was none.
+`mu4e--server-buf' is updated as well, with all processed sexp data
+removed."
+  (ignore-errors ;; the server may die in the middle...
+    (let ((b (string-match mu4e--server-cookie-matcher-rx mu4e--server-buf))
+          (sexp-len) (objcons))
+      (when b
+        (setq sexp-len (string-to-number (match-string 1 mu4e--server-buf) 16))
+        ;; does mu4e--server-buf contain the full sexp?
+        (when (>= (length mu4e--server-buf) (+ sexp-len (match-end 0)))
+          ;; clear-up start
+          (setq mu4e--server-buf (substring mu4e--server-buf (match-end 0)))
+          ;; note: we read the input in binary mode -- here, we take the part
+          ;; that is the sexp, and convert that to utf-8, before we interpret
+          ;; it.
+          (setq objcons (read-from-string
+                         (decode-coding-string
+                          (substring mu4e--server-buf 0 sexp-len)
+                          'utf-8 t)))
+          (when objcons
+            (setq mu4e--server-buf (substring mu4e--server-buf sexp-len))
+            (car objcons)))))))
+
+(defun mu4e--server-filter (_proc str)
+  "Filter string STR from PROC.
+This processes the \"mu server\" output. It accumulates the
+strings into valid sexpsv and evaluating those.
+
+The server output is as follows:
+
+   1. an error
+      (:error 2 :message \"unknown command\")
+      ;; eox
+   => passed to `mu4e-error-func'.
+
+   2a. a header exp looks something like:
+  (:headers
+      ( ;; message 1
+        :docid 1585
+        :from ((\"Donald Duck\" . \"donald@example.com\"))
+        :to ((\"Mickey Mouse\" . \"mickey@example.com\"))
+        :subject \"Wicked stuff\"
+        :date (20023 26572 0)
+        :size 15165
+        :references (\"200208121222.g7CCMdb80690@msg.id\")
+        :in-reply-to \"200208121222.g7CCMdb80690@msg.id\"
+        :message-id \"foobar32423847ef23@pluto.net\"
+        :maildir: \"/archive\"
+        :path \"/home/mickey/Maildir/inbox/cur/1312_3.32282.pluto,4cd5bd4e9:2,\"
+        :priority high
+        :flags (new unread)
+        :meta <meta-data>
+       )
+       (  .... more messages  )
+)
+;; eox
+   => this will be passed to `mu4e-headers-append-func'.
+
+  2b. After the list of headers has been returned (see 2a.),
+  we'll receive a sexp that looks like
+  (:found <n>) with n the number of messages found. The <n> will be
+  passed to `mu4e-found-func'.
+
+  3. a view looks like:
+  (:view <msg-sexp>)
+  => the <msg-sexp> (see 2.) will be passed to `mu4e-view-func'.
+     the <msg-sexp> also contains :body-txt and/or :body-html
+
+  4. a database update looks like:
+  (:update <msg-sexp> :move <nil-or-t>)
+    like :header
+
+   => the <msg-sexp> (see 2.) will be passed to
+   `mu4e-update-func', :move tells us whether this is a move to
+   another maildir, or merely a flag change.
+
+  5. a remove looks like:
+  (:remove <docid>)
+  => the docid will be passed to `mu4e-remove-func'
+
+  6. a compose looks like:
+  (:compose <reply|forward|edit|new> [:original<msg-sexp>] [:include <attach>])
+  `mu4e-compose-func'. :original looks like :view."
+  (mu4e-log 'misc "* Received %d byte(s)" (length str))
+  (setq mu4e--server-buf (concat mu4e--server-buf str)) ;; update our buffer
+  (let ((sexp (mu4e--server-eat-sexp-from-buf)))
+    (with-local-quit
+      (while sexp
+        (mu4e-log 'from-server "%s" sexp)
+        (cond
+         ;; a header plist can be recognized by the existence of a :date field
+         ((plist-get sexp :headers)
+          (funcall mu4e-headers-append-func (plist-get sexp :headers)))
+
+         ;; the found sexp, we receive after getting all the headers
+         ((plist-get sexp :found)
+          (funcall mu4e-found-func (plist-get sexp :found)))
+
+         ;; viewing a specific message
+         ((plist-get sexp :view)
+          (funcall mu4e-view-func (plist-get sexp :view)))
+
+         ;; receive an erase message
+         ((plist-get sexp :erase)
+          (funcall mu4e-erase-func))
+
+         ;; receive a :sent message
+         ((plist-get sexp :sent)
+          (funcall mu4e-sent-func
+                   (plist-get sexp :docid)
+                   (plist-get sexp :path)))
+
+         ;; received a pong message
+         ((plist-get sexp :pong)
+         (setq mu4e--server-props (plist-get sexp :props))
+          (funcall mu4e-pong-func sexp))
+
+         ;; received a contacts message
+         ;; note: we use 'member', to match (:contacts nil)
+         ((plist-member sexp :contacts)
+          (funcall mu4e-contacts-func
+                   (plist-get sexp :contacts)
+                   (plist-get sexp :tstamp)))
+
+         ;; something got moved/flags changed
+         ((plist-get sexp :update)
+          (funcall mu4e-update-func
+                   (plist-get sexp :update)
+                   (plist-get sexp :move)
+                   (plist-get sexp :maybe-view)))
+
+         ;; a message got removed
+         ((plist-get sexp :remove)
+          (funcall mu4e-remove-func (plist-get sexp :remove)))
+
+         ;; start composing a new message
+         ((plist-get sexp :compose)
+          (funcall mu4e-compose-func
+                   (plist-get sexp :compose)
+                   (plist-get sexp :original)
+                   (plist-get sexp :include)))
+
+         ;; get some info
+         ((plist-get sexp :info)
+          (funcall mu4e-info-func sexp))
+
+         ;; receive an error
+         ((plist-get sexp :error)
+          (funcall mu4e-error-func
+                   (plist-get sexp :error)
+                   (plist-get sexp :message)))
+
+         (t (mu4e-message "Unexpected data from server [%S]" sexp)))
+
+        (setq sexp (mu4e--server-eat-sexp-from-buf))))))
+
+(defun mu4e--kill-stale ()
+  "Kill stale mu4e server process.
+As per issue #2198."
+  (seq-each
+   (lambda(proc)
+     (when (and (process-live-p proc)
+               (string-prefix-p mu4e--server-name (process-name proc)))
+       (mu4e-message "killing stale mu4e server")
+       (ignore-errors
+        (signal-process proc 'SIGINT) ;; nicely
+        (sit-for 1.0)
+        (signal-process proc 'SIGKILL)))) ;; forcefully
+   (process-list)))
+
+(defun mu4e--server-start ()
+  "Start the mu server process."
+  (let ((default-directory temporary-file-directory)) ;;ensure it's local.
+    ;; sanity-check 1
+  (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary))
+    (mu4e-error
+     "Cannot find mu, please set `mu4e-mu-binary' to the mu executable path"))
+  ;; sanity-check 2
+  (let ((version (let ((s (shell-command-to-string
+                          (concat mu4e-mu-binary " --version"))))
+                   (and (string-match "version \\([.0-9]+\\)" s)
+                        (match-string 1 s)))))
+    (unless (string= version mu4e-mu-version)
+      (mu4e-error
+       (concat
+        "Found mu version %s, but mu4e needs version %s"
+       "; please set `mu4e-mu-binary' "
+        "accordingly") version mu4e-mu-version)))
+  ;; kill old/stale servers, if any.
+  (mu4e--kill-stale)
+  (let* ((process-connection-type nil) ;; use a pipe
+         (args (when mu4e-mu-home `(,(format"--muhome=%s" mu4e-mu-home))))
+         (args (if mu4e-mu-debug (cons "--debug" args) args))
+         (args (cons "server" args)))
+    (setq mu4e--server-buf "")
+    (setq mu4e--server-process (apply 'start-process
+                                      mu4e--server-name mu4e--server-name
+                                      mu4e-mu-binary args))
+    ;; register a function for (:info ...) sexps
+    (unless mu4e--server-process
+      (mu4e-error "Failed to start the mu4e backend"))
+    (set-process-query-on-exit-flag mu4e--server-process nil)
+    (set-process-coding-system mu4e--server-process 'binary 'utf-8-unix)
+    (set-process-filter mu4e--server-process 'mu4e--server-filter)
+    (set-process-sentinel mu4e--server-process 'mu4e--server-sentinel))))
+
+(defun mu4e--server-kill ()
+  "Kill the mu server process."
+  (let* ((buf (get-buffer mu4e--server-name))
+         (proc (and (buffer-live-p buf) (get-buffer-process buf))))
+    (when proc
+      (mu4e-message "shutting down")
+      (set-process-filter mu4e--server-process nil)
+      (set-process-sentinel mu4e--server-process nil)
+      (let ((delete-exited-processes t))
+        (mu4e--server-call-mu '(quit)))
+      ;; try sending SIGINT (C-c) to process, so it can exit gracefully
+      (ignore-errors
+        (signal-process proc 'SIGINT))))
+  (setq
+   mu4e--server-process nil
+   mu4e--server-buf nil))
+
+;; error codes are defined in src/mu-util
+;;(defconst mu4e-xapian-empty 19 "Error code: xapian is empty/non-existent")
+
+(defun mu4e--server-sentinel (proc _msg)
+  "Function called when the server process PROC terminates with MSG."
+  (let ((status (process-status proc)) (code (process-exit-status proc)))
+    (setq mu4e--server-process nil)
+    (setq mu4e--server-buf "") ;; clear any half-received sexps
+    (cond
+     ((eq status 'signal)
+      (cond
+       ((or(eq code 9) (eq code 2)) (message nil))
+       ;;(message "the mu server process has been stopped"))
+       (t (error (format "mu server process received signal %d" code)))))
+     ((eq status 'exit)
+      (cond
+       ((eq code 0)
+        (message nil)) ;; don't do anything
+       ((eq code 19)
+        (error "Database is locked by another process"))
+       (t (error "Mu server process ended with exit code %d" code))))
+     (t
+      (error "Something bad happened to the mu server process")))))
+
+(defun mu4e--server-call-mu (form)
+  "Call the mu server with some command FORM."
+  (unless (mu4e-running-p) (mu4e--server-start))
+  (let* ((print-length nil) (print-level nil)
+         (cmd (format "%S" form)))
+    (mu4e-log 'to-server "%s" cmd)
+    (process-send-string mu4e--server-process (concat cmd "\n"))))
+
+(defun mu4e--server-add (path)
+  "Add the message at PATH to the database.
+On success, we receive `'(:info add :path <path> :docid <docid>)'
+as well as `'(:update <msg-sexp>)`'; otherwise, we receive an error."
+  (mu4e--server-call-mu `(add :path ,path)))
+
+(defun mu4e--server-compose (type decrypt &optional docid)
+  "Compose a message of TYPE, DECRYPT it and use DOCID.
+TYPE is a symbol, either `forward', `reply', `edit', `resend' or
+`new', based on an original message (ie, replying to, forwarding,
+editing, resending) with DOCID or nil for type `new'.
+
+The result is delivered to the function registered as
+`mu4e-compose-func'."
+  (mu4e--server-call-mu
+   `(compose
+     :type ,type
+     :decrypt ,(and decrypt t)
+     :docid   ,docid)))
+
+(defun mu4e--server-contacts (personal after maxnum tstamp)
+  "Ask for contacts with PERSONAL AFTER MAXNUM TSTAMP.
+
+S-expression (:contacts (<list>) :tstamp \"<tstamp>\")
+is expected in response.
+
+If PERSONAL is non-nil, only get personal contacts, if AFTER is
+non-nil, get only contacts seen AFTER (the time_t value). If MAX is non-nil,
+get at most MAX contacts."
+  (mu4e--server-call-mu
+   `(contacts
+     :personal ,(and personal t)
+     :after    ,(or after nil)
+     :tstamp   ,(or tstamp nil)
+     :maxnum   ,(or maxnum nil))))
+
+(defun mu4e--server-find (query threads sortfield sortdir maxnum skip-dups
+                               include-related)
+  "Run QUERY with THREADS SORTFIELD SORTDIR MAXNUM SKIP-DUPS INCLUDE-RELATED.
+
+If THREADS is non-nil, show results in threaded fashion,
+SORTFIELD is a symbol describing the field to sort by (or nil);
+see `mu4e~headers-sortfield-choices'. If SORT is `descending',
+sort Z->A, if it's `ascending', sort A->Z. MAXNUM determines the
+maximum number of results to return, or nil for unlimited. If
+SKIP-DUPS is non-nil, show only one of duplicate messages (see
+`mu4e-headers-skip-duplicates'). If INCLUDE-RELATED is non-nil,
+include messages related to the messages matching the search
+query (see `mu4e-headers-include-related').
+
+For each result found, a function is called, depending on the
+kind of result. The variables `mu4e-error-func' contain the
+function that to be be called for, resp., a message (header)
+or an error."
+  (mu4e--server-call-mu
+   `(find
+     :query ,query
+     :threads ,(and threads t)
+     :sortfield ,sortfield
+     :descending ,(if (eq sortdir 'descending) t nil)
+     :maxnum ,maxnum
+     :skip-dups ,(and skip-dups t)
+     :include-related ,(and include-related t))))
+
+(defun mu4e--server-index (&optional cleanup lazy-check)
+  "Index messages.
+If CLEANUP is non-nil, remove messages which are in the database
+but no longer in the filesystem. If LAZY-CHECK is non-nil, only
+consider messages for which the time stamp (ctime) of the
+directory they reside in has not changed since the previous
+indexing run. This is much faster than the non-lazy check, but
+won't update messages that have change (rather than having been
+added or removed), since merely editing a message does not update
+the directory time stamp."
+  (mu4e--server-call-mu
+   `(index :cleanup ,(and cleanup t)
+          :lazy-check ,(and lazy-check t))))
+
+(defun mu4e--server-mkdir (path)
+  "Create a new maildir-directory at filesystem PATH."
+  (mu4e--server-call-mu `(mkdir :path ,path)))
+
+(defun mu4e--server-move (docid-or-msgid &optional maildir flags no-view)
+  "Move message identified by DOCID-OR-MSGID.
+Optionally to MAILDIR and optionally setting FLAGS. If MAILDIR is
+nil, message will be moved within the same maildir.
+
+At least one of MAILDIR and FLAGS must be specified. Note that
+even when MAILDIR is nil, this is still a filesystem move, since
+a change in flags implies a change in message filename.
+
+MAILDIR must be a maildir, that is, the part _without_ cur/ or new/
+or the root-maildir-prefix. E.g. \"/archive\". This directory must
+already exist.
+
+The FLAGS parameter can have the following forms:
+  1. a list of flags such as `(passed replied seen)'
+  2. a string containing the one-char versions of the flags, e.g. \"PRS\"
+  3. a delta-string specifying the changes with +/- and the one-char flags,
+     e.g. \"+S-N\" to set Seen and remove New.
+
+The flags are any of `deleted', `flagged', `new', `passed', `replied' `seen' or
+`trashed', or the corresponding \"DFNPRST\" as defined in [1]. See
+`mu4e-string-to-flags' and `mu4e-flags-to-string'.
+The server reports the results for the operation through
+`mu4e-update-func'.
+
+If the variable `mu4e-change-filenames-when-moving' is
+non-nil, moving to a different maildir generates new names forq
+the target files; this helps certain tools (such as mbsync).
+
+If NO-VIEW is non-nil, do not update the view.
+
+Returns either (:update ... ) or (:error ) sexp, which are handled my
+`mu4e-update-func' and `mu4e-error-func', respectively."
+  (unless (or maildir flags)
+    (mu4e-error "At least one of maildir and flags must be specified"))
+  (unless (or (not maildir)
+              (file-exists-p (concat (mu4e-root-maildir) "/" maildir "/")))
+    (mu4e-error "Target dir does not exist"))
+  (mu4e--server-call-mu
+   `(move
+     :docid ,(if (stringp docid-or-msgid) nil docid-or-msgid)
+     :msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil)
+     :flags ,(or flags nil)
+     :maildir ,(or maildir nil)
+     :rename  ,(and maildir mu4e-change-filenames-when-moving t)
+     :no-view ,(and no-view t))))
+
+(defun mu4e--server-ping (&optional queries)
+  "Sends a ping to the mu server, expecting a (:pong ...) in response.
+QUERIES is a list of queries for the number of results with
+read/unread status are returned in the pong-response."
+  (mu4e--server-call-mu `(ping :queries ,queries)))
+
+(defun mu4e--server-remove (docid)
+  "Remove message  with DOCID.
+The results are reporter through either (:update ... )
+or (:error) sexp, which are handled my `mu4e-error-func',
+respectively."
+  (mu4e--server-call-mu `(remove :docid ,docid)))
+
+(defun mu4e--server-sent (path)
+  "Tell the mu server we sent a message at PATH.
+If this works, we will receive (:info add :path <path> :docid
+<docid> :fcc <path>)."
+  (mu4e--server-call-mu `(sent :path ,path)))
+
+(defun mu4e--server-view (docid-or-msgid &optional mark-as-read)
+  "View a message referred to by DOCID-OR-MSGID.
+Optionally, if MARK-AS-READ is non-nil, the backend marks the
+message as \"read\" before returning, if not already. The result
+will be delivered to the function registered as `mu4e-view-func'."
+  (mu4e--server-call-mu
+   `(view
+     :docid ,(if (stringp docid-or-msgid) nil docid-or-msgid)
+     :msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil)
+     :mark-as-read ,(and mark-as-read t)
+     ;; when moving (due to mark-as-read), change filenames
+     ;; if so configured. Note: currently this *ignored*
+     ;; because mbsync seems to get confused.
+     :rename  ,(and mu4e-change-filenames-when-moving t))))
+
+\f
+(provide 'mu4e-server)
+;;; mu4e-server.el ends here
diff --git a/mu4e/mu4e-speedbar.el b/mu4e/mu4e-speedbar.el
new file mode 100644 (file)
index 0000000..c2c414e
--- /dev/null
@@ -0,0 +1,133 @@
+;;; mu4e-speedbar --- Speedbar support for mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2021 Antono Vasiljev, Dirk-Jan C. Binnema
+
+;; Author: Antono Vasiljev <self@antono.info>
+;; Version: 0.1
+;; Keywords: file, tags, tools
+
+;; This file is not part of GNU Emacs.
+
+;; 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, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Speedbar provides a frame in which files, and locations in files
+;; are displayed.  These functions provide mu4e specific support,
+;; showing maildir list in the side-bar.
+;;
+;;   This file requires speedbar.
+
+;;; Code:
+
+(require 'speedbar)
+(require 'mu4e-vars)
+(require 'mu4e-headers)
+(require 'mu4e-context)
+(require 'mu4e-bookmarks)
+
+(defvar mu4e-main-speedbar-key-map nil
+  "Keymap used when in mu4e display mode.")
+(defvar mu4e-headers-speedbar-key-map nil
+  "Keymap used when in mu4e display mode.")
+(defvar mu4e-view-speedbar-key-map nil
+  "Keymap used when in mu4e display mode.")
+
+(defvar mu4e-main-speedbar-menu-items nil
+  "Additional menu-items to add to speedbar frame.")
+(defvar mu4e-headers-speedbar-menu-items nil
+  "Additional menu-items to add to speedbar frame.")
+(defvar mu4e-view-speedbar-menu-items nil
+  "Additional menu-items to add to speedbar frame.")
+
+
+(defun mu4e-speedbar-install-variables ()
+  "Install those variables used by speedbar to enhance mu4e."
+  (add-hook 'mu4e-context-changed-hook
+            #'mu4e~speedbar-context-changed-hook-fn)
+  (dolist (keymap
+           '( mu4e-main-speedbar-key-map
+              mu4e-headers-speedbar-key-map
+              mu4e-view-speedbar-key-map))
+    (unless keymap
+      (setq keymap (speedbar-make-specialized-keymap))
+      (define-key keymap "RET" 'speedbar-edit-line)
+      (define-key keymap "e" 'speedbar-edit-line))))
+
+(defun mu4e~speedbar-context-changed-hook-fn ()
+  (when (buffer-live-p speedbar-buffer)
+    (with-current-buffer speedbar-buffer
+      (let ((inhibit-read-only t))
+        (mu4e-speedbar-buttons)))))
+
+(with-eval-after-load 'speedbar
+  (mu4e-speedbar-install-variables))
+
+(defun mu4e~speedbar-render-maildir-list ()
+  "Insert the list of maildirs in the speedbar."
+  (interactive)
+  (when (buffer-live-p speedbar-buffer)
+    (with-current-buffer speedbar-buffer
+      (mapcar (lambda (maildir-name)
+                (speedbar-insert-button
+                 (concat "  " maildir-name)
+                 'mu4e-highlight-face
+                 'highlight
+                 'mu4e~speedbar-maildir
+                 maildir-name))
+              (mu4e-get-maildirs)))))
+
+(defun mu4e~speedbar-maildir (&optional _text token _ident)
+  "Jump to maildir TOKEN. TEXT and INDENT are not used."
+  (dframe-with-attached-buffer
+   (mu4e-search (concat "\"maildir:" token "\"") current-prefix-arg)))
+
+(defun mu4e~speedbar-render-bookmark-list ()
+  "Insert the list of bookmarks in the speedbar"
+  (interactive)
+  (mapcar (lambda (bookmark)
+            (unless (plist-get bookmark :hide)
+              (speedbar-insert-button
+               (concat "  " (plist-get bookmark :name))
+               'mu4e-highlight-face
+               'highlight
+               'mu4e~speedbar-bookmark
+               (plist-get bookmark :query))))
+          (mu4e-bookmarks)))
+
+(defun mu4e~speedbar-bookmark (&optional _text token _ident)
+  "Run bookmarked query TOKEN. TEXT and INDENT are not used."
+  (dframe-with-attached-buffer
+   (mu4e-search token current-prefix-arg)))
+
+;;;###autoload
+(defun mu4e-speedbar-buttons (&optional _buffer)
+  "Create buttons for any mu4e BUFFER."
+  (interactive)
+  (erase-buffer)
+  (insert (propertize "* mu4e\n\n" 'face 'mu4e-title-face))
+
+  (insert (propertize " Bookmarks\n" 'face 'mu4e-title-face))
+  (mu4e~speedbar-render-bookmark-list)
+  (insert "\n")
+  (insert (propertize " Maildirs\n" 'face 'mu4e-title-face))
+  (mu4e~speedbar-render-maildir-list))
+
+(defun mu4e-main-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer))
+(defun mu4e-headers-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer))
+(defun mu4e-view-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer))
+
+;;; _
+(provide 'mu4e-speedbar)
+;;; mu4e-speedbar.el ends here
diff --git a/mu4e/mu4e-update.el b/mu4e/mu4e-update.el
new file mode 100644 (file)
index 0000000..9246fe9
--- /dev/null
@@ -0,0 +1,324 @@
+;;; mu4e-update.el -- part of mu4e, -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2021 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Updating the mu4e message store: calling a mail retrieval program and
+;; re-running the index.
+
+;;; Code:
+
+(require 'mu4e-helpers)
+(require 'mu4e-server)
+\f
+;;; Customization
+
+(defcustom mu4e-get-mail-command "true"
+  "Shell command for retrieving new mail.
+Common values are \"offlineimap\", \"fetchmail\" or \"mbsync\", but
+arbitrary shell-commands can be used.
+
+When set to the literal string \"true\" (the default), the
+command simply finishes successfully (running the \"true\"
+command) without retrieving any mail. This can be useful when
+mail is already retrieved in another way, such as a local MDA."
+  :type 'string
+  :group 'mu4e
+  :safe 'stringp)
+
+(defcustom mu4e-index-update-error-warning t
+  "Whether to display warnings during the retrieval process.
+This depends on the `mu4e-get-mail-command' exit code."
+  :type 'boolean
+  :group 'mu4e
+  :safe 'booleanp)
+
+(defcustom mu4e-index-update-error-continue t
+  "Whether to continue with indexing after an error during retrieval."
+  :type 'boolean
+  :group 'mu4e
+  :safe 'booleanp)
+
+(defcustom mu4e-index-update-in-background t
+  "Whether to retrieve mail in the background."
+  :type 'boolean
+  :group 'mu4e
+  :safe 'booleanp)
+
+(defcustom mu4e-index-cleanup t
+  "Whether to run a cleanup phase after indexing.
+
+That is, validate that each message in the message store has a
+corresponding message file in the filesystem.
+
+Having this option as t ensures that no non-existing messages are
+shown but can slow with large message stores on slow file-systems."
+  :type 'boolean
+  :group 'mu4e
+  :safe 'booleanp)
+
+(defcustom mu4e-index-lazy-check nil
+  "Whether to only use a \"lazy\" check during reindexing.
+This influences how we decide whether a message
+needs (re)indexing or not.
+
+When this is set to non-nil, mu only uses the directory
+timestamps to decide whether it needs to check the messages
+beneath it. This makes indexing much faster, but might miss some
+changes. For this, you might want to occasionally call
+`mu4e-update-index-nonlazy'; `mu4e-update-pre-hook' can be used
+to automate this."
+  :type 'boolean
+  :group 'mu4e
+  :safe 'booleanp)
+
+(defcustom mu4e-update-interval nil
+  "Number of seconds between mail retrieval/indexing.
+If nil, don't update automatically. Note, changes in
+`mu4e-update-interval' only take effect after restarting mu4e."
+  :type '(choice (const :tag "No automatic update" nil)
+                 (integer :tag "Seconds"))
+  :group 'mu4e
+  :safe 'integerp)
+
+(defvar mu4e-update-pre-hook nil
+  "Hook run just *before* the mail-retrieval / database updating process starts.
+You can use this hook for example to `mu4e-get-mail-command' with
+some specific setting.")
+
+(defcustom mu4e-hide-index-messages nil
+  "Whether to hide the \"Indexing...\" and contacts messages."
+  :type 'boolean
+  :group 'mu4e)
+
+(defvar mu4e-index-updated-hook nil
+  "Hook run when the indexing process has completed.
+The variable `mu4e-index-update-status' can be used to get
+information about what changed.")
+
+(defvar mu4e-message-changed-hook nil
+  "Hook run when there is a message changed in the data store.
+For new messages, it depends on `mu4e-index-updated-hook'. This
+can be used as a simple way to invoke some action when a message
+changed")
+
+(defvar mu4e-index-update-status nil
+  "Last-seen completed update status, based on server status messages.
+
+If non-nil, this is a plist of the form:
+\(
+:checked     <number of messages processed> (checked whether up-to-date)
+:updated     <number of messages updated/added
+:cleaned-up  <number of stale messages removed from store
+:stamp       <emacs (current-time) timestamp for the status)")
+
+
+\f
+;;; Internal variables
+(defvar mu4e--progress-reporter nil
+  "Internal, the progress reporter object.")
+(defvar mu4e--update-timer nil
+  "The mu4e update timer.")
+(defconst mu4e--update-name " *mu4e-update*"
+  "Name of the process and buffer to update mail.")
+(defconst mu4e--update-buffer-height 8
+  "Height of the mu4e message retrieval/update buffer.")
+(defvar mu4e--get-mail-ask-password "mu4e get-mail: Enter password: "
+  "Query string for `mu4e-get-mail-command' password.")
+(defvar mu4e--get-mail-password-regexp "^Remote: Enter password: $"
+  "Regexp for a `mu4e-get-mail-command' password query.")
+\f
+
+(defun mu4e--get-mail-process-filter (proc msg)
+  "Filter the MSG output of the `mu4e-get-mail-command' PROC.
+
+Currently the filter only checks if the command asks for a
+password by matching the output against
+`mu4e~get-mail-password-regexp'. The messages are inserted into
+the process buffer.
+
+Also scrolls to the final line, and update the progress
+throbber."
+  (when mu4e--progress-reporter
+    (progress-reporter-update mu4e--progress-reporter))
+
+  (when (string-match mu4e--get-mail-password-regexp msg)
+    (if (process-get proc 'x-interactive)
+        (process-send-string proc
+                             (concat (read-passwd mu4e--get-mail-ask-password)
+                                     "\n"))
+      ;; TODO kill process?
+      (mu4e-error "Unrecognized password request")))
+  (when (process-buffer proc)
+    (let ((inhibit-read-only t)
+          (procwin (get-buffer-window (process-buffer proc))))
+      ;; Insert at end of buffer. Leave point alone.
+      (with-current-buffer (process-buffer proc)
+        (goto-char (point-max))
+        (if (string-match ".*\r\\(.*\\)" msg)
+            (progn
+              ;; kill even with \r
+              (end-of-line)
+              (let ((end (point)))
+                (beginning-of-line)
+                (delete-region (point) end))
+              (insert (match-string 1 msg)))
+          (insert msg)))
+      ;; Auto-scroll unless user is interacting with the window.
+      (when (and (window-live-p procwin)
+                 (not (eq (selected-window) procwin)))
+        (with-selected-window procwin
+          (goto-char (point-max)))))))
+
+(defun mu4e-index-message (frm &rest args)
+  "Display FRM with ARGS like `mu4e-message' for index messages.
+However, if `mu4e-hide-index-messages' is non-nil, do not display anything."
+  (unless mu4e-hide-index-messages
+    (apply 'mu4e-message frm args)))
+
+(defun mu4e-update-index ()
+  "Update the mu4e index."
+  (interactive)
+  (mu4e--server-index mu4e-index-cleanup mu4e-index-lazy-check))
+
+(defun mu4e-update-index-nonlazy ()
+  "Update the mu4e index non-lazily.
+This is just a convenience wrapper for indexing the non-lazy way
+if you otherwise want to use `mu4e-index-lazy-check'."
+  (interactive)
+  (let ((mu4e-index-cleanup t) (mu4e-index-lazy-check nil))
+    (mu4e-update-index)))
+
+(defvar mu4e--update-buffer nil
+  "The buffer of the update process when updating.")
+
+(define-derived-mode mu4e--update-mail-mode
+special-mode "mu4e:update"
+  "Major mode used for retrieving new e-mail messages in `mu4e'.")
+
+(define-key mu4e--update-mail-mode-map (kbd "q") 'mu4e-kill-update-mail)
+
+(defun mu4e--temp-window (buf height)
+  "Create a temporary window with HEIGHT at the bottom BUF."
+  (let ((win
+         (split-window
+          (frame-root-window)
+          (- (window-height (frame-root-window)) height))))
+    (set-window-buffer win buf)
+    (set-window-dedicated-p win t)
+    win))
+
+(defun mu4e--update-sentinel-func (proc _msg)
+  "Sentinel function for the update process PROC."
+  (when mu4e--progress-reporter
+    (progress-reporter-done mu4e--progress-reporter)
+    (setq mu4e--progress-reporter nil))
+  (unless mu4e-hide-index-messages
+    (message nil))
+  (if (or (not (eq (process-status proc) 'exit))
+          (/= (process-exit-status proc) 0))
+      (progn
+        (when mu4e-index-update-error-warning
+          (mu4e-message "Update process returned with non-zero exit code")
+          (sit-for 5))
+        (when mu4e-index-update-error-continue
+          (mu4e-update-index)))
+    (mu4e-update-index))
+  (when (buffer-live-p mu4e--update-buffer)
+    (unless (eq mu4e-split-view 'single-window)
+      (mapc #'delete-window (get-buffer-window-list mu4e--update-buffer)))
+    (kill-buffer mu4e--update-buffer)))
+
+;; complicated function, as it:
+;;   - needs to check for errors
+;;   - (optionally) pop-up a window
+;;   - (optionally) check password requests
+(defun mu4e--update-mail-and-index-real (run-in-background)
+  "Get a new mail by running `mu4e-get-mail-command'.
+If
+RUN-IN-BACKGROUND is non-nil (or called with prefix-argument),
+run in the background; otherwise, pop up a window."
+  (let* ((process-connection-type t)
+         (proc (start-process-shell-command
+                "mu4e-update" mu4e--update-name
+                mu4e-get-mail-command))
+         (buf (process-buffer proc))
+         (win (or run-in-background
+                  (mu4e--temp-window buf mu4e--update-buffer-height))))
+    (setq mu4e--update-buffer buf)
+    (when (window-live-p win)
+      (with-selected-window win
+        ;; ;;(switch-to-buffer buf)
+        ;; (set-window-dedicated-p win t)
+        (erase-buffer)
+        (insert "\n") ;; FIXME -- needed so output starts
+        (mu4e--update-mail-mode)))
+    (setq mu4e--progress-reporter
+          (unless mu4e-hide-index-messages
+            (make-progress-reporter
+             (mu4e-format "Retrieving mail..."))))
+    (set-process-sentinel proc 'mu4e--update-sentinel-func)
+    ;; if we're running in the foreground, handle password requests
+    (unless run-in-background
+      (process-put proc 'x-interactive (not run-in-background))
+      (set-process-filter proc 'mu4e--get-mail-process-filter))))
+
+(defun mu4e-update-mail-and-index (run-in-background)
+  "Retrieve new mail by running `mu4e-get-mail-command'.
+If RUN-IN-BACKGROUND is non-nil (or called with prefix-argument),
+run in the background; otherwise, pop up a window."
+  (interactive "P")
+  (unless mu4e-get-mail-command
+    (mu4e-error "`mu4e-get-mail-command' is not defined"))
+  (if (and (buffer-live-p mu4e--update-buffer)
+           (process-live-p (get-buffer-process mu4e--update-buffer)))
+      (mu4e-message "Update process is already running")
+    (progn
+      (run-hooks 'mu4e-update-pre-hook)
+      (mu4e--update-mail-and-index-real run-in-background))))
+
+(defun mu4e-kill-update-mail ()
+  "Stop the update process by killing it."
+  (interactive)
+  (let* ((proc (and (buffer-live-p mu4e--update-buffer)
+                    (get-buffer-process mu4e--update-buffer))))
+    (when (process-live-p proc)
+      (kill-process proc t))))
+
+(define-obsolete-function-alias 'mu4e-interrupt-update-mail
+  'mu4e-kill-update-mail "1.0-alpha0")
+
+(define-minor-mode mu4e-update-minor-mode
+  "Mode for triggering mu4e updates."
+  :global nil
+  :init-value nil ;; disabled by default
+  :group 'mu4e
+  :lighter ""
+  :keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map  (kbd "C-S-u")   #'mu4e-update-mail-and-index)
+    ;; for terminal users
+    (define-key map  (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+    map))
+
+(provide 'mu4e-update)
+;;; mu4e-update.el ends here
diff --git a/mu4e/mu4e-vars.el b/mu4e/mu4e-vars.el
new file mode 100644 (file)
index 0000000..dd0ffde
--- /dev/null
@@ -0,0 +1,364 @@
+;;; mu4e-vars.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'message)
+(require 'mu4e-helpers)
+\f
+;;; Configuration
+(defgroup mu4e nil
+  "Mu4e - an email-client for Emacs."
+  :group 'mail)
+
+(defcustom mu4e-headers-include-related t
+  "Wether to include \"related\" messages in queries.
+With this option set to non-nil, not just return the matches for
+a searches, but also messages that are related (through their
+references) to these messages. This can be useful e.g. to include
+sent messages into message threads."
+  :type 'boolean
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-skip-duplicates t
+  "Whether to skip duplicate messages.
+With this option set to non-nil, show only one of duplicate
+messages. This is useful when you have multiple copies of the same
+message, which is a common occurrence for example when using Gmail
+and offlineimap."
+  :type 'boolean
+  :group 'mu4e-headers)
+
+(defcustom mu4e-date-format-long "%c"
+  "Date format to use in the message view.
+Follows the format of `format-time-string'."
+  :type 'string
+  :group 'mu4e)
+
+\f
+;;; Faces
+
+(defgroup mu4e-faces nil
+  "Type faces (fonts) used in mu4e."
+  :group 'mu4e
+  :group 'faces)
+
+(defface mu4e-unread-face
+  '((t :inherit font-lock-keyword-face :weight bold))
+  "Face for an unread message header."
+  :group 'mu4e-faces)
+
+(defface mu4e-trashed-face
+  '((t :inherit font-lock-comment-face :strike-through t))
+  "Face for an message header in the trash folder."
+  :group 'mu4e-faces)
+
+(defface mu4e-draft-face
+  '((t :inherit font-lock-string-face))
+  "Face for a draft message header.
+I.e. a message with the draft flag set."
+  :group 'mu4e-faces)
+
+(defface mu4e-flagged-face
+  '((t :inherit font-lock-constant-face :weight bold))
+  "Face for a flagged message header."
+  :group 'mu4e-faces)
+
+(defface mu4e-replied-face
+  '((t :inherit font-lock-builtin-face :weight normal :slant normal))
+  "Face for a replied message header."
+  :group 'mu4e-faces)
+
+(defface mu4e-forwarded-face
+  '((t :inherit font-lock-builtin-face :weight normal :slant normal))
+  "Face for a passed (forwarded) message header."
+  :group 'mu4e-faces)
+
+(defface mu4e-header-face
+  '((t :inherit default))
+  "Face for a header without any special flags."
+  :group 'mu4e-faces)
+
+(defface mu4e-related-face
+  '((t :inherit default :slant italic))
+  "Face for a \='related' header." :group 'mu4e-faces)
+
+(defface mu4e-header-title-face
+  '((t :inherit font-lock-type-face))
+  "Face for a header title in the headers view."
+  :group 'mu4e-faces)
+
+(defface mu4e-header-highlight-face
+  `((t :inherit hl-line :weight bold :underline t
+       ,@(and (>= emacs-major-version 27) '(:extend t))))
+  "Face for the header at point."
+  :group 'mu4e-faces)
+
+(defface mu4e-header-marks-face
+  '((t :inherit font-lock-preprocessor-face))
+  "Face for the mark in the headers list."
+  :group 'mu4e-faces)
+
+(defface mu4e-header-key-face
+  '((t :inherit message-header-name :weight bold))
+  "Face for a header key (such as \"Foo\" in \"Subject:\ Foo\")."
+  :group 'mu4e-faces)
+
+(defface mu4e-header-value-face
+  '((t :inherit font-lock-type-face))
+  "Face for a header value (such as \"Re: Hello!\")."
+  :group 'mu4e-faces)
+
+(defface mu4e-special-header-value-face
+  '((t :inherit font-lock-builtin-face))
+  "Face for special header values."
+  :group 'mu4e-faces)
+
+(defface mu4e-link-face
+  '((t :inherit link))
+  "Face for showing URLs and attachments in the message view."
+  :group 'mu4e-faces)
+
+(defface mu4e-contact-face
+  '((t :inherit font-lock-variable-name-face))
+  "Face for showing URLs and attachments in the message view."
+  :group 'mu4e-faces)
+
+(defface mu4e-highlight-face
+  '((t :inherit highlight))
+  "Face for highlighting things."
+  :group 'mu4e-faces)
+
+(defface mu4e-title-face
+  '((t :inherit font-lock-type-face :weight bold))
+  "Face for a header title in the headers view."
+  :group 'mu4e-faces)
+
+(defface mu4e-modeline-face
+  '((t :inherit font-lock-string-face :weight bold))
+  "Face for the query in the mode-line."
+  :group 'mu4e-faces)
+
+(defface mu4e-footer-face
+  '((t :inherit font-lock-comment-face))
+  "Face for message footers (signatures)."
+  :group 'mu4e-faces)
+
+(defface mu4e-url-number-face
+  '((t :inherit font-lock-constant-face :weight bold))
+  "Face for the number tags for URLs."
+  :group 'mu4e-faces)
+
+(defface mu4e-system-face
+  '((t :inherit font-lock-comment-face :slant italic))
+  "Face for system message (such as the footers for message headers)."
+  :group 'mu4e-faces)
+
+(defface mu4e-ok-face
+  '((t :inherit font-lock-comment-face :weight bold :slant normal))
+  "Face for things that are okay."
+  :group 'mu4e-faces)
+
+(defface mu4e-warning-face
+  '((t :inherit font-lock-warning-face :weight bold :slant normal))
+  "Face for warnings / error."
+  :group 'mu4e-faces)
+
+(defface mu4e-compose-separator-face
+  '((t :inherit message-separator :slant italic))
+  "Face for the headers/message separator in mu4e-compose-mode."
+  :group 'mu4e-faces)
+
+(defface mu4e-region-code
+    '((t (:background "DarkSlateGray")))
+  "Face for highlighting marked region in mu4e-view buffer."
+  :group 'mu4e-faces)
+
+;;; Header information
+
+(defconst mu4e-header-info
+  '((:bcc
+     . (:name "Bcc"
+        :shortname "Bcc"
+        :help "Blind Carbon-Copy recipients for the message"
+        :sortable t))
+    (:cc
+     . (:name "Cc"
+        :shortname "Cc"
+        :help "Carbon-Copy recipients for the message"
+        :sortable t))
+    (:changed
+     . (:name "Changed"
+        :shortname "Chg"
+        :help "Date/time when the message was changed most recently"
+        :sortable t))
+    (:date
+     . (:name "Date"
+        :shortname "Date"
+        :help "Date/time when the message was sent"
+        :sortable t))
+    (:human-date
+     . (:name "Date"
+        :shortname "Date"
+        :help "Date/time when the message was sent"
+        :sortable :date))
+    (:flags
+     . (:name "Flags"
+        :shortname "Flgs"
+        :help "Flags for the message"
+        :sortable nil))
+    (:from
+     . (:name "From"
+        :shortname "From"
+        :help "The sender of the message"
+        :sortable t))
+    (:from-or-to
+     . (:name "From/To"
+        :shortname "From/To"
+        :help "Sender of the message if it's not me; otherwise the recipient"
+        :sortable nil))
+    (:maildir
+     . (:name "Maildir"
+        :shortname "Maildir"
+        :help "Maildir for this message"
+        :sortable t))
+    (:list
+     . (:name "List-Id"
+        :shortname "List"
+        :help "Mailing list id for this message"
+        :sortable t))
+    (:mailing-list
+     . (:name "List"
+        :shortname "List"
+        :help "Mailing list friendly name for this message"
+        :sortable :list))
+    (:message-id
+     . (:name "Message-Id"
+        :shortname "MsgID"
+        :help "Message-Id for this message"
+        :sortable nil))
+    (:path
+     . (:name "Path"
+        :shortname "Path"
+        :help "Full filesystem path to the message"
+        :sortable t))
+    (:size
+     . (:name "Size"
+        :shortname "Size"
+        :help "Size of the message"
+        :sortable t))
+    (:subject
+     . (:name "Subject"
+        :shortname "Subject"
+        :help "Subject of the message"
+        :sortable t))
+    (:tags
+     . (:name "Tags"
+        :shortname "Tags"
+        :help "Tags for the message"
+        :sortable nil))
+    (:thread-subject
+     . (:name "Subject"
+        :shortname "Subject"
+        :help "Subject of the thread"
+        :sortable :subject))
+    (:to
+     . (:name "To"
+        :shortname "To"
+        :help "Recipient of the message"
+        :sortable t)))
+  "An alist of all possible header fields and information about them.
+This is used in the user-interface (the column headers in the header list, and
+the fields the message view).
+
+Most fields should be self-explanatory. A special one is
+`:from-or-to', which is equal to `:from' unless `:from' matches
+one of the addresses in `(mu4e-personal-addresses)', in which
+case it will be equal to `:to'.
+
+Furthermore, the property `:sortable' determines whether we can
+sort by this field. This can be either a boolean (nil or t), or a
+symbol for /another/ field. For example, the `:human-date' field
+uses `:date' for that.
+
+Note, `:sortable' is not supported for custom header fields.")
+
+(defvar mu4e-header-info-custom
+  '(
+    ;; some examples & debug helpers.
+
+    (:thread-path
+     . ;; Shows the internal thread-path
+     ( :name "Thread-path"
+       :shortname "Thp"
+       :help "The thread-path"
+       :function (lambda (msg)
+                   (let ((thread (mu4e-message-field msg :thread)))
+                     (or (and thread (plist-get thread :path)) "")))))
+
+    (:thread-date
+     . ;; Shows the internal thread-date
+     ( :name "Thread-date"
+       :shortname "Thd"
+       :help "The thread-date"
+       :function (lambda (msg)
+                   (let* ((thread (mu4e-message-field msg :thread))
+                          (tdate (and thread (plist-get thread :date-tstamp))))
+                     (format-time-string "%F %T " (or tdate 0))))))
+    (:recipnum
+     .
+     ( :name "Number of recipients"
+       :shortname "Recip#"
+       :help "Number of recipients for this message"
+       :function
+       (lambda (msg)
+         (format "%d"
+                 (+ (length (mu4e-message-field msg :to))
+                    (length (mu4e-message-field msg :cc))))))))
+
+  "A list of custom (user-defined) headers.
+The format is similar to `mu4e-header-info', but adds a :function
+property, which should point to a function that takes a message
+plist as argument, and returns a string. See the default value of
+`mu4e-header-info-custom for an example.
+
+Note that when using the gnus-based view, you only have access to
+a limited set of message fields: only the ones used in the
+header-view, not including, for instance, the message body.")
+
+;;; Internals
+
+(defvar mu4e~headers-view-win nil
+  "The view window connected to this headers view.")
+
+;; It's useful to have the current view message available to
+;; `mu4e-view-mode-hooks' functions, and we set up this variable
+;; before calling `mu4e-view-mode'.  However, changing the major mode
+;; clobbers any local variables.  Work around that by declaring the
+;; variable permanent-local.
+(defvar mu4e~view-message nil "The message being viewed in view mode.")
+(put 'mu4e~view-message 'permanent-local t)
+;;; _
+(provide 'mu4e-vars)
+;;; mu4e-vars.el ends here
diff --git a/mu4e/mu4e-view.el b/mu4e/mu4e-view.el
new file mode 100644 (file)
index 0000000..07c95ed
--- /dev/null
@@ -0,0 +1,1354 @@
+;;; mu4e-view.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file we define mu4e-view-mode (+ helper functions), which is used for
+;; viewing e-mail messages
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'calendar)
+(require 'gnus-art)
+(require 'comint)
+(require 'browse-url)
+(require 'button)
+(require 'epa)
+(require 'epg)
+(require 'thingatpt)
+
+(require 'mu4e-actions)
+(require 'mu4e-compose)
+(require 'mu4e-context)
+(require 'mu4e-headers)
+(require 'mu4e-mark)
+(require 'mu4e-message)
+(require 'mu4e-server)
+(require 'mu4e-search)
+ ;; utility functions
+(require 'mu4e-contacts)
+(require 'mu4e-vars)
+
+;;; Options
+
+(defcustom mu4e-view-scroll-to-next t
+  "Move to the next message with `mu4e-view-scroll-up-or-next'.
+When at the end of a message, move to the next one, if any.
+Otherwise, don't move to the next message."
+  :type 'boolean
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-fields
+  '(:from :to  :cc :subject :flags :date :maildir :mailing-list :tags
+         :attachments :signature :decryption)
+  "Header fields to display in the message view buffer.
+For the complete list of available headers, see
+`mu4e-header-info'.
+
+Note, when using the gnus-based viewer you can only use this add
+fields that are otherwise not shows; you can further tweak the
+fields using e.g. `gnus-article-hide-boring-headers',
+`gnus-article-hide-headers' etc., see the gnus documentation for
+details."
+  :type (list 'symbol)
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-actions
+  (seq-filter 'identity
+             `( ("capture message"  . mu4e-action-capture-message)
+                ("view in browser"  . mu4e-action-view-in-browser)
+                ,(when (fboundp 'xwidget-webkit-browse-url)
+                   '("xview in xwidget" . mu4e-action-view-in-xwidget))
+                ("show this thread" . mu4e-action-show-thread)))
+  "List of actions to perform on messages in view mode.
+The actions are cons-cells of the form:
+  (NAME . FUNC)
+where:
+* NAME is the name of the action (e.g. \"Count lines\")
+* FUNC is a function which receives a message plist as an argument.
+
+The first letter of NAME is used as a shortcut character."
+  :group 'mu4e-view
+  :type '(alist :key-type string :value-type function))
+
+(defcustom mu4e-view-open-program
+  (pcase system-type
+    ('darwin "open")
+    ('cygwin "cygstart")
+    (_ "xdg-open"))
+  "Tool to open the correct program for a given file.
+May also be a function of a single argument, the file to be
+opened.
+
+In the function-valued case a likely candidate is
+`mailcap-view-file' although note that there was an Emacs bug up
+to Emacs 29 which prevented opening a file if `mailcap-mime-data'
+specified a function as viewer."
+  :type '(choice string function)
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-max-specpdl-size 4096
+  "The value of `max-specpdl-size' for displaying messages with Gnus."
+  :type 'integer
+  :group 'mu4e-view)
+
+
+
+\f
+;;; Old options
+
+;; Options from the old message view.
+(make-obsolete-variable 'mu4e-view-show-addresses
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-wrap-lines nil "0.9.9-dev7")
+(make-obsolete-variable 'mu4e-view-hide-cited nil "0.9.9-dev7")
+(make-obsolete-variable 'mu4e-view-date-format
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-image-max-width
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-image-max-height
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-save-multiple-attachments-without-asking
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-attachment-assoc
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-attachment-actions
+                       "See mu4e-view-mime-part-actions" "1.7.0")
+(make-obsolete-variable 'mu4e-view-header-field-keymap
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-header-field-keymap
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-contacts-header-keymap
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-attachments-header-keymap
+                       "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-imagemagick-identify nil "1.7.0")
+(make-obsolete-variable 'mu4e-view-show-images
+                       "No longer used" "1.7.0")
+(make-obsolete-variable 'mu4e-view-gnus     "Old view is gone" "1.7.0")
+(make-obsolete-variable 'mu4e-view-use-gnus "Gnus view is the default" "1.5.10")
+
+(make-obsolete-variable 'mu4e-cited-regexp "No longer used" "1.7.0")
+
+\f
+
+;; Helpers
+
+(defun mu4e~view-quit-buffer ()
+  "Quit the mu4e-view buffer.
+This is a rather complex function, to ensure we don't disturb
+other windows."
+  (interactive)
+  (if (eq mu4e-split-view 'single-window)
+      (when (buffer-live-p (mu4e-get-view-buffer))
+       (kill-buffer (mu4e-get-view-buffer)))
+    (unless (eq major-mode 'mu4e-view-mode)
+      (mu4e-error "Must be in mu4e-view-mode (%S)" major-mode))
+    (let ((curbuf (current-buffer))
+         (curwin (selected-window))
+         (headers-win))
+      (walk-windows
+       (lambda (win)
+        ;; check whether the headers buffer window is visible
+        (when (eq (mu4e-get-headers-buffer) (window-buffer win))
+          (setq headers-win win))
+        ;; and kill any _other_ (non-selected) window that shows the current
+        ;; buffer
+        (when
+            (and
+             (eq curbuf (window-buffer win)) ;; does win show curbuf?
+             (not (eq curwin win))         ;; but it's not the curwin?
+             (not (one-window-p))) ;; and not the last one on the frame?
+          (delete-window win))))  ;; delete it!
+      ;; now, all *other* windows should be gone.
+      ;; if the headers view is also visible, kill ourselves + window; otherwise
+      ;; switch to the headers view
+      (if (window-live-p headers-win)
+         ;; headers are visible
+         (progn
+           (kill-buffer-and-window) ;; kill the view win
+           (setq mu4e~headers-view-win nil)
+           (select-window headers-win)) ;; and switch to the headers win...
+       ;; headers are not visible...
+       (progn
+         (kill-buffer)
+         (setq mu4e~headers-view-win nil)
+         (when (buffer-live-p (mu4e-get-headers-buffer))
+           (switch-to-buffer (mu4e-get-headers-buffer))))))))
+
+
+(defconst mu4e~view-raw-buffer-name " *mu4e-raw-view*"
+  "Name for the raw message view buffer.")
+
+(defun mu4e-view-raw-message ()
+  "Display the raw contents of message at point in a new buffer."
+  (interactive)
+  (let ((path (mu4e-message-readable-path))
+       (buf (get-buffer-create mu4e~view-raw-buffer-name)))
+    (with-current-buffer buf
+      (let ((inhibit-read-only t))
+       (erase-buffer)
+       (insert-file-contents path)
+       (view-mode)
+       (goto-char (point-min))))
+    (switch-to-buffer buf)))
+
+(defun mu4e-view-pipe (cmd)
+  "Pipe the message at point through shell command CMD.
+Then, display the results."
+  (interactive "sShell command: ")
+  (let ((path (mu4e-message-readable-path)))
+    (mu4e-process-file-through-pipe path cmd)))
+
+(defmacro mu4e~view-in-headers-context (&rest body)
+  "Evaluate BODY in the context of the headers buffer."
+  `(progn
+     (unless (buffer-live-p (mu4e-get-headers-buffer))
+       (mu4e-error "No headers buffer connected"))
+     (let* ((msg (mu4e-message-at-point))
+           (docid (mu4e-message-field msg :docid)))
+       (unless docid
+        (mu4e-error "Message without docid: action is not possible"))
+       (with-current-buffer (mu4e-get-headers-buffer)
+        (unless (eq mu4e-split-view 'single-window)
+          (when (get-buffer-window)
+            (select-window (get-buffer-window))))
+        (if (mu4e~headers-goto-docid docid)
+            ,@body
+          (mu4e-error "Cannot find message in headers buffer"))))))
+
+(defun mu4e-view-headers-next (&optional n)
+  "Move point to the next message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth next
+header."
+  (interactive "P")
+  (mu4e~view-in-headers-context
+   (mu4e~headers-move (or n 1))))
+
+(defun mu4e-view-headers-prev (&optional n)
+  "Move point to the previous message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth
+previous header."
+  (interactive "P")
+  (mu4e~view-in-headers-context
+   (mu4e~headers-move (- (or n 1)))))
+
+(defun mu4e~view-prev-or-next-unread (backwards)
+  "Move point to the next or previous message.
+Go to the previous message if BACKWARDS is non-nil.
+unread message header in the headers buffer connected with this
+message view. If this succeeds, return the new docid. Otherwise,
+return nil."
+  (mu4e~view-in-headers-context
+   (mu4e~headers-prev-or-next-unread backwards))
+  (if (eq mu4e-split-view 'single-window)
+      (when (eq (window-buffer) (mu4e-get-view-buffer))
+       (with-current-buffer (mu4e-get-headers-buffer)
+         (mu4e-headers-view-message)))
+    (mu4e-select-other-view)
+    (mu4e-headers-view-message)))
+
+(defun mu4e-view-headers-prev-unread ()
+  "Move point to the previous unread message header.
+If this succeeds, return the new docid. Otherwise, return nil."
+  (interactive)
+  (mu4e~view-prev-or-next-unread t))
+
+(defun mu4e-view-headers-next-unread ()
+  "Move point to the next unread message header.
+If this succeeds, return the new docid. Otherwise, return nil."
+  (interactive)
+  (mu4e~view-prev-or-next-unread nil))
+
+\f
+;;; Interactive functions
+(defun mu4e-view-action (&optional msg)
+  "Ask user for some action to apply on MSG, then do it.
+If MSG is nil apply action to message returned
+bymessage-at-point.  The actions are specified in
+`mu4e-view-actions'."
+  (interactive)
+  (let* ((msg (or msg (mu4e-message-at-point)))
+        (actionfunc (mu4e-read-option "Action: " mu4e-view-actions)))
+    (funcall actionfunc msg)))
+
+(defun mu4e-view-mark-pattern ()
+  "Mark messages that match a certain pattern.
+Ask user for a kind of mark, (move, delete etc.), a field to
+match and a regular expression to match with. Then, mark all
+matching messages with that mark."
+  (interactive)
+  (mu4e~view-in-headers-context (mu4e-headers-mark-pattern)))
+
+(defun mu4e-view-mark-thread (&optional markpair)
+  "Mark whole thread with a certain mark.
+Ask user for a kind of mark (move, delete etc.), and apply it
+to all messages in the thread at point in the headers view. The
+optional MARKPAIR can also be used to provide the mark
+selection."
+  (interactive)
+  (mu4e~view-in-headers-context
+   (if markpair (mu4e-headers-mark-thread nil markpair)
+     (call-interactively 'mu4e-headers-mark-thread))))
+
+(defun mu4e-view-mark-subthread (&optional markpair)
+  "Mark subthread with a certain mark.
+Ask user for a kind of mark (move, delete etc.), and apply it
+to all messages in the subthread at point in the headers view.
+The optional MARKPAIR can also be used to provide the mark
+selection."
+  (interactive)
+  (mu4e~view-in-headers-context
+   (if markpair (mu4e-headers-mark-subthread markpair)
+     (mu4e-headers-mark-subthread))))
+
+(defun mu4e-view-search-narrow ()
+  "Run `mu4e-headers-search-narrow' in the headers buffer."
+  (interactive)
+  (mu4e~view-in-headers-context (mu4e-search-narrow)))
+
+(defun mu4e-view-search-edit ()
+  "Run `mu4e-search-edit' in the headers buffer."
+  (interactive)
+  (mu4e~view-in-headers-context (mu4e-search-edit)))
+
+(defun mu4e-mark-region-code ()
+  "Highlight region marked with `message-mark-inserted-region'.
+Add this function to `mu4e-view-mode-hook' to enable this feature."
+  (require 'message)
+  (let (beg end ov-beg ov-end ov-inv)
+    (save-excursion
+      (goto-char (point-min))
+      (while (re-search-forward
+             (concat "^" message-mark-insert-begin) nil t)
+       (setq ov-beg (match-beginning 0)
+             ov-end (match-end 0)
+             ov-inv (make-overlay ov-beg ov-end)
+             beg    ov-end)
+       (overlay-put ov-inv 'invisible t)
+       (overlay-put ov-inv 'mu4e-overlay t)
+       (when (re-search-forward
+              (concat "^" message-mark-insert-end) nil t)
+         (setq ov-beg (match-beginning 0)
+               ov-end (match-end 0)
+               ov-inv (make-overlay ov-beg ov-end)
+               end    ov-beg)
+         (overlay-put ov-inv 'invisible t))
+       (when (and beg end)
+         (let ((ov (make-overlay beg end)))
+           (overlay-put ov 'mu4e-overlay t)
+           (overlay-put ov 'face 'mu4e-region-code))
+         (setq beg nil end nil))))))
+
+;;; View Utilities
+
+(defun mu4e-view-mark-custom ()
+  "Run some custom mark function."
+  (mu4e~view-in-headers-context
+   (mu4e-headers-mark-custom)))
+
+(defun mu4e~view-split-view-p ()
+  "Return t if we're in split-view, nil otherwise."
+  (member mu4e-split-view '(horizontal vertical)))
+
+;;; Scroll commands
+
+(defun mu4e-view-scroll-up-or-next ()
+  "Scroll-up the current message.
+If `mu4e-view-scroll-to-next' is non-nil, and we cannot scroll up
+any further, go the next message."
+  (interactive)
+  (condition-case nil
+      (scroll-up)
+    (error
+     (when mu4e-view-scroll-to-next
+       (mu4e-view-headers-next)))))
+
+(defun mu4e-scroll-up ()
+  "Scroll text of selected window up one line."
+  (interactive)
+  (scroll-up 1))
+
+(defun mu4e-scroll-down ()
+  "Scroll text of selected window down one line."
+  (interactive)
+  (scroll-down 1))
+
+;;; Mark commands
+
+(defun mu4e-view-unmark-all ()
+  "If we're in split-view, unmark all messages.
+Otherwise, warn user that unmarking only works in the header
+list."
+  (interactive)
+  (if (mu4e~view-split-view-p)
+      (mu4e~view-in-headers-context (mu4e-mark-unmark-all))
+    (mu4e-message "Unmarking needs to be done in the header list view")))
+
+(defun mu4e-view-unmark ()
+  "If we're in split-view, unmark message at point.
+Otherwise, warn user that unmarking only works in the header
+list."
+  (interactive)
+  (if (mu4e~view-split-view-p)
+      (mu4e-view-mark-for-unmark)
+    (mu4e-message "Unmarking needs to be done in the header list view")))
+
+(defmacro mu4e~view-defun-mark-for (mark)
+  "Define a function mu4e-view-mark-for- MARK."
+  (let ((funcname (intern (format "mu4e-view-mark-for-%s" mark)))
+       (docstring (format "Mark the current message for %s." mark)))
+    `(progn
+       (defun ,funcname () ,docstring
+             (interactive)
+             (mu4e~view-in-headers-context
+              (mu4e-headers-mark-and-next ',mark)))
+       (put ',funcname 'definition-name ',mark))))
+
+(mu4e~view-defun-mark-for move)
+(mu4e~view-defun-mark-for refile)
+(mu4e~view-defun-mark-for delete)
+(mu4e~view-defun-mark-for flag)
+(mu4e~view-defun-mark-for unflag)
+(mu4e~view-defun-mark-for unmark)
+(mu4e~view-defun-mark-for something)
+(mu4e~view-defun-mark-for read)
+(mu4e~view-defun-mark-for unread)
+(mu4e~view-defun-mark-for trash)
+(mu4e~view-defun-mark-for untrash)
+
+(defun mu4e-view-marked-execute ()
+  "Execute the marked actions."
+  (interactive)
+  (mu4e~view-in-headers-context
+   (mu4e-mark-execute-all)))
+
+
+;;; URL handling
+
+(defvar mu4e~view-link-map nil
+  "A map of some number->url so we can jump to url by number.")
+(put 'mu4e~view-link-map 'permanent-local t)
+
+(defvar mu4e-view-active-urls-keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map [down-mouse-1] 'mu4e~view-browse-url-from-binding)
+    (define-key map [mouse-1] 'mu4e~view-browse-url-from-binding)
+    (define-key map (kbd "M-<return>") 'mu4e~view-browse-url-from-binding)
+    map)
+  "Keymap used for the urls inside the body.")
+
+(defvar mu4e~view-beginning-of-url-regexp
+  "https?\\://\\|mailto:"
+  "Regexp that matches the beginning of certain URLs.
+Match-string 1 will contain the matched URL, if any.")
+
+
+(defun mu4e~view-browse-url-from-binding (&optional url)
+  "View in browser the url at point, or click location.
+If the optional argument URL is provided, browse that instead.
+If the url is mailto link, start writing an email to that address."
+  (interactive)
+  (let* (( url (or url (mu4e~view-get-property-from-event 'mu4e-url))))
+    (when url
+      (if (string-match-p "^mailto:" url)
+         (browse-url-mail url)
+       (browse-url url)))))
+
+
+(defun mu4e~view-get-property-from-event (prop)
+  "Get the property PROP at point, or the location of the mouse.
+The action is chosen based on the `last-command-event'.
+Meant to be evoked from interactive commands."
+  (if (and (eventp last-command-event)
+          (mouse-event-p last-command-event))
+      (let ((posn (event-end last-command-event)))
+       (when (numberp (posn-point posn))
+         (get-text-property
+          (posn-point posn)
+          prop
+          (window-buffer (posn-window posn)))))
+    (get-text-property (point) prop)))
+
+;; this is fairly simplistic...
+(defun mu4e~view-activate-urls ()
+  "Turn things that look like URLs into clickable things.
+Also number them so they can be opened using `mu4e-view-go-to-url'."
+  (let ((num 0))
+    (save-excursion
+      (setq mu4e~view-link-map ;; buffer local
+           (make-hash-table :size 32 :weakness nil))
+      (goto-char (point-min))
+      (while (re-search-forward mu4e~view-beginning-of-url-regexp nil t)
+       (let ((bounds (thing-at-point-bounds-of-url-at-point)))
+         (when bounds
+           (let* ((url (thing-at-point-url-at-point))
+                  (ov (make-overlay (car bounds) (cdr bounds))))
+             (puthash (cl-incf num) url mu4e~view-link-map)
+             (add-text-properties
+              (car bounds)
+              (cdr bounds)
+              `(face mu4e-link-face
+                     mouse-face highlight
+                     mu4e-url ,url
+                     keymap ,mu4e-view-active-urls-keymap
+                     help-echo
+                     "[mouse-1] or [M-RET] to open the link"))
+             (overlay-put ov 'mu4e-overlay t)
+             (overlay-put ov 'after-string
+                          (propertize (format "\u200B[%d]" num)
+                                      'face 'mu4e-url-number-face)))))))))
+
+
+(defun mu4e~view-get-urls-num (prompt &optional multi)
+  "Ask the user with PROMPT for an URL number for MSG.
+The number is [1..n] for URLs \[0..(n-1)] in the message. If
+MULTI is nil, return the number for the URL; otherwise (MULTI is
+non-nil), accept ranges of URL numbers, as per
+`mu4e-split-ranges-to-numbers', and return the corresponding
+string."
+  (let* ((count (hash-table-count mu4e~view-link-map)) (def))
+    (when (zerop count) (mu4e-error "No links for this message"))
+    (if (not multi)
+       (if (= count 1)
+           (read-number (mu4e-format "%s: " prompt) 1)
+         (read-number (mu4e-format "%s (1-%d): " prompt count)))
+      (progn
+       (setq def (if (= count 1) "1" (format "1-%d" count)))
+       (read-string (mu4e-format "%s (default %s): " prompt def)
+                    nil nil def)))))
+
+(defun mu4e-view-go-to-url (&optional multi)
+  "Offer to go visit one or more URLs.
+If MULTI (prefix-argument) is non-nil, offer to go to a range of URLs."
+  (interactive "P")
+  (mu4e~view-handle-urls "URL to visit"
+                        multi
+                        (lambda (url) (mu4e~view-browse-url-from-binding url))))
+
+(defun mu4e-view-save-url (&optional multi)
+  "Offer to save URLs to the kill ring.
+If MULTI (prefix-argument) is nil, save a single one, otherwise, offer
+to save a range of URLs."
+  (interactive "P")
+  (mu4e~view-handle-urls "URL to save" multi
+                        (lambda (url)
+                          (kill-new url)
+                          (mu4e-message "Saved %s to the kill-ring" url))))
+
+(defun mu4e-view-fetch-url (&optional multi)
+  "Offer to fetch (download) URLs.
+If MULTI (prefix-argument) is nil,
+download a single one, otherwise, offer to fetch a range of
+URLs. The urls are fetched to `mu4e-attachment-dir'."
+  (interactive "P")
+  (mu4e~view-handle-urls
+   "URL to fetch" multi
+   (lambda (url)
+     (let ((target (concat (mu4e~get-attachment-dir url) "/"
+                          (file-name-nondirectory url))))
+       (url-copy-file url target)
+       (mu4e-message "Fetched %s -> %s" url target)))))
+
+(defun mu4e~view-handle-urls (prompt multi urlfunc)
+  "Handle URLs.
+If MULTI is nil, apply URLFUNC to a single uri, otherwise, apply
+it to a range of uris. PROMPT is the query to present to the user."
+  (if multi
+      (mu4e~view-handle-multi-urls prompt urlfunc)
+    (mu4e~view-handle-single-url prompt urlfunc)))
+
+(defun mu4e~view-handle-single-url (prompt urlfunc &optional num)
+  "Apply URLFUNC to some URL with NUM in the current message.
+Prompting the user with PROMPT for the number."
+  (let* ((num (or num (mu4e~view-get-urls-num prompt)))
+        (url (gethash num mu4e~view-link-map)))
+    (unless url (mu4e-warn "Invalid number for URL"))
+    (funcall urlfunc url)))
+
+(defun mu4e~view-handle-multi-urls (prompt urlfunc)
+  "Apply URLFUNC to a a range of URLs in the current message.
+
+Prompting the user with PROMPT for the numbers.
+
+Default is to apply it to all URLs, [1..n], where n is the number
+of urls. You can type multiple values separated by space, e.g.  1
+3-6 8 will visit urls 1,3,4,5,6 and 8.
+
+Furthermore, there is a shortcut \"a\" which means all urls, but as
+this is the default, you may not need it."
+  (let* ((linkstr (mu4e~view-get-urls-num
+                  "URL number range (or 'a' for 'all')" t))
+        (count (hash-table-count mu4e~view-link-map))
+        (linknums (mu4e-split-ranges-to-numbers linkstr count)))
+    (dolist (num linknums)
+      (mu4e~view-handle-single-url prompt urlfunc num))))
+
+(defun mu4e-view-for-each-uri (func)
+  "Evaluate FUNC(uri) for each uri in the current message."
+  (maphash (lambda (_num uri) (funcall func uri)) mu4e~view-link-map))
+
+(defun mu4e-view-message-with-message-id (msgid)
+  "View message with message-id MSGID.
+This (re)creates a
+headers-buffer with a search for MSGID, then open a view for that
+message."
+  (mu4e-search (concat "msgid:" msgid) nil nil t msgid t))
+
+
+;;; Variables
+
+(defvar gnus-icalendar-additional-identities)
+(defvar helm-comp-read-use-marked)
+(defvar-local mu4e~view-rendering nil)
+
+(define-obsolete-variable-alias 'mu4e-view-blocked-images 'gnus-blocked-images
+  "1.5.12")
+(define-obsolete-variable-alias 'mu4e-view-inhibit-images 'gnus-inhibit-images
+  "1.5.12")
+;;; Main
+
+;; remember the mime-handles, so we can clean them up when
+;; we quit this buffer.
+(defvar-local mu4e~gnus-article-mime-handles nil)
+(put 'mu4e~gnus-article-mime-handles 'permanent-local t)
+
+(defun mu4e-view (msg)
+  "Display the message MSG in a new buffer, and keep in sync with HDRSBUF.
+\"In sync\" here means that moving to the next/previous message
+in the the message view affects HDRSBUF, as does marking etc.
+
+As a side-effect, a message that is being viewed loses its
+`unread' marking if it still had that."
+
+  (mu4e~headers-update-handler msg nil nil) ;; update headers, if necessary.
+
+  (when (bufferp gnus-article-buffer)
+    (kill-buffer gnus-article-buffer))
+  (setq gnus-article-buffer mu4e-view-buffer-name)
+  (with-current-buffer (get-buffer-create gnus-article-buffer)
+    (let ((inhibit-read-only t))
+      (remove-overlays (point-min)(point-max) 'mu4e-overlay t)
+      (erase-buffer)
+      (insert-file-contents-literally
+       (mu4e-message-readable-path msg) nil nil nil t)))
+  (switch-to-buffer gnus-article-buffer)
+  (setq mu4e~view-message msg)
+  (mu4e~view-render-buffer msg))
+
+(defun mu4e-view-message-text (msg)
+  "Return the pristine MSG as a string."
+  ;; we need this for replying/forwarding, since the mu4e-compose
+  ;; wants it that way.
+
+  (with-temp-buffer
+    (insert-file-contents-literally
+     (mu4e-message-readable-path msg) nil nil nil t)
+    (mu4e~view-render-buffer msg)
+    (buffer-substring-no-properties (point-min) (point-max))))
+
+(defun mu4e-action-view-in-browser (msg &optional skip-headers)
+  "Show current MSG in browser if it includes an HTML-part.
+If SKIP-HEADERS is set, do not show include message headers.
+The variables `browse-url-browser-function',
+`browse-url-handlers', and `browse-url-default-handlers'
+determine which browser function to use."
+  (with-temp-buffer
+    (insert-file-contents-literally
+     (mu4e-message-readable-path msg) nil nil nil t)
+    (run-hooks 'gnus-article-decode-hook)
+    (let ((header (unless skip-headers
+                   (cl-loop for field in '("from" "to" "cc" "date" "subject")
+                            when (message-fetch-field field)
+                            concat (format "%s: %s\n" (capitalize field) it))))
+         (parts (mm-dissect-buffer t t)))
+      ;; If singlepart, enforce a list.
+      (when (and (bufferp (car parts))
+                (stringp (car (mm-handle-type parts))))
+       (setq parts (list parts)))
+      ;; Process the list
+      (unless (gnus-article-browse-html-parts parts header)
+       (mu4e-warn "Message does not contain a \"text/html\" part"))
+      (mm-destroy-parts parts))))
+
+(defun mu4e-action-view-in-xwidget (msg)
+  "Show current MSG in an embedded xwidget, if available."
+  (unless (fboundp 'xwidget-webkit-browse-url)
+    (mu4e-error "No xwidget support available"))
+  (let ((browse-url-handlers nil)
+        (browse-url-browser-function
+         (lambda (url &optional _rest)
+           (xwidget-webkit-browse-url url))))
+    (mu4e-action-view-in-browser msg)))
+
+(defun mu4e~view-render-buffer (msg)
+  "Render current buffer with MSG using Gnus' article mode."
+  (setq gnus-summary-buffer (get-buffer-create " *appease-gnus*"))
+  (let* ((inhibit-read-only t)
+        (max-specpdl-size mu4e-view-max-specpdl-size)
+        (mm-decrypt-option 'known)
+        (ct (mail-fetch-field "Content-Type"))
+        (ct (and ct (mail-header-parse-content-type ct)))
+        (charset (mail-content-type-get ct 'charset))
+        (charset (and charset (intern charset)))
+        (mu4e~view-rendering t); Needed if e.g. an ics file is buttonized
+        (gnus-article-emulate-mime t)
+        (gnus-unbuttonized-mime-types '(".*/.*"))
+        (gnus-buttonized-mime-types
+           (append (list "multipart/signed" "multipart/encrypted")
+                   gnus-buttonized-mime-types))
+        (gnus-newsgroup-charset
+         (if (and charset (coding-system-p charset)) charset
+           (detect-coding-region (point-min) (point-max) t)))
+        ;; Possibly add headers (before "Attachments")
+        (gnus-display-mime-function (mu4e~view-gnus-display-mime msg))
+        (gnus-icalendar-additional-identities
+         (mu4e-personal-addresses 'no-regexp)))
+    (condition-case err
+       (progn
+         (mm-enable-multibyte)
+         (mu4e-view-mode)
+         (run-hooks 'gnus-article-decode-hook)
+         (gnus-article-prepare-display)
+         (mu4e~view-activate-urls)
+         (setq mu4e~gnus-article-mime-handles gnus-article-mime-handles
+               gnus-article-decoded-p gnus-article-decode-hook)
+         (set-buffer-modified-p nil)
+         (add-hook 'kill-buffer-hook #'mu4e~view-kill-mime-handles))
+      (epg-error
+       (mu4e-warn "EPG error: %s; fall back to raw view"
+                 (error-message-string err))))))
+
+(defun mu4e~view-kill-mime-handles ()
+  "Kill cached MIME-handles, if any."
+  (when mu4e~gnus-article-mime-handles
+    (mm-destroy-parts mu4e~gnus-article-mime-handles)
+    (setq mu4e~gnus-article-mime-handles nil)))
+
+(defun mu4e-view-refresh ()
+  "Refresh the message view."
+  (interactive)
+  (when (derived-mode-p 'mu4e-view-mode)
+    (kill-buffer)
+    (mu4e-view mu4e~view-message)))
+
+(defun mu4e-view-toggle-show-mime-parts()
+  "Toggle whether to show all MIME-parts."
+  (interactive)
+  (setq gnus-inhibit-mime-unbuttonizing
+       (not gnus-inhibit-mime-unbuttonizing))
+  (mu4e-view-refresh))
+
+(defun mu4e-view-toggle-fill-flowed()
+  "Toggle flowed-message text filling."
+  (interactive)
+  (setq mm-fill-flowed (not mm-fill-flowed))
+  (mu4e-view-refresh))
+
+(defun mu4e~view-gnus-display-mime (msg)
+  "Like `gnus-display-mime' but include mu4e headers to MSG."
+  (lambda (&optional ihandles)
+    (gnus-display-mime ihandles)
+    (unless ihandles
+      (save-restriction
+       (article-goto-body)
+       (forward-line -1)
+       (narrow-to-region (point) (point))
+       (dolist (field mu4e-view-fields)
+         (let ((fieldval (mu4e-message-field msg field)))
+           (pcase field
+             ((or ':path ':maildir :list ':user-agent ':message-id)
+              (mu4e~view-gnus-insert-header field fieldval))
+             (':mailing-list
+              (let ((list (plist-get msg :list)))
+                (if list (mu4e-get-mailing-list-shortname list) "")))
+             ((or ':flags ':tags)
+              (let ((flags (mapconcat (lambda (flag)
+                                        (if (symbolp flag)
+                                            (symbol-name flag)
+                                          flag)) fieldval ", ")))
+                (mu4e~view-gnus-insert-header field flags)))
+             (':size (mu4e~view-gnus-insert-header
+                     field (mu4e-display-size fieldval)))
+             ((or ':subject ':to ':from ':cc ':bcc ':from-or-to
+                  ':date :attachments ':signature
+                  ':decryption)) ; handled by Gnus
+             (_
+              (mu4e~view-gnus-insert-header-custom msg field)))))
+       (let ((gnus-treatment-function-alist
+              '((gnus-treat-highlight-headers
+                 gnus-article-highlight-headers))))
+         (gnus-treat-article 'head))))))
+
+(defun mu4e~view-gnus-insert-header (field val)
+  "Insert a header FIELD with value VAL."
+  (let* ((info (cdr (assoc field mu4e-header-info)))
+        (key (plist-get info :name))
+        (help (plist-get info :help)))
+    (if (and val (> (length val) 0))
+       (insert (propertize (concat key ":") 'help-echo help)
+               " " val "\n"))))
+
+(defun mu4e~view-gnus-insert-header-custom (msg field)
+  "Insert MSG's custom FIELD."
+  (let* ((info (cdr-safe (or (assoc field mu4e-header-info-custom)
+                            (mu4e-error "Custom field %S not found" field))))
+        (key (plist-get info :name))
+        (func (or (plist-get info :function)
+                  (mu4e-error "No :function defined for custom field %S %S"
+                              field info)))
+        (val (funcall func msg))
+        (help (plist-get info :help)))
+    (when (and val (> (length val) 0))
+      (insert (propertize (concat key ":") 'help-echo help) " " val "\n"))))
+
+(define-advice gnus-icalendar-event-from-handle
+    (:filter-args (handle-attendee) mu4e~view-fix-missing-charset)
+  "Avoid error when displaying an ical attachment without a charset."
+  (if (and (boundp 'mu4e~view-rendering) mu4e~view-rendering)
+      (let* ((handle (car handle-attendee))
+            (attendee (cadr handle-attendee))
+            (buf (mm-handle-buffer handle))
+            (ty (mm-handle-type handle))
+            (rest (cddr handle)))
+       ;; Put the fallback at the end:
+       (setq ty (append ty '((charset . "utf-8"))))
+       (setq handle (cons buf (cons ty rest)))
+       (list handle attendee))
+  handle-attendee))
+
+(defun mu4e~view-mode-p ()
+  "Is the buffer in mu4e-view-mode or one of its descendants?"
+  (or (eq major-mode 'mu4e-view-mode)
+      (derived-mode-p '(mu4e-view-mode))))
+
+(defun mu4e~view-nop (func &rest args)
+  "Do not invoke FUNC with ARGS when in mu4e-view-mode.
+This is useful for advising some Gnus-functionality that does not work in mu4e."
+  (unless (mu4e~view-mode-p)
+    (apply func args)))
+
+(defun mu4e~view-button-reply (func &rest args)
+  "Advise FUNC with ARGS to make `gnus-button-reply' links work in mu4e."
+  (if (mu4e~view-mode-p)
+      (mu4e-compose-reply)
+    (apply func args)))
+
+(defun mu4e~view-msg-mail (func &rest args)
+  "Advise FUNC with ARGS  to make `gnus-msg-mail' links compose with mu4e."
+  (if (mu4e~view-mode-p)
+      (apply 'mu4e~compose-mail args)
+    (apply func args)))
+
+(defvar mu4e-view-mode-map
+  (let ((map (make-keymap)))
+    (define-key map  (kbd "C-S-u") #'mu4e-update-mail-and-index)
+    (define-key map  (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+
+    (define-key map "q" #'mu4e~view-quit-buffer)
+
+    ;; note, 'z' is by-default bound to 'bury-buffer'
+    ;; but that's not very useful in this case
+    (define-key map "z" #'ignore)
+
+    (define-key map "%" #'mu4e-view-mark-pattern)
+    (define-key map "t" #'mu4e-view-mark-subthread)
+    (define-key map "T" #'mu4e-view-mark-thread)
+    (define-key map "j" #'mu4e~headers-jump-to-maildir)
+
+    (define-key map "g" #'mu4e-view-go-to-url)
+    (define-key map "k" #'mu4e-view-save-url)
+    (define-key map "f" #'mu4e-view-fetch-url)
+
+    (define-key map "F" #'mu4e-compose-forward)
+    (define-key map "R" #'mu4e-compose-reply)
+    (define-key map "C" #'mu4e-compose-new)
+    (define-key map "E" #'mu4e-compose-edit)
+
+    (define-key map "." #'mu4e-view-raw-message)
+    (define-key map "," #'mu4e-sexp-at-point)
+    (define-key map "|" #'mu4e-view-pipe)
+    (define-key map "a" #'mu4e-view-action)
+    (define-key map "A" #'mu4e-view-mime-part-action)
+    (define-key map "e" #'mu4e-view-save-attachments)
+
+    ;; toggle header settings
+    (define-key map "O" #'mu4e-headers-change-sorting)
+    (define-key map "P" #'mu4e-headers-toggle-threading)
+    (define-key map "Q" #'mu4e-headers-toggle-full-search)
+    (define-key map "W" #'mu4e-headers-toggle-include-related)
+
+    ;; change the number of headers
+    (define-key map (kbd "C-+") #'mu4e-headers-split-view-grow)
+    (define-key map (kbd "C--") #'mu4e-headers-split-view-shrink)
+    (define-key map (kbd "<C-kp-add>") #'mu4e-headers-split-view-grow)
+    (define-key map (kbd "<C-kp-subtract>") #'mu4e-headers-split-view-shrink)
+
+    ;; intra-message navigation
+    (define-key map (kbd "S-SPC") #'scroll-down)
+    (define-key map (kbd "SPC") #'mu4e-view-scroll-up-or-next)
+    (define-key map (kbd "RET")  #'mu4e-scroll-up)
+    (define-key map (kbd "<backspace>") #'mu4e-scroll-down)
+
+    ;; navigation between messages
+    (define-key map "p" #'mu4e-view-headers-prev)
+    (define-key map "n" #'mu4e-view-headers-next)
+    ;; the same
+    (define-key map (kbd "<M-down>") #'mu4e-view-headers-next)
+    (define-key map (kbd "<M-up>") #'mu4e-view-headers-prev)
+
+    (define-key map (kbd "[") #'mu4e-view-headers-prev-unread)
+    (define-key map (kbd "]") #'mu4e-view-headers-next-unread)
+
+    ;; switching from view <-> headers (when visible)
+    (define-key map "y" #'mu4e-select-other-view)
+
+    ;; marking/unmarking
+    (define-key map "d" #'mu4e-view-mark-for-trash)
+    (define-key map (kbd "<delete>") #'mu4e-view-mark-for-delete)
+    (define-key map (kbd "<deletechar>") #'mu4e-view-mark-for-delete)
+    (define-key map (kbd "D") #'mu4e-view-mark-for-delete)
+    (define-key map (kbd "m") #'mu4e-view-mark-for-move)
+    (define-key map (kbd "r") #'mu4e-view-mark-for-refile)
+
+    (define-key map (kbd "?") #'mu4e-view-mark-for-unread)
+    (define-key map (kbd "!") #'mu4e-view-mark-for-read)
+
+    (define-key map (kbd "+") #'mu4e-view-mark-for-flag)
+    (define-key map (kbd "-") #'mu4e-view-mark-for-unflag)
+    (define-key map (kbd "=") #'mu4e-view-mark-for-untrash)
+    (define-key map (kbd "&") #'mu4e-view-mark-custom)
+
+    (define-key map (kbd "*")             #'mu4e-view-mark-for-something)
+    (define-key map (kbd "<kp-multiply>") #'mu4e-view-mark-for-something)
+    (define-key map (kbd "<insert>")     #'mu4e-view-mark-for-something)
+    (define-key map (kbd "<insertchar>") #'mu4e-view-mark-for-something)
+
+    (define-key map (kbd "#") #'mu4e-mark-resolve-deferred-marks)
+    ;; misc
+    (define-key map "M" #'mu4e-view-massage)
+
+    (define-key map "w" #'visual-line-mode)
+    (define-key map "h" #'mu4e-view-toggle-html)
+    (define-key map (kbd "M-q") #'article-fill-long-lines)
+
+    (define-key map "c" #'mu4e-copy-thing-at-point)
+
+    ;; next 3 only warn user when attempt in the message view
+    (define-key map "u" #'mu4e-view-unmark)
+    (define-key map "U" #'mu4e-view-unmark-all)
+    (define-key map "x" #'mu4e-view-marked-execute)
+
+    (define-key map "$" #'mu4e-show-log)
+    (define-key map "H" #'mu4e-display-manual)
+
+    ;; menu
+    ;;(define-key map [menu-bar] (make-sparse-keymap))
+    (let ((menumap (make-sparse-keymap)))
+      (define-key map [menu-bar headers] (cons "Mu4e" menumap))
+
+      (define-key menumap [quit-buffer]
+                 '("Quit view" . mu4e~view-quit-buffer))
+      (define-key menumap [display-help] '("Help" . mu4e-display-manual))
+
+      (define-key menumap [sepa0] '("--"))
+      (define-key menumap [wrap-lines]
+                 '("Toggle wrap lines" . visual-line-mode))
+      (define-key menumap [raw-view]
+                 '("View raw message" . mu4e-view-raw-message))
+      (define-key menumap [pipe]
+                 '("Pipe through shell" . mu4e-view-pipe))
+
+      (define-key menumap [sepa1] '("--"))
+      (define-key menumap [mark-delete]
+                 '("Mark for deletion" . mu4e-view-mark-for-delete))
+      (define-key menumap [mark-untrash]
+                 '("Mark for untrash" .  mu4e-view-mark-for-untrash))
+      (define-key menumap [mark-trash]
+                 '("Mark for trash" .  mu4e-view-mark-for-trash))
+      (define-key menumap [mark-move]
+                 '("Mark for move" . mu4e-view-mark-for-move))
+
+      (define-key menumap [sepa2] '("--"))
+      (define-key menumap [resend]  '("Resend" . mu4e-compose-resend))
+      (define-key menumap [forward]  '("Forward" . mu4e-compose-forward))
+      (define-key menumap [reply]  '("Reply" . mu4e-compose-reply))
+      (define-key menumap [compose-new]  '("Compose new" . mu4e-compose-new))
+      (define-key menumap [sepa3] '("--"))
+
+      (define-key menumap [query-next]
+                 '("Next query" . mu4e-headers-query-next))
+      (define-key menumap [query-prev]
+                 '("Previous query" . mu4e-headers-query-prev))
+      (define-key menumap [narrow-search]
+                 '("Narrow search" . mu4e-headers-search-narrow))
+      (define-key menumap [bookmark]
+                 '("Search bookmark" . mu4e-headers-search-bookmark))
+      (define-key menumap [jump]
+                 '("Jump to maildir" . mu4e~headers-jump-to-maildir))
+      (define-key menumap [search]
+                 '("Search" . mu4e-headers-search))
+
+      (define-key menumap [sepa4]     '("--"))
+      (define-key menumap [next]      '("Next" . mu4e-view-headers-next))
+      (define-key menumap [previous]  '("Previous" . mu4e-view-headers-prev)))
+
+    ;; Make 0..9 shortcuts for digit-argument.  Actually, none of the bound
+    ;; functions seem to use a prefix arg but those bindings existed because we
+    ;; used to use `suppress-keymap'.  And possibly users added their own
+    ;; prefix arg consuming commands.
+    (dotimes (i 10)
+      (define-key map (kbd (format "%d" i)) #'digit-argument))
+
+    (set-keymap-parent map special-mode-map)
+    map)
+  "Keymap for mu4e-view mode.")
+
+(set-keymap-parent mu4e-view-mode-map button-buffer-map)
+
+(defcustom mu4e-view-mode-hook nil
+  "Hook run when entering Mu4e-View mode."
+  :options '(turn-on-visual-line-mode)
+  :type 'hook
+  :group 'mu4e-view)
+
+;;  "Define the major-mode for the mu4e-view."
+(define-derived-mode mu4e-view-mode gnus-article-mode "mu4e:view"
+  "Major mode for viewing an e-mail message in mu4e.
+Based on Gnus' article-mode."
+  ;; Restore C-h b default behavior
+  (define-key mu4e-view-mode-map (kbd "C-h b") 'describe-bindings)
+  ;; ;; turn off gnus modeline changes and menu items
+  (advice-add 'gnus-set-mode-line :around #'mu4e~view-nop)
+  (advice-add 'gnus-button-reply :around #'mu4e~view-button-reply)
+  (advice-add 'gnus-msg-mail :around #'mu4e~view-msg-mail)
+
+  ;; advice gnus-block-private-groups to always return "."
+  ;; so that by default we block images.
+  (advice-add 'gnus-block-private-groups :around
+             (lambda(func &rest args)
+               (if (mu4e~view-mode-p)
+                   "." (apply func args))))
+  (use-local-map mu4e-view-mode-map)
+  (mu4e-context-minor-mode)
+  (mu4e-search-minor-mode)
+  (setq buffer-undo-list t);; don't record undo info
+
+  ;; support bookmarks.
+  (set (make-local-variable 'bookmark-make-record-function)
+       'mu4e--make-bookmark-record)
+
+  ;; autopair mode gives error when pressing RET
+  ;; turn it off
+  (when (boundp 'autopair-dont-activate)
+    (setq autopair-dont-activate t)))
+
+;;; Massaging the message view
+
+(defcustom mu4e-view-massage-options
+  '( ("ctoggle citations"           . gnus-article-hide-citation)
+     ("htoggle headers"             . gnus-article-hide-headers)
+     ("ytoggle crypto"              . gnus-article-hide-pem)
+     ("ftoggle fill-flowed"         . mu4e-view-toggle-fill-flowed)
+     ("mtoggle show all MIME parts" . mu4e-view-toggle-show-mime-parts))
+"Various options for \"massaging\" the message view. See `(gnus)
+Article Treatment' for more options."
+  :group 'mu4e-view
+  :type '(alist :key-type string :value-type function))
+
+(defun mu4e-view-massage()
+  "Massage current message view as per `mu4e-view-massage-options'."
+  (interactive)
+  (funcall (mu4e-read-option "Massage: " mu4e-view-massage-options)))
+
+;;; MIME-parts
+(defvar-local mu4e~view-mime-parts nil
+  "MIME parts for this message.")
+
+(defun mu4e~view-gather-mime-parts ()
+  "Gather all MIME parts as an alist.
+The alist uniquely maps the number to the gnus-part."
+  (let ((parts '()))
+    (save-excursion
+      (goto-char (point-min))
+      (while (not (eobp))
+       (let ((part (get-text-property (point) 'gnus-data))
+             (index (get-text-property (point) 'gnus-part)))
+         (when (and part (numberp index) (not (assoc index parts))
+           (push `(,index . ,part) parts)))
+         (goto-char (or (next-single-property-change (point) 'gnus-part)
+                        (point-max))))))
+    parts))
+
+
+(defun mu4e-view-save-attachments (&optional arg)
+  "Save MIME-parts from current mu4e gnus view buffer.
+
+When helm-mode is enabled provide completion on attachments and
+possibility to mark candidates to save, otherwise completion on
+attachments is done with `completing-read-multiple', in this case
+use \",\" to separate candidate, completion is provided after
+each \",\".
+
+ARG is specific for the handler, see below.
+
+Note, currently this does not work well with file names
+containing commas."
+  (interactive "P")
+  (cl-assert (and (eq major-mode 'mu4e-view-mode)
+                 (derived-mode-p 'gnus-article-mode)))
+  (let* ((parts (mu4e~view-gather-mime-parts))
+        (handles '())
+        (files '())
+        (compfn (if (and (boundp 'helm-mode) helm-mode)
+                    #'completing-read
+                  ;; Fallback to `completing-read-multiple' with poor
+                  ;; completion
+                  #'completing-read-multiple))
+       dir)
+    (dolist (part parts)
+      (let ((fname (or (cdr (assoc 'filename (assoc "attachment" (cdr part))))
+                       (cl-loop for item in part
+                                for name = (and (listp item)
+                                               (assoc-default 'name item))
+                                thereis (and (stringp name) name)))))
+       (when fname
+         (push `(,fname . ,(cdr part)) handles)
+         (push fname files))))
+    (if files
+       (progn
+         (setq files (let ((helm-comp-read-use-marked t))
+                       (funcall compfn "Save part(s): " files))
+               dir (if arg (read-directory-name "Save to directory: ")
+                     mu4e-attachment-dir))
+         (cl-loop for (f . h) in handles
+                  when (member f files)
+                  do (mm-save-part-to-file
+                      h (let ((file (expand-file-name f dir)))
+                          (if (file-exists-p file)
+                              (let (newname (count 1))
+                                (while (and
+                                        (setq newname
+                                              (concat
+                                               (file-name-sans-extension file)
+                                               (format "(%s)" count)
+                                               (file-name-extension file t)))
+                                        (file-exists-p newname))
+                                  (cl-incf count))
+                                newname)
+                            file)))))
+      (mu4e-message "No attached files found"))))
+
+
+(defvar mu4e-view-mime-part-actions
+  '(
+    ;;
+    ;; some basic ones
+    ;;
+
+    ;; save MIME-part to a file
+    (:name "save"  :handler gnus-article-save-part :receives index)
+    ;; pipe MIME-part to some arbitrary shell command
+    (:name "|pipe" :handler gnus-article-pipe-part :receives index)
+    ;; open with the default handler, if any
+    (:name "open" :handler mu4e~view-open-file :receives temp)
+    ;; open with some custom file.
+    (:name "wopen-with" :handler (lambda (file)(mu4e~view-open-file file t))
+          :receives temp)
+
+    ;;
+    ;; some more examples
+    ;;
+
+    ;; import GPG key
+    (:name "gpg" :handler epa-import-keys :receives temp)
+    ;; count the number of lines in a MIME-part
+    (:name "line-count" :handler "wc -l" :receives pipe)
+    ;; open in this emacs instance; tries to use the attachment name,
+    ;; so emacs can use specific modes etc.
+    (:name "emacs" :handler find-file-read-only :receives temp)
+    ;; open in this emacs instance, "raw"
+    (:name "raw" :handler (lambda (str)
+                           (let ((tmpbuf
+                                  (get-buffer-create " *mu4e-raw-mime*")))
+                                 (with-current-buffer tmpbuf
+                                   (insert str)
+                                   (view-mode)
+                                   (goto-char (point-min)))
+                                 (switch-to-buffer tmpbuf)))  :receives pipe))
+
+  "Specifies actions for MIME-parts.
+
+Each of the actions is a plist with keys
+`(:name <name>         ;; name of the action; shortcut is first letter of name
+
+  :handler             ;; one of:
+                      ;; - a function receiving the index/temp/pipe
+                      ;; - a string, which is taken as a shell command
+
+  :receives            ;;  a symbol specifying what the handler receives
+                      ;; - index: the index number of the mime part (default)
+                      ;; - temp: the full path to the mime part in a
+                      ;;         temporary file, which is deleted immediately
+                      ;;         after invoking handler
+                      ;; - pipe:  the attachment is piped to some shell command
+                      ;;          or as a string parameter to a function
+).")
+
+
+(defun mu4e~view-mime-part-to-temp-file (handle)
+  "Write MIME-part HANDLE to a temporary file and return the file name.
+The filename is deduced from the MIME-part's filename, or
+otherwise random; the result is placed in a temporary directory
+with a unique name. Returns the full path for the file created.
+The directory and file are self-destructed."
+  (let* ((tmpdir (make-temp-file "mu4e-temp-" t))
+        (fname (mm-handle-filename handle))
+        (fname (and fname
+                    (gnus-map-function mm-file-name-rewrite-functions
+                                       (file-name-nondirectory fname))))
+        (fname (if fname
+                   (concat tmpdir "/" (replace-regexp-in-string "/" "-" fname))
+                 (let ((temporary-file-directory tmpdir))
+                   (make-temp-file "mimepart")))))
+    (mm-save-part-to-file handle fname)
+    (run-at-time "30 sec" nil
+                (lambda () (ignore-errors (delete-directory tmpdir t))))
+    fname))
+
+
+(defun mu4e~view-open-file (file &optional force-ask)
+  "Open FILE with default handler, if any.
+Otherwise, or if FORCE-ASK is set, ask user for the program to
+open with."
+  (if (and (not force-ask)
+           (functionp mu4e-view-open-program))
+      (funcall mu4e-view-open-program file)
+    (let ((opener
+          (or (and (not force-ask) mu4e-view-open-program
+                   (executable-find mu4e-view-open-program))
+              (read-shell-command "Open MIME-part with: "))))
+      (call-process opener nil 0 nil file))))
+
+(defun mu4e-view-mime-part-action (&optional n)
+  "Apply some action to MIME-part N in the current messsage.
+If N is not specified, ask for it. For instance, '3 A o' opens
+the third MIME-part."
+  (interactive "NNumber of MIME-part: ")
+  (let* ((parts (mu4e~view-gather-mime-parts))
+        (options
+         (mapcar (lambda (action) `(,(plist-get action :name) . ,action))
+                 mu4e-view-mime-part-actions))
+        (handle
+         (or (cdr-safe (seq-find (lambda (part) (eq (car part) n)) parts))
+             (mu4e-error "MIME-part %s not found" n)))
+        (action
+         (or (and options (mu4e-read-option "Action on MIME-part: " options))
+             (mu4e-error "No such action")))
+        (handler
+         (or (plist-get action :handler)
+             (mu4e-error "No :handler item found for action %S" action)))
+        (receives
+         (or (plist-get action :receives)
+             (mu4e-error "No :receives item found for action %S" action))))
+    (save-excursion
+      (cond
+       ((functionp handler)
+       (cond
+        ((eq receives 'index) (funcall handler n))
+        ((eq receives 'pipe)  (funcall handler (mm-with-unibyte-buffer
+                                                 (mm-insert-part handle)
+                                                 (buffer-string))))
+        ((eq receives 'temp)
+         (funcall handler (mu4e~view-mime-part-to-temp-file handle)))
+        (t (mu4e-error "Invalid :receive for %S" action))))
+       ((stringp handler)
+       (cond
+        ((eq receives 'index)
+         (shell-command (concat handler " " (shell-quote-argument n))))
+        ((eq receives 'pipe)  (mm-pipe-part handle handler))
+        ((eq receives 'temp)
+         (shell-command
+          (shell-command (concat handler " "
+                                 (shell-quote-argument
+                                  (mu4e~view-mime-part-to-temp-file handle))))))
+        (t (mu4e-error "Invalid action %S" action))))))))
+
+(defun mu4e-view-toggle-html ()
+  "Toggle html-display of the first html-part found."
+  (interactive)
+  ;; This function assumes `gnus-article-mime-handle-alist' is sorted by
+  ;; pertinence, i.e. the first HTML part found in it is the most important one.
+  (save-excursion
+    (if-let ((html-part
+             (seq-find (lambda (handle)
+                         (equal (mm-handle-media-type (cdr handle)) "text/html"))
+                       gnus-article-mime-handle-alist)))
+        (gnus-article-inline-part (car html-part))
+      (mu4e-warn "No html part in this message"))))
+
+(defun mu4e-process-file-through-pipe (path pipecmd)
+  "Process file at PATH through a pipe with PIPECMD."
+  (let ((buf (get-buffer-create "*mu4e-output")))
+    (with-current-buffer buf
+      (let ((inhibit-read-only t))
+        (erase-buffer)
+        (call-process-shell-command pipecmd path t t)
+        (view-mode)))
+    (switch-to-buffer buf)))
+\f
+;;; Bug Reference mode support
+
+;; This is Emacs 28 stuff but there is no need to guard it with some (f)boundp
+;; checks (which would return nil if bug-reference.el is not loaded before
+;; mu4e) since the function definition doesn't hurt and `add-hook' works fine
+;; for not yet defined variables (by creating them).
+(declare-function bug-reference-maybe-setup-from-mail "ext:bug-reference")
+(defun mu4e--view-try-setup-bug-reference-mode ()
+  "Try to guess bug-reference setup from the current mu4e mail.
+Looks at the maildir and the mail headers List, List-Id, Maildir,
+To, From, Cc, and Subject and tries to guess suitable values for
+`bug-reference-bug-regexp' and `bug-reference-url-format' by
+matching the maildir name against GROUP-REGEXP and each header
+value against HEADER-REGEXP in
+`bug-reference-setup-from-mail-alist'."
+  (when (derived-mode-p 'mu4e-view-mode)
+    (let (header-values)
+      (save-excursion
+        (goto-char (point-min))
+        (dolist (field '("list" "list-id" "to" "from" "cc" "subject"))
+          (let ((val (mail-fetch-field field)))
+            (when val
+              (push val header-values)))))
+      (bug-reference-maybe-setup-from-mail
+       (mail-fetch-field "maildir")
+       header-values))))
+
+(add-hook 'bug-reference-auto-setup-functions
+         #'mu4e--view-try-setup-bug-reference-mode)
+
+\f
+(provide 'mu4e-view)
+;;; mu4e-view.el ends here
diff --git a/mu4e/mu4e.el b/mu4e/mu4e.el
new file mode 100644 (file)
index 0000000..edd942e
--- /dev/null
@@ -0,0 +1,257 @@
+;;; mu4e.el --- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: email
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'mu4e-vars)
+(require 'mu4e-helpers)
+(require 'mu4e-folders)
+(require 'mu4e-context)
+(require 'mu4e-contacts)
+(require 'mu4e-headers)
+(require 'mu4e-view)
+(require 'mu4e-compose)
+(require 'mu4e-bookmarks)
+(require 'mu4e-update)
+(require 'mu4e-main)
+(require 'mu4e-server)     ;; communication with backend
+
+\f
+
+(defcustom mu4e-confirm-quit t
+  "Whether to confirm to quit mu4e."
+  :type 'boolean
+  :group 'mu4e)
+
+(defcustom mu4e-org-support t
+  "Support Org-mode links."
+  :type 'boolean
+  :group 'mu4e)
+
+(defcustom mu4e-speedbar-support nil
+  "Support having a speedbar to navigate folders/bookmarks."
+  :type 'boolean
+  :group 'mu4e)
+
+(when mu4e-speedbar-support
+  (require 'mu4e-speedbar)) ;; support for speedbar
+(when mu4e-org-support
+  (require 'mu4e-org))      ;; support for org-mode links
+
+;; We can't properly use compose buffers that are revived using
+;; desktop-save-mode; so let's turn that off.
+(with-eval-after-load 'desktop
+  (eval '(add-to-list 'desktop-modes-not-to-save 'mu4e-compose-mode)))
+\f
+;;;###autoload
+(defun mu4e (&optional background)
+  "If mu4e is not running yet, start it.
+Then, show the main window, unless BACKGROUND (prefix-argument)
+is non-nil."
+  (interactive "P")
+  ;; start mu4e, then show the main view
+  (mu4e--init-handlers)
+  (mu4e--start (unless background 'mu4e--main-view)))
+
+(defun mu4e-quit()
+  "Quit the mu4e session."
+  (interactive)
+  (if mu4e-confirm-quit
+      (when (y-or-n-p (mu4e-format "Are you sure you want to quit?"))
+        (mu4e--stop))
+    (mu4e--stop)))
+\f
+;;; Internals
+
+(defun mu4e--check-requirements ()
+  "Check for the settings required for running mu4e."
+  (unless (>= emacs-major-version 25)
+    (mu4e-error "Emacs >= 25.x is required for mu4e"))
+  (when (mu4e-server-properties)
+    (unless (string= (mu4e-server-version) mu4e-mu-version)
+      (mu4e-error "The mu server has version %s, but we need %s"
+                  (mu4e-server-version) mu4e-mu-version)))
+  (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary))
+    (mu4e-error "Please set `mu4e-mu-binary' to the full path to the mu
+    binary"))
+  (dolist (var '(mu4e-sent-folder mu4e-drafts-folder
+                                  mu4e-trash-folder))
+    (unless (and (boundp var) (symbol-value var))
+      (mu4e-error "Please set %S" var))
+    (unless (functionp (symbol-value var)) ;; functions are okay, too
+      (let* ((dir (symbol-value var))
+             (path (concat (mu4e-root-maildir) dir)))
+        (unless (string= (substring dir 0 1) "/")
+          (mu4e-error "%S must start with a '/'" dir))
+        (unless (mu4e-create-maildir-maybe path)
+          (mu4e-error "%s (%S) does not exist" path var))))))
+
+;;; Starting / getting mail / updating the index
+
+(defun mu4e--pong-handler (_data func)
+  "Handle \"pong\" responses from the mu server.
+Invoke FUNC if non-nil."
+  (let ((doccount (plist-get (mu4e-server-properties) :doccount)))
+    (mu4e--check-requirements)
+    (when func (funcall func))
+    (when (zerop doccount)
+      (mu4e-message "Store is empty; try indexing (M-x mu4e-update-index)."))
+    (when (and mu4e-update-interval (null mu4e--update-timer))
+      (setq mu4e--update-timer
+            (run-at-time 0 mu4e-update-interval
+                         (lambda () (mu4e-update-mail-and-index
+                                     mu4e-index-update-in-background)))))))
+
+(defun mu4e--start (&optional func)
+  "Start mu4e.
+If `mu4e-contexts' have been defined, but we don't have a context
+yet, switch to the matching one, or none matches, the first. If
+mu4e is already running, invoke FUNC (if non-nil).
+
+Otherwise, check requirements, then start mu4e. When successful,
+invoke
+ FUNC (if non-nil) afterwards."
+  (unless (mu4e-context-current)
+    (mu4e--context-autoswitch nil mu4e-context-policy))
+  (setq mu4e-pong-func (lambda (info) (mu4e--pong-handler info func)))
+  (mu4e--server-ping
+   (mapcar ;; send it a list of queries we'd like to see read/unread info for
+    (lambda (bm)
+      (funcall (or mu4e-query-rewrite-function #'identity)
+               (plist-get bm :query)))
+    ;; exclude bookmarks that are not strings, and with certain flags
+    (seq-filter (lambda (bm)
+                  (and (stringp (plist-get bm :query))
+                       (not (or (plist-get bm :hide)
+                               (plist-get bm :hide-unread)))))
+                (append (mu4e-bookmarks)
+                        (mu4e--maildirs-with-query)))))
+  ;; maybe request the list of contacts, automatically refreshed after
+  ;; reindexing
+  (unless mu4e--contacts-set (mu4e--request-contacts-maybe)))
+
+(defun mu4e--stop ()
+  "Stop mu4e."
+  (when mu4e--update-timer
+    (cancel-timer mu4e--update-timer)
+    (setq mu4e--update-timer nil))
+  (mu4e-clear-caches)
+  (mu4e--server-kill)
+  ;; kill all mu4e buffers
+  (mapc
+   (lambda (buf)
+     ;; When using the Gnus-based viewer, the view buffer has the
+     ;; kill-buffer-hook function mu4e~view-kill-buffer-hook-fn which kills the
+     ;; mm-* buffers created by Gnus' article mode.  Those have been returned by
+     ;; `buffer-list' but might already be deleted in case the view buffer has
+     ;; been killed first.  So we need a `buffer-live-p' check here.
+     (when (buffer-live-p buf)
+       (with-current-buffer buf
+         (when (member major-mode
+                       '(mu4e-headers-mode mu4e-view-mode mu4e-main-mode))
+           (kill-buffer)))))
+   (buffer-list)))
+\f
+;;; Handlers
+(defun mu4e--default-handler (&rest args)
+  "Dummy handler function with arbitrary ARGS."
+  (mu4e-error "Not handled: %S" args))
+
+(defun mu4e--error-handler (errcode errmsg)
+  "Handler function for showing an error with ERRCODE and ERRMSG."
+  ;; don't use mu4e-error here; it's running in the process filter context
+  (pcase errcode
+    ('4 (mu4e-warn "No matches for this search query."))
+    ('110 (display-warning 'mu4e errmsg :error)) ;; schema version.
+    (_ (mu4e-error "Error %d: %s" errcode errmsg))))
+
+(defun mu4e--update-status (info)
+  "Update the status message with INFO."
+  (setq mu4e-index-update-status
+       `(:tstamp ,(current-time)
+         :checked ,(plist-get info :checked)
+          :updated  ,(plist-get info :updated)
+         :cleaned-up ,(plist-get info :cleaned-up))))
+
+(defun mu4e--info-handler (info)
+  "Handler function for (:INFO ...) sexps received from server."
+  (let* ((type (plist-get info :info))
+         (checked (plist-get info :checked))
+         (updated (plist-get info :updated))
+         (cleaned-up (plist-get info :cleaned-up))
+         (mainbuf (get-buffer mu4e-main-buffer-name)))
+    (cond
+     ((eq type 'add) t) ;; do nothing
+     ((eq type 'index)
+      (if (eq (plist-get info :status) 'running)
+          (mu4e-index-message
+           "Indexing... checked %d, updated %d" checked updated)
+        (progn ;; i.e. 'complete
+         (mu4e--update-status info)
+          (mu4e-index-message
+           "%s completed; checked %d, updated %d, cleaned-up %d"
+           (if mu4e-index-lazy-check "Lazy indexing" "Indexing")
+           checked updated cleaned-up)
+          (run-hooks 'mu4e-index-updated-hook)
+         ;; backward compatibility...
+         (unless (zerop (+ updated cleaned-up))
+           mu4e-message-changed-hook)
+         (unless (and (not (string= mu4e--contacts-tstamp "0"))
+                       (zerop (plist-get info :updated)))
+            (mu4e--request-contacts-maybe))
+          (when (and (buffer-live-p mainbuf) (get-buffer-window mainbuf))
+            (save-window-excursion
+              (select-window (get-buffer-window mainbuf))
+              (mu4e--main-view 'refresh))))))
+     ((plist-get info :message)
+      (mu4e-index-message "%s" (plist-get info :message))))))
+
+(defun mu4e--init-handlers()
+  "Initialize the server message handlers.
+Only set set them if they were nil before, so overriding has a
+chance."
+  (mu4e-setq-if-nil mu4e-error-func            #'mu4e--error-handler)
+  (mu4e-setq-if-nil mu4e-update-func           #'mu4e~headers-update-handler)
+  (mu4e-setq-if-nil mu4e-remove-func           #'mu4e~headers-remove-handler)
+  (mu4e-setq-if-nil mu4e-view-func             #'mu4e~headers-view-handler)
+  (mu4e-setq-if-nil mu4e-headers-append-func   #'mu4e~headers-append-handler)
+  (mu4e-setq-if-nil mu4e-found-func            #'mu4e~headers-found-handler)
+  (mu4e-setq-if-nil mu4e-erase-func            #'mu4e~headers-clear)
+
+  (mu4e-setq-if-nil mu4e-sent-func     #'mu4e--default-handler)
+  (mu4e-setq-if-nil mu4e-compose-func  #'mu4e~compose-handler)
+  (mu4e-setq-if-nil mu4e-contacts-func #'mu4e--update-contacts)
+  (mu4e-setq-if-nil mu4e-info-func     #'mu4e--info-handler)
+  (mu4e-setq-if-nil mu4e-pong-func     #'mu4e--default-handler))
+\f
+(defun mu4e-clear-caches ()
+  "Clear any cached resources."
+  (setq
+   mu4e-maildir-list nil
+   mu4e--contacts-set nil
+   mu4e--contacts-tstamp "0"))
+;;; _
+(provide 'mu4e)
+;;; mu4e.el ends here
diff --git a/mu4e/mu4e.texi b/mu4e/mu4e.texi
new file mode 100644 (file)
index 0000000..95f4fcf
--- /dev/null
@@ -0,0 +1,4574 @@
+\input texinfo.tex    @c -*-texinfo-*-
+@documentencoding UTF-8
+@include version.texi
+@c %**start of header
+@setfilename mu4e.info
+@settitle Mu4e @value{VERSION} user manual
+
+@c Use proper quote and backtick for code sections in PDF output
+@c Cf. Texinfo manual 14.2
+@set txicodequoteundirected
+@set txicodequotebacktick
+@c %**end of header
+
+@copying
+Copyright @copyright{} 2012-2022 Dirk-Jan C. Binnema
+
+@quotation
+Permission is granted to copy, distribute and/or modify this document
+under the terms of the GNU Free Documentation License, Version 1.3 or
+any later version published by the Free Software Foundation; with no
+Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.  A
+copy of the license is included in the section entitled ``GNU Free
+Documentation License.''
+@end quotation
+@end copying
+
+@titlepage
+@title @t{Mu4e} --- an e-mail client for GNU Emacs
+@subtitle version  @value{VERSION}, @value{UPDATED}
+@author Dirk-Jan C. Binnema
+
+@c The following two commands start the copyright page.
+@page
+@vskip 0pt plus 1filll
+@insertcopying
+@end titlepage
+
+@dircategory Emacs
+@direntry
+* Mu4e: (Mu4e).  An email client for GNU Emacs.
+@end direntry
+
+@contents
+
+@ifnottex
+@node Top
+@top mu4e manual
+@end ifnottex
+
+@iftex
+@node Welcome to mu4e
+@unnumbered Welcome to mu4e
+@end iftex
+
+Welcome to @t{mu4e} @value{VERSION}.
+
+@t{mu4e} (@t{mu}-for-emacs) is an e-mail client for GNU Emacs version
+25.3 or newer, built on top of the
+@t{mu}@footnote{@url{https://www.djcbsoftware.nl/code/mu}} e-mail search
+engine. @t{mu4e} is optimized for quickly processing large amounts of
+e-mail.
+
+Some of its highlights:
+@itemize
+@item Fully search-based: there are no folders@footnote{that is, instead of
+folders, you use queries that match messages in a particular folder},
+only queries.
+@item Fully documented, with example configurations
+@item User-interface optimized for speed, with quick key strokes for common actions
+@item Support for non-English languages (so ``angstrom''  matches ``Ångström'')
+@item Asynchronous: heavy actions don't block @t{emacs}@footnote{currently,
+the only exception to this is @emph{sending mail}; there are solutions
+for that though --- see the @ref{FAQ}}
+@item Support for cryptography --- signing, encrypting and decrypting
+@item Address auto-completion based on the contacts in your messages
+@item Extendable with your own snippets of elisp
+@end itemize
+
+In this manual, we go through the installation of @t{mu4e}, do some
+basic configuration and explain its daily use. We also show you how you
+can customize @t{mu4e} for your special needs.
+
+At the end of the manual, there are some example configurations, to get
+you up to speed quickly: @ref{Example configs}. There's also a section
+with answers to frequently asked questions, @ref{FAQ}.
+
+@menu
+* Introduction:: Where to begin
+* Getting started:: Setting things up
+* Main view:: The @t{mu4e} overview
+* Headers view:: Lists of message headers
+* Message view:: Viewing specific messages
+* Editor view:: Creating and editing messages
+* Searching:: Some more background on searching/queries`
+* Marking:: Marking messages and performing actions
+* Contexts:: Defining contexts and switching between them
+* Dynamic folders:: Folders that change based on circumstances
+* Actions:: Defining and using custom actions
+* Extending mu4e:: Writing code for @t{mu4e}
+
+Appendices
+* Other tools:: mu4e and the rest of the world
+* Example configs:: Some examples to set you up quickly
+* FAQ:: Common questions and answers
+* Tips and Tricks:: Useful tips
+* How it works:: Some notes about the implementation of @t{mu4e}
+* Debugging:: How to debug problems in @t{mu4e}
+
+* GNU Free Documentation License::  The license of this manual
+@end menu
+
+@node Introduction
+@chapter Introduction
+
+Let's get started!
+
+@menu
+* Why another e-mail client::Aren't there enough already
+* Other mail clients::Where @t{mu4e} takes its inspiration from
+* What mu4e does not do::Focus on the core-business, delegate the rest
+* Becoming a mu4e user::Joining the club
+@end menu
+
+@node Why another e-mail client
+@section Why another e-mail client?
+
+I (@t{mu4e}'s author) spend a @emph{lot} of time dealing with e-mail,
+both professionally and privately. Having an efficient e-mail client
+is essential. Since none of the existing ones worked the way I wanted,
+I thought about creating my own.
+
+Emacs is an integral part of my workflow, so it made a lot of
+sense to use it for e-mail as well. And as I had already written an
+e-mail search engine (@t{mu}), it seemed only logical to use that as a
+basis.
+
+@node Other mail clients
+@section Other mail clients
+
+Under the hood, @t{mu4e} is fully search-based, similar to programs
+like @t{notmuch}@footnote{@url{https://notmuchmail.org/}} and
+@t{sup}@footnote{@url{https://sup-heliotrope.github.io/}}.
+
+However, @t{mu4e}'s user-interface is quite different. @t{mu4e}'s mail
+handling (deleting, moving, etc.)@: is inspired by
+Wanderlust@footnote{@url{http://www.gohome.org/wl/}} (another
+Emacs-based e-mail client),
+@t{mutt}@footnote{@url{http://www.mutt.org/}} and the @t{dired}
+file-manager for emacs.
+
+@t{mu4e} keeps all the `state' in your maildirs, so you can easily
+switch between clients, synchronize over @abbr{IMAP}, backup with
+@t{rsync} and so on. The Xapian-database that @t{mu} maintains is
+merely a @emph{cache}; if you delete it, you won't lose any
+information.
+
+@node What mu4e does not do
+@section What @t{mu4e} does not do
+
+There are a number of things that @t{mu4e} does @b{not} do, by design:
+@itemize
+@item @t{mu}/@t{mu4e} do @emph{not} get your e-mail messages from
+a mail server. Nor does it sync-back any changes. Those tasks are
+delegated to other tools, such as
+@t{offlineimap}@footnote{@url{https://www.offlineimap.org/}},
+@t{isync/mbsync}@footnote{@url{http://isync.sourceforge.net/}} or
+@t{fetchmail}@footnote{@url{http://www.fetchmail.info/}}; As long as
+the messages end up in a maildir, @t{mu4e} and @t{mu} are happy to
+deal with them.
+@item @t{mu4e} also does @emph{not} implement sending of messages; instead, it depends on @t{smtpmail} (@ref{Top,,smtpmail}), which is part of Emacs. In addition, @t{mu4e} piggybacks on Gnus' message editor.
+@end itemize
+
+Thus, many of the things an e-mail client traditionally needs to do, are
+delegated to other tools. This leaves @t{mu4e} to concentrate on what it does
+best: quickly finding the mails you are looking for, and handle them as
+efficiently as possible.
+
+@node Becoming a mu4e user
+@section Becoming a @t{mu4e} user
+
+If @t{mu4e} sounds like something for you, give it a shot! We're trying
+hard to make it as easy as possible to set up and use; and while you can
+use elisp in various places to augment @t{mu4e}, a lot of knowledge
+about programming or elisp shouldn't be required. The idea is to provide
+sensible defaults, and allow for customization.
+
+When you take @t{mu4e} into use, it's a good idea to subscribe to the
+@t{mu}/@t{mu4e}-mailing
+list@footnote{@url{https://groups.google.com/group/mu-discuss}}.
+
+Sometimes, you might encounter some unexpected behavior while using
+@t{mu4e}. It could be a bug in @t{mu4e}, it could be an issue in other
+software. Or it could just be a misunderstanding. In any case, if you
+want to report this (either to the mailing list or to
+@url{https://github.com/djcb/mu/issues}, the latter is preferred),
+please always include the following information:
+
+@itemize
+@item what did you expect that should happen? what actually happened?
+@item can you provide some exact steps to reproduce?
+@item what version of @t{mu4e} and @t{emacs} were you using? What operating system?
+@item can you reproduce it with @command{emacs -q} and only loading @t{mu4e}?
+@item if the problem is related to some specific message, please include the raw message file (appropriately anonymized, of course)
+@end itemize
+
+@node Getting started
+@chapter Getting started
+
+In this chapter, we go through the installation of @t{mu4e} and its
+basic setup. After we have succeeded in @ref{Getting mail}, and
+@pxref{Indexing your messages}, we discuss the @ref{Basic
+configuration}.
+
+After these steps, @t{mu4e} should be ready to go!
+
+@menu
+* Requirements:: What is needed
+* Versions:: Available stable and development versions
+* Installation:: How to install @t{mu} and @t{mu4e}
+* Getting mail:: Getting mail from a server
+* Initializing the message store:: Settings things up
+* Indexing your messages:: Creating and maintaining the index
+* Basic configuration:: Settings for @t{mu4e}
+* Folders::  Setting up standard folders
+* Retrieval and indexing:: Doing it from @t{mu4e}
+* Sending mail:: How to send mail
+* Running mu4e:: Overview of the @t{mu4e} views
+
+@end menu
+
+@node Requirements
+@section Requirements
+
+@t{mu}/@t{mu4e} are known to work on a wide variety of Unix- and
+Unix-like systems, including many Linux distributions, OS X and FreeBSD,
+and even on MS-Windows (with Cygwin). Emacs 25.3 or higher is required,
+as well as Xapian@footnote{@url{https://xapian.org/}} and
+GMime@footnote{@url{http://spruce.sourceforge.net/gmime/}}.
+
+@t{mu} has optional support for both versions 2.2 and 3.0 of the Guile
+(Scheme) programming language. There are also some GUI-toys, which
+require GTK+ 3.x and Webkit.
+
+If you intend to compile @t{mu} yourself, you need to have the typical
+development tools, such as C and C++17 compilers (both @command{gcc} and
+@command{clang} should work), @command{meson} and @command{make}, and
+the development packages for GMime 3.x, GLib and Xapian. Optionally, you
+also need the development packages for GTK+, Webkit and Guile.
+
+@node Versions
+@section Versions
+
+The stable (release) versions have even minor version numbers, while the
+development versions have odd ones. So, for example, 1.6.10 is a stable
+version, while the 1.7.15 is the development version.
+
+The stable versions only receive bug fixes after being released, while
+the development versions get new features, fixes, and, perhaps, bugs,
+and are meant for people with a tolerance for that.
+
+There is support for one release branch; so, when the 1.8 release is
+available (and a new 1.9 development series start), no more changes are
+expected for the 1.6 releases.
+
+@node Installation
+@section Installation
+
+@t{mu4e} is part of @t{mu} --- by installing the latter, the former is
+installed as well. Some Linux distributions provide packaged versions of
+@t{mu}/@t{mu4e}; if you can use those, there is no need to compile
+anything yourself. However, if there are no packages for your
+distribution, if they are outdated, or if you want to use the latest
+development versions, you can follow the steps below.
+
+First, you need make sure you have the necessary dependencies; the
+details depend on your distribution. If you're using another
+distribution (or another OS), the below can at least be helpful in
+identifying the packages to install.
+
+We provide some instructions for Debian, Ubuntu and Fedora; if those
+do not apply to you, you can follow either @ref{Building from a
+release tarball} or @ref{Building from git}.
+
+@subsection Dependencies for Debian/Ubuntu
+
+@example
+$ sudo apt-get install libgmime-3.0-dev libxapian-dev emacs
+@end example
+
+@subsection Dependencies for Fedora
+
+@example
+$ sudo yum install gmime30-devel xapian-core-devel emacs
+@end example
+
+@subsection Building from a release tarball
+@anchor{Building from a release tarball}
+
+Using a release-tarball (as available from
+GitHub@footnote{@url{https://github.com/djcb/mu/releases}}),
+installation follows the typical steps:
+
+@example
+$ tar xvfz mu-<version>.xz  # use the specific version
+$ cd mu-<version>
+# On the BSDs: use gmake instead of make
+$ ./configure && make
+$ sudo make install
+@end example
+
+Xapian, GMime and their dependencies must be installed.
+
+@subsection Building from git
+@anchor{Building from git}
+
+By default, @t{mu} uses the
+Meson@footnote{@url{https://mesonbuild.com/}} build-system. For
+ease-of-use, we also provide a @t{Makefile} with some basic options. Of
+course, you can also just use the corresponding @t{meson}/@t{ninja}
+commands directly.
+
+@example
+$ git clone git://github.com/djcb/mu.git
+$ cd mu
+$ ./autogen.sh
+$ make
+$ make install
+@end example
+
+After that, @t{make} (which is just @t{ninja -C build} under the covers)
+should be enough for rebuilding.
+
+Alternatively, you can also use the (now deprecated) @t{autotools} build
+setup, assuming you have autotools (@t{autoconf}, @t{automake},
+@t{libtool}, @t{texinfo}) installed:
+
+@example
+# get from git (alternatively, use a github tarball)
+$ git clone git://github.com/djcb/mu.git
+
+$ cd mu
+$ ./autogen.sh && make
+# On the BSDs: use gmake instead of make
+$ sudo make install
+@end example
+
+(Xapian, GMime and their dependencies must be installed).
+
+After this, @t{mu} and @t{mu4e} should be installed @footnote{there's a
+hard dependency between versions of @t{mu4e} and @t{mu} --- you cannot
+combine different versions} on your system, and be available from the
+command line and in Emacs.
+
+You may need to restart Emacs, so it can find @t{mu4e} in its
+@code{load-path}. If, even after restarting, Emacs cannot find @t{mu4e},
+you may need to add it to your @code{load-path} explicitly; check where
+@t{mu4e} is installed, and add something like the following to your
+configuration before trying again:
+@lisp
+;; the exact path may differ --- check it
+(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e")
+@end lisp
+
+
+@subsection mu4e and emacs customization
+
+There is some support for using the Emacs customization system in
+@t{mu4e}, but for now, we recommend setting the values
+manually. Please refer to @ref{Example configs} for a couple of
+examples of this; here we go through things step-by-step.
+
+@node Getting mail
+@section Getting mail
+
+In order for @t{mu} (and, by extension, @t{mu4e}) to work, you need to
+have your e-mail messages stored in a
+@emph{maildir}@footnote{@url{https://en.wikipedia.org/wiki/Maildir};
+in this manual we use the term `maildir' for both the standard and the
+hierarchy of maildirs that store your messages} --- a specific
+directory structure with one-file-per-message.
+
+If you are already using a maildir, you are lucky. If not, some setup
+is required:
+@itemize
+@item @emph{Using an external IMAP or POP server} --- if you are using an
+@abbr{IMAP} or @abbr{POP} server, you can use tools like @t{getmail},
+@t{fetchmail}, @t{offlineimap} or @t{isync} to download your messages
+into a maildir (@file{~/Maildir}, often). Because it is such a common
+case, there is a full example of setting @t{mu4e} up with
+@t{offlineimap} and Gmail; @pxref{Gmail configuration}.
+@item @emph{Using a local mail server} --- if you are using a local mail- server
+(such as @t{postfix} or @t{qmail}), you can teach them to deliver into
+a maildir as well, maybe in combination with @t{procmail}. A bit of
+googling should be able to provide you with the details.
+@end itemize
+
+While a @t{mu} only supports a single Maildir, it can be spread across
+different file-systems; and symbolic links are supported.
+
+@node Initializing the message store
+@section Initializing the message store
+
+The first time you run @t{mu}, you need to initialize its store
+(database). The default location for that is @t{~/.cache/mu/xapian}, but
+you can change this using the @t{--muhome} option, and remember to pass
+that to the other commands as well. Alternatively, you can use an
+environment variable @t{MUHOME}.
+
+Assuming that your maildir is at @file{~/Maildir}, we issue the
+following command:
+@example
+  $ mu init --maildir=~/Maildir
+@end example
+
+You can add some e-mail addresses, so @t{mu} recognizes them as yours:
+
+@example
+  $ mu init --maildir=~/Maildir --my-address=jim@@example.com --my-address=bob@@example.com
+@end example
+
+@t{mu} remembers the maildir and your addresses and uses them when
+indexing messages. If you want to change them, you need to @t{init}
+once again.
+
+The addresses may also be basic POSIX regular expressions, wrapped in
+slashes, for example:
+
+@example
+  $ mu init --maildir=~/Maildir '--my-address=/foo-.*@@example\.com/'
+@end example
+
+If you want to see the values for your message-store, you can use
+@command{mu info}.
+
+@node Indexing your messages
+@section Indexing your messages
+
+After you have succeeded in @ref{Getting mail} and initialized the
+message database, we need to @emph{index} the messages. That is --- we
+need to scan the messages in the maildir and store the information
+about them in a special database.
+
+We can do that from @t{mu4e} --- @ref{Main view}, but the first time,
+it is a good idea to run it from the command line, which makes it
+easier to verify that everything works correctly.
+
+Assuming that your maildir is at @file{~/Maildir}, we issue the
+following command:
+@example
+  $ mu index
+@end example
+
+This should scan your messages and fill the database, and give
+progress information while doing so.
+
+The indexing process may take a few minutes the first time you do it
+(for thousands of e-mails); afterwards it is much faster, since @t{mu}
+only scans messages that are new or have changed. Indexing is discussed
+in full detail in the @t{mu-index} man-page.
+
+After the indexing process has finished, you can quickly test if
+everything worked, by trying some command-line searches, for example
+@example
+  $ mu find hello
+@end example
+
+which lists all messages that match @t{hello}. For more examples of
+searches, see @ref{Queries}, or check the @t{mu-find} and @t{mu-easy}
+man pages. If all of this worked well, we are well on our way setting
+things up; the next step is to do some basic configuration for @t{mu4e}.
+
+@node Basic configuration
+@section Basic configuration
+
+Before we can start using @t{mu4e}, we need to tell Emacs to load
+it. So, add to your @file{~/.emacs} (or its moral equivalent, such as
+@file{~/.emacs.d/init.el}) something like:
+
+@lisp
+(require 'mu4e)
+@end lisp
+
+If Emacs complains that it cannot find @t{mu4e}, check your
+@code{load-path} and make sure that @t{mu4e}'s installation directory is
+part of it. If not, you can add it:
+
+@lisp
+(add-to-list 'load-path MU4E-PATH)
+@end lisp
+
+with @t{MU4E-PATH} replaced with the actual path.
+
+@node Folders
+@section Folders
+
+The next step is to tell @t{mu4e} where it can find your Maildir, and
+some special folders.
+
+So, for example@footnote{Note that the folders (@t{mu4e-sent-folder},
+@t{mu4e-drafts-folder}, @t{mu4e-trash-folder} and
+@t{mu4e-refile-folder}) can also be @emph{functions} that are evaluated
+at runtime. This allows for dynamically changing them depending on the
+situation. See @ref{Dynamic folders} for details.}:
+@lisp
+;; these are actually the defaults
+(setq
+  mu4e-sent-folder   "/sent"       ;; folder for sent messages
+  mu4e-drafts-folder "/drafts"     ;; unfinished messages
+  mu4e-trash-folder  "/trash"      ;; trashed messages
+  mu4e-refile-folder "/archive")   ;; saved messages
+@end lisp
+
+The folder (maildir) names are all relative to the root-maildir (see the
+output of @command{mu info}). If you use @t{mu4e-context}, see
+@ref{Contexts and special folders} for what that means for these special
+folders.
+
+@node Retrieval and indexing
+@section Retrieval and indexing with mu4e
+
+As we have seen, we can do all of the mail retrieval @emph{outside} of
+Emacs/@t{mu4e}. However, you can also do it from within
+@t{mu4e}.
+
+@subsection Basics
+
+To set up mail-retrieval from within @t{mu4e}, set the variable
+@code{mu4e-get-mail-command} to the program or shell command you want to
+use for retrieving mail. You can then get your e-mail using @kbd{M-x
+mu4e-update-mail-and-index}, or @kbd{C-S-u} in all @t{mu4e}-views;
+alternatively, you can use @kbd{C-c C-u}, which may be more convenient
+if you use emacs in a terminal.
+
+You can kill the (foreground) update process with @kbd{q}.
+
+It is possible to update your mail and index periodically in the
+background or foreground, by setting the variable
+@code{mu4e-update-interval} to the number of seconds between these
+updates. If set to @code{nil}, it won't update at all. After you make
+changes to @code{mu4e-update-interval}, @t{mu4e} must be restarted
+before the changes take effect. By default, this will run in
+background and to change it to run in foreground, set
+@code{mu4e-index-update-in-background} to @code{nil}.
+
+@subsection Handling errors during mail retrieval
+
+If the mail-retrieval process returns with a non-zero exit code,
+@t{mu4e} shows a warning (unless @code{mu4e-index-update-error-warning}
+is set to @code{nil}), but then try to index your maildirs anyway
+(unless @code{mu4e-index-update-error-continue} is set to @code{nil}).
+
+Reason for these defaults is that some of the mail-retrieval programs
+may return non-zero, even when the updating process succeeded; however,
+it is hard to tell such pseudo-errors from real ones like `login
+failed'.
+
+If you need more refinement, it may be useful to wrap the mail-retrieval
+program in a shell-script, for example @t{fetchmail} returns 1 to
+indicate `no mail'; we can handle that with:
+@lisp
+(setq mu4e-get-mail-command "fetchmail -v || [ $? -eq 1 ]")
+@end lisp
+A similar approach can be used with other mail retrieval programs,
+although not all of them have their exit codes documented.
+
+@subsection Implicit mail retrieval
+
+If you don't have a specific command for getting mail, for example
+because you are running your own mail-server, you can leave
+@code{mu4e-get-mail-command} at @t{"true"} (the default), in which case
+@t{mu4e} won't try to get new mail, but still re-index your messages.
+
+@subsection Speeding up indexing
+
+If you have a large number of e-mail messages in your store,
+(re)indexing might take a while. The defaults for indexing are to
+ensure that we always have correct, up-to-date information about your
+messages, even if other programs have modified the Maildir.
+
+The downside of this thoroughness (which is the default) is that it is
+relatively slow, something that can be noticeable with large e-mail
+corpa on slow file-systems. For a faster approach, you can use the
+following:
+
+@lisp
+(setq
+  mu4e-index-cleanup nil      ;; don't do a full cleanup check
+  mu4e-index-lazy-check t)    ;; don't consider up-to-date dirs
+@end lisp
+
+In many cases, the mentioned thoroughness might not be needed, and
+these settings give a very significant speed-up. If it does not work
+for you (e.g., @t{mu4e} fails to find some new messages), simply leave
+at the default.
+
+Note that you can occasionally run a thorough indexing round using
+@code{mu4e-update-index-nonlazy}.
+
+For further details, please refer to the @t{mu-index} manpage; in
+particular, see @t{.noindex} and @t{.noupdate} which can help reducing
+the indexing time.
+
+@subsection Example setup
+
+A simple setup could look something like:
+
+@lisp
+(setq
+  mu4e-get-mail-command "offlineimap"   ;; or fetchmail, or ...
+  mu4e-update-interval 300)             ;; update every 5 minutes
+@end lisp
+
+A hook @code{mu4e-update-pre-hook} is available which is run right
+before starting the process. That can be useful, for example, to
+influence, @code{mu4e-get-mail-command} based on the the current
+situation (location, time of day, ...).
+
+It is possible to get notifications when the indexing process does any
+updates --- for example when receiving new mail. See
+@code{mu4e-index-updated-hook} and some tips on its usage in the
+@ref{FAQ}.
+
+@node Sending mail
+@section Sending mail
+
+@t{mu4e} re-uses Gnus' @code{message-mode} (@ref{Top,,message}) for
+writing mail and inherits the setup for sending mail as well.
+
+For sending mail using @abbr{SMTP}, @t{mu4e} uses @t{smtpmail}
+(@ref{Top,,smtpmail}). This package supports many different ways to
+send mail; please refer to its documentation for the details.
+
+Here, we only provide some simple examples --- for more, see
+@ref{Example configs}.
+
+A very minimal setup:
+
+@lisp
+;; tell message-mode how to send mail
+(setq message-send-mail-function 'smtpmail-send-it)
+;; if our mail server lives at smtp.example.org; if you have a local
+;; mail-server, simply use 'localhost' here.
+(setq smtpmail-smtp-server "smtp.example.org")
+@end lisp
+
+Since @t{mu4e} (re)uses the same @t{message mode} and @t{smtpmail} that
+Gnus uses, many settings for those also apply to @t{mu4e}.
+
+@subsection Dealing with sent messages
+
+By default, @t{mu4e} puts a copy of messages you sent in the folder
+determined by @code{mu4e-sent-folder}. In some cases, this may not be
+what you want - for example, when using Gmail-over-@abbr{IMAP}, this
+interferes with Gmail's handling of the sent messages folder, and you
+may end up with duplicate messages.
+
+You can use the variable @code{mu4e-sent-messages-behavior} to customize
+what happens with sent messages. The default is the symbol @code{sent}
+which, as mentioned, causes the message to be copied to your
+sent-messages folder. Other possible values are the symbols @code{trash}
+(the sent message is moved to the trash-folder
+(@code{mu4e-trash-folder}), and @code{delete} to simply discard the sent
+message altogether (so Gmail can deal with it).
+
+For Gmail-over-@abbr{IMAP}, you could add the following to your
+settings:
+@verbatim
+;; don't save messages to Sent Messages, Gmail/IMAP takes care of this
+(setq mu4e-sent-messages-behavior 'delete)
+@end verbatim
+And that's it! We should now be ready to go.
+
+For more complex needs, @code{mu4e-sent-messages-behavior} can also be
+a parameter-less function that returns one of the mentioned symbols;
+see the built-in documentation for the variable.
+
+@node Running mu4e
+@section Running mu4e
+
+After following the steps in this chapter, we now (hopefully!) have a
+working @t{mu4e} setup. Great! In the next chapters, we walk you
+through the various views in @t{mu4e}.
+
+For your orientation, the diagram below shows how the views relate to each
+other, and the default key-bindings to navigate between them.
+
+@cartouche
+@verbatim
+
+   [C]       +--------+   [RFCE]
+   --------> | editor | <--------
+  /          +--------+          \
+ /         [RFCE]^                \
+/                |                 \
++-------+ [sjbB]+---------+  [RET]  +---------+
+| main  | <---> | headers | <---->  | message |
++-------+  [q]  +---------+ [qbBjs] +---------+
+                   [sjbB]                ^
+[.] | [q]
+    V
+  +-----+
+  | raw |
+  +-----+
+
+Default bindings
+----------------
+R: Reply      s: search            .: raw view (toggle)
+F: Forward    j: jump-to-maildir   q: quit
+C: Compose    b: bookmark-search
+E: Edit       B: edit bookmark-search
+
+@end verbatim
+@end cartouche
+
+@node Main view
+@chapter The main view
+
+After you have installed @t{mu4e} (@pxref{Getting started}), you can start it
+with @kbd{M-x mu4e}. @t{mu4e} does some checks to ensure everything is set up
+correctly, and then shows you the @t{mu4e} main view. Its major mode is
+@code{mu4e-main-mode}.
+
+@menu
+* Overview: MV Overview. What is the main view
+* Basic actions::What can we do
+* Bookmarks: MV Bookmarks. Jumping to other places
+* Miscellaneous::Notes
+@end menu
+
+@node MV Overview
+@section Overview
+
+The main view looks something like the following:
+
+@cartouche
+@verbatim
+* mu4e - mu for emacs version x.y.z
+
+  Basics
+
+       * [j]ump to some maildir
+       * enter a [s]earch query
+       * [C]ompose a new message
+
+  Bookmarks
+
+       * [bu] Unread messages      (13085/13085)
+       * [bt] Today's messages
+       * [bw] Last 7 days          (53/128)
+       * [bp] Messages with images (75/2441)
+
+  Maildirs
+
+       * [ja] /archive             (2101/18837)
+       * [ji] /inbox               (1/2)
+       * [jb] /bulk                (33/35)
+       * [jB] /bulkarchive         (179/2090)
+       * [jm] /mu                  (694/17687)
+       * [jn] /sauron
+       * [js] /sent
+
+  Misc
+
+       * [;]Switch context
+       * [U]pdate email & database
+       * toggle [m]ail sending mode (currently direct)
+       * [f]lush 1 queued mail
+
+       * [N]ews
+       * [A]bout mu4e
+       * [H]elp
+       * [q]uit
+
+  Info
+
+       * last updated        : Sat May  7 20:37:37 2022
+       * database-path       : /home/pam/.cache/mu/xapian
+       * maildir             : /home/pam/Maildir
+       * in store            : 86179 messages
+       * personal addresses  : /.*example.com/, pam@fo
+
+@end verbatim
+@end cartouche
+
+Let's walk through the menu.
+
+@node Basic actions
+@section Basic actions
+
+First, the @emph{Basics}:
+@itemize
+@item @t{[j]ump to some maildir}: after pressing @key{j} (``jump''),
+@t{mu4e} asks you for a maildir to visit. These are the maildirs you
+set in @ref{Basic configuration} and any of your own. If you choose
+@key{o} (``other'') or @key{/}, you can choose from all maildirs under
+the root-maildir. After choosing a maildir, the messages in that
+maildir are listed, in the @ref{Headers view}.
+@item @t{enter a [s]earch query}: after pressing @key{s}, @t{mu4e} asks
+you for a search query, and after entering one, shows the results in the
+@ref{Headers view}.
+@item @t{[C]ompose a new message}: after pressing @key{C}, you are dropped in
+the @ref{Editor view} to write a new message.
+@end itemize
+
+@node MV Bookmarks
+@section Bookmarks
+
+The next item in the Main view is @emph{Bookmarks}.
+
+Bookmarks are predefined queries with a descriptive name and a
+shortcut --- in the example above, we see the default bookmarks. You
+can view the list of messages matching a certain bookmark by pressing
+@key{b} followed by the bookmark's shortcut. If you'd like to edit the
+bookmarked query first before invoking it, use @key{B}.
+
+Next to each bookmark there is the number of (unread/all) messages
+that match.
+
+Bookmarks are stored in the variable @code{mu4e-bookmarks}; you can add
+your own and/or replace the default ones; @xref{Bookmarks}. For
+instance:
+@lisp
+(add-to-list 'mu4e-bookmarks
+  ;; add bookmark for recent messages on the Mu mailing list.
+  '( :name "Mu7Days"
+     :key  ?m
+     :query "list:mu-discuss.googlegroups.com AND date:7d..now"))
+@end lisp
+
+There are optional keys @t{:hide} to hide the bookmark from the main
+menu, but still have it available (using @key{b})) and
+@t{:hide-unread} to avoid generating the unread-number; that can be
+useful if you have bookmarks for slow queries. Note that
+@t{:hide-unread} is implied when the query is not a string; this for
+the common case where the query function involves some user input,
+which would be disruptive in this case.
+
+@node Miscellaneous
+@section Miscellaneous
+
+Finally, there are some @emph{Misc} (miscellaneous) actions:
+@itemize
+@item @t{[U]pdate email & database} executes the shell-command in the variable
+@code{mu4e-get-mail-command}, and afterwards updates the @t{mu}
+database; see @ref{Indexing your messages} and @ref{Getting mail} for
+details.
+@item @t{toggle [m]ail sending mode (direct)} toggles between sending
+mail directly, and queuing it first (for example, when you are offline),
+and @t{[f]lush queued mail} flushes any queued mail. This item is
+visible only if you have actually set up mail-queuing. @ref{Queuing
+mail}
+@item @t{[A]bout mu4e} provides general information about the program
+@item @t{[H]elp} shows help information for this view
+@item Finally, @t{[q]uit mu4e} quits your @t{mu4e}-session
+@end itemize
+
+@node Headers view
+@chapter The headers view
+
+The headers view shows the results of a query. The header-line shows
+the names of the fields. Below that, there is a line with those
+fields, for each matching message, followed by a footer line. The
+major-mode for the headers view is @code{mu4e-headers-mode}.
+
+@menu
+* Overview: HV Overview. What is the Header View
+* Keybindings::Do things with your keyboard
+* Marking: HV Marking. Selecting messages for doing things
+* Sorting and threading::Influencing the display
+* Custom headers: HV Custom headers. Adding your own headers
+* Actions: HV Actions. Defining and using actions
+* Split view::Seeing both headers and messages
+@end menu
+
+@node HV Overview
+@section Overview
+
+An example headers view:
+@cartouche
+@verbatim
+Date V       Flgs  From/To             List       Subject
+06:32        Nu    To Edmund Dantès    GstDev     Gstreamer-V4L2SINK ...
+15:08        Nu    Abbé Busoni         GstDev     ├>                 ...
+18:20        Nu    Pierre Morrel       GstDev     │└>                ...
+07:48        Nu    To Edmund Dantès    GstDev     └>                 ...
+2013-03-18   S     Jacopo              EmacsUsr   emacs server on win...
+2013-03-18   S     Mercédès            EmacsUsr   └>                 ...
+2013-03-18   S     Beachamp            EmacsUsr   Re: Copying a whole...
+22:07        Nu    Albert de Moncerf   EmacsUsr   └>                 ...
+2013-03-18   S     Gaspard Caderousse  GstDev     Issue with GESSimpl...
+2013-03-18   Ss    Baron Danglars      GuileUsr   Guile-SDL 0.4.2 ava...
+End of search results
+@end verbatim
+@end cartouche
+
+Some notes to explain what you see in the example:
+
+@itemize
+@item The fields shown in the headers view can be influenced by customizing
+the variable @code{mu4e-headers-fields}; see @code{mu4e-header-info} for
+the list of built-in fields. Apart from the built-in fields, you can
+also create custom fields using @code{mu4e-header-info-custom}; see
+@ref{HV Custom headers} for details.
+@item By default, the date is shown with the @t{:human-date} field, which
+shows the @emph{time} for today's messages, and the @emph{date} for
+older messages. If you do not want to distinguish between `today' and
+`older', you can use the @t{:date} field instead.
+@item You can customize the date and time formats with the variable
+@code{mu4e-headers-date-format} and @code{mu4e-headers-time-format},
+respectively. In the example, we use @code{:human-date}, which shows the
+time when the message was sent today, and the date otherwise.
+@item By default, the subject is shown using the @t{:subject} field;
+however, it is also possible to use @t{:thread-subject}, which shows
+the subject of a thread only once, similar to the display of the
+@t{mutt} e-mail client.
+@item The header field used for sorting is indicated by ``@t{V}'' or
+``@t{^}''@footnote{or you can use little graphical triangles; see
+variable @code{mu4e-use-fancy-chars}}, corresponding to the sort order
+(descending or ascending, respectively). You can influence this by a
+mouse click, or @key{O}. Not all fields allow sorting.
+@item Instead of showing the @t{From:} and @t{To:} fields separately, you
+can use From/To (@t{:from-or-to} in @code{mu4e-headers-fields} as a more
+compact way to convey the most important information: it shows @t{From:}
+@emph{except} when the e-mail was sent by the user (i.e., you) --- in
+that case it shows @t{To:} (prefixed by @t{To}@footnote{You can
+customize this by changing the variable
+@code{mu4e-headers-from-or-to-prefix} (a cons cell)}, as in the example
+above).
+@item The `List' field shows the mailing-list a message is sent to;
+@code{mu4e} tries to create a convenient shortcut for the mailing-list
+name; the variable @code{mu4e-user-mailing-lists} can be used to add
+your own shortcuts. You can use @code{mu4e-mailing-list-patterns} to
+specify generic shortcuts. For instance, to shorten list names to the
+part before @t{-list}, you could use:
+@lisp
+(setq mu4e-mailing-list-patterns '("\\`\\([-_a-z0-9.]+\\)-list"))
+@end lisp
+@item The letters in the `Flags' field correspond to the following: D=@emph{draft},
+F=@emph{flagged} (i.e., `starred'), N=@emph{new}, P=@emph{passed} (i.e.,
+forwarded), R=@emph{replied}, S=@emph{seen}, T=@emph{trashed},
+a=@emph{has-attachment}, x=@emph{encrypted}, s=@emph{signed},
+u=@emph{unread}. The tooltip for this field also contains this information.
+@item The subject field also indicates the discussion threads @footnote{using
+Jamie Zawinski's mail threading algorithm,
+@url{https://www.jwz.org/doc/threading.html}}.
+@item The headers view is @emph{automatically updated} if any changes are
+found during the indexing process, and if there is no current
+user-interaction. If you do not want such automatic updates, set
+@code{mu4e-headers-auto-update} to @code{nil}.
+@item Just before executing a search, a hook-function
+@code{mu4e-headers-search-hook} is invoked, which receives the search
+expression as its parameter.
+@item Also, there is a hook-function @code{mu4e-headers-found-hook} available which
+is invoked just after @t{mu4e} has completed showing the messages in the
+headers-view.
+@end itemize
+
+@node Keybindings
+@section Keybindings
+
+Using the below key bindings, you can do various things with these
+messages; these actions are also listed in the @t{Mu4e} menu in the
+Emacs menu bar.
+
+@verbatim
+key          description
+===========================================================
+n,p          view the next, previous message
+],[          move to the next, previous unread message
+y            select the message view (if it's visible)
+RET          open the message at point in the message view
+
+searching
+---------
+s            search
+S            edit last query
+/            narrow the search
+b            search bookmark
+B            edit bookmark before search
+j            jump to maildir
+M-left,\     previous query
+M-right      next query
+
+O            change sort order
+P            toggle threading
+Q            toggle full-search
+V            toggle skip-duplicates
+W            toggle include-related
+
+marking
+-------
+d            mark for moving to the trash folder
+=            mark for removing trash flag ('untrash')
+DEL,D        mark for complete deletion
+m            mark for moving to another maildir folder
+r            mark for refiling
++,-          mark for flagging/unflagging
+?,!          mark message as unread, read
+
+u            unmark message at point
+U            unmark *all* messages
+
+%            mark based on a regular expression
+T,t          mark whole thread, subthread
+
+<insert>,*   mark for 'something' (decide later)
+#            resolve deferred 'something' marks
+
+x            execute actions for the marked messages
+
+composition
+-----------
+R,F,C        reply/forward/compose
+E            edit (only allowed for draft messages)
+
+
+misc
+----
+a            execute some custom action on a header
+|            pipe message through shell command
+C-+,C--      increase / decrease the number of headers shown
+H            get help
+C-S-u        update mail & reindex
+q            leave the headers buffer
+@end verbatim
+
+Furthermore, a number of keybindings are available through minor modes:
+@itemize
+@item Context; see @pxref{Contexts}.
+@end itemize
+
+@node HV Marking
+@section Marking
+
+You can @emph{mark} messages for a certain action, such as deletion or
+move. After one or more messages are marked, you can then execute
+(@code{mu4e-mark-execute-all}, @key{x}) these actions. This two-step
+mark-execute sequence is similar to what e.g. @t{dired} does. It is how
+@t{mu4e} tries to be as quick as possible, while avoiding accidents.
+
+The mark/unmark commands support the @emph{region} (i.e., ``selection'')
+--- so, for example, if you select some messages and press @key{DEL},
+all messages in the region are marked for deletion.
+
+You can mark all messages that match a certain pattern with @key{%}. In
+addition, you can mark all messages in the current thread (@key{T}) or
+sub-thread (@key{t}).
+
+When you do a new search or refresh the headers buffer while you still
+have marked messages, you are asked what to do with those marks ---
+whether to @emph{apply} them before leaving, or @emph{ignore} them. This
+behavior can be influenced with the variable
+@code{mu4e-headers-leave-behavior}.
+
+For more information about marking, see @ref{Marking}.
+
+@node Sorting and threading
+@section Sorting and threading
+
+By default, @t{mu4e} sorts messages by date, in descending order: the
+most recent messages are shown at the top. In addition, be default
+@t{mu4e} shows the message @emph{threads}, i.e., the tree structure
+representing a discussion thread; this also affects the sort order:
+the top-level messages are sorted by the date of the @emph{newest}
+message in the thread.
+
+The header field used for sorting is indicated by ``@t{V}'' or
+``@t{^}''@footnote{or you can use little graphical triangles; see
+variable @code{mu4e-use-fancy-chars}}, indicating the sort order
+(descending or ascending, respectively).
+
+You can change the sort order by clicking the corresponding column with
+the mouse, or with @kbd{M-x mu4e-headers-change-sorting} (@key{O}); note
+that not all fields can be used for sorting. You can toggle threading
+on/off using @kbd{M-x mu4e-headers-toggle-threading} or @key{P}. For
+both of these functions, unless you provide a prefix argument
+(@key{C-u}), the current search is updated immediately using the new
+parameters. You can toggle full-search (@ref{Searching}) using @kbd{M-x
+mu4e-headers-toggle-full-search} or @key{Q}.
+
+Note that with threading enabled, the sorting is exclusively by date,
+regardless of the column clicked.
+
+If you want to change the defaults for these settings, you can use the
+variables @code{mu4e-headers-sort-field} and
+@code{mu4e-headers-show-threads}, as well as
+@code{mu4e-headers-change-sorting} to change the sorting of the current
+search results.
+
+@node HV Custom headers
+@section Custom headers
+
+Sometimes the normal headers that @t{mu4e} offers (Date, From, To,
+Subject, etc.)@: may not be enough. For these cases, @t{mu4e} offers
+@emph{custom headers} in both the headers-view and the message-view.
+
+You can do so by adding a description of your custom header to
+@code{mu4e-header-info-custom}, which is a list of custom headers.
+
+Let's look at an example --- suppose we want to add a custom header that
+shows the number of recipients for a message, i.e., the sum of the
+number of recipients in the @t{To:} and @t{Cc:} fields. Let's further
+suppose that our function takes a message-plist as its argument
+(@ref{Message functions}).
+
+@lisp
+(add-to-list 'mu4e-header-info-custom
+  '(:recipnum .
+     ( :name "Number of recipients"  ;; long name, as seen in the message-view
+       :shortname "Recip#"           ;; short name, as seen in the headers view
+       :help "Number of recipients for this message" ;; tooltip
+       :function (lambda (msg)
+          (format "%d"
+            (+ (length (mu4e-message-field msg :to))
+               (length (mu4e-message-field msg :cc))))))))
+@end lisp
+
+Or, let's get the full mailing-list name:
+@lisp
+(add-to-list 'mu4e-header-info-custom
+ '(:full-mailing-list .
+     ( :name "Mailing-list"               ;; long name, as seen in the message-view
+       :shortname "ML"                    ;; short name, as seen in the headers view
+       :help "Full name for mailing list" ;; tooltip
+       :function (lambda (msg)
+           (or (mu4e-message-field msg :mailing-list) "")))))
+@end lisp
+
+You can then add the custom header to your @code{mu4e-headers-fields},
+just like the built-in headers. After evaluation, your headers-view
+should include a new header @t{Recip#} with the number of recipients,
+and/or @t{ML} with the full mailing-list name.
+
+This function can be used in both the headers-view and the message-view;
+if you need something specific for one of these, you can check for the
+mode in your function, or create separate functions.
+
+@node HV Actions
+@section Actions
+
+@code{mu4e-headers-action} (@key{a}) lets you pick custom actions to perform
+on the message at point. You can specify these actions using the variable
+@code{mu4e-headers-actions}. See @ref{Actions} for the details.
+
+@t{mu4e} defines some default actions. One of those is for @emph{capturing} a
+message: @key{a c} `captures' the current message. Next, when you're editing
+some message, you can include the previously captured message as an
+attachment, using @code{mu4e-compose-attach-captured-message}. See
+@file{mu4e-actions.el} in the @t{mu4e} source distribution for more example
+actions.
+
+@node Split view
+@section Split view
+
+Using the @emph{Split view}, we can see the @ref{Headers view} and the
+@ref{Message view} next to each other, with the message selected in the
+former, visible in the latter. You can influence the way the splitting
+is done by customizing the variable @code{mu4e-split-view}. Possible
+values are:
+
+@itemize
+@item @t{horizontal} (this is the default): display the message view below the
+header view. Use @code{mu4e-headers-visible-lines} the set the number of
+lines shown (default: 8).
+@item @t{vertical}: display the message view on the
+right side of the header view. Use @code{mu4e-headers-visible-columns} to set
+the number of visible columns (default: 30).
+@item @t{single-window}: single window mode. Single-window mode tries to
+minimize mu4e window operations (opening, killing, resizing, etc) and
+buffer changes, while still retaining the view and headers buffers. In
+addition, it replaces mu4e main view with a minibuffer prompt containing
+the same information.
+@item anything else: don't do any splitting
+@end itemize
+
+@noindent
+Some useful key bindings in the split view:
+@itemize
+@item @key{C-+} and @key{C--}: interactively change the number of columns or
+headers shown
+@item You can change the selected window from the
+headers-view to the message-view and vice-versa with
+@code{mu4e-select-other-view}, bound to @key{y}
+@end itemize
+
+
+@node Message view
+@chapter The message view
+
+This chapter discusses the message view; this is new (since version
+1.6) message view, based on Gnus' Article Mode, which replaces the
+older one.
+
+After selecting a message in the @ref{Headers view}, it appears in a
+message view window, which shows the message headers, followed by the
+message body. Its major mode is @code{mu4e-view-mode}, which derives
+from @t{gnus-article-mode}.
+
+@menu
+* Overview: MSGV Overview. What is the Message View
+* Keybindings: MSGV Keybindings. Do things with your keyboard
+* Rich-text and images: MSGV Rich-text and images. Reading rich-text messages
+* Custom headers: MSGV Custom headers. Your very own headers
+* Actions: MSGV Actions. Defining and using actions.
+@end menu
+
+@node MSGV Overview
+@section Overview
+
+An example message view:
+
+@cartouche
+@verbatim
+    From: randy@epiphyte.com
+    To: julia@eruditorum.org
+    Subject: Re: some pics
+    Flags: seen, attach
+    Date: Thu, 11 Feb 2021 12:59:30 +0200 (4 weeks, 3 days, 21 hours ago)
+    Maildir: /inbox
+    Attachments: [2. image/jpeg; DSCN4961.JPG]... [3. image/jpeg; DSCN4962.JPG]...
+
+    Hi Julia,
+
+    Some pics from our trip to Cerin Amroth. Enjoy!
+
+    All the best,
+    Randy.
+
+    On Sun 21 Dec 2003 09:06:34 PM EET, Julia wrote:
+
+    [....]
+@end verbatim
+@end cartouche
+
+Some notes:
+@itemize
+@item The variable @code{mu4e-view-fields} determines the header fields to be
+shown; see @code{mu4e-header-info} for a list of built-in fields. Apart
+from the built-in fields, you can also create custom fields using
+@code{mu4e-header-info-custom}; see @ref{MSGV Custom headers}.
+@item For search-related operations, see @ref{Searching}.
+@item You can scroll down the message using @key{SPC}; if you do this at the
+end of a message,it automatically takes you to the next one. If you want
+to prevent this behavior, set @code{mu4e-view-scroll-to-next} to
+@code{nil}.
+@end itemize
+
+@node MSGV Keybindings
+@section Keybindings
+
+You can find most things you can do with this message in the @emph{Mu4e}
+menu, or by using the keyboard; the default bindings are:
+
+@verbatim
+key          description
+==============================================================
+n,p          view the next, previous message
+],[          move to the next, previous unread message
+y            select the headers view (if it's visible)
+
+RET          scroll down
+M-RET        open URL at point / attachment at point
+
+SPC          scroll down, if at end, move to next message
+S-SPC        scroll up
+
+searching
+---------
+s            search
+S            edit last query
+/            narrow the search
+b            search bookmark
+B            edit bookmark before search
+j            jump to maildir
+
+M-left       previous query
+M-right      next query
+
+marking messages
+----------------
+d            mark for moving to the trash folder
+=            mark for removing trash flag ('untrash')
+DEL,D        mark for complete deletion
+m            mark for moving to another maildir folder
+r            mark for refiling
++,-          mark for flagging/unflagging
+
+u            unmark message at point
+U            unmark *all* messages
+
+%            mark based on a regular expression
+T,t          mark whole thread, subthread
+
+<insert>,*   mark for 'something' (decide later)
+#            resolve deferred 'something' marks
+
+x            execute actions for the marked messages
+
+composition
+-----------
+R,F,C        reply/forward/compose
+E            edit (only allowed for draft messages)
+
+actions
+-------
+g            go to (visit) numbered URL (using `browse-url')
+(or: <mouse-1> or M-RET with point on url)
+C-u g visits multiple URLs
+f            fetch (download )the numbered URL.
+C-u f fetches multiple URLs
+k            save the numbered URL in the kill-ring.
+C-u k saves multiple URLs
+
+e            extract (save) one or more attachments (asks for numbers)
+(or: <mouse-2> or S-RET with point on attachment)
+a            execute some custom action on the message
+A            execute some custom action on the message's MIME-parts
+
+misc
+----
+.            show the raw message view. 'q' takes you back.
+C-+,C--      increase / decrease the number of headers shown
+H            get help
+C-S-u        update mail & reindex
+q            leave the message view
+@end verbatim
+
+Furthermore, a number of keybindings are available through minor modes:
+@itemize
+@item Context; see @pxref{Contexts}.
+@end itemize
+
+For the marking commands, please refer to @ref{Marking messages}.
+
+@node MSGV Rich-text and images
+@section Reading rich-text messages
+
+These days, many e-mail messages contain rich-text (typically, HTML);
+either as an alternative to a text-only version, or even as the only
+option.
+
+By default, mu4e tries to display the 'richest' option, which is the
+last MIME-part of the alternatives. You can customize this to prefer
+the text version, if available, with something like the following in
+your configuration (and see the docstring for
+@t{mm-discouraged-alternatives} for details):
+
+@lisp
+(with-eval-after-load "mm-decode"
+  (add-to-list 'mm-discouraged-alternatives "text/html")
+  (add-to-list 'mm-discouraged-alternatives "text/richtext"))
+@end lisp
+
+When displaying rich-text messages inline, @t{mu4e} (through @t{gnus})
+uses the @t{shr} built-in HTML-renderer. If you're using a dark color
+theme, and the messages are hard to read, it can help to change the
+luminosity, e.g.:
+@lisp
+(setq shr-color-visible-luminance-min 80)
+@end lisp
+
+Note that you can switch between the HTML and text versions by
+clicking on the relevant part in the messages headers; you can make it
+even clearer by indicating them in the message itself, using:
+
+@lisp
+(setq gnus-unbuttonized-mime-types nil)
+@end lisp
+
+@subsection Inline images
+When you run Emacs in graphical mode, by default images attached to
+messages are shown inline in the message view buffer.
+
+To disable this, set @code{gnus-inhibit-images} to @t{t}.  By default,
+external images in HTML are not retrieved from external URLs because
+they can be used to track you.
+
+Apart from that, you can also control whether to load remote images;
+since loading remote images is often used for privacy violations, by
+default this is not allowed.
+
+You can specify what URLs to block by setting
+@code{gnus-blocked-images} to a regular expression or to a function
+that will receive a single parameter which is not meaningful for
+@t{mu4e}.
+
+For example, to enable images in Github notifications, you could use
+the following:
+
+@lisp
+(setq gnus-blocked-images
+   (lambda(&optional _ignore)
+     (if (mu4e-message-contact-field-matches
+         (mu4e-message-at-point) :from "notifications@@github.com")
+        nil ".")))
+@end lisp
+
+@code{mu4e} inherits the default @t{gnus-blocked-images} from Gnus and
+ensures that it works with @t{mu4e} too. However, mu4e is not Gnus, so
+if you have Gnus-specific settings for @t{gnus-blocked-images}, you
+should verify that they have the desired effect in @code{mu4e} as
+well.
+
+@node MSGV Custom headers
+@section Custom headers
+
+Sometimes the normal headers (Date, From, To, Subject, etc.)@: may not be
+enough. For these cases, @t{mu4e} offers @emph{custom headers} in both
+the headers-view and the message-view.
+
+See @ref{HV Custom headers} for an example of this; the difference for
+the message-view is that you should add your custom header to
+@code{mu4e-view-fields} rather than @code{mu4e-headers-fields}.
+
+@node MSGV Actions
+@section Actions
+
+You can perform custom functions (``actions'') on messages and their
+attachments. For a general discussion on how to define your own, see
+@ref{Actions}.
+
+@subsection Message actions
+@code{mu4e-view-action} (@key{a}) lets you pick some custom action to perform
+on the current message. You can specify these actions using the variable
+@code{mu4e-view-actions}; @t{mu4e} defines a number of example actions.
+
+@subsection MIME-part actions
+MIME-part actions allow you to act upon MIME-parts in a message - such
+as attachments.  For now, these actions are defined and documented in
+@code{mu4e-view-mime-part-actions}.
+
+
+@node Editor view
+@chapter The editor view
+
+Writing e-mail messages takes place in the Editor View. @t{mu4e}'s editor view
+builds on top of Gnus' @t{message-mode}. Most of the @t{message-mode}
+functionality is available, as well some @t{mu4e}-specifics. Its major mode is
+@code{mu4e-compose-mode}.
+
+@menu
+* Overview: EV Overview. What is the Editor view
+* Keybindings: EV Keybindings. Doing things with your keyboard
+* Address autocompletion:: Quickly entering known addresses
+* Compose hooks::Calling functions when composing
+* Signing and encrypting:: Support for cryptography
+* Queuing mail:: Sending mail when the time is ripe
+* Message signatures:: Adding your personal footer to messages
+* Other settings::Miscellanea
+@end menu
+
+@node EV Overview
+@section Overview
+
+@cartouche
+@verbatim
+    From: Rupert the Monkey <rupert@example.com>
+    To: Wally the Walrus <wally@example.com>
+    Subject: Re: Eau-qui d'eau qui?
+    --text follows this line--
+
+    On Mon 16 Jan 2012 10:18:47 AM EET, Wally the Walrus wrote:
+
+    > Hi Rupert,
+    >
+    > Dude - how are things?
+    >
+    > Later -- wally.
+@end verbatim
+@end cartouche
+
+@node EV Keybindings
+@section Keybindings
+
+@t{mu4e}'s editor view derives from Gnus' message editor and shares most of
+its keybindings. Here are some of the more useful ones (you can use the menu
+to find more):
+
+@verbatim
+key          description
+---          -----------
+C-c C-c      send message
+C-c C-d      save to drafts and leave
+C-c C-k      kill the message buffer (the message remains in the draft folder)
+C-c C-a      attach a file (pro-tip: drag & drop works as well)
+C-c C-;      switch the context
+
+(mu4e-specific)
+C-S-u        update mail & reindex
+@end verbatim
+
+@node Address autocompletion
+@section Address autocompletion
+
+@t{mu4e} supports@footnote{GNU Emacs 24.4 or higher is required}
+autocompleting addresses when composing e-mail messages. @t{mu4e} uses the
+e-mail addresses from the messages you sent or received as the source for
+this. Address auto-completion is enabled by default; if you want to disable it
+for some reason, set @t{mu4e-compose-complete-addresses} to @t{nil}.
+
+Emacs 24 also supports cycling through the alternatives. When there are more
+than @emph{5} matching addresses, they are shown in a @t{*Completions*}
+buffer. Once the number of matches gets below this number, one is inserted in
+the address field and you can cycle through the alternatives using @key{TAB}.
+
+@subsection Limiting the number of addresses
+
+If you have a lot of mail, especially from mailing lists and the like, there
+can be a @emph{lot} of e-mail addresses, many of which may not be very useful
+when auto-completing. For this reason, @t{mu4e} attempts to limit the number
+of e-mail addresses in the completion pool by filtering out the ones that are
+not likely to be relevant. The following variables are available for tuning
+this:
+
+@itemize
+@item @code{mu4e-compose-complete-only-personal} --- when set to @t{t},
+only consider addresses that were seen in @emph{personal} messages ---
+that is, messages in which one of my e-mail addresses was seen in one
+of the address fields. This is to exclude mailing list posts. You can
+define what is considered `my e-mail address' using the
+@t{--my-address} parameter to @t{mu init}.
+
+@item @code{mu4e-compose-complete-only-after} --- only consider e-mail
+addresses last seen after some date. Parameter is a string, parseable by
+@code{org-parse-time-string}. This excludes old e-mail addresses. The
+default is @t{"2010-01-01"}, i.e., only consider e-mail addresses seen
+since the start of 2010.
+@item @code{mu4e-compose-complete-max} -- the maximum number of contacts to use. This adds a hard limit to the 2000 (default) contacts; those are sorted by recency /frequency etc. so should include the ones you most likely need.
+@item @code{mu4e-contact-process-function} --- a function to rewrite or
+exclude certain addresses.
+@end itemize
+
+@node Compose hooks
+@section Compose hooks
+
+If you want to change some setting, or execute some custom action before
+message composition starts, you can define a @emph{hook function}. @t{mu4e}
+offers two hooks:
+@itemize
+@item @code{mu4e-compose-pre-hook}: this hook is run @emph{before} composition
+starts; if you are composing a @emph{reply}, @emph{forward} a message, or
+@emph{edit} an existing message, the variable
+@code{mu4e-compose-parent-message} points to the message being replied to,
+forwarded or edited, and you can use @code{mu4e-message-field} to get the
+value of various properties (and see @ref{Message functions}).
+@item @code{mu4e-compose-mode-hook}: this hook is run just before composition
+starts, when the whole buffer has already been set up. This is a good place
+for editing-related settings. @code{mu4e-compose-parent-message} (see above)
+is also at your disposal.
+@end itemize
+
+@noindent
+Let's look at some examples. First, suppose we want to set the
+@t{From:}-address for a reply message based on the receiver of the original:
+@lisp
+;; 1) messages to me@@foo.example.com should be replied with From:me@@foo.example.com
+;; 2) messages to me@@bar.example.com should be replied with From:me@@bar.example.com
+;; 3) all other mail should use From:me@@cuux.example.com
+(add-hook 'mu4e-compose-pre-hook
+  (defun my-set-from-address ()
+    "Set the From address based on the To address of the original."
+    (let ((msg mu4e-compose-parent-message)) ;; msg is shorter...
+      (when msg
+        (setq user-mail-address
+          (cond
+            ((mu4e-message-contact-field-matches msg :to "me@@foo.example.com")
+              "me@@foo.example.com")
+            ((mu4e-message-contact-field-matches msg :to "me@@bar.example.com")
+              "me@@bar.example.com")
+            (t "me@@cuux.example.com")))))))
+@end lisp
+
+Secondly, as mentioned, @code{mu4e-compose-mode-hook} is especially
+useful for editing-related settings. For example:
+@lisp
+(add-hook 'mu4e-compose-mode-hook
+  (defun my-do-compose-stuff ()
+    "My settings for message composition."
+    (set-fill-column 72)
+    (flyspell-mode)))
+@end lisp
+
+This hook is also useful for adding headers or changing headers, since the
+message is fully formed when this hook runs. For example, to add a
+@t{Bcc:}-header, you could add something like the following, using
+@code{message-add-header} from @code{message-mode}.
+
+@lisp
+(add-hook 'mu4e-compose-mode-hook
+  (defun my-add-bcc ()
+    "Add a Bcc: header."
+    (save-excursion (message-add-header "Bcc: me@@example.com\n"))))
+@end lisp
+
+Or to something context-specific:
+
+@lisp
+(add-hook 'mu4e-compose-mode-hook
+  (lambda()
+    (let* ((ctx (mu4e-context-current))
+            (name (if ctx (mu4e-context-name ctx))))
+      (when name
+        (cond
+          ((string= name "account1")
+            (save-excursion (message-add-header "Bcc: account1@@example.com\n")))
+          ((string= name "account2")
+            (save-excursion (message-add-header "Bcc: account2@@example.com\n"))))))))
+@end lisp
+
+@noindent
+For a more general discussion about extending @t{mu4e}, see @ref{Extending
+mu4e}.
+
+@node Signing and encrypting
+@section Signing and encrypting
+
+Signing and encrypting of messages is possible using @t{emacs-mime},
+most easily accessed through the @t{Attachments}-menu while composing a
+message, or with @kbd{M-x mml-secure-message-encrypt-pgp}, @kbd{M-x
+mml-secure-message-sign-pgp}.
+
+Important note: the messages are encrypted when they are @emph{sent}:
+this means that draft messages are @emph{not} encrypted. So if you are
+using e.g. @t{offlineimap} or @t{mbsync} to synchronize with some
+remote IMAP-service, make sure the drafts folder is @emph{not} in the
+set of synchronized folders, for obvious reasons.
+
+@node Queuing mail
+@section Queuing mail
+
+If you cannot send mail right now, for example because you are
+currently offline, you can @emph{queue} the mail, and send it when you
+have restored your internet connection. You can control this from the
+@ref{Main view}.
+
+To allow for queuing, you need to tell @t{smtpmail} where you want to store
+the queued messages. For example:
+
+@lisp
+(setq smtpmail-queue-mail t  ;; start in queuing mode
+  smtpmail-queue-dir   "~/Maildir/queue/cur")
+@end lisp
+
+For convenience, we put the queue directory somewhere in our normal
+maildir. If you want to use queued mail, you should create this directory
+before starting @t{mu4e}. The @command{mu mkdir} command may be useful here,
+so for example:
+
+@verbatim
+  $ mu mkdir ~/Maildir/queue
+  $ touch ~/Maildir/queue/.noindex
+@end verbatim
+
+The file created by the @command{touch} command tells @t{mu} to ignore this
+directory for indexing, which makes sense since it contains @t{smtpmail}
+meta-data rather than normal messages; see the @t{mu-mkdir} and @t{mu-index}
+man-pages for details.
+
+@emph{Warning}: when you switch on queued-mode, your messages @emph{won't}
+reach their destination until you switch it off again; so, be careful not to
+do this accidentally!
+
+@node Message signatures
+@section Message signatures
+
+Message signatures are the standard footer blobs in e-mail messages where you
+can put in information you want to include in every message. The text to
+include is set with @code{mu4e-compose-signature}.
+
+If you don't want to include this automatically with each message,
+you can set @code{mu4e-compose-signature-auto-include} to @code{nil}; you can
+then still include the signature manually, using the function
+@code{message-insert-signature}, typically bound to @kbd{C-c C-w}.
+
+@node Other settings
+@section Other settings
+
+@itemize
+@item If you want use @t{mu4e} as Emacs' default program for sending mail,
+see @ref{Emacs default}.
+@item Normally, @t{mu4e} @emph{buries} the message buffer after sending; if you want
+to kill the buffer instead, add something like the following to your
+configuration:
+@lisp
+(setq message-kill-buffer-on-exit t)
+@end lisp
+@item If you want to exclude your own e-mail address when ``replying to
+all'', set @code{mu4e-compose-dont-reply-to-self} to @code{t}. In
+order for this to work properly you need to pass your address to
+@command{mu init --my-address=} at database initialization time.
+@end itemize
+
+@node Searching
+@chapter Searching
+
+@t{mu4e} is fully search-based: even if you `jump to a folder', you are
+executing a query for messages that happen to have the property of being in a
+certain folder (maildir).
+
+Normally, queries return up to @code{mu4e-headers-results-limit} (default:
+500) results. That is usually more than enough, and makes things significantly
+faster. Sometimes, however, you may want to show @emph{all} results; you can
+enable this with @kbd{M-x mu4e-headers-toggle-full-search}, or by customizing
+the variable @code{mu4e-headers-full-search}. This applies to all search
+commands.
+
+You can also influence the sort order and whether threads are shown or not;
+see @ref{Sorting and threading}.
+
+@menu
+* Queries:: Searching for messages.
+* Bookmarks:: Remembering queries.
+* Maildir searches:: Queries for maildirs.
+* Other search functionality:: Some more tricks.
+@end menu
+
+@node Queries
+@section Queries
+
+@t{mu4e} queries are the same as the ones that @t{mu find}
+understands@footnote{with the caveat that command-line queries are
+subject to the shell's interpretation before @t{mu} sees them}. You can
+consult the @code{mu-query} man page for the details.
+
+Additionally, @t{mu4e} supports @kbd{TAB}-completion for queries. There
+there is completion for all search keywords such as @code{and},
+@code{from:}, or @code{date:} and also for certain values, i.e., the
+possible values for @code{flag:}, @code{prio:}, @code{mime:}, and
+@code{maildir:}.
+
+Let's look at some examples here.
+
+@itemize
+
+@item Get all messages regarding @emph{bananas}:
+@verbatim
+bananas
+@end verbatim
+
+@item Get all messages regarding @emph{bananas} from @emph{John} with an attachment:
+@verbatim
+from:john and flag:attach and bananas
+@end verbatim
+
+@item Get all messages with subject @emph{wombat} in June 2017
+@verbatim
+subject:wombat and date:20170601..20170630
+@end verbatim
+
+@item Get all messages with PDF attachments in the @t{/projects} folder
+@verbatim
+maildir:/projects and mime:application/pdf
+@end verbatim
+
+@item Get all messages about @emph{Rupert} in the @t{/Sent Items} folder. Note that
+maildirs with spaces must be quoted.
+@verbatim
+"maildir:/Sent Items" and rupert
+@end verbatim
+
+@item Get all important messages which are signed:
+@verbatim
+flag:signed and prio:high
+@end verbatim
+
+@item Get all messages from @emph{Jim} without an attachment:
+@verbatim
+from:jim and not flag:attach
+@end verbatim
+
+@item Get all messages with Alice in one of the contacts-fields (@t{to}, @t{from},
+@t{cc}, @t{bcc}):
+@verbatim
+contact:alice
+@end verbatim
+
+@item Get all unread messages where the subject mentions Ångström: (search is
+case-insensitive and accent-insensitive, so this matches Ångström, angstrom,
+aNGstrøM, ...)
+@verbatim
+subject:Ångström and flag:unread
+@end verbatim
+
+@item Get all unread messages between Mar-2012 and Aug-2013 about some bird:
+@verbatim
+date:20120301..20130831 and nightingale and flag:unread
+@end verbatim
+
+@item Get today's messages:
+@verbatim
+date:today..now
+@end verbatim
+
+@item Get all messages we got in the last two weeks regarding @emph{emacs}:
+@verbatim
+date:2w.. and emacs
+@end verbatim
+
+@item Get messages from the @emph{Mu} mailing list:
+@verbatim
+list:mu-discuss.googlegroups.com
+@end verbatim
+
+Note --- in the @ref{Headers view} you may see the `friendly name' for a
+list; however, when searching you need the real name. You can see the
+real name for a mailing list from the friendly name's tool-tip.
+
+@item Get messages with a subject soccer, Socrates, society, ...; note that
+the `*'-wildcard can only appear as a term's rightmost character:
+@verbatim
+subject:soc*
+@end verbatim
+
+@item Get all messages @emph{not} sent to a mailing-list:
+@verbatim
+NOT flag:list
+@end verbatim
+
+@item Get all mails with attachments with filenames starting with @emph{pic}; note
+that the `*' wildcard can only appear as the term's rightmost character:
+@verbatim
+file:pic*
+@end verbatim
+
+@item Get all messages with PDF-attachments:
+@verbatim
+mime:application/pdf
+@end verbatim
+
+Get all messages with image attachments, and note that the `*' wildcard can
+only appear as the term's rightmost character:
+@verbatim
+mime:image/*
+@end verbatim
+
+Get all messages with files that end in @t{.ppt}; this uses the
+regular-expression support, which is powerful but relatively slow:
+@verbatim
+file:/\.ppt$/
+@end verbatim
+
+@end itemize
+
+@node Bookmarks
+@section Bookmarks
+
+If you have queries that you use often, you may want to store them as
+@emph{bookmarks}. Bookmark searches are available in the main view
+(@pxref{Main view}), header view (@pxref{Headers view}), and message
+view (@pxref{Message view}), using (by default) the key @key{b}
+(@kbd{M-x mu4e-search-bookmark}), or @key{B} (@kbd{M-x
+mu4e-search-bookmark-edit}) which lets you edit the bookmark first.
+
+@subsection Setting up bookmarks
+
+@t{mu4e} provides a number of default bookmarks. Their definition may
+be instructive:
+
+@lisp
+(defcustom mu4e-bookmarks
+  '(( :name  "Unread messages"
+      :query "flag:unread AND NOT flag:trashed"
+      :key ?u)
+    ( :name "Today's messages"
+      :query "date:today..now"
+      :key ?t)
+    ( :name "Last 7 days"
+      :query "date:7d..now"
+      :hide-unread t
+      :key ?w)
+    ( :name "Messages with images"
+      :query "mime:image/*"
+      :key ?p))
+  "List of pre-defined queries that are shown on the main screen.
+
+Each of the list elements is a plist with at least:
+:name  - the name of the query
+:query - the query expression
+:key   - the shortcut key.
+
+Optionally, you add the following:
+:hide  - if t, bookmark is hdden from the main-view and speedbar.
+:hide-unread - do not show the counts of unread/total number
+ of matches for the query. This can be useful if a bookmark uses
+ a very slow query. :hide-unread is implied from :hide.
+"
+  :type '(repeat (plist))
+  :group 'mu4e)
+@end lisp
+
+You can replace these or add your own items, by putting in your
+configuration (@file{~/.emacs}) something like:
+@lisp
+(add-to-list 'mu4e-bookmarks
+  '( :name  "Big messages"
+     :query "size:5M..500M"
+     :key   ?b))
+ @end lisp
+
+This prepends your bookmark to the list, and assigns the key @key{b} to it. If
+you want to @emph{append} your bookmark, you can use @code{t} as the third
+argument to @code{add-to-list}.
+
+In the various @t{mu4e} views, pressing @key{b} lists all the bookmarks
+defined in the echo area, with the shortcut key highlighted. So, to invoke the
+bookmark we just defined (to get the list of "Big Messages"), all you need to
+type is @kbd{bb}.
+
+@subsection Lisp expressions or functions as bookmarks
+
+Instead of using strings, it is also possible to use Lisp expressions as
+bookmarks. Either the expression evaluates to a query string or the expression
+is a function taking no argument that returns a query string.
+
+For example, to get all the messages that are at most a week old in your
+inbox:
+
+@lisp
+(add-to-list 'mu4e-bookmarks
+  '( :name  "Inbox messages in the last 7 days"
+     :query (lambda () (concat "maildir:/inbox AND date:"
+            (format-time-string "%Y%m%d"
+                (subtract-time (current-time) (days-to-time 7)))))
+     :key   ?w) t)
+@end lisp
+
+Another example where the user is prompted how many days old messages should be
+shown:
+
+@lisp
+(defun my/mu4e-bookmark-num-days-old-query (days-old)
+  (interactive (list (read-number "Show days old messages: " 7)))
+  (let ((start-date (subtract-time (current-time) (days-to-time days-old))))
+    (concat "maildir:/inbox AND date:"
+            (format-time-string "%Y%m%d" start-date))))
+
+(add-to-list 'mu4e-bookmarks
+  `(:name  "Inbox messages in the last 7 days"
+    :query ,(lambda () (call-interactively 'my/mu4e-bookmark-num-days-old-query))
+    :key  ?o) t)
+@end lisp
+
+It is defining a function to make the code more readable.
+
+@subsection Editing bookmarks before searching
+
+There is also @kbd{M-x mu4e-headers-search-bookmark-edit} (key @key{B}), which
+lets you edit the bookmarked query before invoking it. This can be useful if
+you have many similar queries, but need to change some parameter. For example,
+you could have a bookmark @samp{"date:today..now AND "}@footnote{Not a valid
+search query by itself}, which limits any result to today's messages.
+
+@node Maildir searches
+@section Maildir searches
+
+Maildir searches are quite similar to bookmark searches (see @ref{Bookmarks}),
+with the difference being that the target is always a maildir --- maildir
+queries provide a `traditional' folder-like interface to a search-based e-mail
+client. By default, maildir searches are available in the @ref{Main view},
+@ref{Headers view}, and @ref{Message view}, with the key @key{j}
+(@code{mu4e-jump-to-maildir}). If a prefix argument is given, the maildir
+query can be refined before execution.
+
+@subsection Setting up maildir shortcuts
+
+You can search for maildirs like any other message property
+(e.g. with a query like @t{maildir:/myfolder}), but since it is so common,
+@t{mu4e} offers a shortcut for this.
+
+For this to work, you need to set the variable
+@code{mu4e-maildir-shortcuts} to the list of maildirs you want to have
+quick access to, for example:
+
+@lisp
+(setq mu4e-maildir-shortcuts
+  '( (:maildir "/inbox"     :key  ?i)
+     (:maildir "/archive"   :key  ?a)
+     (:maildir "/lists"     :key  ?l)
+     (:maildir "/work"      :key  ?w)
+     (:maildir "/sent"      :key  ?s)))
+@end lisp
+
+This sets @key{i} as a shortcut for the @t{/inbox} folder --- effectively a
+query @t{maildir:/inbox}. There is a special shortcut @key{o} or @key{/} for
+@emph{other} (so don't use those for your own shortcuts!), which allows you to
+choose from @emph{all} maildirs that you have. There is support for
+autocompletion; note that the list of maildirs is determined when @t{mu4e}
+starts; if there are changes in the maildirs while @t{mu4e} is running, you
+need to restart @t{mu4e}.
+
+Each of the folder names is relative to your top-level maildir directory; so
+if you keep your mail in @file{~/Maildir}, @file{/inbox} would refer to
+@file{~/Maildir/inbox}. With these shortcuts, you can jump around your
+maildirs (folders) very quickly --- for example, getting to the @t{/lists}
+folder only requires you to type @kbd{jl}, then change to @t{/work} with
+@kbd{jw}.
+
+While in queries you need to quote folder names (maildirs) with spaces in
+them, you should @emph{not} quote them when used in
+@code{mu4e-maildir-shortcuts}, since @t{mu4e} does that automatically for you.
+
+The very same shortcuts are used by @kbd{M-x mu4e-mark-for-move} (default
+shortcut @key{m}); so, for example, if you want to move a message to the
+@t{/archive} folder, you can do so by typing @kbd{ma}.
+
+@node Other search functionality
+@section Other search functionality
+
+@subsection Navigating through search queries
+You can navigate through previous/next queries using
+@code{mu4e-headers-query-prev} and @code{mu4e-headers-query-next}, which are
+bound to @key{M-left} and @key{M-right}, similar to what some web browsers do.
+
+@t{mu4e} tries to be smart and not record duplicate queries. Also, the number
+of queries remembered has a fixed limit, so @t{mu4e} won't use too much
+memory, even if used for a long time. However, if you want to forget
+previous/next queries, you can use @kbd{M-x mu4e-headers-forget-queries}.
+
+@subsection Narrowing search results
+
+It can be useful to narrow existing search results, that is, to add some
+clauses to the current query to match fewer messages.
+
+For example, suppose you're looking at some mailing list, perhaps by
+jumping to a maildir (@kbd{M-x mu4e-headers-jump-to-maildir}, @key{j}) or
+because you followed some bookmark (@kbd{M-x mu4e-headers-search-bookmark},
+@key{b}). Now, you want to narrow things down to only those messages that have
+attachments.
+
+This is when @kbd{M-x mu4e-headers-search-narrow} (@key{/}) comes in handy. It
+asks for an additional search pattern, which is appended to the current search
+query, in effect getting you the subset of the currently shown headers that
+also match this extra search pattern. @key{\} takes you back to the previous
+query, so, effectively `widens' the search. Technically, narrowing the results
+of query @t{x} with expression @t{y} implies doing a search @t{(x) AND (y)}.
+
+Note that messages that were not in your original search results because
+of @code{mu4e-headers-results-limit} may show up in the narrowed query.
+
+@subsection Including related messages
+@anchor{Including related messages}
+
+It can be useful to not only show the messages that directly match a certain
+query, but also include messages that are related to these messages. That is,
+messages that belong to the same discussion threads are included in the
+results, just like e.g. Gmail does it. You can enable this behavior by setting
+@code{mu4e-headers-include-related} to @code{t}, and you can toggle between
+including/not-including with @key{W}.
+
+Be careful though when e.g. deleting ranges of messages from a certain
+folder --- the list may now also include messages from @emph{other}
+folders.
+
+@subsection Skipping duplicates
+@anchor{Skipping duplicates}
+
+Another useful feature is skipping of @emph{duplicate messages}. When you have
+copies of messages, there's usually little value in including more than one in
+search results. A common reason for having multiple copies of messages is the
+combination of Gmail and @t{offlineimap}, since that is the way the labels /
+virtual folders in Gmail are represented. You can enable skipping duplicates
+by setting @code{mu4e-headers-skip-duplicates} to @code{t}, and you can toggle
+between the skipping/not skipping with @key{V}.
+
+Note, messages are considered duplicates when they have the same
+@t{Message-Id}.
+
+@node Marking
+@chapter Marking
+
+In @t{mu4e}, the common way to do things with messages is a two-step process -
+first you @emph{mark} them for a certain action, then you @emph{execute}
+(@key{x}) those marks. This is similar to the way @t{dired} operates. Marking
+can happen in both the @ref{Headers view} and the @ref{Message view}.
+
+@menu
+* Marking messages::Selecting message do something with them
+* What to mark for::What can we do with them
+* Executing the marks::Do it
+* Trashing messages::Exceptions for mailboxes like Gmail
+* Leaving the headers buffer::Handling marks automatically when leaving
+* Built-in marking functions::Helper functions for dealing with them
+* Custom mark functions::Define your own mark function
+* Adding a new kind of mark::Adding your own marks
+@end menu
+
+@node Marking messages
+@section Marking messages
+
+There are multiple ways to mark messages:
+@itemize
+@item @emph{message at point}: you can put a mark on the message-at-point in
+either the @ref{Headers view} or @ref{Message view}
+@item @emph{region}: you can put a mark on all messages in the current region
+(selection) in the @ref{Headers view}
+@item @emph{pattern}: you can put a mark on all messages in the @ref{Headers
+view} matching a certain pattern with @kbd{M-x mu4e-headers-mark-pattern}
+(@key{%})
+@item @emph{thread/subthread}: You can put a mark on all the messages in the
+thread/subthread at point with @kbd{M-x mu4e-headers-mark-thread} and @kbd{M-x
+mu4e-headers-mark-subthread}, respectively
+@end itemize
+
+@node What to mark for
+@section What to mark for
+
+@t{mu4e} supports a number of marks:
+
+@cartouche
+@verbatim
+mark for/as  | keybinding  | description
+-------------+-------------+------------------------------
+'something'  | *, <insert> | mark now, decide later
+delete       | D, <delete> | delete
+flag         | +           | mark as 'flagged' ('starred')
+move         | m           | move to some maildir
+read         | !           | mark as read
+refile       | r           | mark for refiling
+trash        | d           | move to the trash folder
+untrash      | =           | remove 'trash' flag
+unflag       | -           | remove 'flagged' mark
+unmark       | u           | remove mark at point
+unmark all   | U           | remove all marks
+unread       | ?           | marks as unread
+action       | a           | apply some action
+@end verbatim
+@end cartouche
+
+After marking a message, the left-most columns in the headers view indicate
+the kind of mark. This is informative, but if you mark many (say, thousands)
+messages, this slows things down significantly@footnote{this uses an
+Emacs feature called @emph{overlays}, which are slow when used a lot
+in a buffer}. For this reason, you can disable this by setting
+@code{mu4e-headers-show-target} to @code{nil}.
+
+@t{something} is a special kind of mark; you can use it to mark messages
+for `something', and then decide later what the `something' should
+be@footnote{This kind of `deferred marking' is similar to the facility
+in @t{dired}, @t{midnight commander}
+(@url{https://www.midnight-commander.org/}) and the like, and uses the
+same key binding (@key{insert}).}  Later, you can set the actual mark
+using @kbd{M-x mu4e-mark-resolve-deferred-marks}
+(@key{#}). Alternatively, @t{mu4e} will ask you when you try to execute
+the marks (@key{x}).
+
+@node Executing the marks
+@section Executing the marks
+
+After you have marked some messages, you can execute them with @key{x}
+(@kbd{M-x mu4e-mark-execute-all}).
+
+A hook, @code{mu4e-mark-execute-pre-hook}, is available which is run
+right before execution of each mark. The hook is called with two
+arguments, the mark and the message itself.
+
+@node Trashing messages
+@section Trashing messages
+
+For regular mailboxes, trashing works like other marks: when executed,
+the message is flagged as trashed.  Depending on your mailbox provider,
+the trash flag is used to automatically move the message to the trash
+folder (@code{mu4e-trash-folder}) for instance.
+
+Some mailboxes behave differently however and they don't interpret the
+trash flag.  In cases like Gmail, the message must be @emph{moved} to
+the trash folder and the trash flag must not be used.
+
+@node Leaving the headers buffer
+@section Leaving the headers buffer
+
+When you quit or update a headers buffer that has marked messages (for
+example, by doing a new search), @t{mu4e} asks you what to do with them,
+depending on the value of the variable @code{mu4e-headers-leave-behavior} ---
+see its documentation.
+
+@node Built-in marking functions
+@section Built-in marking functions
+
+Some examples of @t{mu4e}'s built-in marking functions.
+
+@itemize
+@item @emph{Mark the message at point for trashing}: press @key{d}
+@item @emph{Mark all messages in the buffer as unread}: press @kbd{C-x h o}
+@item @emph{Delete the messages in the current thread}: press @kbd{T D}
+@item @emph{Mark messages with a subject matching ``hello'' for flagging}:
+press @kbd{% s hello RET}.
+@end itemize
+
+@node Custom mark functions
+@section Custom mark functions
+
+Sometimes, the built-in functions to mark messages may not be sufficient for
+your needs. For this, @t{mu4e} offers an easy way to define your own custom
+mark functions. You can choose one of the custom marker functions by pressing
+@key{&} in the @ref{Headers view} and @ref{Message view}.
+
+Custom mark functions are to be appended to the list
+@code{mu4e-headers-custom-markers}. Each of the elements of this list
+('markers') is a list with two or three elements:
+@enumerate
+@item The name of the marker --- a short string describing this marker. The
+first character of this string determines its shortcut, so these should be
+unique. If necessary, simply prefix the name with a unique character.
+@item a predicate function, taking two arguments @var{msg} and @var{param}.
+@var{msg} is the message plist (see @ref{Message functions}) and @var{param} is
+a parameter provided by the third of the marker elements (see the next
+item). The predicate function should return non-@t{nil} if the message
+matches.
+@item (optionally) a function that is evaluated once, and the result is passed as a
+parameter to the predicate function. This is useful when user-input is needed.
+@end enumerate
+
+Let's look at an example: suppose we want to match all messages that have more
+than @emph{n} recipients --- we could do this with the following recipe:
+
+@lisp
+(add-to-list 'mu4e-headers-custom-markers
+  '("More than n recipients"
+     (lambda (msg n)
+       (> (+ (length (mu4e-message-field msg :to))
+            (length (mu4e-message-field msg :cc))) n))
+     (lambda ()
+       (read-number "Match messages with more recipients than: "))) t)
+@end lisp
+
+After evaluating this expression, you can use it by pressing @key{&} in
+the headers buffer to select a custom marker function, and then @key{M}
+to choose this particular one (@t{M} because it is the first character
+of the description).
+
+As you can see, it's not very hard to define simple functions to match
+messages. There are more examples in the defaults for
+@code{mu4e-headers-custom-markers}; see @file{mu4e-headers.el} and see
+@ref{Extending mu4e} for general information about writing your own functions.
+
+
+@node Adding a new kind of mark
+@section Adding a new kind of mark
+
+It is possible to configure new marks. To do so, one can add entries
+in the list @code{mu4e-marks}. Such an element must have the following form:
+
+@lisp
+(SYMBOL
+  :char STRING
+  :prompt STRING
+  :ask-target (lambda () TARGET)
+  :dyn-target (lambda (TARGET MSG) DYN-TARGET)
+  :show-target (lambda (DYN-TARGET) STRING)
+  :action (lambda (DOCID MSG DYN-TARGET) nil))
+@end lisp
+
+The symbol can be any symbol, except for @code{'unmark} and
+@code{'something}, which are reserved. The rest is a plist with the
+following elements:
+
+@itemize
+@item @code{:char} --- the character to display in the headers view.
+@item @code{:prompt} --- the prompt to use when asking for marks
+(used for example when marking a whole thread).
+@item @code{:ask-target} --- a function run once per bulk-operation, and thus suitable for
+querying the user about a target for move-like marks. If @t{nil}, the
+@t{TARGET} passed to @code{:dyn-target} is @t{nil}.
+@item @code{:dyn-target} --- a function run once per message
+(The message is passed as @t{MSG} to the function). This function allows
+to compute a per-message target, for refile-like marks. If @t{nil}, the
+@t{DYN-TARGET} passed to the @code{:action} is the @t{TARGET} obtained as above.
+@item @code{:show-target} --- how to display the target in the headers view.
+If @code{:show-target} is @t{nil} the @t{DYN-TARGET} is shown (and
+@t{DYN-TARGET} must be a string).
+@item @code{:action} --- the action to apply on the message when the mark is executed.
+@end itemize
+
+As an example, suppose we would like to add a mark for tagging
+messages (GMail-style), then we can run the following code (after
+loading @t{mu4e}):
+
+@lisp
+(add-to-list 'mu4e-marks
+  '(tag
+     :char       "g"
+     :prompt     "gtag"
+     :ask-target (lambda () (read-string "What tag do you want to add?"))
+     :action      (lambda (docid msg target)
+                    (mu4e-action-retag-message msg (concat "+" target)))))
+@end lisp
+
+As another example, suppose we would like to ``archive and mark read''
+a message (GMail-style), then we can run the following code (after
+loading @t{mu4e}):
+
+@lisp
+(add-to-list 'mu4e-marks
+  '(archive
+     :char       "A"
+     :prompt     "Archive"
+     :show-target (lambda (target) "archive")
+     :action      (lambda (docid msg target)
+                    ;; must come before proc-move since retag runs
+                    ;; 'sed' on the file
+                    (mu4e-action-retag-message msg "-\\Inbox")
+                    (mu4e--server-move docid nil "+S-u-N"))))
+@end lisp
+
+Adding to @code{mu4e-marks} list allows to use the mark in bulk operations
+(for example when tagging a whole thread), but does not bind the mark
+to a key to use at the top-level. This must be done separately. In our
+example:
+
+@lisp
+(mu4e~headers-defun-mark-for tag)
+(mu4e~headers-defun-mark-for archive)
+(define-key mu4e-headers-mode-map (kbd "g") 'mu4e-headers-mark-for-tag)
+(define-key mu4e-headers-mode-map (kbd "A") 'mu4e-headers-mark-for-archive)
+@end lisp
+
+@node Contexts
+@chapter Contexts
+
+@menu
+* What are contexts::Defining the concept
+* Context policies::How to determine the current context
+* Contexts and special folders::Using context variables to determine them
+* Contexts example::How to define contexts
+@end menu
+
+It can be useful to switch between different sets of settings in
+@t{mu4e}; a typical example is the case where you have different e-mail
+accounts for private and work email, each with their own values for
+folders, e-mail addresses, mailservers and so on.
+
+The @code{mu4e-context} system is a @t{mu4e}-specific mechanism to allow
+for that; users can define different @i{contexts} corresponding with
+groups of setting and either manually switch between them, or let
+@t{mu4e} determine the right context based on some user-provided
+function.
+
+Note that there are a number of existing ways to switch accounts in
+@t{mu4e}, for example using the method described in the @ref{Tips and
+Tricks} section of this manual. Those still work --- but the new mechanism
+has the benefit of being a core part of @code{mu4e}, thus allowing for
+deeper integration.
+
+@node What are contexts
+@section What are contexts
+
+Let's see what's contained in a context. Most of it is optional.
+
+A @code{mu4e-context} is Lisp object with the following members:
+@itemize
+@item @t{name}: the name of the context, e.g. @t{work} or @t{private}
+@item @t{vars}:
+an association-list (alist) of variable settings for this account.
+@item @t{enter-func}:
+an (optional) function that takes no parameter and is invoked when entering
+the context. You can use this for extra setup etc.
+@item @t{leave-func}:
+an (optional) function that takes no parameter and is invoked when leaving
+the context. You can use this for clearing things up.
+@item @t{match-func}:
+an (optional) function that takes an @t{MSG} message plist as argument,
+and returns non-@t{nil} if this context matches the situation. @t{mu4e}
+uses the first context that matches, in a couple of situations:
+@itemize
+@item when starting @t{mu4e} to determine the
+starting context; in this case, @t{MSG} is nil. You can use e.g. the
+host you're running or the time of day to determine which context
+matches.
+@item before replying to or forwarding a
+message with the given message plist as parameter, or @t{nil} when
+composing a brand new message. The function should return @t{t} when
+this context is the right one for this message, or @t{nil} otherwise.
+@item when determining the target folders for deleting, refiling etc;
+see @ref{Contexts and special folders}.
+@end itemize
+@end itemize
+
+@t{mu4e} uses a variable @code{mu4e-contexts}, which is a list of such
+objects.
+
+@node Context policies
+@section Context policies
+
+When you have defined contexts and you start @t{mu4e} it decides which
+context to use based on the variable @code{mu4e-context-policy};
+similarly, when you compose a new message, the context is determined
+using @code{mu4e-compose-context-policy}.
+
+For both of these, you can choose one of the following policies:
+@itemize
+@item a symbol @code{always-ask}: unconditionally ask the user what context to pick.
+@end itemize
+
+The other choices @b{only apply if none of the contexts match} (i.e.,
+none of the contexts' match-functions returns @code{t}). We have the
+following options:
+
+@itemize
+@item a symbol @code{ask}: ask the user if @t{mu4e} can't figure
+things out the context by itself (through the match-function). This is a
+good policy if there are no match functions, or if the match functions
+don't cover all cases.
+@item a symbol @code{ask-if-none}: if there's already a context, don't change it;
+otherwise, ask the user.
+@item a symbol @code{pick-first}: pick the first (default) context. This is a
+good choice if
+you want to specify context for special case, and fall back to the first
+one if none match.
+@item @code{nil}: don't change the context; this is useful if you don't change
+contexts very often, and e.g. manually changes contexts with @kbd{M-x
+mu4e-context-switch}.
+@end itemize
+
+@node Contexts and special folders
+@section Contexts and special folders
+
+As we discussed in @ref{Folders} and @ref{Dynamic folders}, @t{mu4e}
+recognizes a number of special folders: @code{mu4e-sent-folder},
+@code{mu4e-drafts-folder}, @code{mu4e-trash-folder} and
+@code{mu4e-refile-folder}.
+
+When you have a headers-buffer with messages that belong to different
+contexts (say, a few different accounts), it is desirable for each of
+them to use the specific folders for their own context --- so, for
+instance, if you trash a message, it needs to go to the trash-folder for
+the account it belongs to, which is not necessarily the current context.
+
+To make this easy to do, whenever @t{mu4e} needs to know the value for
+such a special folder for a given message, it tries to determine the
+appropriate context using @code{mu4e-context-determine} (and policy
+@t{nil}; see @ref{Context policies}). If it finds a matching context, it
+let-binds the @code{vars} for that account, and then determines the
+value for the folder. It does not, however, call the @code{enter-func}
+or @code{leave-func}, since we are not really switching contexts.
+
+In practice, this means that as long as each of the accounts has a good
+@t{match-func}, all message operations automatically find the
+appropriate folders.
+
+@node Contexts example
+@section Example
+
+Let's explain how contexts work by looking at an example. We define two
+contexts, `Private' and `Work' for a fictional user @emph{Alice
+Derleth}.
+
+Note that in this case, we automatically switch to the first context
+when starting; see the discussion in the previous section.
+
+@lisp
+
+ (setq mu4e-contexts
+    `( ,(make-mu4e-context
+          :name "Private"
+          :enter-func (lambda () (mu4e-message "Entering Private context"))
+          :leave-func (lambda () (mu4e-message "Leaving Private context"))
+          ;; we match based on the contact-fields of the message
+          :match-func (lambda (msg)
+                        (when msg
+                          (mu4e-message-contact-field-matches msg
+                            :to "aliced@@home.example.com")))
+          :vars '( ( user-mail-address     . "aliced@@home.example.com"  )
+                   ( user-full-name        . "Alice Derleth" )
+                   ( message-user-organization . "Homebase" )
+                   ( mu4e-compose-signature .
+                     (concat
+                       "Alice Derleth\n"
+                       "Lauttasaari, Finland\n"))))
+       ,(make-mu4e-context
+          :name "Work"
+          :enter-func (lambda () (mu4e-message "Switch to the Work context"))
+          ;; no leave-func
+          ;; we match based on the maildir of the message
+          ;; this matches maildir /Arkham and its sub-directories
+          :match-func (lambda (msg)
+                        (when msg
+                          (string-match-p "^/Arkham" (mu4e-message-field msg :maildir))))
+          :vars '( ( user-mail-address      . "aderleth@@miskatonic.example.com" )
+                   ( user-full-name         . "Alice Derleth" )
+                   ( message-user-organization . "Miskatonic University" )
+                   ( mu4e-compose-signature  .
+                     (concat
+                       "Prof. Alice Derleth\n"
+                       "Miskatonic University, Dept. of Occult Sciences\n"))))
+
+       ,(make-mu4e-context
+          :name "Cycling"
+          :enter-func (lambda () (mu4e-message "Switch to the Cycling context"))
+          ;; no leave-func
+          ;; we match based on the maildir of the message; assume all
+          ;; cycling-related messages go into the /cycling maildir
+          :match-func (lambda (msg)
+                        (when msg
+                          (string= (mu4e-message-field msg :maildir) "/cycling")))
+          :vars '( ( user-mail-address      . "aderleth@@example.com" )
+                   ( user-full-name         . "AliceD" )
+                   ( mu4e-compose-signature  . nil)))))
+
+  ;; set `mu4e-context-policy` and `mu4e-compose-policy` to tweak when mu4e should
+  ;; guess or ask the correct context, e.g.
+
+  ;; start with the first (default) context;
+  ;; default is to ask-if-none (ask when there's no context yet, and none match)
+  ;; (setq mu4e-context-policy 'pick-first)
+
+  ;; compose with the current context is no context matches;
+  ;; default is to ask
+  ;; (setq mu4e-compose-context-policy nil)
+@end lisp
+
+A couple of notes about this example:
+@itemize
+@item You can manually switch the context use @code{M-x mu4e-context-switch},
+by default bound to @kbd{;} in headers, view and main mode.
+The current context appears in the mode-line.
+@item Normally, @code{M-x mu4e-context-switch} does not call the enter or
+leave functions if the 'new' context is the same as the old one.
+However, with a prefix-argument (@kbd{C-u}), you can force @t{mu4e} to
+invoke those function even in that case.
+@item The function @code{mu4e-context-current} returns the current-context;
+the current context is also visible in the mode-line when in
+headers, view or main mode.
+@item You can set any kind of variable; including settings for mail servers etc.
+However, settings such as @code{mu4e-mu-home} are not changeable after
+they have been set without quitting @t{mu4e} first.
+@item @code{leave-func} (if defined) for the context we are leaving, is invoked
+before the @code{enter-func} (if defined) of the
+context we are entering.
+@item @code{enter-func} (if defined) is invoked before setting the variables.
+@item @code{match-func} (if defined) is invoked just before @code{mu4e-compose-pre-hook}.
+@item See the variables @code{mu4e-context-policy} and
+@code{mu4e-compose-context-policy} to tweak what @t{mu4e} should do when
+no context matches (or if you always want to be asked).
+@item Finally, be careful to get the quotations right --- backticks, single quotes
+and commas and note the '.' between variable name and its value.
+@end itemize
+
+@node Dynamic folders
+@chapter Dynamic folders
+
+In @ref{Folders}, we explained how you can set up @t{mu4e}'s special
+folders:
+@lisp
+(setq
+  mu4e-sent-folder   "/sent"       ;; sent messages
+  mu4e-drafts-folder "/drafts"     ;; unfinished messages
+  mu4e-trash-folder  "/trash"      ;; trashed messages
+  mu4e-refile-folder "/archive")   ;; saved messages
+@end lisp
+
+In some cases, having such static folders may not suffice --- perhaps you want
+to change the folders depending on the context. For example, the folder for
+refiling could vary, based on the sender of the message.
+
+To make this possible, instead of setting the standard folders to a string,
+you can set them to be a @emph{function} that takes a message as its
+parameter, and returns the desired folder name. This chapter shows you how to
+do that. For a more general discussion of how to extend @t{mu4e} and writing
+your own functions, see @ref{Extending mu4e}.
+
+If you use @t{mu4e-context}, see @ref{Contexts and special folders} for
+what that means for these special folders.
+
+@menu
+* Smart refiling:: Automatically choose the target folder
+* Other dynamic folders:: Flexible folders for sent, trash, drafts
+@end menu
+
+
+@node Smart refiling
+@section Smart refiling
+
+When refiling messages, perhaps to archive them, it can be useful to have
+different target folders for different messages, based on some property of
+those message --- smart refiling.
+
+To accomplish this, we can set the refiling folder (@code{mu4e-refile-folder})
+to a function that returns the actual refiling folder for the particular
+message. An example should clarify this:
+
+@lisp
+(setq mu4e-refile-folder
+  (lambda (msg)
+    (cond
+      ;; messages to the mu mailing list go to the /mu folder
+      ((mu4e-message-contact-field-matches msg :to
+         "mu-discuss@@googlegroups.com")
+        "/mu")
+      ;; messages sent directly to some spefic address me go to /private
+      ((mu4e-message-contact-field-matches msg :to "me@@example.com")
+        "/private")
+      ;; messages with football or soccer in the subject go to /football
+      ((string-match "football\\|soccer"
+         (mu4e-message-field msg :subject))
+        "/football")
+      ;; messages sent by me go to the sent folder
+      ((mu4e-message-sent-by-me msg
+         (mu4e-personal-addresses))
+        mu4e-sent-folder)
+      ;; everything else goes to /archive
+      ;; important to have a catch-all at the end!
+      (t  "/archive"))))
+@end lisp
+
+@noindent
+This can be very powerful; you can select some messages in the headers view,
+then press @key{r}, and have them all marked for refiling to their particular
+folders.
+
+Some notes:
+@itemize
+@item We set @code{mu4e-refile-folder} to an anonymous (@t{lambda}) function. This
+function takes one argument, a message plist@footnote{a property list
+describing a message}. The plist corresponds to the message at point. See
+@ref{Message functions} for a discussion on how to deal with them.
+@item In our function, we use a @t{cond} control structure; the function
+returns the first of the clauses that matches. It's important to make the last
+clause a catch-all, so we always return @emph{some} folder.
+@item We use
+the convenience function @code{mu4e-message-contact-field-matches},
+which evaluates to @code{t} if any of the names or e-mail addresses in a
+contact field (in this case, the @t{To:}-field) matches the regular
+expression. With @t{mu4e} version 0.9.16 or newer, the contact field can
+in fact be a list instead of a single value, such as @code{'(:to :cc)'}.
+@end itemize
+
+@node Other dynamic folders
+@section Other dynamic folders
+
+Using the same mechanism, you can create dynamic sent-, trash-, and
+drafts-folders. The message-parameter you receive for the sent and drafts
+folder is the @emph{original} message, that is, the message you reply to, or
+forward, or edit. If there is no such message (for example when composing a
+brand new message) the message parameter is @t{nil}.
+
+Let's look at an example. Suppose you want a different trash folder for
+work-email. You can achieve this with something like:
+
+@lisp
+(setq mu4e-trash-folder
+(lambda (msg)
+;; the 'and msg' is to handle the case where msg is nil
+(if (and msg
+(mu4e-message-contact-field-matches msg :to "me@@work.example.com"))
+"/trash-work"
+"/trash")))
+@end lisp
+
+@noindent
+Good to remember:
+@itemize
+@item The @var{msg} parameter you receive in the function refers to the
+@emph{original message}, that is, the message being replied to or
+forwarded. When re-editing a message, it refers to the message being
+edited. When you compose a totally new message, the @var{msg} parameter is
+@code{nil}.
+@item When re-editing messages, the value of @code{mu4e-drafts-folder} is ignored.
+@end itemize
+
+
+@node Actions
+@chapter Actions
+
+@t{mu4e} lets you define custom actions for messages in @ref{Headers view}
+and for both messages and attachments in @ref{Message view}. Custom
+actions allow you to easily extend @t{mu4e} for specific needs --- for example,
+marking messages as spam in a spam filter or applying an attachment with a
+source code patch.
+
+You can invoke the actions with key @key{a} for actions on messages, and key
+@key{A} for actions on attachments.
+
+For general information extending @t{mu4e} and writing your own functions, see
+@ref{Extending mu4e}.
+
+@menu
+* Defining actions::How to create an action
+* Headers view actions::Doing things with message headers
+* Message view actions::Doing things with messages
+* MIME-part actions::Doing things with MIME-parts such as attachments
+* Example actions::Some more examples
+@end menu
+
+@node Defining actions
+@section Defining actions
+
+Defining a new custom action comes down to writing an elisp-function to do the
+work. Functions that operate on messages receive a @var{msg} parameter, which
+corresponds to the message at point. Something like:
+@lisp
+(defun my-action-func (msg)
+  "Describe my message function."
+  ;; do stuff
+  )
+@end lisp
+
+@noindent
+Functions that operate on attachments receive a @var{msg} parameter, which
+corresponds to the message at point, and an @var{attachment-num}, which is the
+number of the attachment as seen in the message view. An attachment function
+looks like:
+@lisp
+(defun my-attachment-action-func (msg attachment-num)
+  "Describe my attachment function."
+  ;; do stuff
+  )
+@end lisp
+
+@noindent
+After you have defined your function, you can add it to the list of
+actions@footnote{Instead of defining the functions separately, you can
+obviously also add a @code{lambda}-function directly to the list; however,
+separate functions are easier to change}, either @code{mu4e-headers-actions},
+@code{mu4e-view-actions} or @code{mu4e-view-mime-part-actions}.  The
+format@footnote{Note, the format of the actions has changed since version
+0.9.8.4, and you must change your configuration to use the new format;
+@t{mu4e} warns you when you are using the old format.} of each action is a
+cons-cell, @code{(DESCRIPTION . VALUE)}; see below for some examples. If your
+shortcut is not also the first character of the description, simply prefix the
+description with that character.
+
+Let's look at some examples.
+
+@node Headers view actions
+@section Headers view actions
+
+Suppose we want to inspect the number of recipients for a message in the
+@ref{Headers view}. We add the following to our configuration:
+
+@lisp
+(defun show-number-of-recipients (msg)
+  "Display the number of recipients for the message at point."
+  (message "Number of recipients: %d"
+    (+ (length (mu4e-message-field msg :to))
+      (length (mu4e-message-field msg :cc)))))
+
+;; define 'N' (the first letter of the description) as the shortcut
+;; the 't' argument to add-to-list puts it at the end of the list
+(add-to-list 'mu4e-headers-actions
+  '("Number of recipients" . show-number-of-recipients) t)
+@end lisp
+
+After evaluating this, @kbd{a N} in the headers view shows the number of
+recipients for the message at point.
+
+@node Message view actions
+@section Message view actions
+
+As another example, suppose we would like to search for messages by the
+sender of the message at point:
+
+@lisp
+(defun search-for-sender (msg)
+  "Search for messages sent by the sender of the message at point."
+  (mu4e-headers-search
+    (concat "from:"
+      (mu4e-contact-email (car (mu4e-message-field msg :from))))))
+
+;; define 'x' as the shortcut
+(add-to-list 'mu4e-view-actions
+  '("xsearch for sender" . search-for-sender) t)
+@end lisp
+
+@indent
+If you wonder why we use @code{car}, remember that the @t{From:}-field
+is a list of @code{(:name NAME :email EMAIL)} plists; so this code gets
+us the e-mail address of the first in the list. @t{From:}-fields rarely
+have more that one address.
+
+@node MIME-part actions
+@section MIME-part actions
+
+Finally, let's define a MIME-part action. As mentioned, MIME-part
+functions receive @emph{2} arguments, the message and the attachment
+number to use.
+
+The following example action counts the number of lines in an
+attachment, and defines @key{n} as its shortcut key (the @key{n} is
+prefixed to the description).
+
+@lisp
+(defun count-lines-in-attachment (msg attachnum)
+  "Count the number of lines in an attachment."
+  (mu4e-view-pipe-attachment msg attachnum "wc -l"))
+
+;; defining 'n' as the shortcut
+(add-to-list 'mu4e-view-mime-part-actions
+  '("ncount lines" . count-lines-in-attachment) t)
+@end lisp
+
+@node Example actions
+@section Example actions
+
+@t{mu4e} includes a number of example actions in the file
+@file{mu4e-actions.el} in the source distribution (see @kbd{C-h f
+mu4e-action-TAB}). For example, for viewing messages in an external web
+browser.
+
+@node Extending mu4e
+@chapter Extending mu4e
+
+@t{mu4e} is designed to be easily extensible --- that is, write your own
+emacs-lisp to make @t{mu4e} behave exactly as you want. Here, we provide some
+guidelines for doing so.
+
+@menu
+* Extension points::Where to hook into @t{mu4e}
+* Available functions::General helper functions
+* Message functions::Working with messages
+* Contact functions::Working with contacts
+* Utility functions::Miscellaneous helpers
+@end menu
+
+@node Extension points
+@section Extension points
+
+There are a number of places where @t{mu4e} lets you plug in your own
+functions:
+@itemize
+@item Custom functions for message header --- see @ref{HV Custom headers}
+@item Using message-specific folders for drafts, trash, sent messages and
+refiling, based on a function --- see @ref{Dynamic folders}
+@item Using an attachment-specific download-directory --- see the
+variable @code{mu4e-attachment-dir}.
+@item Apply a function to a message in the headers view -
+see @ref{Headers view actions}
+@item Apply a function to a message in the message view ---
+see @ref{Message view actions}
+@item Add a new kind of mark for use in the headers view
+- see @ref{Adding a new kind of mark}
+@item Apply a function to a MIME-part --- see @ref{MIME-part actions}
+@item Custom function to mark certain messages ---
+see @ref{Custom mark functions}
+@item Using various @emph{mode}-hooks, @code{mu4e-compose-pre-hook} (see
+@ref{Compose hooks}), @code{mu4e-index-updated-hook} (see @ref{FAQ})
+@end itemize
+
+@noindent
+You can also write your own functions without using the above. If you
+want to do so, key useful functions are @code{mu4e-message-at-point}
+(see below), @code{mu4e-headers-for-each} (to iterate over all
+headers, see its docstring) and @code{mu4e-view-for-each-part} (to
+iterate over all parts/attachments, see its docstring). There is also
+@code{mu4e-view-for-each-uri} to iterate of all the URIs in the
+current message.
+
+Another useful function is
+@code{mu4e-headers-find-if} which searches for a message matching a
+certain pattern; again, see its docstring.
+
+@node Available functions
+@section Available functions
+
+The whole of @t{mu4e} consists of hundreds of elisp functions. However,
+the majority of those are for @emph{internal} use only; you can
+recognize them easily, because they all start with @code{mu4e~} or
+@code{mu4e--}. These functions make all kinds of assumptions, and they
+are subject to change, and should therefore @emph{not} be used. The same
+is true for @emph{variables} with the same prefix; don't touch them. Let
+me repeat that:
+@verbatim
+Do not use mu4e~... or mu4e-- functions or variables!
+@end verbatim
+
+@noindent
+In addition, you should use functions in the right context; functions
+that start with @t{mu4e-view-} are only applicable to the message view,
+while functions starting with @t{mu4e-headers-} are only applicable to
+the headers view. Functions without such prefixes are applicable
+everywhere.
+
+@node Message functions
+@section Message functions
+
+Many functions in @t{mu4e} deal with message plists (property
+lists). They contain information about messages, such as sender and
+recipient, subject, date and so on. To deal with these plists, there are
+a number of @code{mu4e-message-} functions (in @file{mu4e-message.el}),
+such as @code{mu4e-message-field} and @code{mu4e-message-at-point}, and
+a shortcut to combine the two, @code{mu4e-message-field-at-point}.
+
+For example, to get the subject of the message at point, in either the headers
+view or the message view, you could write:
+@lisp
+(mu4e-message-field (mu4e-message-at-point) :subject)
+@end lisp
+@noindent
+Note that:
+@itemize
+@item The contact fields (To, From, Cc, Bcc) are lists of cons-pairs
+@code{(name . email)}; @code{name} may be @code{nil}. So, for example:
+@lisp
+  (mu4e-message-field some-msg :to)
+  ;; => (("Jack" . "jack@@example.com") (nil . "foo@@example.com"))
+@end lisp
+
+If you are only looking for a match in this list (e.g., ``Is Jack one of the
+recipients of the message?''), there is a convenience function
+@code{mu4e-message-contact-field-matches} to make this easy.
+@item The message body is only available in the message view, not in the
+headers view.
+@end itemize
+
+Note that in headers-mode, you only have access to a reduced message
+plist, without the information about the message-body or mime-parts;
+@t{mu4e} does this for performance reasons. And even in view-mode, you
+do not have access to arbitrary message-headers.
+
+However, it is possible to get the information indirectly, using the
+raw-message and some third-party tool like @t{procmail}'s @t{formail}:
+
+@lisp
+(defun my-mu4e-any-message-field-at-point (hdr)
+  "Quick & dirty way to get an arbitrary header HDR at
+point. Requires the 'formail' tool from procmail."
+  (replace-regexp-in-string "\n$" ""
+    (shell-command-to-string
+      (concat "formail -x " hdr " -c < "
+        (shell-quote-argument (mu4e-message-field-at-point :path))))))
+@end lisp
+
+@node Contact functions
+@section Contact functions
+
+It can sometimes be useful to discard or rewrite the contact information
+that @t{mu4e} provides, for example to fix spelling errors, or omit
+unwanted contacts.
+
+To handle this, @t{mu4e} provides @code{mu4e-contact-process-function},
+which, if defined, is applied to each contact. If the result is @t{nil},
+the contact is discarded, otherwise the (modified or not) contact
+information is used.
+
+Each contact is a full e-mail address as you would see in a
+contact-field of an e-mail message, e.g.,
+@verbatim
+"Foo Bar" <foo.bar@example.com>
+@end verbatim
+or
+@verbatim
+cuux@example.com
+@end verbatim
+
+An example @code{mu4e-contact-process-function} might look like:
+
+@lisp
+(defun my-contact-processor (contact)
+  (cond
+    ;; remove unwanted
+    ((string-match-p "evilspammer@@example.com" contact) nil)
+    ((string-match-p "noreply" contact) nil)
+    ;;
+    ;; jonh smiht --> John Smith
+    ((string-match "jonh smiht" contact)
+       (replace-regexp-in-string "jonh smiht" "John Smith" contact))
+    (t contact)))
+
+(setq mu4e-contact-process-function 'my-contact-processor)
+@end lisp
+
+
+@node Utility functions
+@section Utility functions
+
+@file{mu4e-utils} contains a number of utility functions; we list a few
+here. See their docstrings for details:
+@itemize
+@item @code{mu4e-read-option}: read one option from a list. For example:
+@lisp
+(mu4e-read-option "Choose an animal: "
+'(("Monkey" . monkey) ("Gnu" . gnu) ("xMoose" . moose)))
+@end lisp
+The user is presented with:
+@example
+Choose an animal: [M]onkey, [G]nu, [x]Moose
+@end example
+@item @code{mu4e-ask-maildir}: ask for a maildir; try one of the
+shortcuts (@code{mu4e-maildir-shortcuts}), or the full set of available
+maildirs.
+@item @code{mu4e-running-p}: return @code{t} if the @t{mu4e} process is
+running, @code{nil} otherwise.
+@item @code{(mu4e-user-mail-address-p addr)}: return @code{t} if @var{addr} is
+one of the user's e-mail addresses (as per @code{(mu4e-personal-addresses)}).
+@item @code{mu4e-log} logs to the @t{mu4e} debugging log if it is enabled;
+see @code{mu4e-toggle-logging}.
+@item @code{mu4e-message}, @code{mu4e-warning}, @code{mu4e-error} are the
+@t{mu4e} equivalents of the normal elisp @code{message},
+@code{user-error} and @code{error} functions.
+@end itemize
+
+@node Other tools
+@appendix Other tools
+
+In this chapter, we discuss some ways in which @t{mu4e} can cooperate
+with other tools.
+
+@menu
+* Emacs default::Making mu4e the default emacs e-mail program
+* Emacs bookmarks::Using Emacs' bookmark system
+* Org-mode links::Adding mu4e to your organized life
+* Org-contacts::Hooking up with org-contacts
+* BBDB::Hooking up with the Insidious Big Brother Database
+* iCalendar::Enabling iCalendar invite processing
+* Sauron::Getting new mail notifications with Sauron
+* Speedbar::A special frame with your folders
+* Dired:: Attaching files using @t{dired}
+* Hydra:: Custom shortcut menus
+@end menu
+
+@node Emacs default
+@section Emacs default
+
+Emacs allows you to select an e-mail program as the default
+program it uses when you press @key{C-x m} (@code{compose-mail}), call
+@code{report-emacs-bug} and so on. If you want to use @t{mu4e} for this,
+you can do so by adding the following to your configuration:
+
+@lisp
+(setq mail-user-agent 'mu4e-user-agent)
+@end lisp
+
+Similarly, to specify @t{mu4e} as your preferred method for reading
+mail, customize the variable @code{read-mail-command}.
+
+@lisp
+(set-variable 'read-mail-command 'mu4e)
+@end lisp
+
+(@pxref{Top,,Emacs,Sending Mail, Mail Methods})
+
+
+@node Emacs bookmarks
+@section Emacs bookmarks
+
+@t{mu4e} supports linking to the message-at-point through the normal
+Emacs built-in bookmark system. The links are based on the message's
+message-id, and thus the bookmarks stay valid even if you move the
+message around.
+
+
+@node Org-mode links
+@section Org-mode links
+
+It can be useful to include links to e-mail messages or search queries
+in your org-mode files. @t{mu4e} supports this by default, unless you
+set @t{mu4e-support-org} to @code{nil}.
+
+You can use the normal @t{org-mode} mechanisms to store links:
+@kbd{M-x org-store-link} stores a link to a particular message when
+you are in @ref{Message view}. When you are in @ref{Headers view},
+@kbd{M-x org-store-link} links to the @emph{query} if
+@code{mu4e-org-link-query-in-headers-mode} is non-@code{nil}, and to
+the particular message otherwise (which is the default). You can
+customize the link description using @code{mu4e-org-link-desc-func}.
+
+You can insert this link later with @kbd{M-x org-insert-link}. From
+@t{org-mode}, you can go to the query or message the link points to
+with either @kbd{M-x org-agenda-open-link} in agenda buffers, or
+@kbd{M-x org-open-at-point} elsewhere --- both typically bound to
+@kbd{C-c C-o}.
+
+You can also directly @emph{capture} such links --- for example, to
+add e-mail messages to your todo-list. For that, @t{mu4e-org} has a
+function @code{mu4e-org-store-and-capture}. This captures the
+message-at-point (or header --- see the discussion on
+@code{mu4e-org-link-query-in-headers-mode} above), then calls
+@t{org-mode}'s capture functionality.
+
+You can add some specific capture-template for this. In your capture
+templates, the following mu4e-specific values are available:
+
+@cartouche
+@verbatim
+item                                                 | description
+-----------------------------------------------------+------------------------
+%:date, %:date-timestamp, %d:date-timestamp-inactive | date, org timestamps
+%:from, %:fromname, %:fromaddress                    | sender, name/address
+%:to, %:toname, %:toaddress                          | recipient, name/address
+%:maildir                                            | maildir for the message
+%:message-id                                         | message-id
+%:path                                               | file system path
+%:subject                                            | message subject
+@end verbatim
+@end cartouche
+
+For example, to add a message to your todo-list, and set a deadline
+for processing it within two days, you could add this to
+@code{org-capture-templates}:
+
+@lisp
+  ("P" "process-soon" entry (file+headline "todo.org" "Todo")
+  "* TODO %:fromname: %a %?\nDEADLINE: %(org-insert-time-stamp (org-read-date nil t \"+2d\"))")
+@end lisp
+
+If you use the functionality a lot, you may want to define
+key-bindings for that in headers and view mode:
+
+@lisp
+  (define-key mu4e-headers-mode-map (kbd "C-c c") 'mu4e-org-store-and-capture)
+  (define-key mu4e-view-mode-map    (kbd "C-c c") 'mu4e-org-store-and-capture)
+@end lisp
+
+
+@node Org-contacts
+@section Org-contacts
+
+Note, @t{mu4e} supports built-in address autocompletion; @ref{Address
+autocompletion}, and that is the recommended way to do this. However, it
+is also possible to manage your addresses with @t{org-mode}, using
+@t{org-contacts}@footnote{@url{https://julien.danjou.info/projects/emacs-packages#org-contacts}}.
+
+@t{mu4e-actions} defines a useful action (@ref{Actions}) for adding a
+contact based on the @t{From:}-address in the message at point. To
+enable this, add to your configuration something like:
+
+@lisp
+  (setq mu4e-org-contacts-file  <full-path-to-your-org-contacts-file>)
+  (add-to-list 'mu4e-headers-actions
+    '("org-contact-add" . mu4e-action-add-org-contact) t)
+  (add-to-list 'mu4e-view-actions
+    '("org-contact-add" . mu4e-action-add-org-contact) t)
+@end lisp
+
+@noindent
+After this, you should be able to add contacts using @key{a o} in the
+headers view and the message view, using the @t{org-capture} mechanism.
+Note, the shortcut character @key{o} is due to the first character of
+@t{org-contact-add}.
+
+@node BBDB
+@section BBDB
+
+Note, @t{mu4e} supports built-in address autocompletion; @ref{Address
+autocompletion}, and that is the recommended way to do this. However, it
+is also possible to manage your addresses with the current (2015-06-23)
+development release of @t{BBDB}, or releases of @t{BBDB} after
+3.1.2.@footnote{@url{https://savannah.nongnu.org/projects/bbdb/}}.
+
+To enable BBDB, add to your @file{~/.emacs} (or its moral equivalent,
+such as @file{~/.emacs.d/init.el}) the following @emph{after} the
+@code{(require 'mu4e)} line:
+
+@lisp
+  ;; Load BBDB (Method 1)
+  (require 'bbdb-loaddefs)
+  ;; OR (Method 2)
+  ;; (require 'bbdb-loaddefs "/path/to/bbdb/lisp/bbdb-loaddefs.el")
+  ;; OR (Method 3)
+  ;; (autoload 'bbdb-insinuate-mu4e "bbdb-mu4e")
+  ;; (bbdb-initialize 'message 'mu4e)
+
+  (setq bbdb-mail-user-agent 'mu4e-user-agent)
+  (setq mu4e-view-mode-hook 'bbdb-mua-auto-update)
+  (setq mu4e-compose-complete-addresses nil)
+  (setq bbdb-mua-pop-up t)
+  (setq bbdb-mua-pop-up-window-size 5)
+  (setq mu4e-view-show-addresses t)
+@end lisp
+
+@noindent
+After this, you should be able to:
+@itemize
+@item In mu4e-view mode, add the sender of the email to BBDB with @key{C-u :}
+@item Tab-complete addresses from BBDB when composing emails
+@item View the BBDB contact while viewing a message
+@end itemize
+
+@node iCalendar
+@section iCalendar
+
+When Gnus' article-mode is chosen (@ref{Message view}), it is possible
+to view and reply to iCalendar events. To enable this feature, add
+
+@lisp
+(require 'mu4e-icalendar)
+(mu4e-icalendar-setup)
+@end lisp
+
+to your configuration. If you want that the original invitation message
+be automatically trashed after sending the message created by clicking
+on the buttons “Accept”, “Tentative”, or “Decline”, also add:
+
+@lisp
+(setq mu4e-icalendar-trash-after-reply t)
+@end lisp
+
+When you reply to an iCal event, a line may be automatically added to
+the diary file of your choice. You can specify that file with
+
+@lisp
+(setq mu4e-icalendar-diary-file "/path/to/your/diary")
+@end lisp
+
+Note that, if the specified file is not your main diary file, add
+@t{#include "/path/to/your/diary"} to you main diary file to display
+the events.
+
+To enable optional iCalendar→Org sync functionality, add the following:
+
+@lisp
+(setq gnus-icalendar-org-capture-file "~/org/notes.org")
+(setq gnus-icalendar-org-capture-headline '("Calendar"))
+(gnus-icalendar-org-setup)
+@end lisp
+
+Both the capture file and the headline(s) inside it must already exist.
+
+By default, @code{gnus-icalendar-org-setup} adds a temporary capture
+template to the variable @code{org-capture-templates}, with the
+description ``used by gnus-icalendar-org'', and the shortcut key ``#''.
+If you want to use your own template, create it using the same key and
+description. This will prevent the temporary one from being installed
+next time you @code{gnus-icalendar-org-setup} is called.
+
+The full default capture template is:
+
+@lisp
+("#" "used by gnus-icalendar-org" entry
+   (file+olp ,gnus-icalendar-org-capture-file
+             ,gnus-icalendar-org-capture-headline)
+   "%i" :immediate-finish t)
+@end lisp
+
+where the values of the variables @code{gnus-icalendar-org-capture-file}
+and @code{gnus-icalendar-org-capture-headline} are inserted via macro
+expansion.
+
+If, for example, you wanted to store ical events in a date tree,
+prompting for the date, you could use the following:
+
+@lisp
+("#" "used by gnus-icalendar-org" entry
+   (file+olp+datetree path-to-capture-file)
+   "%i" :immediate-finish t :time-prompt t)
+@end lisp
+
+Note that the default behaviour for @code{datetree} targets in this
+situation is to store the event at the date that you capture it, not at
+the date that it is scheduled. That's why I've suggested using the
+@code{:timeprompt t} argument. This gives you an opportunity to set the
+time to the correct value yourself.
+
+You can extract the event time directly, and have the @code{org-capture}
+functions use that to set the @code{datetree} location:
+
+@lisp
+(defun my-catch-event-time (orig-fun &rest args)
+  "Set org-overriding-default-time to the start time of the capture event"
+  (let ((org-overriding-default-time (date-to-time
+                                      (gnus-icalendar-event:start (car args)))))
+    (apply orig-fun args)))
+
+(advice-add 'gnus-icalendar:org-event-save :around #'my-catch-event-time)
+@end lisp
+
+If you do this, you'll want to omit the @code{:timeprompt t} setting
+from your capture template.
+
+@node Sauron
+@section Sauron
+
+The Emacs package @t{sauron}@footnote{Sauron can be found at
+@url{https://github.com/djcb/sauron}, or in the MELPA (Milkypostman’s
+Emacs Lisp Package Archive) package repository at
+@url{https://melpa.org/}} (by the same author) can be used to
+get notifications about new mails. If you run something like the below
+script from your @t{crontab} (or have some other way of having it
+execute every @emph{n} minutes), you receive notifications in the
+@t{sauron}-buffer when new messages arrive.
+
+@verbatim
+#!/bin/sh
+
+# the mu binary
+MU=mu
+
+# put the path to your Inbox folder here
+CHECKDIR="/home/$LOGNAME/Maildir/Inbox"
+
+sauron_msg () {
+DBUS_COOKIE="/home/$LOGNAME/.sauron-dbus"
+if test "x$DBUS_SESSION_BUS_ADDRESS" = "x"; then
+  if test -e $DBUS_COOKIE; then
+    export DBUS_SESSION_BUS_ADDRESS="`cat $DBUS_COOKIE`"
+  fi
+fi
+if test -n "x$DBUS_SESSION_BUS_ADDRESS"; then
+  dbus-send --session                          \
+    --dest="org.gnu.Emacs"                     \
+    --type=method_call                         \
+      "/org/gnu/Emacs/Sauron"                  \
+      "org.gnu.Emacs.Sauron.AddMsgEvent"       \
+      string:shell uint32:3 string:"$1"
+fi
+}
+
+#
+# -mmin -5: consider only messages that were created / changed in the
+# the last 5 minutes
+#
+for f in `find $CHECKDIR -mmin -5 -a -type f -not -iname '.uidvalidity'`; do
+  subject=`$MU view $f | grep '^Subject:' | sed 's/^Subject://'`
+  sauron_msg "mail: $subject"
+done
+@end verbatim
+
+@noindent
+You might want to put:
+@lisp
+(setq sauron-dbus-cookie t)
+@end lisp
+@noindent
+in your setup, to allow the script to find the D-Bus session bus, even when
+running outside its session.
+
+@node Speedbar
+@section Speedbar
+
+@code{speedbar} is an Emacs-extension that shows navigational
+information for an Emacs buffer in a separate frame. Using
+@code{mu4e-speedbar}, @t{mu4e} lists your bookmarks and maildir
+folders and allows for one-click access to them.
+
+To enable this, add @t{(require 'mu4e-speedbar)} to your configuration;
+then, all you need to do to activate it is @kbd{M-x speedbar}. Then,
+when then switching to the @ref{Main view}, the speedbar-frame is
+updated with your bookmarks and maildirs.
+
+For speed reasons, the list of maildirs is determined when @t{mu4e}
+starts; if the list of maildirs changes while @t{mu4e} is running, you
+need to restart @t{mu4e} to have those changes reflected in the speedbar
+and in other places that use this list, such as auto-completion when
+jumping to a maildir.
+
+@node Dired
+@section Dired
+
+It is possible to attach files to @t{mu4e} messages using @t{dired}
+(@ref{Dired,,emacs}), using the following steps (based on a post on
+the @t{mu-discuss} mailing list by @emph{Stephen Eglen}).
+
+
+@lisp
+(add-hook 'dired-mode-hook 'turn-on-gnus-dired-mode)
+@end lisp
+
+Then, mark the file(s) in @t{dired} you would like to attach and press
+@t{C-c RET C-a}, and you'll be asked whether to attach them to an
+existing message, or create a new one.
+
+@node Hydra
+@section Hydra
+
+People sometimes ask about having multi-character shortcuts for
+bookmarks; an easy way to achieve this, is by using an emacs package
+called Hydra@footnote{@url{https://github.com/abo-abo/hydra}}.
+
+With Hydra installed, we can add multi-character shortcuts, for instance:
+@lisp
+(defhydra my-mu4e-bookmarks-work (:color blue)
+  "work bookmarks"
+  ("b" (mu4e-headers-search "banana AND maildir:/work") "banana")
+  ("u" (mu4e-headers-search "flag:unread AND maildir:/work")   "unread"))
+
+(defhydra my-mu4e-bookmarks-personal (:color blue)
+  "personal bookmarks"
+  ("c" (mu4e-headers-search "capybara AND maildir:/personal") "capybara")
+  ("u" (mu4e-headers-search "flag:unread AND maildir:/personal")  "unread"))
+
+(defhydra my-mu4e-bookmarks (:color blue)
+  "mu4e bookmarks"
+  ("p" (my-mu4e-bookmarks-personal/body) "Personal")
+  ("w" (my-mu4e-bookmarks-work/body) "Work"))
+
+Now, you can bind a convenient key to my-mu4e-bookmarks/body.
+@end lisp
+
+@node Example configs
+@appendix Example configs
+
+In this chapter, we show some example configurations. While it is very useful
+to see some working settings, we'd like to warn against blindly copying such
+things.
+
+@menu
+* Minimal configuration::Simplest configuration to get you going
+* Longer configuration::A more extensive setup
+* Gmail configuration::GMail-specific setup
+* Other settings:CONF Other settings. Some other useful configuration
+
+@end menu
+
+@node Minimal configuration
+@section Minimal configuration
+
+An (almost) minimal configuration for @t{mu4e} might look like this --- as you
+see, most of it is commented-out.
+
+@lisp
+;; example configuration for mu4e
+
+;; make sure mu4e is in your load-path
+(require 'mu4e)
+
+;; use mu4e for e-mail in emacs
+(setq mail-user-agent 'mu4e-user-agent)
+
+;; these must start with a "/", and must exist
+;; (i.e.. /home/user/Maildir/sent must exist)
+;; you use e.g. 'mu mkdir' to make the Maildirs if they don't
+;; already exist
+
+;; below are the defaults; if they do not exist yet, mu4e offers to
+;; create them. they can also functions; see their docstrings.
+;; (setq mu4e-sent-folder   "/sent")
+;; (setq mu4e-drafts-folder "/drafts")
+;; (setq mu4e-trash-folder  "/trash")
+
+;; smtp mail setting; these are the same that `gnus' uses.
+(setq
+   message-send-mail-function   'smtpmail-send-it
+   smtpmail-default-smtp-server "smtp.example.com"
+   smtpmail-smtp-server         "smtp.example.com"
+   smtpmail-local-domain        "example.com")
+@end lisp
+
+
+@node Longer configuration
+@section Longer configuration
+
+A somewhat longer configuration, showing some more things that you can
+customize.
+
+@lisp
+;; example configuration for mu4e
+(require 'mu4e)
+
+;; use mu4e for e-mail in emacs
+(setq mail-user-agent 'mu4e-user-agent)
+
+;; the next are relative to the root maildir
+;; (see `mu info`).
+;; instead of strings, they can be functions too, see
+;; their docstring or the chapter 'Dynamic folders'
+(setq mu4e-sent-folder   "/sent"
+      mu4e-drafts-folder "/drafts"
+      mu4e-trash-folder  "/trash")
+
+;; the maildirs you use frequently; access them with 'j' ('jump')
+(setq   mu4e-maildir-shortcuts
+    '((:maildir "/archive" :key ?a)
+      (:maildir "/inbox"   :key ?i)
+      (:maildir "/work"    :key ?w)
+      (:maildir "/sent"    :key ?s)))
+
+;; the headers to show in the headers list -- a pair of a field
+;; and its width, with `nil' meaning 'unlimited'
+;; (better only use that for the last field.
+;; These are the defaults:
+(setq mu4e-headers-fields
+    '( (:date          .  25)    ;; alternatively, use :human-date
+       (:flags         .   6)
+       (:from          .  22)
+       (:subject       .  nil))) ;; alternatively, use :thread-subject
+
+;; program to get mail; alternatives are 'fetchmail', 'getmail'
+;; isync or your own shellscript. called when 'U' is pressed in
+;; main view.
+
+;; If you get your mail without an explicit command,
+;; use "true" for the command (this is the default)
+(setq mu4e-get-mail-command "offlineimap")
+
+;; general emacs mail settings; used when composing e-mail
+;; the non-mu4e-* stuff is inherited from emacs/message-mode
+(setq mu4e-compose-reply-to-address "foo@@bar.example.com"
+      user-mail-address "foo@@bar.example.com"
+      user-full-name  "Foo X. Bar")
+(setq mu4e-compose-signature
+   "Foo X. Bar\nhttp://www.example.com\n")
+
+;; smtp mail setting
+(setq
+   message-send-mail-function 'smtpmail-send-it
+   smtpmail-default-smtp-server "smtp.example.com"
+   smtpmail-smtp-server "smtp.example.com"
+   smtpmail-local-domain "example.com"
+
+   ;; if you need offline mode, set these -- and create the queue dir
+   ;; with 'mu mkdir', i.e.. mu mkdir /home/user/Maildir/queue
+   smtpmail-queue-mail  nil
+   smtpmail-queue-dir  "/home/user/Maildir/queue/cur")
+
+;; don't keep message buffers around
+(setq message-kill-buffer-on-exit t)
+@end lisp
+
+
+@node Gmail configuration
+@section Gmail configuration
+
+@emph{Gmail} is a popular e-mail provider; let's see how we can make it
+work with @t{mu4e}. Since we are using @abbr{IMAP}, you must enable that
+in the Gmail web interface (in the settings, under the ``Forwarding and
+POP/IMAP''-tab).
+
+Gmail users may also be interested in @ref{Including related messages},
+and in @ref{Skipping duplicates}.
+
+@subsection Setting up offlineimap
+
+First of all, we need a program to get the e-mail from Gmail to our
+local machine; for this we use @t{offlineimap}; on Debian (and
+derivatives like Ubuntu), this is as easy as:
+
+@verbatim
+$ sudo apt-get install offlineimap
+@end verbatim
+
+while on Fedora (and similar) you need:
+@verbatim
+$ sudo yum install offlineimap
+@end verbatim
+
+Then, we can configure @t{offlineimap} by editing @file{~/.offlineimaprc}:
+
+@verbatim
+[general]
+accounts = Gmail
+maxsyncaccounts = 3
+
+[Account Gmail]
+localrepository = Local
+remoterepository = Remote
+
+[Repository Local]
+type = Maildir
+localfolders = ~/Maildir
+
+[Repository Remote]
+type = IMAP
+remotehost = imap.gmail.com
+remoteuser = USERNAME@gmail.com
+remotepass = PASSWORD
+ssl = yes
+maxconnections = 1
+@end verbatim
+
+Obviously, you need to replace @t{USERNAME} and @t{PASSWORD} with your actual
+Gmail username and password. After this, you should be able to download your
+mail:
+
+@verbatim
+$ offlineimap
+ OfflineIMAP 6.3.4
+Copyright 2002-2011 John Goerzen & contributors.
+Licensed under the GNU GPL v2+ (v2 or any later version).
+
+Account sync Gmail:
+ ***** Processing account Gmail
+ Copying folder structure from IMAP to Maildir
+ Establishing connection to imap.gmail.com:993.
+Folder sync [Gmail]:
+ Syncing INBOX: IMAP -> Maildir
+ Syncing [Gmail]/All Mail: IMAP -> Maildir
+ Syncing [Gmail]/Drafts: IMAP -> Maildir
+ Syncing [Gmail]/Sent Mail: IMAP -> Maildir
+ Syncing [Gmail]/Spam: IMAP -> Maildir
+ Syncing [Gmail]/Starred: IMAP -> Maildir
+ Syncing [Gmail]/Trash: IMAP -> Maildir
+Account sync Gmail:
+ ***** Finished processing account Gmail
+@end verbatim
+
+We can now run @command{mu} to make sure things work:
+
+@verbatim
+$ mu index
+mu: indexing messages under /home/foo/Maildir [/home/foo/.cache/mu/xapian]
+| processing mail; checked: 520; updated/new: 520, cleaned-up: 0
+mu: elapsed: 3 second(s), ~ 173 msg/s
+mu: cleaning up messages [/home/foo/.cache/mu/xapian]
+/ processing mail; checked: 520; updated/new: 0, cleaned-up: 0
+mu: elapsed: 0 second(s)
+@end verbatim
+
+We can run both the @t{offlineimap} and the @t{mu index} from within
+@t{mu4e}, but running it from the command line makes it a bit easier to
+troubleshoot as we are setting things up.
+
+Note: when using encryption, you probably do @emph{not} want to
+synchronize your Drafts-folder, since it contains the unencrypted
+messages. You can use OfflineIMAP's @t{folderfilter} for that.
+
+@subsection Settings
+
+Next step: let's make a @t{mu4e} configuration for this:
+
+@lisp
+(require 'mu4e)
+
+;; use mu4e for e-mail in emacs
+(setq mail-user-agent 'mu4e-user-agent)
+
+(setq mu4e-drafts-folder "/[Gmail].Drafts")
+(setq mu4e-sent-folder   "/[Gmail].Sent Mail")
+(setq mu4e-trash-folder  "/[Gmail].Trash")
+
+;; don't save message to Sent Messages, Gmail/IMAP takes care of this
+(setq mu4e-sent-messages-behavior 'delete)
+
+;; (See the documentation for `mu4e-sent-messages-behavior' if you have
+;; additional non-Gmail addresses and want assign them different
+;; behavior.)
+
+;; setup some handy shortcuts
+;; you can quickly switch to your Inbox -- press ``ji''
+;; then, when you want archive some messages, move them to
+;; the 'All Mail' folder by pressing ``ma''.
+
+(setq mu4e-maildir-shortcuts
+    '( (:maildir "/INBOX"              :key ?i)
+       (:maildir "/[Gmail].Sent Mail"  :key ?s)
+       (:maildir "/[Gmail].Trash"      :key ?t)
+       (:maildir "/[Gmail].All Mail"   :key ?a)))
+
+;; allow for updating mail using 'U' in the main view:
+(setq mu4e-get-mail-command "offlineimap")
+
+;; something about ourselves
+(setq
+   user-mail-address "USERNAME@@gmail.com"
+   user-full-name  "Foo X. Bar"
+   mu4e-compose-signature
+    (concat
+      "Foo X. Bar\n"
+      "http://www.example.com\n"))
+
+;; sending mail -- replace USERNAME with your gmail username
+;; also, make sure the gnutls command line utils are installed
+;; package 'gnutls-bin' in Debian/Ubuntu
+
+(require 'smtpmail)
+(setq message-send-mail-function 'smtpmail-send-it
+   starttls-use-gnutls t
+   smtpmail-starttls-credentials '(("smtp.gmail.com" 587 nil nil))
+   smtpmail-auth-credentials
+     '(("smtp.gmail.com" 587 "USERNAME@@gmail.com" nil))
+   smtpmail-default-smtp-server "smtp.gmail.com"
+   smtpmail-smtp-server "smtp.gmail.com"
+   smtpmail-smtp-service 587)
+
+;; alternatively, for emacs-24 you can use:
+;;(setq message-send-mail-function 'smtpmail-send-it
+;;     smtpmail-stream-type 'starttls
+;;     smtpmail-default-smtp-server "smtp.gmail.com"
+;;     smtpmail-smtp-server "smtp.gmail.com"
+;;     smtpmail-smtp-service 587)
+
+;; don't keep message buffers around
+(setq message-kill-buffer-on-exit t)
+@end lisp
+
+And that's it --- put the above in your @file{~/.emacs}, change @t{USERNAME}
+etc.@: to your own, and restart Emacs, and run @kbd{M-x mu4e}.
+
+@node CONF Other settings
+@section Other settings
+
+Finally, here are some more settings that are useful, but not enabled by
+default for various reasons.
+
+@lisp
+;; use 'fancy' non-ascii characters in various places in mu4e
+(setq mu4e-use-fancy-chars t)
+
+;; save attachment to my desktop (this can also be a function)
+(setq mu4e-attachment-dir "~/Desktop")
+
+;; attempt to show images when viewing messages
+(setq mu4e-view-show-images t)
+@end lisp
+
+@node FAQ
+@appendix FAQ --- Frequently Asked Questions
+
+In this chapter we list a number of actual and anticipated questions and their
+answers.
+
+@menu
+* General::General questions and answers about @t{mu4e}
+* Retrieving mail::Getting mail and indexing
+* Reading messages::Dealing with incoming messages
+* Writing messages::Dealing with outgoing messages
+* Known issues::Limitations we know about
+@end menu
+
+@node General
+@section General
+
+@subsection Results from @t{mu} and @t{mu4e} differ - why?
+@anchor{mu-mu4e-differ} In general, the same queries for @command{mu}
+and @t{mu4e} should yield the same results. If they differ, this is
+usually because one of the following reasons:
+@itemize
+@item different options:
+@t{mu4e} defaults to having @t{mu4e-headers-include-related}, and
+@t{mu4e-headers-results-limit} set to 500. However, the command-line
+@command{mu find}'s corresponding @t{--include-related} is false, and
+there's no limit (@t{--maxnum}).
+@item reverse sorting:
+The results may be different when @t{mu4e} and @command{mu find} do
+not both sort their results in the same direction.
+@item shell quoting issues:
+Depending on the shell, various shell metacharacters in search query
+(such as @t{*}) may be expanded by the shell before @command{mu} ever
+sees them, and the query may not be what you think it is. Quoting is
+necessary.
+@end itemize
+
+@subsection The unread/all counts in the main-screen differ from the 'real' numbers - what's going on?
+For speed reasons, the counts do not exclude messages that no longer
+exist in the file-system, nor does it exclude duplicate messages; @xref{mu-mu4e-differ}.
+
+@subsection How can I quickly delete/move/trash a lot of messages?
+You can select ('mark' in Emacs-speak) the messages like you would
+select text in a buffer; the actions you then take (e.g., @key{DEL}
+for delete, @key{m} for move and @key{t} for trash) apply to all
+selected messages. You can also use functions like
+@code{mu4e-headers-mark-thread} (@key{T}),
+@code{mu4e-headers-mark-subthread} (@key{t}) to mark whole threads at
+the same time, and @code{mu4e-headers-mark-pattern} (@key{%}) to mark
+all messages matching a certain regular expression.
+
+@subsection Can I automatically apply the marks on messages when leaving the headers buffer?
+Yes you can --- see the documentation for the variable
+@t{mu4e-headers-leave-behavior}.
+
+@subsection How can I set @t{mu4e} as the default e-mail client in Emacs?
+See @ref{Emacs default}.
+
+@subsection Can @t{mu4e} use some fancy Unicode instead of these boring plain-ASCII ones?
+Glad you asked! Yes, if you set @code{mu4e-use-fancy-chars} to @t{t},
+@t{mu4e} uses such fancy characters in a number of places. Since not
+all fonts include all characters, you may want to install the
+@t{unifont} and/or @t{symbola} fonts on your system.
+
+@subsection Can I start @t{mu4e} in the background?
+Yes --- if you provide a prefix-argument (@key{C-u}), @t{mu4e} starts,
+but does not show the main-window.
+
+@subsection Does @t{mu4e} support searching for CJK (Chinese-Japanese-Korean) characters?
+Yes, if you have @t{Xapian} 1.2.8 or newer, and set the environment
+variable @t{XAPIAN_CJK_NGRAM} to non-empty before indexing, both when
+using @t{mu} from the command-line and from @t{mu4e}.
+
+@subsection How can I customize the function to select a folder?
+The @t{mu4e-completing-read-function} variable can be customized to
+select a folder in any way. The variable can be set to a function that
+receives five arguments, following @t{completing-read}. The default
+value is @code{ido-completing-read}; to use emacs's default behavior,
+set the variable to @code{completing-read}. Helm users can use the
+same value, and by enabling @code{helm-mode} use helm-style
+completion.
+
+@subsection With a lot of Maildir folders, jumping to them can get slow. What can I do?
+Set @code{mu4e-cache-maildir-list} to @code{t} (make sure to read
+its docstring).
+
+@subsection How can I hide messages from the search results?
+See the variable @code{mu4e-headers-hide-predicate}.
+
+For example, to filter out GMail's spam, set it to:
+@lisp
+(setq mu4e-headers-hide-predicate
+     (lambda (msg)
+       (string-suffix-p "Spam" (mu4e-message-field msg :maildir))))
+@end lisp
+
+@subsection I'm getting an error 'Variable binding depth exceeds max-specpdl-size' when using mu4e -- what can I do about it?
+The error occurs because @t{mu4e} is binding more variables than
+@t{emacs} allows for, by default. You can avoid this by setting a
+higher value, e.g. by adding the following to your configuration:
+@lisp
+(setq max-specpdl-size 5000)
+@end lisp
+
+@node Retrieving mail
+@section Retrieving mail
+
+@subsection How can I get notifications when receiving mail?
+There is @code{mu4e-index-updated-hook}, which gets triggered when the
+indexing process triggered sees an update (not just new mail though). To
+use this hook, put something like the following in your setup (assuming
+you have @t{aplay} and some soundfile, change as needed):
+@lisp
+(add-hook 'mu4e-index-updated-hook
+  (defun new-mail-sound ()
+    (shell-command "aplay ~/Sounds/boing.wav&")))
+@end lisp
+
+@subsection Getting mail through a local mailserver. What should I use for @code{mu4e-get-mail-command}?
+Use the literal string @t{"true"} (or don't do anything, it's the
+default) which then uses @t{/bin/true} (a command that does nothing and
+always succeeds). This makes getting mail a no-op, but the messages are
+still re-indexed.
+
+@subsection How can I re-index my messages without getting new mail?
+Use @kbd{M-x mu4e-update-index}
+
+@subsection When I try to run @t{mu index} while @t{mu4e} is running I get errors
+For instance:
+@verbatim
+mu: mu_store_new_writable: xapian error
+  'Unable to get write lock on ~/.cache/mu/xapian: already locked
+@end verbatim
+What to do about this? You get this error because the underlying Xapian
+database is locked by some other process; it can be opened only once in
+read-write mode. There is not much @t{mu4e} can do about this, but if is
+another @command{mu} instance that is holding the lock, you can ask it
+to (gracefully) terminate:
+@verbatim
+   pkill -2 -u $UID mu # send SIGINT
+   sleep 1
+   mu index
+@end verbatim
+@t{mu4e} automatically restarts @t{mu} when it needs it. In practice, this
+seems to work quite well.
+
+@subsection How can I disable the @t{Indexing...} messages?
+Set the variable @code{mu4e-hide-index-messages} to non-@t{nil}.
+
+@subsection IMAP-synchronization and file-name changes
+Some IMAP-synchronization programs such as @t{mbsync} (but not
+@t{offlineimap}) don't like it when message files do not change their
+names when they are moved to different folders. @t{mu4e} can attempt to
+help with this - you can set the variable
+@code{mu4e-change-filenames-when-moving} to non-@t{nil}.
+
+@subsection @command{offlineimap} and UTF-7
+@command{offlineimap} uses IMAP's UTF-7 for encoding non-ascii folder
+names, while @command{mu} expects UTF-8 (so, e.g. @t{/まりもえ
+お}@footnote{some Japanese characters} becomes @t{/&MH4wijCCMEgwSg-}).
+
+This is best solved by telling @command{offlineimap} to use UTF-8
+instead --- see
+@url{https://github.com/djcb/mu/issues/68#issuecomment-8598652}.
+
+@subsection @command{mbsync} or @command{offlineimap} do not sync properly
+Unfortunately, @command{mbsync} and/or @command{offlineimap} do not
+always agree with @t{mu} about the meaning of various Maildir-flags. If
+you encounter unexpected behavior, it is recommended you check before
+and after a sync-operation. If the problem only shows up @emph{after}
+sync'ing, the problem is with the sync-program, and it's most productive
+to complain there.
+
+Also, you may want to ensure that @t{mu4e-index-lazy-check} is kept at
+its default (@t{nil}) value, since it seems @command{mbsync} can make
+changes that escape a 'lazy' check.
+
+Furthermore, there have been quite a few related queries on the
+mailing-list; worthwhile to check out.
+
+@node Reading messages
+@section Reading messages
+
+@subsection Opening messages is slower than expected - why?
+@t{mu4e} is designed to be very fast, even with large amounts of mail.
+However, if you experience slowdowns, here are some things to consider:
+@itemize
+@item opening messages while indexing:
+@t{mu4e} corresponds with the @t{mu} server synchronously; this means
+that you can do only one thing at a time. The one operation that
+potentially does take a bit of time is indexing of mail. Since the 1.7.x
+series you don't have to wait for the indexing to complete, but it can
+be still be a bit slower because @t{mu} is very busy at that time.
+
+For some strategies to reduce that time, see the next question.
+@item getting contact information can take some time:
+especially when opening @t{mu4e} the first time and you have a
+@emph{lot} of contacts, it can take a few seconds to process those. Note
+that @t{mu4e} 1.3 and higher only get @emph{changed} contacts in
+subsequent updates (after and indexing operation), so this should be
+less of a concern. And you can tweak what contacts you get using
+@var{mu4e-compose-complete-only-personal},
+@var{mu4e-compose-complete-only-after} and
+@var{mu4e-compose-complete-max}.
+@item decryption / sign verification:
+encrypted / signed messages sometimes require network access, and this
+may take a while; certainly if the needed servers cannot be found.
+Part of this may be that influential environment variables are not set
+in the emacs environment.
+@end itemize
+
+If you still experience unexpected slowness, you can of course file a
+ticket, but please be sure to mention the following:
+
+@itemize
+@item are all messages slow or only some messages?
+@item if it's only some messages, is there something specific about them?
+@item in addition, please a (sufficiently censored version of) a message that is slow
+@item is opening @emph{always} slow or only sometimes? When?
+@end itemize
+
+@subsection How can I word-wrap long lines in when viewing a message?
+You can toggle between wrapped and non-wrapped states using
+@key{w}. If you want to do this automatically, invoke @code{visual-line-mode} in
+your @code{mu4e-view-mode-hook}.
+@subsection How can I perform custom actions on messages and attachments?
+See @ref{Actions}.
+@subsection How can I prevent @t{mu4e} from automatically marking messages as `read' when I read them?
+Set @code{mu4e-view-auto-mark-as-read} to @code{nil}.
+@subsection Does @t{mu4e} support including all related messages in a thread, like Gmail does?
+Yes --- see @ref{Including related messages}.
+@subsection There seems to be a lot of duplicate messages --- how can I get rid of them?
+See @ref{Skipping duplicates}.
+@subsection Some messages are almost unreadable in emacs --- can I view them in an external web browser?
+Indeed, airlines often send messages that heavily depend on html and
+are hard to digest inside emacs. Fortunately, there's an @emph{action}
+(@ref{Message view actions}) defined for this. Simply add to your
+configuration:
+@lisp
+(add-to-list 'mu4e-view-actions
+  '("ViewInBrowser" . mu4e-action-view-in-browser) t)
+@end lisp
+Now, when viewing such a difficult message, type @kbd{aV}, and the
+message opens inside a web browser. You can influence the browser to
+use with @code{browse-url-generic-program}.
+@subsection How can I read encrypted messages that I sent?
+Since you do not own the recipient's key you typically cannot read
+those mails --- so the trick is to encrypt outgoing mails with your
+key, too. This can be automated by adding the following snippet to
+your configuration (courtesy of user @t{kpachnis}):
+@lisp
+(require 'epg-config)
+(setq mml2015-use 'epg
+      epg-user-id "gpg_key_id"
+      mml2015-encrypt-to-self t
+      mml2015-sign-with-sender t)
+@end lisp
+@subsection Can I `bounce' or `resend' messages?
+Somewhat --- it is possible to edit a (copy of) an existing message and
+then send it, using @code{M-x mu4e-compose-resend}. This gives you a
+raw copy of the message, including all headers, encoded parts and so
+on. Reason for this is that for resending, it is important not to
+change anything (except perhaps for the @t{To:} address when
+bouncing); since we cannot losslessly decode an existing message, you
+get the raw version.
+
+@node Writing messages
+@section Writing messages
+
+@subsection What's the deal with replies to messages I wrote myself?
+Like many other mail-clients, @t{mu4e} treats replies to messages you
+wrote yourself as special --- these messages keep the same @t{To:} and
+@t{Cc:} as the original message. This is to ease the common case of
+following up to a message you wrote earlier.
+
+@subsection How can I automatically set the @t{From:}-address for a reply-message?
+See @ref{Compose hooks}.
+
+@subsection How can I dynamically determine the folders for draft/sent/trashed messages?
+See @ref{Dynamic folders}.
+
+@subsection How can I define aliases for (groups of) e-mail addresses?
+See @ref{(emacs) Mail Aliases}.
+
+@subsection How can I automatically add some header to an outgoing message?
+See @ref{Compose hooks}.
+
+@subsection How can I influence the way the original message looks when replying/forwarding?
+Since @code{mu4e-compose-mode} derives from @code{message-mode}, you can
+re-use many of its facilities.
+
+@subsection How can I easily include attachments in the messages I write?
+You can drag-and-drop from your desktop; alternatively, you can use
+@t{dired} --- see @ref{Dired}.
+
+@subsection @t{mu4e} seems to remove myself from the @t{Cc:}-list; how can I prevent that?
+Set @code{mu4e-compose-keep-self-cc} to @t{t} in your configuration.
+
+@subsection @t{mu4e} include myself from the @t{Cc:}-list; how can I prevent that?
+You need list your personal addresses by passing one or more
+@t{--my-address=...} to @t{mu init}. Note that the
+@code{mu4e-user-mail-address-list} which was used in older @t{mu4e}
+versions is no longer used. Also see the entries for version 1.4 in
+@t{NEWS.org} (@kbd{N}) in the main-menu.
+
+@subsection How can I start a new message-thread from a reply?
+Remove the @t{In-Reply-To} header, and @t{mu4e} automatically removes
+the (hidden) @t{References} header as well when sending it. This makes
+the message show up as a top-level message rather than as a response.
+
+@subsection How can I attach an existing message?
+Use @code{mu4e-action-capture-message} (i.e., @kbd{a c} in the headers
+ view) to `capture' the to-be-attached message, then when editing the
+ message, use @kbd{M-x mu4e-compose-attach-captured-message}.
+
+@subsection How can I sign or encrypt messages?
+You can do so using Emacs' MIME-support --- check the
+@t{Attachments}-menu while composing a message. Also see @ref{Signing
+and encrypting}.
+
+You can do so using Emacs' MIME-support --- check the
+@t{Attachments}-menu while composing a message. Also see @ref{Signing
+and encrypting}.
+
+@subsection Address auto-completion does not work?
+If you have set @code{mu4e-compose-complete-only-personal} to non-nil,
+@t{mu4e} only completes 'personal' addresses - so you tell it about your
+e-mail addresses when setting up the database (@t{mu init});
+@ref{Initializing the message store}.
+
+If you cannot find specific addresses you'd expect to find, inspect the
+values of @var{mu4e-compose-complete-only-personal},
+@var{mu4e-compose-complete-only-after} and
+@var{mu4e-compose-complete-max}.
+
+@subsection Can I use @t{BBDB} with @t{mu4e}?
+Yes, with releases of BBDB after 3.1.2. @ref{BBDB}.
+
+@subsection How can I get rid of the message buffer after sending?
+@lisp
+(setq message-kill-buffer-on-exit t)
+@end lisp
+
+@subsection Sending big messages is slow and blocks emacs --- what can I do about it?
+
+For this, there's @url{https://github.com/jwiegley/emacs-async} (also
+available from the Emacs package repository); add the following snippet
+to your configuration:
+@lisp
+(require 'smtpmail-async)
+(setq
+  send-mail-function 'async-smtpmail-send-it
+  message-send-mail-function 'async-smtpmail-send-it)
+@end lisp
+With this, messages are sent using a background Emacs instance.
+
+A word of warning though, this tends to not be as reliable as sending
+the message in the normal, synchronous fashion, and people have reported
+silent failures, where mail sending fails for some reason without any
+indication of that.
+
+You can check the progress of the background by checking the
+@t{*Messages*}-buffer, which should show something like:
+@verbatim
+Delivering message to "William Shakespeare" <will@example.com>...
+Mark set
+Saving file /home/djcb/Maildir/sent/cur/20130706-044350-darklady:2,S...
+Wrote /home/djcb/Maildir/sent/cur/20130706-044350-darklady:2,S
+Sending...done
+@end verbatim
+The first and final messages are the most important, and there may be
+considerable time between them, depending on the size of the message.
+
+@subsection Is it possible to compose messages in a separate frame?
+Yes --- set the variable @code{mu4e-compose-in-new-frame} to @code{t}.
+
+@subsection How can I apply format=flowed to my outgoing messages?
+This enables receiving clients that support this feature to reflow
+paragraphs. Plain text emails with @t{Content-Type: text/plain;
+format=flowed} can be reflowed (i.e. line endings removed, paragraphs
+refilled) by receiving clients that support this standard. Clients
+that don't support this, show them as is, which means this feature is
+truly non-invasive.
+
+Here's an explanatory blog post which also shows why this is a
+desirable feature:
+@url{https://mathiasbynens.be/notes/gmail-plain-text} (if you don't
+have it, your mails mostly look quite bad especially on mobile
+devices) and here's the RFC with all the details:
+@url{https://www.ietf.org/rfc/rfc2646.txt}.
+
+Since version 0.9.17, @t{mu4e} sends emails with @t{format=flowed} by
+setting
+@lisp
+(setq mu4e-compose-format-flowed t)
+@end lisp
+
+@noindent
+in your Emacs init file (@file{~/.emacs} or @file{~/.emacs.d/init.el}).
+The transformation of your message into the proper format is done at the
+time of sending. For this to happen properly, you should write each
+paragraph of your message of as a long line (i.e. without carriage
+return). If you introduce unwanted newlines in your paragraph, use
+@kbd{M-q} to reformat it as a single line.
+
+If you want to send the message with paragraphs on single lines but
+without @t{format=flowed} (because, say, the receiver does not
+understand the latter as it is the case for Google or Github), use
+@kbd{M-x use-hard-newlines} (to turn @code{use-hard-newlines} off) or
+uncheck the box @t{format=flowed} in the @t{Text} menu when composing a
+message.
+
+@subsection How can I force images to be shown at the end of my messages, regardless of where I insert them?
+User Marcin Borkowski has a solution:
+@lisp
+(defun mml-attach-file--go-to-eob (orig-fun &rest args)
+  "Go to the end of buffer before attaching files."
+  (save-excursion
+    (save-restriction
+      (widen)
+      (goto-char (point-max))
+      (apply orig-fun args))))
+
+(advice-add 'mml-attach-file :around #'mml-attach-file--go-to-eob)
+@end lisp
+
+@subsection How can I avoid Outlook display issues?
+
+Limited testing shows that certain Outlook clients do not work well with
+inline replies, and the entire message including-and-below the first
+quoted section is collapsed. This means recipients may not even notice
+important inline text, especially if there is some top-posted content.
+This has been observed on OS X, Windows, and Web-based Outlook clients
+accessing Office 365.
+
+It appears the bug is triggered by the standard reply regex "On ...
+wrote:". Changing "On", or removing the trailing ":" appears to fix the
+bug (in limited testing). Therefore, a simple work-around is to set
+`message-citation-line-format` to something slightly non-standard, such
+as:
+@lisp
+(setq  message-citation-line-format "On %Y-%m-%d at %R %Z, %f wrote...")
+@end lisp
+
+@node Known issues
+@section Known issues
+
+Although they are not really @emph{questions}, we end this chapter with
+a list of known issues and/or missing features in @t{mu4e}. Thus, users
+won't have to search in vain for things that are not there (yet), and
+the author can use it as a todo-list.
+
+@subsection UTF-8 language environment is required
+@t{mu4e} does not work well if the Emacs language environment is not
+UTF-8; so, if you encounter problems with encodings, be sure to have
+@code{(set-language-environment "UTF-8")} in your @file{~/.emacs} (or
+its moral equivalents in other places).
+
+@subsection Thread handling is incomplete
+While threads are calculated and are visible in the headers buffer,
+you cannot collapse/open them.
+
+@subsection Key-bindings are @emph{somewhat} hard-coded.
+That is, the main menu assumes the default key-bindings, as do the
+clicks-on-bookmarks.
+
+For a more complete list, please refer to the issues-list in the
+github-repository.
+
+@node Tips and Tricks
+@appendix Tips and Tricks
+
+@menu
+* Fancy characters:: Non-ascii characters in the UI
+* Refiling messages:: Moving message to some archive folder
+* Saving outgoing messages:: Automatically save sent messages
+* Confirmation before sending:: Check messages before sending
+@end menu
+
+@node Fancy characters
+@section Fancy characters
+
+When using `fancy characters' (@code{mu4e-use-fancy-chars}) with the
+@emph{Inconsolata}-font (and likely others as well), the display may be
+slightly off; the reason for this issue is that Inconsolata does not
+contain the glyphs for the `fancy' arrows and the glyphs that are used
+as replacements are too high.
+
+To fix this, you can use something like the following workaround (in
+your @t{.emacs}-file):
+@lisp
+(when (equal window-system 'x)
+      (set-fontset-font "fontset-default" 'unicode "Dejavu Sans Mono")
+      (set-face-font 'default "Inconsolata-10"))
+@end lisp
+
+Other fonts with good support for Unicode are @t{unifont} and
+@t{symbola}.
+
+For a more complete solution, but with greater overhead, you can also
+try the @emph{unicode-fonts} package:
+@lisp
+(require 'unicode-fonts)
+(require 'persistent-soft) ; To cache the fonts and reduce load time
+(unicode-fonts-setup)
+@end lisp
+
+@node Refiling messages
+@section Refiling messages
+
+By setting @code{mu4e-refile-folder} to a function, you can dynamically
+determine where messages are to be refiled. If you want to do this based
+on the subject of a message, you can use a function that matches the
+subject against a list of regexes in the following way. First, set up a
+variable @code{my-mu4e-subject-alist} containing regexes plus associated
+mail folders:
+
+@lisp
+(defvar my-mu4e-subject-alist '(("kolloqui\\(um\\|a\\)" . "/Kolloquium")
+                                ("Calls" . "/Calls")
+                                ("Lehr" . "/Lehre")
+                                ("webseite\\|homepage\\|website" . "/Webseite"))
+  "List of subjects and their respective refile folders.")
+@end lisp
+
+Now you can use the following function to automatically refile messages
+based on their subject line:
+
+@lisp
+(defun my-mu4e-refile-folder-function (msg)
+  "Set the refile folder for MSG."
+  (let ((subject (mu4e-message-field msg :subject))
+        (folder (or (cdar (member* subject my-mu4e-subject-alist
+                                   :test #'(lambda (x y)
+                                             (string-match (car y) x))))
+                    "/General")))
+    folder))
+@end lisp
+
+Note the @t{"/General"} folder: it is the default folder in case the
+subject does not match any of the regexes in
+@code{my-mu4e-subject-alist}.
+
+In order to make this work, you'll of course need to set
+@code{mu4e-refile-folder} to this function:
+
+@lisp
+(setq mu4e-refile-folder 'my-mu4e-refile-folder-function)
+@end lisp
+
+If you have multiple accounts, you can accommodate them as well:
+
+@lisp
+(defun my-mu4e-refile-folder-function (msg)
+  "Set the refile folder for MSG."
+  (let ((maildir (mu4e-message-field msg :maildir))
+        (subject (mu4e-message-field msg :subject))
+        folder)
+    (cond
+     ((string-match "Account1" maildir)
+      (setq folder (or (catch 'found
+                         (dolist (mailing-list my-mu4e-mailing-lists)
+                           (if (mu4e-message-contact-field-matches
+                             msg :to (car mailing-list))
+                               (throw 'found (cdr mailing-list)))))
+                       "/Account1/General")))
+     ((string-match "Gmail" maildir)
+      (setq folder "/Gmail/All Mail"))
+     ((string-match "Account2" maildir)
+      (setq folder (or (cdar (member* subject my-mu4e-subject-alist
+                                      :test #'(lambda (x y)
+                                                (string-match
+                                                   (car y) x))))
+                       "/Account2/General"))))
+    folder))
+@end lisp
+
+This function actually uses different methods to determine the refile
+folder, depending on the account: for @emph{Account2}, it uses
+@code{my-mu4e-subject-alist}, for the @emph{Gmail} account it simply uses the
+folder ``All Mail''. For Account1, it uses another method: it files the
+message based on the mailing list to which it was sent. This requires
+another variable:
+
+@lisp
+(defvar my-mu4e-mailing-lists
+  '(("mu-discuss@@googlegroups.com" . "/Account1/mu4e")
+    ("pandoc-discuss@@googlegroups.com" . "/Account1/Pandoc")
+    ("auctex@@gnu.org" . "/Account1/AUCTeX"))
+  "List of mailing list addresses and folders where
+   their messages are saved.")
+@end lisp
+
+@node Saving outgoing messages
+@section Saving outgoing messages
+
+Like @code{mu4e-refile-folder}, the variable @code{mu4e-sent-folder} can
+also be set to a function, in order to dynamically determine the save
+folder. One might, for example, wish to automatically put messages going
+to mailing lists into the trash (because you'll receive them back from
+the list anyway). If you have set up the variable
+@code{my-mu4e-mailing-lists} as mentioned, you can use the following
+function to determine a 'sent'-folder:
+
+@lisp
+(defun my-mu4e-sent-folder-function (msg)
+  "Set the sent folder for the current message."
+  (let ((from-address (message-field-value "From"))
+        (to-address (message-field-value "To")))
+    (cond
+     ((string-match "my.address@@account1.example.com" from-address)
+      (if (member* to-address my-mu4e-mailing-lists
+                   :test #'(lambda (x y)
+                             (string-match (car y) x)))
+          "/Trash"
+        "/Account1/Sent"))
+     ((string-match "my.address@@gmail.com" from-address)
+      "/Gmail/Sent Mail")
+     (t (mu4e-ask-maildir-check-exists "Save message to maildir: ")))))
+@end lisp
+
+Note that this function doesn't use @code{(mu4e-message-field msg
+:maildir)} to determine which account the message is being sent from.
+The reason is that the function in @code{mu4e-sent-folder} is
+called when you send the message, but before @t{mu4e} has created the
+message struct from the compose buffer, so that
+@code{mu4e-message-field} cannot be used. Instead, the function uses
+@code{message-field-value}, which extracts the values of the headers in
+the compose buffer. This means that it is not possible to extract the
+account name from the message's maildir, so instead the from address is
+used to determine the account.
+
+Again, the function shows three different possibilities: for the first
+account (@t{my.address@@account1.example.com}) it uses
+@code{my-mu4e-mailing-lists} again to determine if the message goes to a
+mailing list. If so, the message is put in the trash folder, if not, it
+is saved in @t{/Account1/Sent}. For the second (Gmail) account, sent
+mail is simply saved in the Sent Mail folder.
+
+If the from address is not associated with Account1 or with the Gmail
+account, the function uses @code{mu4e-ask-maildir-check-exists} to ask
+the user for a maildir to save the message in.
+
+@node Confirmation before sending
+@section Confirmation before sending
+
+To protect yourself from sending messages too hastily, you can add a
+final confirmation, which you can of course make as elaborate as you
+wish.
+
+@lisp
+(add-hook 'message-send-hook
+  (lambda ()
+    (unless (yes-or-no-p "Sure you want to send this?")
+      (signal 'quit nil))))
+@end lisp
+
+Another option is to simply set @code{message-confirm-send} to
+non-@t{nil} so the question ``Send message?'' is asked for confirmation.
+
+@node How it works
+@appendix How it works
+
+While perhaps not interesting for all users of @t{mu4e}, some curious
+souls may want to know how @t{mu4e} does its job.
+
+@menu
+* High-level overview::How the pieces fit together
+* mu server::The mu process running in the background
+* Reading from the server::Processing responses from the server
+* The message s-expression::What messages look like from the inside
+@end menu
+
+@node High-level overview
+@section High-level overview
+
+At a high level, we can summarize the structure of the @t{mu4e} system using
+some ascii-art:
+
+@cartouche
+@example
+              +---------+
+              | emacs   |
+              |    +------+
+              +----| mu4e | --> send mail (smtpmail)
+                   +------+
+                    |  A
+                    V  |  ---/ search, view, move mail
+              +---------+    \
+              |   mu    |
+              +---------+
+                |    A
+                V    |
+              +---------+
+              | Maildir |  <--- receive mail (fetchmail,
+              +---------+                     offlineimap, ...)
+@end example
+@end cartouche
+
+In words:
+@itemize
+@item Your e-mail messages are stored in a Maildir-directory
+(typically, @file{~/Maildir} and its subdirectories), and new mail comes in
+using tools like @t{fetchmail}, @t{offlineimap}, or through a local mail
+server.
+@item @t{mu} indexes these messages periodically, so you can quickly search for
+them. @t{mu} can run in a special @t{server}-mode, where it provides services
+ to client software.
+@item @t{mu4e}, which runs inside Emacs is
+ such a client; it communicates with @command{mu} (in its @t{server}-mode) to
+ search for messages, and manipulate them.
+@item @t{mu4e} uses the facilities
+ offered by Emacs (the Gnus message editor and @t{smtpmail}) to send
+ messages.
+@end itemize
+
+@node mu server
+@section @t{mu server}
+
+@t{mu4e} is based on the @t{mu} e-mail searching/indexer. The latter
+is a C++-program; there are different ways to communicate with a
+client that is emacs-based.
+
+One way to implement this, would be to call the @t{mu} command-line
+tool with some parameters and then parse the output. In fact, that was
+the first approach --- @t{mu4e} would invoke e.g., @t{mu find} and
+process the output in Emacs.
+
+However, with this approach, we need to load the entire e-mail
+@emph{Xapian} database (in which the message is stored) for each
+invocation. Wouldn't it be nicer to keep a running @t{mu} instance
+around? Indeed, it would --- and thus, the @t{mu server} sub-command
+was born. Running @t{mu server} starts a simple shell, in which you
+can give commands to @command{mu}, which then spits out the
+results/errors. @command{mu server} is not meant for humans, but it
+can be used manually, which is great for debugging.
+
+@node Reading from the server
+@section Reading from the server
+
+In the design, the next question was what format @t{mu} should use for its
+output for @t{mu4e} (Emacs) to process. Some other programs use
+@abbr{JSON} here, but it seemed easier (and possibly, more efficient) just to
+talk to Emacs in its native language: @emph{s-expressions}, and
+interpret those using the Emacs-function
+@code{read-from-string}. See @ref{The message s-expression} for details on the
+format.
+
+So, now let's look at how we process the data from @t{mu server} in
+Emacs. We'll leave out a lot of details, @t{mu4e}-specifics, and look
+at a bit more generic approach.
+
+The first thing to do is to create a process (for example, with
+@code{start-process}), and then register a filter function for it, which is
+invoked whenever the process has some data for us. Something like:
+
+@lisp
+  (let ((proc (start-process <arguments>)))
+    (set-process-filter proc 'my-process-filter)
+    (set-process-sentinel proc 'my-process-sentinel))
+@end lisp
+
+Note, the process sentinel is invoked when the process is terminated
+--- so there you can clean things up. The function
+@code{my-process-filter} is a user-defined function that takes the
+process and the chunk of output as arguments; in @t{mu4e} it looks
+something like (pseudo-lisp):
+
+@lisp
+(defun my-process-filter (proc str)
+  ;; mu4e-buf: a global string variable to which data gets appended
+  ;; as we receive it
+  (setq mu4e-buf (concat mu4e-buf str))
+  (when <we-have-received-a-full-expression>
+      <eat-expression-from mu4e-buf>
+      <evaluate-expression>))
+@end lisp
+
+@code{<evaluate-expression>} de-multiplexes the s-expression we got.
+For example, if the s-expression looks like an e-mail message header,
+it is processed by the header-handling function, which appends it to
+the header list. If the s-expression looks like an error message, it
+is reported to the user. And so on.
+
+The language between frontend and backend is documented partly in the
+@t{mu-server} man-page and more completely in the output of @t{mu
+server --commands}.
+
+@t{mu4e} can log these communications; you can use @kbd{M-x
+mu4e-toggle-logging} to turn logging on and off, and you can view the
+log using @kbd{M-x mu4e-show-log} (@key{$}).
+
+@node The message s-expression
+@section The message s-expression
+
+As a word of warning, the details of the s-expression are internal to
+the mu4e - mu communications, and are subject to change between
+versions.
+
+A typical message s-expression looks something like the following:
+
+@lisp
+(:docid 32461
+ :from ((:name "Nikola Tesla" :email "niko@@example.com"))
+ :to ((:name "Thomas Edison" :email "tom@@example.com"))
+ :cc ((:name "Rupert The Monkey" :email "rupert@@example.com"))
+ :subject "RE: what about the 50K?"
+ :date (20369 17624 0)
+ :size 4337
+ :message-id "C8233AB82D81EE81AF0114E4E74@@123213.mail.example.com"
+ :path  "/home/tom/Maildir/INBOX/cur/133443243973_1.10027.atlas:2,S"
+ :maildir "/INBOX"
+ :priority normal
+ :flags (seen attach)
+ ....
+")
+@end lisp
+
+This s-expression forms a property list (@t{plist}), and we can get
+values from it using @t{plist-get}; for example @code{(plist-get msg
+:subject)} would get you the message subject. However, it's better to
+use the function @code{mu4e-message-field} to shield you from some of
+the implementation details that are subject to change; and see the other
+convenience functions in @file{mu4e-message.el}.
+
+Some notes on the format:
+@itemize
+@item The address fields are @emph{lists} of @t{plists} of the form @code{(:name <name>   :email <email>)},
+where @t{name} can be @t{nil}.
+@item The date is in format Emacs uses (for example in
+@code{current-time}).@footnote{Emacs 32-bit integers have only 29 bits
+available for the actual number; the other bits are use by Emacs for
+internal purposes. Therefore, we need to split @t{time_t} in two
+numbers.}
+@end itemize
+
+@subsection Example: ping-pong
+
+As an example of the communication between @t{mu4e} and @command{mu},
+let's look at the @t{ping-pong}-sequence. When @t{mu4e} starts, it
+sends a command @t{ping} to the @t{mu server} backend, to learn about
+its version. @t{mu server} then responds with a @t{pong} s-expression
+to provide this information (this is implemented in
+@file{mu-cmd-server.c}).
+
+We start this sequence when @t{mu4e} is invoked (when the program is
+started). It calls @t{mu4e-server-ping}, and registers a (lambda)
+function for @t{mu4e-server-pong-func}, to handle the response.
+
+@verbatim
+-> (ping)
+<-<prefix>(:pong "mu" :props (:version "x.x.x" :doccount 78545))
+@end verbatim
+
+When we receive such a @t{pong} (in @file{mu4e-server.el}), the lambda
+function we registered is called, and it compares the version we got
+from the @t{pong} with the version we expected, and raises an error if
+they differ.
+
+@node Debugging
+@appendix Debugging
+
+As explained in @ref{How it works}, @t{mu4e} communicates with its
+backend (@t{mu server}) by sending commands and receiving responses
+(s-expressions).
+
+For debugging purposes, it can be very useful to see this data. For
+this reason, @t{mu4e} can log all these messages. Note that the
+`protocol' is documented to some extent in the @t{mu-server} manpage.
+
+You can enable (and disable) logging with @kbd{M-x
+mu4e-toggle-logging}. The log-buffer is called @t{*mu4e-log*}, and in
+the @ref{Main view}, @ref{Headers view} and @ref{Message view},
+there's a keybinding @key{$} that takes you there. You can quit it by
+pressing @key{q}.
+
+Logging can be a bit resource-intensive, so you may not want to leave
+it on all the time. By default, the log only maintains the most recent
+1200 lines. @t{mu} itself keeps a log as well, you can find it in
+@t{<MUHOME>/mu.log}, on Unix typically @t{~/.cache/mu/mu.log}.
+
+@node GNU Free Documentation License
+@appendix GNU Free Documentation License
+
+@include fdl.texi
+
+@bye
+
+@c Local Variables:
+@c coding: utf-8
+@c End:
diff --git a/mu4e/obsolete/org-mu4e.el b/mu4e/obsolete/org-mu4e.el
new file mode 100644 (file)
index 0000000..88b3568
--- /dev/null
@@ -0,0 +1,234 @@
+;;; org-mu4e -- support for links to mu4e messages and writing org-mode messages -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2019 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: outlines, hypermedia, calendar, mail
+;; Version: 0.0
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 of 1the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; OBSOLETE, UNSUPPORTED.
+
+;; Support for links to mu4e messages/queries from within org-mode,
+;; and for writing message in org-mode, sending them as rich-text.
+
+;; At least version 8.x of Org mode is required.
+
+;;; Code:
+
+(require 'org)
+(require 'mu4e-compose)
+
+(declare-function mu4e-last-query                   "mu4e-headers")
+(declare-function mu4e-message-at-point             "mu4e-message")
+(declare-function mu4e-view-message-with-message-id "mu4e-view")
+(declare-function mu4e-headers-search               "mu4e-headers")
+(declare-function mu4e-error                        "mu4e-helpers")
+(declare-function mu4e-message                      "mu4e-message")
+(declare-function mu4e-compose-mode                 "mu4e-compose")
+
+\f
+;;; Editing with org-mode
+;;
+;; below, some functions for the org->html conversion
+;; based on / inspired by Eric Schulte's org-mime.el
+;; Homepage: http://orgmode.org/worg/org-contrib/org-mime.php
+;;
+;; EXPERIMENTAL
+
+(defvar org-export-skip-text-before-1st-heading)
+(defvar org-export-htmlize-output-type)
+(defvar org-export-preserve-breaks)
+(defvar org-export-with-LaTeX-fragments)
+
+(defun org~mu4e-mime-file (ext path id)
+  "Create a file of type EXT at PATH with ID for an attachment."
+  (format (concat "<#part type=\"%s\" filename=\"%s\" "
+                  "disposition=inline id=\"<%s>\">\n<#/part>\n")
+          ext path id))
+
+(defun org~mu4e-mime-multipart (plain html &optional images)
+  "Create a multipart/alternative with PLAIN and HTML alternatives.
+If the html portion of the message includes IMAGES, wrap the html
+and images in a multipart/related part."
+  (concat "<#multipart type=alternative><#part type=text/plain>"
+          plain
+          (when images "<#multipart type=related>")
+          "<#part type=text/html>"
+          html
+          images
+          (when images "<#/multipart>\n")
+          "<#/multipart>\n"))
+
+(defun org~mu4e-mime-replace-images (str current-file)
+  "Replace images in html files STR in CURRENT-FILE with cid links."
+  (let (html-images)
+    (cons
+     (replace-regexp-in-string ;; replace images in html
+      "src=\"\\([^\"]+\\)\""
+      (lambda (text)
+        (format
+         "src=\"cid:%s\""
+         (let* ((url (and (string-match "src=\"\\([^\"]+\\)\"" text)
+                          (match-string 1 text)))
+                (path (expand-file-name
+                       url (file-name-directory current-file)))
+                (ext (file-name-extension path))
+                (id (replace-regexp-in-string "[\/\\\\]" "_" path)))
+           (cl-pushnew (org~mu4e-mime-file
+                        (concat "image/" ext) path id)
+                       html-images
+                       :test 'equal)
+           id)))
+      str)
+     html-images)))
+
+(defun org~mu4e-mime-convert-to-html ()
+  "Convert the current body to html."
+  (unless (fboundp 'org-export-string-as)
+    (mu4e-error "Required function 'org-export-string-as not found"))
+  (let* ((begin
+          (save-excursion
+            (goto-char (point-min))
+            (search-forward mail-header-separator)))
+         (end (point-max))
+         (raw-body (buffer-substring begin end))
+         (tmp-file (make-temp-name (expand-file-name "mail"
+                                                     temporary-file-directory)))
+         ;; because we probably don't want to skip part of our mail
+         (org-export-skip-text-before-1st-heading nil)
+         ;; because we probably don't want to export a huge style file
+         (org-export-htmlize-output-type 'inline-css)
+         ;; makes the replies with ">"s look nicer
+         (org-export-preserve-breaks t)
+         ;; dvipng for inline latex because MathJax doesn't work in mail
+         (org-export-with-LaTeX-fragments
+          (if (executable-find "dvipng") 'dvipng
+            (mu4e-message "Cannot find dvipng, ignore inline LaTeX") nil))
+         ;; to hold attachments for inline html images
+         (html-and-images
+          (org~mu4e-mime-replace-images
+           (org-export-string-as raw-body 'html t)
+           tmp-file))
+         (html-images (cdr html-and-images))
+         (html (car html-and-images)))
+    (delete-region begin end)
+    (save-excursion
+      (goto-char begin)
+      (newline)
+      (insert (org~mu4e-mime-multipart
+               raw-body html (mapconcat 'identity html-images "\n"))))))
+
+;; next some functions to make the org/mu4e-compose-mode switch as smooth as
+;; possible.
+(defun org~mu4e-mime-decorate-headers ()
+  "Make the headers visually distinctive (org-mode)."
+  (save-excursion
+    (goto-char (point-min))
+    (let* ((eoh (when (search-forward mail-header-separator)
+                  (match-end 0)))
+           (olay (make-overlay (point-min) eoh)))
+      (when olay
+        (overlay-put olay 'face 'font-lock-comment-face)))))
+
+(defun org~mu4e-mime-undecorate-headers ()
+  "Don't make the headers visually distinctive.
+\(well, mu4e-compose-mode will take care of that)."
+  (save-excursion
+    (goto-char (point-min))
+    (let* ((eoh (when (search-forward mail-header-separator)
+                  (match-end 0))))
+      (remove-overlays (point-min) eoh))))
+
+(defvar org-mu4e-convert-to-html nil
+  "Whether to do automatic `org-mode' => html conversion when sending messages.")
+
+(defun org~mu4e-mime-convert-to-html-maybe ()
+  "Convert to html if `org-mu4e-convert-to-html' is non-nil.
+This function is called when sending a message (from
+`message-send-hook') and, if non-nil, sends the message as the
+rich-text version of what is assumed to be an org mode body."
+  (when org-mu4e-convert-to-html
+    (mu4e-message "Converting to html")
+    (org~mu4e-mime-convert-to-html)))
+
+(defun org~mu4e-mime-switch-headers-or-body ()
+  "Switch the buffer to either mu4e-compose-mode (when in headers)
+or org-mode (when in the body)."
+  (interactive)
+  (let* ((sepapoint
+          (save-excursion
+            (goto-char (point-min))
+            (search-forward-regexp mail-header-separator nil t))))
+    ;; only do stuff when the sepapoint exist; note that after sending the
+    ;; message, this function maybe called on a message with the sepapoint
+    ;; stripped. This is why we don't use `message-point-in-header'.
+    (when sepapoint
+      (cond
+       ;; we're in the body, but in mu4e-compose-mode?
+       ;; if so, switch to org-mode
+       ((and (> (point) sepapoint) (eq major-mode 'mu4e-compose-mode))
+        (org-mode)
+        (add-hook 'before-save-hook
+                  #'org~mu4e-error-before-save-hook-fn
+                  nil t)
+        (org~mu4e-mime-decorate-headers)
+        (local-set-key (kbd "M-m")
+                       (lambda (keyseq)
+                         (interactive "kEnter mu4e-compose-mode key sequence: ")
+                         (let ((func (lookup-key mu4e-compose-mode-map keyseq)))
+                           (if func (funcall func) (insert keyseq))))))
+       ;; we're in the headers, but in org-mode?
+       ;; if so, switch to mu4e-compose-mode
+       ((and (<= (point) sepapoint) (eq major-mode 'org-mode))
+        (org~mu4e-mime-undecorate-headers)
+        (mu4e-compose-mode)
+        (add-hook 'message-send-hook 'org~mu4e-mime-convert-to-html-maybe nil t)))
+      ;; and add the hook
+      (add-hook 'post-command-hook 'org~mu4e-mime-switch-headers-or-body t t))))
+
+(defun org~mu4e-error-before-save-hook-fn ()
+  (mu4e-error "Switch to mu4e-compose-mode (M-m) before saving"))
+
+(defun org-mu4e-compose-org-mode ()
+  "Defines a pseudo-minor mode for mu4e-compose-mode.
+Edit the message body using org mode. DEPRECATED."
+  (interactive)
+  (unless (member major-mode '(org-mode mu4e-compose-mode))
+    (mu4e-error "Need org-mode or mu4e-compose-mode"))
+  ;; we can check if we're already in org-mu4e-compose-mode by checking if the
+  ;; post-command-hook is set; hackish...but a buffer-local variable does not
+  ;; seem to survive buffer switching
+  (if (not (member 'org~mu4e-mime-switch-headers-or-body post-command-hook))
+      (progn
+        (org~mu4e-mime-switch-headers-or-body)
+        (mu4e-message
+         (concat
+          "org-mu4e-compose-org-mode enabled; "
+          "press M-m before issuing message-mode commands")))
+    (progn ;; otherwise, remove crap
+      (remove-hook 'post-command-hook 'org~mu4e-mime-switch-headers-or-body t)
+      (org~mu4e-mime-undecorate-headers) ;; shut off org-mode stuff
+      (mu4e-compose-mode)
+      (message "org-mu4e-compose-org-mode disabled"))))
+
+;;; _
+(provide 'org-mu4e)
+;;; org-mu4e.el ends here
diff --git a/mu4e/texinfo-klare.css b/mu4e/texinfo-klare.css
new file mode 100644 (file)
index 0000000..e54a882
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+   Custom CSS for HTML documents generated with Texinfo's makeinfo.
+   Public domain 2016 sirgazil. All rights waived.
+*/
+
+
+
+/* NATIVE ELEMENTS */
+a:link,
+a:visited {
+    color: #245C8A;
+    text-decoration: none;
+}
+
+a:active,
+a:focus,
+a:hover {
+    text-decoration: underline;
+}
+
+abbr,
+acronym {
+    cursor: help;
+}
+
+blockquote {
+    color: #555753;
+    font-style: oblique;
+    margin: 30px 0px;
+    padding-left: 3em;
+}
+
+body {
+    background-color: white;
+    box-shadow: 0 0 2px gray;
+    box-sizing: border-box;
+    color: #333;
+    font-family: sans-serif;
+    font-size: 16px;
+    margin: 50px auto;
+    max-width: 960px;
+    padding: 50px;
+}
+
+code,
+samp,
+tt,
+var {
+    color: purple;
+    font-size: 0.8em;
+}
+
+div.example,
+div.lisp {
+    margin: 0px;
+}
+
+dl {
+    margin: 3em 0em;
+}
+
+dl dl {
+    margin: 0em;
+}
+
+dt {
+    background-color: #F5F5F5;
+    padding: 0.5em;
+}
+
+h1,
+h2,
+h2.contents-heading,
+h3,
+h4 {
+    padding: 20px 0px 0px 0px;
+    font-weight: normal;
+}
+
+h1 {
+    font-size: 2.4em;
+}
+
+h2 {
+    font-size: 2.2em;
+    font-weight: bold;
+}
+
+h3 {
+    font-size: 1.8em;
+}
+
+h4 {
+    font-size: 1.4em;
+}
+
+hr {
+    background-color: silver;
+    border-style: none;
+    height: 1px;
+    margin: 0px;
+}
+
+html {
+    background-color: #F5F5F5;
+}
+
+img {
+    max-width: 100%;
+}
+
+li {
+    padding: 5px;
+}
+
+pre.display,
+pre.example,
+pre.format,
+pre.lisp,
+pre.verbatim{
+    overflow: auto;
+}
+
+pre.example,
+pre.lisp,
+pre.verbatim {
+    background-color: #2D3743;
+    border-color: #000;
+    border-style: solid;
+    border-width: thin;
+    color: #E1E1E1;
+    font-size: smaller;
+    padding: 1em;
+}
+
+pre.menu-comment {
+    border-color: #E4E4E4;
+    border-bottom-style: solid;
+    border-width: thin;
+    font-family: sans;
+}
+
+table {
+    border-collapse: collapse;
+    margin: 40px 0px;
+}
+
+table.index-cp *,
+table.index-fn *,
+table.index-ky *,
+table.index-pg *,
+table.index-tp *,
+table.index-vr * {
+    background-color: inherit;
+    border-style: none;
+}
+
+td,
+th {
+    border-color: silver;
+    border-style: solid;
+    border-width: thin;
+    padding: 10px;
+}
+
+th {
+    background-color: #F5F5F5;
+}
+/* END NATIVE ELEMENTS */
+
+
+
+/* CLASSES */
+.contents {
+    margin-bottom: 4em;
+}
+
+.float {
+    margin: 3em 0em;
+}
+
+.float-caption {
+    font-size: smaller;
+    text-align: center;
+}
+
+.float > img {
+    display: block;
+    margin: auto;
+}
+
+.footnote {
+    font-size: smaller;
+    margin: 5em 0em;
+}
+
+.footnote h3 {
+    display: inline;
+    font-size: small;
+}
+
+.header {
+    background-color: #F2F2F2;
+    font-size: small;
+    padding: 0.2em 1em;
+}
+
+.key {
+    color: purple;
+    font-size: 0.8em;
+}
+
+.menu * {
+    border-style: none;
+}
+
+.menu td {
+    padding: 0.5em 0em;
+}
+
+.menu td:last-child {
+    width: 60%;
+}
+
+.menu th {
+    background-color: inherit;
+}
+/* END CLASSES */
diff --git a/mu4e/version.texi.in b/mu4e/version.texi.in
new file mode 100644 (file)
index 0000000..a336ad0
--- /dev/null
@@ -0,0 +1,4 @@
+@set UPDATED @fulldate@
+@set UPDATED-MONTH @monthdate@
+@set EDITION @version@
+@set VERSION @version@
diff --git a/version.texi.in b/version.texi.in
new file mode 100644 (file)
index 0000000..77a2073
--- /dev/null
@@ -0,0 +1,4 @@
+@set UPDATED @UPDATED@
+@set UPDATED-MONTH @UPDATEDMONTH@
+@set EDITION @VERSION@
+@set VERSION @VERSION@