Import maildir-utils_1.4.15.orig.tar.gz
authorNorbert Preining <norbert@preining.info>
Sat, 23 Jan 2021 16:01:10 +0000 (01:01 +0900)
committerNorbert Preining <norbert@preining.info>
Sat, 23 Jan 2021 16:01:10 +0000 (01:01 +0900)
[dgit import orig maildir-utils_1.4.15.orig.tar.gz]

387 files changed:
.dir-locals.el [new file with mode: 0644]
.editorconfig [new file with mode: 0644]
.github/issue_template.md [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.mailmap [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
HACKING [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
NEWS [new file with mode: 0644]
NEWS.org [new file with mode: 0644]
README [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]
c.cfg [new file with mode: 0644]
configure.ac [new file with mode: 0644]
contrib/Makefile.am [new file with mode: 0644]
contrib/gmime-test.c [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/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/mu-guile-message.c [new file with mode: 0644]
guile/mu-guile-message.h [new file with mode: 0644]
guile/mu-guile.c [new file with mode: 0644]
guile/mu-guile.h [new file with mode: 0644]
guile/mu-guile.texi [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/Makefile.am [new file with mode: 0644]
guile/tests/test-mu-guile.c [new file with mode: 0644]
guile/tests/test-mu-guile.scm [new file with mode: 0755]
guile/texi.texi.in [new file with mode: 0644]
lib/Makefile.am [new file with mode: 0644]
lib/doxyfile.in [new file with mode: 0644]
lib/mu-bookmarks.c [new file with mode: 0644]
lib/mu-bookmarks.h [new file with mode: 0644]
lib/mu-contacts.cc [new file with mode: 0644]
lib/mu-contacts.hh [new file with mode: 0644]
lib/mu-container.c [new file with mode: 0644]
lib/mu-container.h [new file with mode: 0644]
lib/mu-flags.c [new file with mode: 0644]
lib/mu-flags.h [new file with mode: 0644]
lib/mu-index.c [new file with mode: 0644]
lib/mu-index.h [new file with mode: 0644]
lib/mu-maildir.c [new file with mode: 0644]
lib/mu-maildir.h [new file with mode: 0644]
lib/mu-msg-crypto.c [new file with mode: 0644]
lib/mu-msg-doc.cc [new file with mode: 0644]
lib/mu-msg-doc.h [new file with mode: 0644]
lib/mu-msg-fields.c [new file with mode: 0644]
lib/mu-msg-fields.h [new file with mode: 0644]
lib/mu-msg-file.c [new file with mode: 0644]
lib/mu-msg-file.h [new file with mode: 0644]
lib/mu-msg-iter.cc [new file with mode: 0644]
lib/mu-msg-iter.h [new file with mode: 0644]
lib/mu-msg-json.c [new file with mode: 0644]
lib/mu-msg-part.c [new file with mode: 0644]
lib/mu-msg-part.h [new file with mode: 0644]
lib/mu-msg-prio.c [new file with mode: 0644]
lib/mu-msg-prio.h [new file with mode: 0644]
lib/mu-msg-priv.h [new file with mode: 0644]
lib/mu-msg-sexp.c [new file with mode: 0644]
lib/mu-msg.c [new file with mode: 0644]
lib/mu-msg.h [new file with mode: 0644]
lib/mu-query.cc [new file with mode: 0644]
lib/mu-query.h [new file with mode: 0644]
lib/mu-runtime.cc [new file with mode: 0644]
lib/mu-runtime.h [new file with mode: 0644]
lib/mu-script.c [new file with mode: 0644]
lib/mu-script.h [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-threader.c [new file with mode: 0644]
lib/mu-threader.h [new file with mode: 0644]
lib/query/Makefile.am [new file with mode: 0644]
lib/query/mu-data.hh [new file with mode: 0644]
lib/query/mu-parser.cc [new file with mode: 0644]
lib/query/mu-parser.hh [new file with mode: 0644]
lib/query/mu-proc-iface.hh [new file with mode: 0644]
lib/query/mu-tokenizer.cc [new file with mode: 0644]
lib/query/mu-tokenizer.hh [new file with mode: 0644]
lib/query/mu-tree.hh [new file with mode: 0644]
lib/query/mu-xapian.cc [new file with mode: 0644]
lib/query/mu-xapian.hh [new file with mode: 0644]
lib/query/parse.cc [new file with mode: 0644]
lib/query/test-parser.cc [new file with mode: 0644]
lib/query/test-tokenizer.cc [new file with mode: 0644]
lib/query/tokenize.cc [new file with mode: 0644]
lib/test-mu-common.c [new file with mode: 0644]
lib/test-mu-common.h [new file with mode: 0644]
lib/test-mu-contacts.cc [new file with mode: 0644]
lib/test-mu-container.c [new file with mode: 0644]
lib/test-mu-date.c [new file with mode: 0644]
lib/test-mu-flags.c [new file with mode: 0644]
lib/test-mu-maildir.c [new file with mode: 0644]
lib/test-mu-msg-fields.c [new file with mode: 0644]
lib/test-mu-msg.c [new file with mode: 0644]
lib/test-mu-store.c [new file with mode: 0644]
lib/testdir/cur/1220863042.12663_1.mindcrime!2,S [new file with mode: 0644]
lib/testdir/cur/1220863060.12663_3.mindcrime!2,S [new file with mode: 0644]
lib/testdir/cur/1220863087.12663_15.mindcrime!2,PS [new file with mode: 0644]
lib/testdir/cur/1220863087.12663_19.mindcrime!2,S [new file with mode: 0644]
lib/testdir/cur/1220863087.12663_5.mindcrime!2,S [new file with mode: 0644]
lib/testdir/cur/1220863087.12663_7.mindcrime!2,RS [new file with mode: 0644]
lib/testdir/cur/1252168370_3.14675.cthulhu!2,S [new file with mode: 0644]
lib/testdir/cur/1283599333.1840_11.cthulhu!2, [new file with mode: 0644]
lib/testdir/cur/1305664394.2171_402.cthulhu!2, [new file with mode: 0644]
lib/testdir/cur/encrypted!2,S [new file with mode: 0644]
lib/testdir/cur/multimime!2,FS [new file with mode: 0644]
lib/testdir/cur/multirecip!2,S [new file with mode: 0644]
lib/testdir/cur/signed!2,S [new file with mode: 0644]
lib/testdir/cur/signed-encrypted!2,S [new file with mode: 0644]
lib/testdir/cur/special!2,Sabc [new file with mode: 0644]
lib/testdir/new/1220863087.12663_21.mindcrime [new file with mode: 0644]
lib/testdir/new/1220863087.12663_23.mindcrime [new file with mode: 0644]
lib/testdir/new/1220863087.12663_25.mindcrime [new file with mode: 0644]
lib/testdir/new/1220863087.12663_9.mindcrime [new file with mode: 0644]
lib/testdir/tmp/1220863087.12663.ignore [new file with mode: 0644]
lib/testdir2/Foo/cur/arto.eml [new file with mode: 0644]
lib/testdir2/Foo/cur/fraiche.eml [new file with mode: 0644]
lib/testdir2/Foo/cur/mail5 [new file with mode: 0644]
lib/testdir2/Foo/new/.noindex [new file with mode: 0644]
lib/testdir2/Foo/tmp/.noindex [new file with mode: 0644]
lib/testdir2/bar/cur/181736.eml [new file with mode: 0644]
lib/testdir2/bar/cur/mail1 [new file with mode: 0644]
lib/testdir2/bar/cur/mail2 [new file with mode: 0644]
lib/testdir2/bar/cur/mail3 [new file with mode: 0644]
lib/testdir2/bar/cur/mail4 [new file with mode: 0644]
lib/testdir2/bar/cur/mail5 [new file with mode: 0644]
lib/testdir2/bar/cur/mail6 [new file with mode: 0644]
lib/testdir2/bar/new/.noindex [new file with mode: 0644]
lib/testdir2/bar/tmp/.noindex [new file with mode: 0644]
lib/testdir2/wom_bat/cur/atomic [new file with mode: 0644]
lib/testdir2/wom_bat/cur/rfc822.1 [new file with mode: 0644]
lib/testdir2/wom_bat/cur/rfc822.2 [new file with mode: 0644]
lib/testdir3/cycle/cur/cycle0 [new file with mode: 0644]
lib/testdir3/cycle/cur/cycle0.0 [new file with mode: 0644]
lib/testdir3/cycle/cur/cycle0.0.0 [new file with mode: 0644]
lib/testdir3/cycle/cur/rogue0 [new file with mode: 0644]
lib/testdir3/cycle/new/.noindex [new file with mode: 0644]
lib/testdir3/cycle/tmp/.noindex [new file with mode: 0644]
lib/testdir3/sort/1st-child-promotes-thread/cur/A [new file with mode: 0644]
lib/testdir3/sort/1st-child-promotes-thread/cur/B [new file with mode: 0644]
lib/testdir3/sort/1st-child-promotes-thread/cur/C [new file with mode: 0644]
lib/testdir3/sort/1st-child-promotes-thread/cur/D [new file with mode: 0644]
lib/testdir3/sort/1st-child-promotes-thread/new/.noindex [new file with mode: 0644]
lib/testdir3/sort/1st-child-promotes-thread/tmp/.noindex [new file with mode: 0644]
lib/testdir3/sort/2nd-child-promotes-thread/cur/A [new file with mode: 0644]
lib/testdir3/sort/2nd-child-promotes-thread/cur/B [new file with mode: 0644]
lib/testdir3/sort/2nd-child-promotes-thread/cur/C [new file with mode: 0644]
lib/testdir3/sort/2nd-child-promotes-thread/cur/D [new file with mode: 0644]
lib/testdir3/sort/2nd-child-promotes-thread/cur/E [new file with mode: 0644]
lib/testdir3/sort/2nd-child-promotes-thread/new/.noindex [new file with mode: 0644]
lib/testdir3/sort/2nd-child-promotes-thread/tmp/.noindex [new file with mode: 0644]
lib/testdir3/sort/child-does-not-promote-thread/cur/A [new file with mode: 0644]
lib/testdir3/sort/child-does-not-promote-thread/cur/X [new file with mode: 0644]
lib/testdir3/sort/child-does-not-promote-thread/cur/Y [new file with mode: 0644]
lib/testdir3/sort/child-does-not-promote-thread/cur/Z [new file with mode: 0644]
lib/testdir3/sort/child-does-not-promote-thread/new/.noindex [new file with mode: 0644]
lib/testdir3/sort/child-does-not-promote-thread/tmp/.noindex [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-only-subthread/cur/A [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-only-subthread/cur/B [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-only-subthread/cur/C [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-only-subthread/cur/D [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-only-subthread/cur/E [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-only-subthread/cur/F [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-only-subthread/cur/G [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-only-subthread/new/.noindex [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-only-subthread/tmp/.noindex [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-thread/cur/A [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-thread/cur/B [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-thread/cur/C [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-thread/cur/D [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-thread/cur/E [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-thread/new/.noindex [new file with mode: 0644]
lib/testdir3/sort/grandchild-promotes-thread/tmp/.noindex [new file with mode: 0644]
lib/testdir3/tree/cur/child0.0 [new file with mode: 0644]
lib/testdir3/tree/cur/child0.1 [new file with mode: 0644]
lib/testdir3/tree/cur/child0.1.0 [new file with mode: 0644]
lib/testdir3/tree/cur/child2.0.0 [new file with mode: 0644]
lib/testdir3/tree/cur/child3.0.0.0.0 [new file with mode: 0644]
lib/testdir3/tree/cur/child4.0 [new file with mode: 0644]
lib/testdir3/tree/cur/child4.1 [new file with mode: 0644]
lib/testdir3/tree/cur/root0 [new file with mode: 0644]
lib/testdir3/tree/cur/root1 [new file with mode: 0644]
lib/testdir3/tree/cur/root2 [new file with mode: 0644]
lib/testdir3/tree/new/.noindex [new file with mode: 0644]
lib/testdir3/tree/tmp/.noindex [new file with mode: 0644]
lib/testdir4/1220863042.12663_1.mindcrime!2,S [new file with mode: 0644]
lib/testdir4/1220863087.12663_19.mindcrime!2,S [new file with mode: 0644]
lib/testdir4/1252168370_3.14675.cthulhu!2,S [new file with mode: 0644]
lib/testdir4/1283599333.1840_11.cthulhu!2, [new file with mode: 0644]
lib/testdir4/1305664394.2171_402.cthulhu!2, [new file with mode: 0644]
lib/testdir4/181736.eml [new file with mode: 0644]
lib/testdir4/encrypted!2,S [new file with mode: 0644]
lib/testdir4/mail1 [new file with mode: 0644]
lib/testdir4/mail5 [new file with mode: 0644]
lib/testdir4/multimime!2,FS [new file with mode: 0644]
lib/testdir4/signed!2,S [new file with mode: 0644]
lib/testdir4/signed-bad!2,S [new file with mode: 0644]
lib/testdir4/signed-encrypted!2,S [new file with mode: 0644]
lib/testdir4/special!2,Sabc [new file with mode: 0644]
lib/utils/Makefile.am [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-date.c [new file with mode: 0644]
lib/utils/mu-date.h [new file with mode: 0644]
lib/utils/mu-error.hh [new file with mode: 0644]
lib/utils/mu-log.c [new file with mode: 0644]
lib/utils/mu-log.h [new file with mode: 0644]
lib/utils/mu-sexp-parser.cc [new file with mode: 0644]
lib/utils/mu-sexp-parser.hh [new file with mode: 0644]
lib/utils/mu-str.c [new file with mode: 0644]
lib/utils/mu-str.h [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.cc [new file with mode: 0644]
lib/utils/mu-utils.hh [new file with mode: 0644]
lib/utils/test-command-parser.cc [new file with mode: 0644]
lib/utils/test-mu-str.c [new file with mode: 0644]
lib/utils/test-mu-util.c [new file with mode: 0644]
lib/utils/test-sexp-parser.cc [new file with mode: 0644]
lib/utils/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_14.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-2.2.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/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-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]
mu/Makefile.am [new file with mode: 0644]
mu/mu-cmd-cfind.c [new file with mode: 0644]
mu/mu-cmd-extract.c [new file with mode: 0644]
mu/mu-cmd-find.c [new file with mode: 0644]
mu/mu-cmd-index.c [new file with mode: 0644]
mu/mu-cmd-script.c [new file with mode: 0644]
mu/mu-cmd-server.cc [new file with mode: 0644]
mu/mu-cmd.c [new file with mode: 0644]
mu/mu-cmd.h [new file with mode: 0644]
mu/mu-config.c [new file with mode: 0644]
mu/mu-config.h [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/test-mu-cmd-cfind.c [new file with mode: 0644]
mu/test-mu-cmd.c [new file with mode: 0644]
mu/test-mu-query.c [new file with mode: 0644]
mu/test-mu-runtime.c [new file with mode: 0644]
mu/test-mu-threads.c [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/mu4e-about.org [new file with mode: 0644]
mu4e/mu4e-actions.el [new file with mode: 0644]
mu4e/mu4e-compose.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-headers.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-meta.el.in [new file with mode: 0644]
mu4e/mu4e-org.el [new file with mode: 0644]
mu4e/mu4e-proc.el [new file with mode: 0644]
mu4e/mu4e-speedbar.el [new file with mode: 0644]
mu4e/mu4e-utils.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/org-mu4e.el [new file with mode: 0644]
toys/Makefile.am [new file with mode: 0644]
toys/msg2pdf/Makefile.am [new file with mode: 0644]
toys/msg2pdf/msg2pdf.c [new file with mode: 0644]
toys/mug/Makefile.am [new file with mode: 0644]
toys/mug/mu-msg-attach-view.c [new file with mode: 0644]
toys/mug/mu-msg-attach-view.h [new file with mode: 0644]
toys/mug/mu-msg-body-view.c [new file with mode: 0644]
toys/mug/mu-msg-body-view.h [new file with mode: 0644]
toys/mug/mu-msg-header-view.c [new file with mode: 0644]
toys/mug/mu-msg-header-view.h [new file with mode: 0644]
toys/mug/mu-msg-view.c [new file with mode: 0644]
toys/mug/mu-msg-view.h [new file with mode: 0644]
toys/mug/mu-widget-util.c [new file with mode: 0644]
toys/mug/mu-widget-util.h [new file with mode: 0644]
toys/mug/mug-msg-list-view.c [new file with mode: 0644]
toys/mug/mug-msg-list-view.h [new file with mode: 0644]
toys/mug/mug-msg-view.c [new file with mode: 0644]
toys/mug/mug-msg-view.h [new file with mode: 0644]
toys/mug/mug-query-bar.c [new file with mode: 0644]
toys/mug/mug-query-bar.h [new file with mode: 0644]
toys/mug/mug-shortcuts.c [new file with mode: 0644]
toys/mug/mug-shortcuts.h [new file with mode: 0644]
toys/mug/mug.c [new file with mode: 0644]
toys/mug/mug.svg [new file with mode: 0644]
www/cheatsheet.md [new file with mode: 0644]
www/graph01.png [new file with mode: 0644]
www/index.md [new file with mode: 0644]
www/mu-guile.md [new file with mode: 0644]
www/mu-guile.org [new file with mode: 0644]
www/mu-small.png [new file with mode: 0644]
www/mu.css [new file with mode: 0644]
www/mu.jpg [new file with mode: 0644]
www/mu.png [new file with mode: 0644]
www/mu4e-1.png [new file with mode: 0644]
www/mu4e-2.png [new file with mode: 0644]
www/mu4e-3.png [new file with mode: 0644]
www/mu4e-splitview-small.png [new file with mode: 0644]
www/mu4e-splitview.png [new file with mode: 0644]
www/mu4e.md [new file with mode: 0644]
www/mu4egraph.png [new file with mode: 0644]
www/mug-full.png [new file with mode: 0644]
www/mug-thumb.png [new file with mode: 0644]
www/mug.org [new file with mode: 0644]
www/old-news.md [new file with mode: 0644]

diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644 (file)
index 0000000..3a491ef
--- /dev/null
@@ -0,0 +1,2 @@
+((emacs-lisp-mode
+  (indent-tabs-mode . nil)))
diff --git a/.editorconfig b/.editorconfig
new file mode 100644 (file)
index 0000000..d7b5d39
--- /dev/null
@@ -0,0 +1,36 @@
+#-*-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", but in
+# practice that's hard to accomplish in many editors.
+#
+# So we use spaces instead, at least that looks consistent for all
+
+[*.{cc,cpp,hh,hpp}]
+indent_style                = space
+indent_size                 = 8
+max_line_length             = 100
+
+[*.{c,h}]
+indent_style                = space
+indent_size                 = 8
+max_line_length             = 80
+
+[configure.ac]
+indent_style                = space
+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.md b/.github/issue_template.md
new file mode 100644 (file)
index 0000000..4527979
--- /dev/null
@@ -0,0 +1,20 @@
+## Expected or desired behavior
+
+Please describe the behavior you expected / want
+
+## Actual behavior
+
+Please describe the behavior you are actually seeing. 
+
+For bug-reports, if applicable, include error messages, emacs stack
+traces 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 details as possible how
+one can reproduce the problem*.
+
+## Versions of mu, mu4e/emacs, operating system etc.
+
+## Any other detail
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..62e23bc
--- /dev/null
@@ -0,0 +1,123 @@
+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-meta.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
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/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..063324a
--- /dev/null
@@ -0,0 +1,35 @@
+language: c
+sudo: required
+compiler:
+  - gcc
+env:
+  global:
+    - BUILD_PKGS="libtool autoconf autoconf-archive automake texinfo"
+    - BUILD_LIBS="libgmime-2.6-dev libxapian-dev guile-2.0-dev libwebkitgtk-dev"
+    - TEST_PKGS="pmccabe"
+  matrix:
+    - EVM_EMACS=emacs-24.1-bin
+    - EVM_EMACS=emacs-24.2-bin
+    - EVM_EMACS=emacs-24.3-bin
+    # - EVM_EMACS=emacs-24.5-travis
+    # - EVM_EMACS=emacs-25.1-travis
+before_install:
+  - git submodule update --init --recursive
+  # The Ubuntu version on travis is way too old, need Autoconf 2.69
+  - sudo add-apt-repository ppa:dns/gnu -y
+  - sudo apt-get -qq update
+  - sudo apt-get install -qq ${BUILD_PKGS} ${BUILD_LIBS} ${TEST_PKGS}
+install:
+  - sudo mkdir /usr/local/evm
+  - sudo chown $(id -u):$(id -g) /usr/local/evm
+  - curl -fsSkL https://raw.github.com/rejeep/evm/master/go | bash
+  - export PATH="$HOME/.evm/bin:$PATH"
+  - evm install $EVM_EMACS --use
+script:
+  # Need recent version of autoconf-archive
+  - curl http://nl.mirror.babylon.network/gnu/autoconf-archive/autoconf-archive-2016.09.16.tar.xz -o /tmp/aa.tar.xz && tar xf /tmp/aa.tar.xz
+  - cp autoconf-archive-2016.09.16/m4/*.m4 m4/
+  - ./autogen.sh
+  - ./configure
+  - make
+  - make check
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/HACKING b/HACKING
new file mode 100644 (file)
index 0000000..4d8ce94
--- /dev/null
+++ b/HACKING
@@ -0,0 +1,144 @@
+* HACKING
+
+  Here are some guidelines for hacking on the 'mu' source code.
+
+  This is a fairly long list -- this is not meant to discourage anyone from
+  working on mu; I think most of the rules are common sense anyway, and some of
+  the more stylistic-aesthetic rules are clearly visible in current source code,
+  so as long as any new code 'fits in', it should go a long way in satisfying
+  them.
+
+  I should add some notes for the Lisp/Scheme code as well...
+
+** Coding style
+
+   For consistency and, more important, to keep things understandable, mu
+   attempts to follow the following rules:
+
+   1. Basic code layout is like in the Linux kernel coding style. Keep the '{'
+      on the same line as the statement, except for functions. Tabs for
+      indentation, space for alignment; use 8-char tabs.
+
+   2. Lines should not exceed 80 characters (C) or 100 characters (C++)
+
+   3. Functions should not exceed 35 lines (with few exceptions). You can easily
+      check if any functions violate this rule with 'make line35', which lists
+      all functions with more than 35 non-comment lines.
+
+   4. Source files should not exceed 1000 lines
+
+   5. A function's cyclomatic complexity should not exceed 10 (there could be
+      rare exceptions, see the toplevel ~Makefile.am~). You can test the
+      cyclomatic complexity with the ~pmccabe~ tool; if you installed that, you
+      can use 'make cc10' to list all functions that violate this rule; there
+      should be none.
+
+   6. Filenames have their components separated with dashes (e.g, ~mu-log.h~), and
+      start with ~mu-~ where appropriate.
+
+   7. Global functions have the prefix based on their module, e.g., ~mu-foo.h~
+      declares a function of 'mu_foo_bar (int a);', mu-foo.c implements this.
+
+   8. Non-global functions *don't* have the module prefix, and are declared
+      static.
+
+   9. Functions have their return type on a separate line before the function
+      name, so:
+#+BEGIN_EXAMPLE
+      int
+      foo (const char *bar)
+      {
+       ....
+      }
+#+END_EXAMPLE
+
+      There is no non-aesthetic reason for this.
+
+   10. In C code, variable-declarations are at the beginning of a block; in
+       principle, C++ follows that same guideline, unless for heavy yet
+       uncertain initializations following RAII.
+
+       In C code, the declaration does *not* initialize the variable. This will
+       give the compiler a chance to warn us if the variable is not initialized
+       in a certain code path.
+
+   11. Returned strings of type char* must be freed by the caller; if they are
+       not to be freed, 'const char*' should be used instead
+
+   12. Functions calls have a space between function name and arguments, unless
+       there are none, so:
+
+       ~foo (12, 3)~;
+
+       and
+
+       ~bar();~
+
+       after a comma, a space should follow.
+
+   13. Functions that do not take arguments are explicitly declared as
+       f(void) and not f(). Reason: f() means that the arguments are
+       /unspecified/ (in C)
+
+   14. C-code should not use ~//~ comments.
+
+
+** Logging
+
+   For logging, mu uses the GLib logging functions/macros as listed below,
+   except when logging may not have been initialized.
+
+   The logging system redirects most logging to the log file (typically,
+   ~/.cache/mu/mu.log). g_warning, g_message and g_critical are shown to the user,
+   except when running with --quiet, in which case g_message is *not* shown.
+
+   - ~g_message~ is for non-error messages the user will see (unless running with
+     ~--quiet~)
+   - ~g_warning~ is for problems the user may be able to do something about (and
+     they are written on ~stderr~)
+   - ~g_critical~ is for mu bugs, serious, internal problems (~g_return_if_fail~ and
+     friends use this). (and they are written on ~stderr~)
+   - don't use ~g_error~
+
+   If you just want to log something in the log file without writing to screen,
+   use ~MU_LOG_WRITE~, as defined in ~mu-util.h~.
+
+** Compiling from git
+
+   For hacking, you're strongly advised to use the latest git version.
+   Compilation from git should be straightforward, if you have the right tools
+   installed.
+
+*** dependencies
+
+    You need to install a few dependencies; e.g. on Debian/Ubuntu:
+#+BEGIN_EXAMPLE
+    sudo apt-get install                 \
+        automake                         \
+        autoconf-archive                 \
+        autotools-dev                    \
+        libglib2.0-dev                   \
+       libxapian-dev                    \
+       libgmime-3.0-dev                 \
+        m4                               \
+        make                             \
+        libtool                          \
+        pkg-config
+#+END_EXAMPLE
+
+   Then, to compile straight from ~git~:
+
+#+BEGIN_EXAMPLE
+   $ git clone https://github.com/djcb/mu
+   $ cd mu
+   $ ./autogen.sh
+   $ make
+#+END_EXAMPLE
+
+   You only need to run ~./autogen.sh~ the first time and after changes in the
+   build system; otherwise you can use ~./configure~.
+
+# Local Variables:
+# mode: org; org-startup-folded: nofold
+# fill-column: 80
+# End:
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..f62c2e7
--- /dev/null
@@ -0,0 +1,61 @@
+## 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
+
+if BUILD_GUILE
+guile=guile
+else
+guile=
+endif
+
+if BUILD_MU4E
+mu4e=mu4e
+else
+mu4e=
+endif
+
+SUBDIRS=m4 man lib $(guile) mu $(mu4e) contrib toys
+
+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                                                    \
+       HACKING                                                 \
+       README                                                  \
+       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/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..28bb44f
--- /dev/null
+++ b/NEWS.org
@@ -0,0 +1,868 @@
+#+STARTUP:showall
+* NEWS (user visible changes)
+
+* 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.
+
+*** 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).
+
+
+** 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
+
+**** 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
+
+* Old news
+  :PROPERTIES:
+  :VISIBILITY: folded
+  :END:
+
+** 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
+10:26 ⭑☐ Nicolas Goaziou Orgmode /bulk ◼ Re: [O] 2 issue with Include function
+11:00 ⭑☐ Leonard Randall Orgmode /bulk ┗▶ 10:55 ⭑☐ Guillermo Rodrigu... GstDev
+/bulk ◼ Re: stop pipeline into a callback function. 12:04 ⭑☐ Enrique Ocaña Gon...
+GstDev /bulk ┗▶ 11:27 ⭑☐ Tim Müller GstDev /bulk ◼ 09:34 ⭑☐ Robert Klein Orgmode
+/bulk ◼ Re: [O] Agenda Tag filtering - has the behaviour changed? #+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 b/README
new file mode 100644 (file)
index 0000000..921565b
--- /dev/null
+++ b/README
@@ -0,0 +1,25 @@
+Welcome to mu & mu4e!
+
+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. See the [mu cheatsheet] for some examples. =mu=
+is fully documented.
+
+After indexing your messages into a [Xapian](http://www.xapian.org)-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 2.2 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.
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..1155320
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+test -f mu/mu.cc || {
+    echo "*** Run this script from the top-level mu source directory"
+    exit 1
+}
+
+# opportunistically; usually not needed, but occasionally it'll
+# avoid build errors that would otherwise confuse users.
+test -f Makefile && {
+    echo "*** clear out old things"
+    make distclean 2> /dev/null
+}
+
+
+command -V autoreconf > /dev/null
+if [ $? != 0 ]; then
+    echo "*** No autoreconf found, please install it ***"
+    exit 1
+fi
+
+rm -f config.cache
+rm -rf autom4te.cache
+
+autoreconf --force --install --verbose || exit $?
+
+if test -z "$*"; then
+    echo "# Configuring without parameters"
+else
+   echo "# Configure with parameters $*"
+fi
+
+./configure --config-cache $@
diff --git a/build-aux/config.rpath b/build-aux/config.rpath
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/c.cfg b/c.cfg
new file mode 100644 (file)
index 0000000..0a1d2d0
--- /dev/null
+++ b/c.cfg
@@ -0,0 +1,1578 @@
+# Uncrustify 0.60
+
+#
+# General options
+#
+
+# The type of line endings
+newlines                                 = lf     # auto/lf/crlf/cr
+
+# The original size of tabs in the input
+input_tab_size                           = 8        # number
+
+# The size of tabs in the output (only used if align_with_tabs=true)
+output_tab_size                          = 8        # number
+
+# The ASCII value of the string escape char, usually 92 (\) or 94 (^). (Pawn)
+string_escape_char                       = 92       # number
+
+# Alternate string escape char for Pawn. Only works right before the quote char.
+string_escape_char2                      = 0        # number
+
+# Allow interpreting '>=' and '>>=' as part of a template in 'void f(list<list<B>>=val);'.
+# If true (default), 'assert(x<0 && y>=3)' will be broken.
+# Improvements to template detection may make this option obsolete.
+tok_split_gte                            = false    # false/true
+
+# Control what to do with the UTF-8 BOM (recommend 'remove')
+utf8_bom                                 = ignore   # ignore/add/remove/force
+
+# If the file contains bytes with values between 128 and 255, but is not UTF-8, then output as UTF-8
+utf8_byte                                = false    # false/true
+
+# Force the output encoding to UTF-8
+utf8_force                               = false    # false/true
+
+#
+# Indenting
+#
+
+# The number of columns to indent per level.
+# Usually 2, 3, 4, or 8.
+indent_columns                           = 8        # number
+
+# The continuation indent. If non-zero, this overrides the indent of '(' and '=' continuation indents.
+# For FreeBSD, this is set to 4. Negative value is absolute and not increased for each ( level
+indent_continue                          = 0        # number
+
+# How to use tabs when indenting code
+# 0=spaces only
+# 1=indent with tabs to brace level, align with spaces
+# 2=indent and align with tabs, using spaces when not on a tabstop
+indent_with_tabs                         = 1        # number
+
+# Comments that are not a brace level are indented with tabs on a tabstop.
+# Requires indent_with_tabs=2. If false, will use spaces.
+indent_cmt_with_tabs                     = false    # false/true
+
+# Whether to indent strings broken by '\' so that they line up
+indent_align_string                      = false    # false/true
+
+# The number of spaces to indent multi-line XML strings.
+# Requires indent_align_string=True
+indent_xml_string                        = 0        # number
+
+# Spaces to indent '{' from level
+indent_brace                             = 0        # number
+
+# Whether braces are indented to the body level
+indent_braces                            = false    # false/true
+
+# Disabled indenting function braces if indent_braces is true
+indent_braces_no_func                    = false    # false/true
+
+# Disabled indenting class braces if indent_braces is true
+indent_braces_no_class                   = false    # false/true
+
+# Disabled indenting struct braces if indent_braces is true
+indent_braces_no_struct                  = false    # false/true
+
+# Indent based on the size of the brace parent, i.e. 'if' => 3 spaces, 'for' => 4 spaces, etc.
+indent_brace_parent                      = false    # false/true
+
+# Whether the 'namespace' body is indented
+indent_namespace                         = false    # false/true
+
+# The number of spaces to indent a namespace block
+indent_namespace_level                   = 0        # number
+
+# If the body of the namespace is longer than this number, it won't be indented.
+# Requires indent_namespace=true. Default=0 (no limit)
+indent_namespace_limit                   = 0        # number
+
+# Whether the 'extern "C"' body is indented
+indent_extern                            = false    # false/true
+
+# Whether the 'class' body is indented
+indent_class                             = false    # false/true
+
+# Whether to indent the stuff after a leading class colon
+indent_class_colon                       = false    # false/true
+
+# Virtual indent from the ':' for member initializers. Default is 2
+indent_ctor_init_leading                 = 2        # number
+
+# Additional indenting for constructor initializer list
+indent_ctor_init                         = 0        # number
+
+# False=treat 'else\nif' as 'else if' for indenting purposes
+# True=indent the 'if' one level
+indent_else_if                           = false    # false/true
+
+# Amount to indent variable declarations after a open brace. neg=relative, pos=absolute
+indent_var_def_blk                       = 0        # number
+
+# Indent continued variable declarations instead of aligning.
+indent_var_def_cont                      = false    # false/true
+
+# True:  force indentation of function definition to start in column 1
+# False: use the default behavior
+indent_func_def_force_col1               = false    # false/true
+
+# True:  indent continued function call parameters one indent level
+# False: align parameters under the open paren
+indent_func_call_param                   = false    # false/true
+
+# Same as indent_func_call_param, but for function defs
+indent_func_def_param                    = false    # false/true
+
+# Same as indent_func_call_param, but for function protos
+indent_func_proto_param                  = false    # false/true
+
+# Same as indent_func_call_param, but for class declarations
+indent_func_class_param                  = false    # false/true
+
+# Same as indent_func_call_param, but for class variable constructors
+indent_func_ctor_var_param               = false    # false/true
+
+# Same as indent_func_call_param, but for templates
+indent_template_param                    = false    # false/true
+
+# Double the indent for indent_func_xxx_param options
+indent_func_param_double                 = false    # false/true
+
+# Indentation column for standalone 'const' function decl/proto qualifier
+indent_func_const                        = 0        # number
+
+# Indentation column for standalone 'throw' function decl/proto qualifier
+indent_func_throw                        = 0        # number
+
+# The number of spaces to indent a continued '->' or '.'
+# Usually set to 0, 1, or indent_columns.
+indent_member                            = 0        # number
+
+# Spaces to indent single line ('//') comments on lines before code
+indent_sing_line_comments                = 0        # number
+
+# If set, will indent trailing single line ('//') comments relative
+# to the code instead of trying to keep the same absolute column
+indent_relative_single_line_comments     = false    # false/true
+
+# Spaces to indent 'case' from 'switch'
+# Usually 0 or indent_columns.
+indent_switch_case                       = 0        # number
+
+# Spaces to shift the 'case' line, without affecting any other lines
+# Usually 0.
+indent_case_shift                        = 0        # number
+
+# Spaces to indent '{' from 'case'.
+# By default, the brace will appear under the 'c' in case.
+# Usually set to 0 or indent_columns.
+indent_case_brace                        = 0        # number
+
+# Whether to indent comments found in first column
+indent_col1_comment                      = false    # false/true
+
+# How to indent goto labels
+#  >0 : absolute column where 1 is the leftmost column
+#  <=0 : subtract from brace indent
+indent_label                             = 1        # number
+
+# Same as indent_label, but for access specifiers that are followed by a colon
+indent_access_spec                       = 1        # number
+
+# Indent the code after an access specifier by one level.
+# If set, this option forces 'indent_access_spec=0'
+indent_access_spec_body                  = false    # false/true
+
+# If an open paren is followed by a newline, indent the next line so that it lines up after the open paren (not recommended)
+indent_paren_nl                          = false    # false/true
+
+# Controls the indent of a close paren after a newline.
+# 0: Indent to body level
+# 1: Align under the open paren
+# 2: Indent to the brace level
+indent_paren_close                       = 1        # number
+
+# Controls the indent of a comma when inside a paren.If TRUE, aligns under the open paren
+indent_comma_paren                       = false    # false/true
+
+# Controls the indent of a BOOL operator when inside a paren.If TRUE, aligns under the open paren
+indent_bool_paren                        = false    # false/true
+
+# If 'indent_bool_paren' is true, controls the indent of the first expression. If TRUE, aligns the first expression to the following ones
+indent_first_bool_expr                   = true     # false/true
+
+# If an open square is followed by a newline, indent the next line so that it lines up after the open square (not recommended)
+indent_square_nl                         = false    # false/true
+
+# Don't change the relative indent of ESQL/C 'EXEC SQL' bodies
+indent_preserve_sql                      = false    # false/true
+
+# Align continued statements at the '='. Default=True
+# If FALSE or the '=' is followed by a newline, the next line is indent one tab.
+indent_align_assign                      = true     # false/true
+
+# Indent OC blocks at brace level instead of usual rules.
+indent_oc_block                          = false    # false/true
+
+# Indent OC blocks in a message relative to the parameter name.
+# 0=use indent_oc_block rules, 1+=spaces to indent
+indent_oc_block_msg                      = 0        # number
+
+# Minimum indent for subsequent parameters
+indent_oc_msg_colon                      = 0        # number
+
+#
+# Spacing options
+#
+
+# Add or remove space around arithmetic operator '+', '-', '/', '*', etc
+sp_arith                                 = force    # ignore/add/remove/force
+
+# Add or remove space around assignment operator '=', '+=', etc
+sp_assign                                = force    # ignore/add/remove/force
+
+# Add or remove space around '=' in C++11 lambda capture specifications. Overrides sp_assign
+sp_cpp_lambda_assign                     = ignore   # ignore/add/remove/force
+
+# Add or remove space after the capture specification in C++11 lambda.
+sp_cpp_lambda_paren                      = ignore   # ignore/add/remove/force
+
+# Add or remove space around assignment operator '=' in a prototype
+sp_assign_default                        = ignore   # ignore/add/remove/force
+
+# Add or remove space before assignment operator '=', '+=', etc. Overrides sp_assign.
+sp_before_assign                         = ignore   # ignore/add/remove/force
+
+# Add or remove space after assignment operator '=', '+=', etc. Overrides sp_assign.
+sp_after_assign                          = ignore   # ignore/add/remove/force
+
+# Add or remove space around assignment '=' in enum
+sp_enum_assign                           = force    # ignore/add/remove/force
+
+# Add or remove space before assignment '=' in enum. Overrides sp_enum_assign.
+sp_enum_before_assign                    = ignore   # ignore/add/remove/force
+
+# Add or remove space after assignment '=' in enum. Overrides sp_enum_assign.
+sp_enum_after_assign                     = ignore   # ignore/add/remove/force
+
+# Add or remove space around preprocessor '##' concatenation operator. Default=Add
+sp_pp_concat                             = add      # ignore/add/remove/force
+
+# Add or remove space after preprocessor '#' stringify operator. Also affects the '#@' charizing operator.
+sp_pp_stringify                          = ignore   # ignore/add/remove/force
+
+# Add or remove space before preprocessor '#' stringify operator as in '#define x(y) L#y'.
+sp_before_pp_stringify                   = ignore   # ignore/add/remove/force
+
+# Add or remove space around boolean operators '&&' and '||'
+sp_bool                                  = force   # ignore/add/remove/force
+
+# Add or remove space around compare operator '<', '>', '==', etc
+sp_compare                               = force   # ignore/add/remove/force
+
+# Add or remove space inside '(' and ')'
+sp_inside_paren                          = ignore   # ignore/add/remove/force
+
+# Add or remove space between nested parens
+sp_paren_paren                           = ignore   # ignore/add/remove/force
+
+# Whether to balance spaces inside nested parens
+sp_balance_nested_parens                 = false    # false/true
+
+# Add or remove space between ')' and '{'
+sp_paren_brace                           = force    # ignore/add/remove/force
+
+# Add or remove space before pointer star '*'
+sp_before_ptr_star                       = force    # ignore/add/remove/force
+
+# Add or remove space before pointer star '*' that isn't followed by a variable name
+# If set to 'ignore', sp_before_ptr_star is used instead.
+sp_before_unnamed_ptr_star               = remove   # ignore/add/remove/force
+
+# Add or remove space between pointer stars '*'
+sp_between_ptr_star                      = remove   # ignore/add/remove/force
+
+# Add or remove space after pointer star '*', if followed by a word.
+sp_after_ptr_star                        = remove   # ignore/add/remove/force
+
+# Add or remove space after a pointer star '*', if followed by a func proto/def.
+sp_after_ptr_star_func                   = ignore   # ignore/add/remove/force
+
+# Add or remove space after a pointer star '*', if followed by an open paren (function types).
+sp_ptr_star_paren                        = ignore   # ignore/add/remove/force
+
+# Add or remove space before a pointer star '*', if followed by a func proto/def.
+sp_before_ptr_star_func                  = ignore   # ignore/add/remove/force
+
+# Add or remove space before a reference sign '&'
+sp_before_byref                          = ignore   # ignore/add/remove/force
+
+# Add or remove space before a reference sign '&' that isn't followed by a variable name
+# If set to 'ignore', sp_before_byref is used instead.
+sp_before_unnamed_byref                  = ignore   # ignore/add/remove/force
+
+# Add or remove space after reference sign '&', if followed by a word.
+sp_after_byref                           = ignore   # ignore/add/remove/force
+
+# Add or remove space after a reference sign '&', if followed by a func proto/def.
+sp_after_byref_func                      = ignore   # ignore/add/remove/force
+
+# Add or remove space before a reference sign '&', if followed by a func proto/def.
+sp_before_byref_func                     = ignore   # ignore/add/remove/force
+
+# Add or remove space between type and word. Default=Force
+sp_after_type                            = force    # ignore/add/remove/force
+
+# Add or remove space before the paren in the D constructs 'template Foo(' and 'class Foo('.
+sp_before_template_paren                 = ignore   # ignore/add/remove/force
+
+# Add or remove space in 'template <' vs 'template<'.
+# If set to ignore, sp_before_angle is used.
+sp_template_angle                        = ignore   # ignore/add/remove/force
+
+# Add or remove space before '<>'
+sp_before_angle                          = ignore   # ignore/add/remove/force
+
+# Add or remove space inside '<' and '>'
+sp_inside_angle                          = ignore   # ignore/add/remove/force
+
+# Add or remove space after '<>'
+sp_after_angle                           = ignore   # ignore/add/remove/force
+
+# Add or remove space between '<>' and '(' as found in 'new List<byte>();'
+sp_angle_paren                           = ignore   # ignore/add/remove/force
+
+# Add or remove space between '<>' and a word as in 'List<byte> m;'
+sp_angle_word                            = ignore   # ignore/add/remove/force
+
+# Add or remove space between '>' and '>' in '>>' (template stuff C++/C# only). Default=Add
+sp_angle_shift                           = add      # ignore/add/remove/force
+
+# Permit removal of the space between '>>' in 'foo<bar<int> >' (C++11 only). Default=False
+# sp_angle_shift cannot remove the space without this option.
+sp_permit_cpp11_shift                    = false    # false/true
+
+# Add or remove space before '(' of 'if', 'for', 'switch', and 'while'
+sp_before_sparen                         = force    # ignore/add/remove/force
+
+# Add or remove space inside if-condition '(' and ')'
+sp_inside_sparen                         = remove   # ignore/add/remove/force
+
+# Add or remove space before if-condition ')'. Overrides sp_inside_sparen.
+sp_inside_sparen_close                   = ignore   # ignore/add/remove/force
+
+# Add or remove space before if-condition '('. Overrides sp_inside_sparen.
+sp_inside_sparen_open                    = ignore   # ignore/add/remove/force
+
+# Add or remove space after ')' of 'if', 'for', 'switch', and 'while'
+sp_after_sparen                          = ignore   # ignore/add/remove/force
+
+# Add or remove space between ')' and '{' of 'if', 'for', 'switch', and 'while'
+sp_sparen_brace                          = force    # ignore/add/remove/force
+
+# Add or remove space between 'invariant' and '(' in the D language.
+sp_invariant_paren                       = ignore   # ignore/add/remove/force
+
+# Add or remove space after the ')' in 'invariant (C) c' in the D language.
+sp_after_invariant_paren                 = ignore   # ignore/add/remove/force
+
+# Add or remove space before empty statement ';' on 'if', 'for' and 'while'
+sp_special_semi                          = ignore   # ignore/add/remove/force
+
+# Add or remove space before ';'. Default=Remove
+sp_before_semi                           = remove   # ignore/add/remove/force
+
+# Add or remove space before ';' in non-empty 'for' statements
+sp_before_semi_for                       = remove   # ignore/add/remove/force
+
+# Add or remove space before a semicolon of an empty part of a for statement.
+sp_before_semi_for_empty                 = force   # ignore/add/remove/force
+
+# Add or remove space after ';', except when followed by a comment. Default=Add
+sp_after_semi                            = add      # ignore/add/remove/force
+
+# Add or remove space after ';' in non-empty 'for' statements. Default=Force
+sp_after_semi_for                        = force    # ignore/add/remove/force
+
+# Add or remove space after the final semicolon of an empty part of a for statement: for ( ; ; <here> ).
+sp_after_semi_for_empty                  = force    # ignore/add/remove/force
+
+# Add or remove space before '[' (except '[]')
+sp_before_square                         = ignore   # ignore/add/remove/force
+
+# Add or remove space before '[]'
+sp_before_squares                        = ignore   # ignore/add/remove/force
+
+# Add or remove space inside a non-empty '[' and ']'
+sp_inside_square                         = ignore   # ignore/add/remove/force
+
+# Add or remove space after ','
+sp_after_comma                           = force    # ignore/add/remove/force
+
+# Add or remove space before ','
+sp_before_comma                          = remove   # ignore/add/remove/force
+
+# Add or remove space between an open paren and comma: '(,' vs '( ,'
+sp_paren_comma                           = force    # ignore/add/remove/force
+
+# Add or remove space before the variadic '...' when preceded by a non-punctuator
+sp_before_ellipsis                       = ignore   # ignore/add/remove/force
+
+# Add or remove space after class ':'
+sp_after_class_colon                     = ignore   # ignore/add/remove/force
+
+# Add or remove space before class ':'
+sp_before_class_colon                    = ignore   # ignore/add/remove/force
+
+# Add or remove space before case ':'. Default=Remove
+sp_before_case_colon                     = remove   # ignore/add/remove/force
+
+# Add or remove space between 'operator' and operator sign
+sp_after_operator                        = ignore   # ignore/add/remove/force
+
+# Add or remove space between the operator symbol and the open paren, as in 'operator ++('
+sp_after_operator_sym                    = ignore   # ignore/add/remove/force
+
+# Add or remove space after C/D cast, i.e. 'cast(int)a' vs 'cast(int) a' or '(int)a' vs '(int) a'
+sp_after_cast                            = ignore   # ignore/add/remove/force
+
+# Add or remove spaces inside cast parens
+sp_inside_paren_cast                     = ignore   # ignore/add/remove/force
+
+# Add or remove space between the type and open paren in a C++ cast, i.e. 'int(exp)' vs 'int (exp)'
+sp_cpp_cast_paren                        = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'sizeof' and '('
+sp_sizeof_paren                          = ignore   # ignore/add/remove/force
+
+# Add or remove space after the tag keyword (Pawn)
+sp_after_tag                             = ignore   # ignore/add/remove/force
+
+# Add or remove space inside enum '{' and '}'
+sp_inside_braces_enum                    = force    # ignore/add/remove/force
+
+# Add or remove space inside struct/union '{' and '}'
+sp_inside_braces_struct                  = ignore   # ignore/add/remove/force
+
+# Add or remove space inside '{' and '}'
+sp_inside_braces                         = force    # ignore/add/remove/force
+
+# Add or remove space inside '{}'
+sp_inside_braces_empty                   = remove   # ignore/add/remove/force
+
+# Add or remove space between return type and function name
+# A minimum of 1 is forced except for pointer return types.
+sp_type_func                             = ignore   # ignore/add/remove/force
+
+# Add or remove space between function name and '(' on function declaration
+sp_func_proto_paren                      = force    # ignore/add/remove/force
+
+# Add or remove space between function name and '(' on function definition
+sp_func_def_paren                        = force    # ignore/add/remove/force
+
+# Add or remove space inside empty function '()'
+sp_inside_fparens                        = ignore   # ignore/add/remove/force
+
+# Add or remove space inside function '(' and ')'
+sp_inside_fparen                         = ignore   # ignore/add/remove/force
+
+# Add or remove space inside the first parens in the function type: 'void (*x)(...)'
+sp_inside_tparen                         = ignore   # ignore/add/remove/force
+
+# Add or remove between the parens in the function type: 'void (*x)(...)'
+sp_after_tparen_close                    = ignore   # ignore/add/remove/force
+
+# Add or remove space between ']' and '(' when part of a function call.
+sp_square_fparen                         = ignore   # ignore/add/remove/force
+
+# Add or remove space between ')' and '{' of function
+sp_fparen_brace                          = ignore   # ignore/add/remove/force
+
+# Add or remove space between function name and '(' on function calls
+sp_func_call_paren                       = force    # ignore/add/remove/force
+
+# Add or remove space between function name and '()' on function calls without parameters.
+# If set to 'ignore' (the default), sp_func_call_paren is used.
+sp_func_call_paren_empty                 = force    # ignore/add/remove/force
+
+# Add or remove space between the user function name and '(' on function calls
+# You need to set a keyword to be a user function, like this: 'set func_call_user _' in the config file.
+sp_func_call_user_paren                  = ignore   # ignore/add/remove/force
+
+# Add or remove space between a constructor/destructor and the open paren
+sp_func_class_paren                      = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'return' and '('
+sp_return_paren                          = ignore   # ignore/add/remove/force
+
+# Add or remove space between '__attribute__' and '('
+sp_attribute_paren                       = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'defined' and '(' in '#if defined (FOO)'
+sp_defined_paren                         = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'throw' and '(' in 'throw (something)'
+sp_throw_paren                           = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'throw' and anything other than '(' as in '@throw [...];'
+sp_after_throw                           = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'catch' and '(' in 'catch (something) { }'
+# If set to ignore, sp_before_sparen is used.
+sp_catch_paren                           = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'version' and '(' in 'version (something) { }' (D language)
+# If set to ignore, sp_before_sparen is used.
+sp_version_paren                         = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'scope' and '(' in 'scope (something) { }' (D language)
+# If set to ignore, sp_before_sparen is used.
+sp_scope_paren                           = ignore   # ignore/add/remove/force
+
+# Add or remove space between macro and value
+sp_macro                                 = force    # ignore/add/remove/force
+
+# Add or remove space between macro function ')' and value
+sp_macro_func                            = force    # ignore/add/remove/force
+
+# Add or remove space between 'else' and '{' if on the same line
+sp_else_brace                            = force    # ignore/add/remove/force
+
+# Add or remove space between '}' and 'else' if on the same line
+sp_brace_else                            = force    # ignore/add/remove/force
+
+# Add or remove space between '}' and the name of a typedef on the same line
+sp_brace_typedef                         = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'catch' and '{' if on the same line
+sp_catch_brace                           = ignore   # ignore/add/remove/force
+
+# Add or remove space between '}' and 'catch' if on the same line
+sp_brace_catch                           = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'finally' and '{' if on the same line
+sp_finally_brace                         = ignore   # ignore/add/remove/force
+
+# Add or remove space between '}' and 'finally' if on the same line
+sp_brace_finally                         = ignore   # ignore/add/remove/force
+
+# Add or remove space between 'try' and '{' if on the same line
+sp_try_brace                             = ignore   # ignore/add/remove/force
+
+# Add or remove space between get/set and '{' if on the same line
+sp_getset_brace                          = ignore   # ignore/add/remove/force
+
+# Add or remove space before the '::' operator
+sp_before_dc                             = ignore   # ignore/add/remove/force
+
+# Add or remove space after the '::' operator
+sp_after_dc                              = ignore   # ignore/add/remove/force
+
+# Add or remove around the D named array initializer ':' operator
+sp_d_array_colon                         = ignore   # ignore/add/remove/force
+
+# Add or remove space after the '!' (not) operator. Default=Remove
+sp_not                                   = remove   # ignore/add/remove/force
+
+# Add or remove space after the '~' (invert) operator. Default=Remove
+sp_inv                                   = remove   # ignore/add/remove/force
+
+# Add or remove space after the '&' (address-of) operator. Default=Remove
+# This does not affect the spacing after a '&' that is part of a type.
+sp_addr                                  = remove   # ignore/add/remove/force
+
+# Add or remove space around the '.' or '->' operators. Default=Remove
+sp_member                                = remove   # ignore/add/remove/force
+
+# Add or remove space after the '*' (dereference) operator. Default=Remove
+# This does not affect the spacing after a '*' that is part of a type.
+sp_deref                                 = remove   # ignore/add/remove/force
+
+# Add or remove space after '+' or '-', as in 'x = -5' or 'y = +7'. Default=Remove
+sp_sign                                  = remove   # ignore/add/remove/force
+
+# Add or remove space before or after '++' and '--', as in '(--x)' or 'y++;'. Default=Remove
+sp_incdec                                = remove   # ignore/add/remove/force
+
+# Add or remove space before a backslash-newline at the end of a line. Default=Add
+sp_before_nl_cont                        = force    # ignore/add/remove/force
+
+# Add or remove space after the scope '+' or '-', as in '-(void) foo;' or '+(int) bar;'
+sp_after_oc_scope                        = ignore   # ignore/add/remove/force
+
+# Add or remove space after the colon in message specs
+# '-(int) f:(int) x;' vs '-(int) f: (int) x;'
+sp_after_oc_colon                        = ignore   # ignore/add/remove/force
+
+# Add or remove space before the colon in message specs
+# '-(int) f: (int) x;' vs '-(int) f : (int) x;'
+sp_before_oc_colon                       = ignore   # ignore/add/remove/force
+
+# Add or remove space after the colon in immutable dictionary expression
+# 'NSDictionary *test = @{@"foo" :@"bar"};'
+sp_after_oc_dict_colon                   = ignore   # ignore/add/remove/force
+
+# Add or remove space before the colon in immutable dictionary expression
+# 'NSDictionary *test = @{@"foo" :@"bar"};'
+sp_before_oc_dict_colon                  = ignore   # ignore/add/remove/force
+
+# Add or remove space after the colon in message specs
+# '[object setValue:1];' vs '[object setValue: 1];'
+sp_after_send_oc_colon                   = ignore   # ignore/add/remove/force
+
+# Add or remove space before the colon in message specs
+# '[object setValue:1];' vs '[object setValue :1];'
+sp_before_send_oc_colon                  = ignore   # ignore/add/remove/force
+
+# Add or remove space after the (type) in message specs
+# '-(int)f: (int) x;' vs '-(int)f: (int)x;'
+sp_after_oc_type                         = ignore   # ignore/add/remove/force
+
+# Add or remove space after the first (type) in message specs
+# '-(int) f:(int)x;' vs '-(int)f:(int)x;'
+sp_after_oc_return_type                  = ignore   # ignore/add/remove/force
+
+# Add or remove space between '@selector' and '('
+# '@selector(msgName)' vs '@selector (msgName)'
+# Also applies to @protocol() constructs
+sp_after_oc_at_sel                       = ignore   # ignore/add/remove/force
+
+# Add or remove space between '@selector(x)' and the following word
+# '@selector(foo) a:' vs '@selector(foo)a:'
+sp_after_oc_at_sel_parens                = ignore   # ignore/add/remove/force
+
+# Add or remove space inside '@selector' parens
+# '@selector(foo)' vs '@selector( foo )'
+# Also applies to @protocol() constructs
+sp_inside_oc_at_sel_parens               = ignore   # ignore/add/remove/force
+
+# Add or remove space before a block pointer caret
+# '^int (int arg){...}' vs. ' ^int (int arg){...}'
+sp_before_oc_block_caret                 = ignore   # ignore/add/remove/force
+
+# Add or remove space after a block pointer caret
+# '^int (int arg){...}' vs. '^ int (int arg){...}'
+sp_after_oc_block_caret                  = ignore   # ignore/add/remove/force
+
+# Add or remove space between the receiver and selector in a message.
+# '[receiver selector ...]'
+sp_after_oc_msg_receiver                 = ignore   # ignore/add/remove/force
+
+# Add or remove space after @property.
+sp_after_oc_property                     = ignore   # ignore/add/remove/force
+
+# Add or remove space around the ':' in 'b ? t : f'
+sp_cond_colon                            = force    # ignore/add/remove/force
+
+# Add or remove space around the '?' in 'b ? t : f'
+sp_cond_question                         = force    # ignore/add/remove/force
+
+# Fix the spacing between 'case' and the label. Only 'ignore' and 'force' make sense here.
+sp_case_label                            = force    # ignore/add/remove/force
+
+# Control the space around the D '..' operator.
+sp_range                                 = ignore   # ignore/add/remove/force
+
+# Control the spacing after ':' in 'for (TYPE VAR : EXPR)' (Java)
+sp_after_for_colon                       = ignore   # ignore/add/remove/force
+
+# Control the spacing before ':' in 'for (TYPE VAR : EXPR)' (Java)
+sp_before_for_colon                      = ignore   # ignore/add/remove/force
+
+# Control the spacing in 'extern (C)' (D)
+sp_extern_paren                          = ignore   # ignore/add/remove/force
+
+# Control the space after the opening of a C++ comment '// A' vs '//A'
+sp_cmt_cpp_start                         = force    # ignore/add/remove/force
+
+# Controls the spaces between #else or #endif and a trailing comment
+sp_endif_cmt                             = ignore   # ignore/add/remove/force
+
+# Controls the spaces after 'new', 'delete', and 'delete[]'
+sp_after_new                             = ignore   # ignore/add/remove/force
+
+# Controls the spaces before a trailing or embedded comment
+sp_before_tr_emb_cmt                     = ignore   # ignore/add/remove/force
+
+# Number of spaces before a trailing or embedded comment
+sp_num_before_tr_emb_cmt                 = 0        # number
+
+# Control space between a Java annotation and the open paren.
+sp_annotation_paren                      = ignore   # ignore/add/remove/force
+
+#
+# Code alignment (not left column spaces/tabs)
+#
+
+# Whether to keep non-indenting tabs
+align_keep_tabs                          = false    # false/true
+
+# Whether to use tabs for aligning
+align_with_tabs                          = false    # false/true
+
+# Whether to bump out to the next tab when aligning
+align_on_tabstop                         = false    # false/true
+
+# Whether to left-align numbers
+align_number_left                        = false    # false/true
+
+# Align variable definitions in prototypes and functions
+align_func_params                        = true     # false/true
+
+# Align parameters in single-line functions that have the same name.
+# The function names must already be aligned with each other.
+align_same_func_call_params              = false    # false/true
+
+# The span for aligning variable definitions (0=don't align)
+align_var_def_span                       = 1        # number
+
+# How to align the star in variable definitions.
+#  0=Part of the type     'void *   foo;'
+#  1=Part of the variable 'void     *foo;'
+#  2=Dangling             'void    *foo;'
+align_var_def_star_style                 = 1        # number
+
+# How to align the '&' in variable definitions.
+#  0=Part of the type
+#  1=Part of the variable
+#  2=Dangling
+align_var_def_amp_style                  = 0        # number
+
+# The threshold for aligning variable definitions (0=no limit)
+align_var_def_thresh                     = 0        # number
+
+# The gap for aligning variable definitions
+align_var_def_gap                        = 0        # number
+
+# Whether to align the colon in struct bit fields
+align_var_def_colon                      = false    # false/true
+
+# Whether to align any attribute after the variable name
+align_var_def_attribute                  = false    # false/true
+
+# Whether to align inline struct/enum/union variable definitions
+align_var_def_inline                     = false    # false/true
+
+# The span for aligning on '=' in assignments (0=don't align)
+align_assign_span                        = 1        # number
+
+# The threshold for aligning on '=' in assignments (0=no limit)
+align_assign_thresh                      = 0        # number
+
+# The span for aligning on '=' in enums (0=don't align)
+align_enum_equ_span                      = 1        # number
+
+# The threshold for aligning on '=' in enums (0=no limit)
+align_enum_equ_thresh                    = 0        # number
+
+# The span for aligning struct/union (0=don't align)
+align_var_struct_span                    = 1        # number
+
+# The threshold for aligning struct/union member definitions (0=no limit)
+align_var_struct_thresh                  = 0        # number
+
+# The gap for aligning struct/union member definitions
+align_var_struct_gap                     = 0        # number
+
+# The span for aligning struct initializer values (0=don't align)
+align_struct_init_span                   = 1        # number
+
+# The minimum space between the type and the synonym of a typedef
+align_typedef_gap                        = 0        # number
+
+# The span for aligning single-line typedefs (0=don't align)
+align_typedef_span                       = 1        # number
+
+# How to align typedef'd functions with other typedefs
+# 0: Don't mix them at all
+# 1: align the open paren with the types
+# 2: align the function type name with the other type names
+align_typedef_func                       = 0        # number
+
+# Controls the positioning of the '*' in typedefs. Just try it.
+# 0: Align on typedef type, ignore '*'
+# 1: The '*' is part of type name: typedef int  *pint;
+# 2: The '*' is part of the type, but dangling: typedef int *pint;
+align_typedef_star_style                 = 0        # number
+
+# Controls the positioning of the '&' in typedefs. Just try it.
+# 0: Align on typedef type, ignore '&'
+# 1: The '&' is part of type name: typedef int  &pint;
+# 2: The '&' is part of the type, but dangling: typedef int &pint;
+align_typedef_amp_style                  = 0        # number
+
+# The span for aligning comments that end lines (0=don't align)
+align_right_cmt_span                     = 1        # number
+
+# If aligning comments, mix with comments after '}' and #endif with less than 3 spaces before the comment
+align_right_cmt_mix                      = false    # false/true
+
+# If a trailing comment is more than this number of columns away from the text it follows,
+# it will qualify for being aligned. This has to be > 0 to do anything.
+align_right_cmt_gap                      = 1        # number
+
+# Align trailing comment at or beyond column N; 'pulls in' comments as a bonus side effect (0=ignore)
+align_right_cmt_at_col                   = 0        # number
+
+# The span for aligning function prototypes (0=don't align)
+align_func_proto_span                    = 0        # number
+
+# Minimum gap between the return type and the function name.
+align_func_proto_gap                     = 0        # number
+
+# Align function protos on the 'operator' keyword instead of what follows
+align_on_operator                        = false    # false/true
+
+# Whether to mix aligning prototype and variable declarations.
+# If true, align_var_def_XXX options are used instead of align_func_proto_XXX options.
+align_mix_var_proto                      = false    # false/true
+
+# Align single-line functions with function prototypes, uses align_func_proto_span
+align_single_line_func                   = false    # false/true
+
+# Aligning the open brace of single-line functions.
+# Requires align_single_line_func=true, uses align_func_proto_span
+align_single_line_brace                  = false    # false/true
+
+# Gap for align_single_line_brace.
+align_single_line_brace_gap              = 0        # number
+
+# The span for aligning ObjC msg spec (0=don't align)
+align_oc_msg_spec_span                   = 0        # number
+
+# Whether to align macros wrapped with a backslash and a newline.
+# This will not work right if the macro contains a multi-line comment.
+align_nl_cont                            = true     # false/true
+
+# # Align macro functions and variables together
+align_pp_define_together                 = true     # false/true
+
+# The minimum space between label and value of a preprocessor define
+align_pp_define_gap                      = 0        # number
+
+# The span for aligning on '#define' bodies (0=don't align)
+align_pp_define_span                     = 1        # number
+
+# Align lines that start with '<<' with previous '<<'. Default=true
+align_left_shift                         = true     # false/true
+
+# Span for aligning parameters in an Obj-C message call on the ':' (0=don't align)
+align_oc_msg_colon_span                  = 0        # number
+
+# If true, always align with the first parameter, even if it is too short.
+align_oc_msg_colon_first                 = false    # false/true
+
+# Aligning parameters in an Obj-C '+' or '-' declaration on the ':'
+align_oc_decl_colon                      = false    # false/true
+
+#
+# Newline adding and removing options
+#
+
+# Whether to collapse empty blocks between '{' and '}'
+nl_collapse_empty_body                   = false    # false/true
+
+# Don't split one-line braced assignments - 'foo_t f = { 1, 2 };'
+nl_assign_leave_one_liners               = false    # false/true
+
+# Don't split one-line braced statements inside a class xx { } body
+nl_class_leave_one_liners                = false    # false/true
+
+# Don't split one-line enums: 'enum foo { BAR = 15 };'
+nl_enum_leave_one_liners                 = false    # false/true
+
+# Don't split one-line get or set functions
+nl_getset_leave_one_liners               = false    # false/true
+
+# Don't split one-line function definitions - 'int foo() { return 0; }'
+nl_func_leave_one_liners                 = false    # false/true
+
+# Don't split one-line if/else statements - 'if(a) b++;'
+nl_if_leave_one_liners                   = false    # false/true
+
+# Don't split one-line OC messages
+nl_oc_msg_leave_one_liner                = false    # false/true
+
+# Add or remove newlines at the start of the file
+nl_start_of_file                         = ignore   # ignore/add/remove/force
+
+# The number of newlines at the start of the file (only used if nl_start_of_file is 'add' or 'force'
+nl_start_of_file_min                     = 0        # number
+
+# Add or remove newline at the end of the file
+nl_end_of_file                           = force    # ignore/add/remove/force
+
+# The number of newlines at the end of the file (only used if nl_end_of_file is 'add' or 'force')
+nl_end_of_file_min                       = 1        # number
+
+# Add or remove newline between '=' and '{'
+nl_assign_brace                          = force    # ignore/add/remove/force
+
+# Add or remove newline between '=' and '[' (D only)
+nl_assign_square                         = ignore   # ignore/add/remove/force
+
+# Add or remove newline after '= [' (D only). Will also affect the newline before the ']'
+nl_after_square_assign                   = ignore   # ignore/add/remove/force
+
+# The number of blank lines after a block of variable definitions at the top of a function body
+# 0 = No change (default)
+nl_func_var_def_blk                      = 0        # number
+
+# The number of newlines before a block of typedefs
+# 0 = No change (default)
+nl_typedef_blk_start                     = 0        # number
+
+# The number of newlines after a block of typedefs
+# 0 = No change (default)
+nl_typedef_blk_end                       = 0        # number
+
+# The maximum consecutive newlines within a block of typedefs
+# 0 = No change (default)
+nl_typedef_blk_in                        = 0        # number
+
+# The number of newlines before a block of variable definitions not at the top of a function body
+# 0 = No change (default)
+nl_var_def_blk_start                     = 0        # number
+
+# The number of newlines after a block of variable definitions not at the top of a function body
+# 0 = No change (default)
+nl_var_def_blk_end                       = 0        # number
+
+# The maximum consecutive newlines within a block of variable definitions
+# 0 = No change (default)
+nl_var_def_blk_in                        = 0        # number
+
+# Add or remove newline between a function call's ')' and '{', as in:
+# list_for_each(item, &list) { }
+nl_fcall_brace                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'enum' and '{'
+nl_enum_brace                            = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'struct and '{'
+nl_struct_brace                          = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'union' and '{'
+nl_union_brace                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'if' and '{'
+nl_if_brace                              = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'else'
+nl_brace_else                            = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'else if' and '{'
+# If set to ignore, nl_if_brace is used instead
+nl_elseif_brace                          = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'else' and '{'
+nl_else_brace                            = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'else' and 'if'
+nl_else_if                               = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'finally'
+nl_brace_finally                         = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'finally' and '{'
+nl_finally_brace                         = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'try' and '{'
+nl_try_brace                             = ignore   # ignore/add/remove/force
+
+# Add or remove newline between get/set and '{'
+nl_getset_brace                          = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'for' and '{'
+nl_for_brace                             = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'catch' and '{'
+nl_catch_brace                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'catch'
+nl_brace_catch                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'while' and '{'
+nl_while_brace                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'scope (x)' and '{' (D)
+nl_scope_brace                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'unittest' and '{' (D)
+nl_unittest_brace                        = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'version (x)' and '{' (D)
+nl_version_brace                         = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'using' and '{'
+nl_using_brace                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline between two open or close braces.
+# Due to general newline/brace handling, REMOVE may not work.
+nl_brace_brace                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'do' and '{'
+nl_do_brace                              = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'while' of 'do' statement
+nl_brace_while                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'switch' and '{'
+nl_switch_brace                          = ignore   # ignore/add/remove/force
+
+# Add a newline between ')' and '{' if the ')' is on a different line than the if/for/etc.
+# Overrides nl_for_brace, nl_if_brace, nl_switch_brace, nl_while_switch, and nl_catch_brace.
+nl_multi_line_cond                       = false    # false/true
+
+# Force a newline in a define after the macro name for multi-line defines.
+nl_multi_line_define                     = true     # false/true
+
+# Whether to put a newline before 'case' statement
+nl_before_case                           = false     # false/true
+
+# Add or remove newline between ')' and 'throw'
+nl_before_throw                          = ignore   # ignore/add/remove/force
+
+# Whether to put a newline after 'case' statement
+nl_after_case                            = false    # false/true
+
+# Add or remove a newline between a case ':' and '{'. Overrides nl_after_case.
+nl_case_colon_brace                      = ignore   # ignore/add/remove/force
+
+# Newline between namespace and {
+nl_namespace_brace                       = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'template<>' and whatever follows.
+nl_template_class                        = ignore   # ignore/add/remove/force
+
+# Add or remove newline between 'class' and '{'
+nl_class_brace                           = ignore   # ignore/add/remove/force
+
+# Add or remove newline after each ',' in the constructor member initialization
+nl_class_init_args                       = ignore   # ignore/add/remove/force
+
+# Add or remove newline between return type and function name in a function definition
+nl_func_type_name                        = force   # ignore/add/remove/force
+
+# Add or remove newline between return type and function name inside a class {}
+# Uses nl_func_type_name or nl_func_proto_type_name if set to ignore.
+nl_func_type_name_class                  = ignore   # ignore/add/remove/force
+
+# Add or remove newline between function scope and name in a definition
+# Controls the newline after '::' in 'void A::f() { }'
+nl_func_scope_name                       = ignore   # ignore/add/remove/force
+
+# Add or remove newline between return type and function name in a prototype
+nl_func_proto_type_name                  = ignore   # ignore/add/remove/force
+
+# Add or remove newline between a function name and the opening '('
+nl_func_paren                            = ignore   # ignore/add/remove/force
+
+# Add or remove newline between a function name and the opening '(' in the definition
+nl_func_def_paren                        = ignore   # ignore/add/remove/force
+
+# Add or remove newline after '(' in a function declaration
+nl_func_decl_start                       = ignore   # ignore/add/remove/force
+
+# Add or remove newline after '(' in a function definition
+nl_func_def_start                        = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_decl_start when there is only one parameter.
+nl_func_decl_start_single                = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_def_start when there is only one parameter.
+nl_func_def_start_single                 = ignore   # ignore/add/remove/force
+
+# Add or remove newline after each ',' in a function declaration
+nl_func_decl_args                        = ignore   # ignore/add/remove/force
+
+# Add or remove newline after each ',' in a function definition
+nl_func_def_args                         = ignore   # ignore/add/remove/force
+
+# Add or remove newline before the ')' in a function declaration
+nl_func_decl_end                         = ignore   # ignore/add/remove/force
+
+# Add or remove newline before the ')' in a function definition
+nl_func_def_end                          = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_decl_end when there is only one parameter.
+nl_func_decl_end_single                  = ignore   # ignore/add/remove/force
+
+# Overrides nl_func_def_end when there is only one parameter.
+nl_func_def_end_single                   = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '()' in a function declaration.
+nl_func_decl_empty                       = ignore   # ignore/add/remove/force
+
+# Add or remove newline between '()' in a function definition.
+nl_func_def_empty                        = ignore   # ignore/add/remove/force
+
+# Whether to put each OC message parameter on a separate line
+# See nl_oc_msg_leave_one_liner
+nl_oc_msg_args                           = false    # false/true
+
+# Add or remove newline between function signature and '{'
+nl_fdef_brace                            = force   # ignore/add/remove/force
+
+# Add or remove a newline between the return keyword and return expression.
+nl_return_expr                           = ignore   # ignore/add/remove/force
+
+# Whether to put a newline after semicolons, except in 'for' statements
+nl_after_semicolon                       = false    # false/true
+
+# Whether to put a newline after brace open.
+# This also adds a newline before the matching brace close.
+nl_after_brace_open                      = false    # false/true
+
+# If nl_after_brace_open and nl_after_brace_open_cmt are true, a newline is
+# placed between the open brace and a trailing single-line comment.
+nl_after_brace_open_cmt                  = false    # false/true
+
+# Whether to put a newline after a virtual brace open with a non-empty body.
+# These occur in un-braced if/while/do/for statement bodies.
+nl_after_vbrace_open                     = false    # false/true
+
+# Whether to put a newline after a virtual brace open with an empty body.
+# These occur in un-braced if/while/do/for statement bodies.
+nl_after_vbrace_open_empty               = false    # false/true
+
+# Whether to put a newline after a brace close.
+# Does not apply if followed by a necessary ';'.
+nl_after_brace_close                     = false    # false/true
+
+# Whether to put a newline after a virtual brace close.
+# Would add a newline before return in: 'if (foo) a++; return;'
+nl_after_vbrace_close                    = false    # false/true
+
+# Control the newline between the close brace and 'b' in: 'struct { int a; } b;'
+# Affects enums, unions, and structures. If set to ignore, uses nl_after_brace_close
+nl_brace_struct_var                      = ignore   # ignore/add/remove/force
+
+# Whether to alter newlines in '#define' macros
+nl_define_macro                          = false    # false/true
+
+# Whether to not put blanks after '#ifxx', '#elxx', or before '#endif'
+nl_squeeze_ifdef                         = false    # false/true
+
+# Add or remove blank line before 'if'
+nl_before_if                             = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'if' statement
+nl_after_if                              = ignore   # ignore/add/remove/force
+
+# Add or remove blank line before 'for'
+nl_before_for                            = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'for' statement
+nl_after_for                             = ignore   # ignore/add/remove/force
+
+# Add or remove blank line before 'while'
+nl_before_while                          = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'while' statement
+nl_after_while                           = ignore   # ignore/add/remove/force
+
+# Add or remove blank line before 'switch'
+nl_before_switch                         = force    # ignore/add/remove/force
+
+# Add or remove blank line after 'switch' statement
+nl_after_switch                          = force    # ignore/add/remove/force
+
+# Add or remove blank line before 'do'
+nl_before_do                             = ignore   # ignore/add/remove/force
+
+# Add or remove blank line after 'do/while' statement
+nl_after_do                              = ignore   # ignore/add/remove/force
+
+# Whether to double-space commented-entries in struct/enum
+nl_ds_struct_enum_cmt                    = false    # false/true
+
+# Whether to double-space before the close brace of a struct/union/enum
+# (lower priority than 'eat_blanks_before_close_brace)'
+nl_ds_struct_enum_close_brace            = false    # false/true
+
+# Add or remove a newline around a class colon.
+# Related to pos_class_colon, nl_class_init_args, and pos_comma.
+nl_class_colon                           = ignore   # ignore/add/remove/force
+
+# Change simple unbraced if statements into a one-liner
+# 'if(b)\n i++;' => 'if(b) i++;'
+nl_create_if_one_liner                   = false    # false/true
+
+# Change simple unbraced for statements into a one-liner
+# 'for (i=0;i<5;i++)\n foo(i);' => 'for (i=0;i<5;i++) foo(i);'
+nl_create_for_one_liner                  = false    # false/true
+
+# Change simple unbraced while statements into a one-liner
+# 'while (i<5)\n foo(i++);' => 'while (i<5) foo(i++);'
+nl_create_while_one_liner                = false    # false/true
+
+#
+# Positioning options
+#
+
+# The position of arithmetic operators in wrapped expressions
+pos_arith                                = ignore   # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force
+
+# The position of assignment in wrapped expressions.
+# Do not affect '=' followed by '{'
+pos_assign                               = ignore   # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force
+
+# The position of boolean operators in wrapped expressions
+pos_bool                                 = ignore   # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force
+
+# The position of comparison operators in wrapped expressions
+pos_compare                              = ignore   # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force
+
+# The position of conditional (b ? t : f) operators in wrapped expressions
+pos_conditional                          = ignore   # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force
+
+# The position of the comma in wrapped expressions
+pos_comma                                = ignore   # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force
+
+# The position of the comma in the constructor initialization list
+pos_class_comma                          = ignore   # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force
+
+# The position of colons between constructor and member initialization
+pos_class_colon                          = ignore   # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force
+
+#
+# Line Splitting options
+#
+
+# Try to limit code width to N number of columns
+code_width                               = 80       # number
+
+# Whether to fully split long 'for' statements at semi-colons
+ls_for_split_full                        = false    # false/true
+
+# Whether to fully split long function protos/calls at commas
+ls_func_split_full                       = false    # false/true
+
+# Whether to split lines as close to code_width as possible and ignore some groupings
+ls_code_width                            = false    # false/true
+
+#
+# Blank line options
+#
+
+# The maximum consecutive newlines
+nl_max                                   = 0        # number
+
+# The number of newlines after a function prototype, if followed by another function prototype
+nl_after_func_proto                      = 0        # number
+
+# The number of newlines after a function prototype, if not followed by another function prototype
+nl_after_func_proto_group                = 0        # number
+
+# The number of newlines after '}' of a multi-line function body
+nl_after_func_body                       = 0        # number
+
+# The number of newlines after '}' of a multi-line function body in a class declaration
+nl_after_func_body_class                 = 0        # number
+
+# The number of newlines after '}' of a single line function body
+nl_after_func_body_one_liner             = 0        # number
+
+# The minimum number of newlines before a multi-line comment.
+# Doesn't apply if after a brace open or another multi-line comment.
+nl_before_block_comment                  = 0        # number
+
+# The minimum number of newlines before a single-line C comment.
+# Doesn't apply if after a brace open or other single-line C comments.
+nl_before_c_comment                      = 0        # number
+
+# The minimum number of newlines before a CPP comment.
+# Doesn't apply if after a brace open or other CPP comments.
+nl_before_cpp_comment                    = 0        # number
+
+# Whether to force a newline after a multi-line comment.
+nl_after_multiline_comment               = false    # false/true
+
+# The number of newlines after '}' or ';' of a struct/enum/union definition
+nl_after_struct                          = 0        # number
+
+# The number of newlines after '}' or ';' of a class definition
+nl_after_class                           = 0        # number
+
+# The number of newlines before a 'private:', 'public:', 'protected:', 'signals:', or 'slots:' label.
+# Will not change the newline count if after a brace open.
+# 0 = No change.
+nl_before_access_spec                    = 0        # number
+
+# The number of newlines after a 'private:', 'public:', 'protected:', 'signals:', or 'slots:' label.
+# 0 = No change.
+nl_after_access_spec                     = 0        # number
+
+# The number of newlines between a function def and the function comment.
+# 0 = No change.
+nl_comment_func_def                      = 0        # number
+
+# The number of newlines after a try-catch-finally block that isn't followed by a brace close.
+# 0 = No change.
+nl_after_try_catch_finally               = 0        # number
+
+# The number of newlines before and after a property, indexer or event decl.
+# 0 = No change.
+nl_around_cs_property                    = 0        # number
+
+# The number of newlines between the get/set/add/remove handlers in C#.
+# 0 = No change.
+nl_between_get_set                       = 0        # number
+
+# Add or remove newline between C# property and the '{'
+nl_property_brace                        = ignore   # ignore/add/remove/force
+
+# Whether to remove blank lines after '{'
+eat_blanks_after_open_brace              = false    # false/true
+
+# Whether to remove blank lines before '}'
+eat_blanks_before_close_brace            = false    # false/true
+
+# How aggressively to remove extra newlines not in preproc.
+# 0: No change
+# 1: Remove most newlines not handled by other config
+# 2: Remove all newlines and reformat completely by config
+nl_remove_extra_newlines                 = 0        # number
+
+# Whether to put a blank line before 'return' statements, unless after an open brace.
+nl_before_return                         = true     # false/true
+
+# Whether to put a blank line after 'return' statements, unless followed by a close brace.
+nl_after_return                          = false    # false/true
+
+# Whether to put a newline after a Java annotation statement.
+# Only affects annotations that are after a newline.
+nl_after_annotation                      = ignore   # ignore/add/remove/force
+
+# Controls the newline between two annotations.
+nl_between_annotation                    = ignore   # ignore/add/remove/force
+
+#
+# Code modifying options (non-whitespace)
+#
+
+# Add or remove braces on single-line 'do' statement
+mod_full_brace_do                        = ignore   # ignore/add/remove/force
+
+# Add or remove braces on single-line 'for' statement
+mod_full_brace_for                       = ignore   # ignore/add/remove/force
+
+# Add or remove braces on single-line function definitions. (Pawn)
+mod_full_brace_function                  = ignore   # ignore/add/remove/force
+
+# Add or remove braces on single-line 'if' statement. Will not remove the braces if they contain an 'else'.
+mod_full_brace_if                        = ignore   # ignore/add/remove/force
+
+# Make all if/elseif/else statements in a chain be braced or not. Overrides mod_full_brace_if.
+# If any must be braced, they are all braced.  If all can be unbraced, then the braces are removed.
+mod_full_brace_if_chain                  = false    # false/true
+
+# Don't remove braces around statements that span N newlines
+mod_full_brace_nl                        = 0        # number
+
+# Add or remove braces on single-line 'while' statement
+mod_full_brace_while                     = ignore   # ignore/add/remove/force
+
+# Add or remove braces on single-line 'using ()' statement
+mod_full_brace_using                     = ignore   # ignore/add/remove/force
+
+# Add or remove unnecessary paren on 'return' statement
+mod_paren_on_return                      = remove   # ignore/add/remove/force
+
+# Whether to change optional semicolons to real semicolons
+mod_pawn_semicolon                       = false    # false/true
+
+# Add parens on 'while' and 'if' statement around bools
+mod_full_paren_if_bool                   = false    # false/true
+
+# Whether to remove superfluous semicolons
+mod_remove_extra_semicolon               = false    # false/true
+
+# If a function body exceeds the specified number of newlines and doesn't have a comment after
+# the close brace, a comment will be added.
+mod_add_long_function_closebrace_comment = 0        # number
+
+# If a switch body exceeds the specified number of newlines and doesn't have a comment after
+# the close brace, a comment will be added.
+mod_add_long_switch_closebrace_comment   = 0        # number
+
+# If an #ifdef body exceeds the specified number of newlines and doesn't have a comment after
+# the #endif, a comment will be added.
+mod_add_long_ifdef_endif_comment         = 1        # number
+
+# If an #ifdef or #else body exceeds the specified number of newlines and doesn't have a comment after
+# the #else, a comment will be added.
+mod_add_long_ifdef_else_comment          = 1        # number
+
+# If TRUE, will sort consecutive single-line 'import' statements [Java, D]
+mod_sort_import                          = false    # false/true
+
+# If TRUE, will sort consecutive single-line 'using' statements [C#]
+mod_sort_using                           = false    # false/true
+
+# If TRUE, will sort consecutive single-line '#include' statements [C/C++] and '#import' statements [Obj-C]
+# This is generally a bad idea, as it may break your code.
+mod_sort_include                         = false    # false/true
+
+# If TRUE, it will move a 'break' that appears after a fully braced 'case' before the close brace.
+mod_move_case_break                      = true     # false/true
+
+# Will add or remove the braces around a fully braced case statement.
+# Will only remove the braces if there are no variable declarations in the block.
+mod_case_brace                           = ignore   # ignore/add/remove/force
+
+# If TRUE, it will remove a void 'return;' that appears as the last statement in a function.
+mod_remove_empty_return                  = true     # false/true
+
+#
+# Comment modifications
+#
+
+# Try to wrap comments at cmt_width columns
+cmt_width                                = 80       # number
+
+# Set the comment reflow mode (default: 0)
+# 0: no reflowing (apart from the line wrapping due to cmt_width)
+# 1: no touching at all
+# 2: full reflow
+cmt_reflow_mode                          = 0        # number
+
+# If false, disable all multi-line comment changes, including cmt_width. keyword substitution, and leading chars.
+# Default is true.
+cmt_indent_multi                         = true     # false/true
+
+# Whether to group c-comments that look like they are in a block
+cmt_c_group                              = false    # false/true
+
+# Whether to put an empty '/*' on the first line of the combined c-comment
+cmt_c_nl_start                           = false    # false/true
+
+# Whether to put a newline before the closing '*/' of the combined c-comment
+cmt_c_nl_end                             = true     # false/true
+
+# Whether to group cpp-comments that look like they are in a block
+cmt_cpp_group                            = false    # false/true
+
+# Whether to put an empty '/*' on the first line of the combined cpp-comment
+cmt_cpp_nl_start                         = false    # false/true
+
+# Whether to put a newline before the closing '*/' of the combined cpp-comment
+cmt_cpp_nl_end                           = true     # false/true
+
+# Whether to change cpp-comments into c-comments
+cmt_cpp_to_c                             = true     # false/true
+
+# Whether to put a star on subsequent comment lines
+cmt_star_cont                            = true     # false/true
+
+# The number of spaces to insert at the start of subsequent comment lines
+cmt_sp_before_star_cont                  = 0        # number
+
+# The number of spaces to insert after the star on subsequent comment lines
+cmt_sp_after_star_cont                   = 0        # number
+
+# For multi-line comments with a '*' lead, remove leading spaces if the first and last lines of
+# the comment are the same length. Default=True
+cmt_multi_check_last                     = true     # false/true
+
+# The filename that contains text to insert at the head of a file if the file doesn't start with a C/C++ comment.
+# Will substitute $(filename) with the current file's name.
+cmt_insert_file_header                   = ""         # string
+
+# The filename that contains text to insert at the end of a file if the file doesn't end with a C/C++ comment.
+# Will substitute $(filename) with the current file's name.
+cmt_insert_file_footer                   = ""         # string
+
+# The filename that contains text to insert before a function implementation if the function isn't preceded with a C/C++ comment.
+# Will substitute $(function) with the function name and $(javaparam) with the javadoc @param and @return stuff.
+# Will also substitute $(fclass) with the class name: void CFoo::Bar() { ... }
+cmt_insert_func_header                   = ""         # string
+
+# The filename that contains text to insert before a class if the class isn't preceded with a C/C++ comment.
+# Will substitute $(class) with the class name.
+cmt_insert_class_header                  = ""         # string
+
+# The filename that contains text to insert before a Obj-C message specification if the method isn't preceded with a C/C++ comment.
+# Will substitute $(message) with the function name and $(javaparam) with the javadoc @param and @return stuff.
+cmt_insert_oc_msg_header                 = ""         # string
+
+# If a preprocessor is encountered when stepping backwards from a function name, then
+# this option decides whether the comment should be inserted.
+# Affects cmt_insert_oc_msg_header, cmt_insert_func_header and cmt_insert_class_header.
+cmt_insert_before_preproc                = false    # false/true
+
+#
+# Preprocessor options
+#
+
+# Control indent of preprocessors inside #if blocks at brace level 0
+pp_indent                                = ignore   # ignore/add/remove/force
+
+# Whether to indent #if/#else/#endif at the brace level (true) or from column 1 (false)
+pp_indent_at_level                       = false    # false/true
+
+# If pp_indent_at_level=false, specifies the number of columns to indent per level. Default=1.
+pp_indent_count                          = 1        # number
+
+# Add or remove space after # based on pp_level of #if blocks
+pp_space                                 = ignore   # ignore/add/remove/force
+
+# Sets the number of spaces added with pp_space
+pp_space_count                           = 0        # number
+
+# The indent for #region and #endregion in C# and '#pragma region' in C/C++
+pp_indent_region                         = 0        # number
+
+# Whether to indent the code between #region and #endregion
+pp_region_indent_code                    = false    # false/true
+
+# If pp_indent_at_level=true, sets the indent for #if, #else, and #endif when not at file-level
+pp_indent_if                             = 0        # number
+
+# Control whether to indent the code between #if, #else and #endif when not at file-level
+pp_if_indent_code                        = false    # false/true
+
+# Whether to indent '#define' at the brace level (true) or from column 1 (false)
+pp_define_at_level                       = false    # false/true
+
+# You can force a token to be a type with the 'type' option.
+# Example:
+# type myfoo1 myfoo2
+#
+# You can create custom macro-based indentation using macro-open,
+# macro-else and macro-close.
+# Example:
+# macro-open  BEGIN_TEMPLATE_MESSAGE_MAP
+# macro-open  BEGIN_MESSAGE_MAP
+# macro-close END_MESSAGE_MAP
+#
+# You can assign any keyword to any type with the set option.
+# set func_call_user _ N_
+#
+# The full syntax description of all custom definition config entries
+# is shown below:
+#
+# define custom tokens as:
+# - embed whitespace in token using '' escape character, or
+#   put token in quotes
+# - these: ' " and ` are recognized as quote delimiters
+#
+# type token1 token2 token3 ...
+#             ^ optionally specify multiple tokens on a single line
+# define def_token output_token
+#                  ^ output_token is optional, then NULL is assumed
+# macro-open token
+# macro-close token
+# macro-else token
+# set id token1 token2 ...
+#               ^ optionally specify multiple tokens on a single line
+#     ^ id is one of the names in token_enum.h sans the CT_ prefix,
+#       e.g. PP_PRAGMA
+#
+# all tokens are separated by any mix of ',' commas, '=' equal signs
+# and whitespace (space, tab)
+#
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..9740eed
--- /dev/null
@@ -0,0 +1,360 @@
+## 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.
+
+AC_PREREQ([2.68])
+AC_INIT([mu],[1.4.15],[https://github.com/djcb/mu/issues],[mu])
+AC_COPYRIGHT([Copyright (C) 2008-2020 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])
+
+m4_ifdef([AX_IS_RELEASE],[AX_IS_RELEASE([git-directory])])
+m4_ifdef([AX_CHECK_ENABLE_DEBUG],[AX_CHECK_ENABLE_DEBUG([yes])])
+
+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_CC_STDC
+AC_PROG_CC_C99
+AC_PROG_INSTALL
+AC_HEADER_STDC
+
+extra_flags="-Wformat-security                                        \
+             -Wstack-protector                                        \
+             -Wstack-protector-all                                    \
+             -Wno-cast-function-type"
+
+AX_CXX_COMPILE_STDCXX_14
+m4_ifdef([AX_COMPILER_FLAGS],[AX_COMPILER_FLAGS(,,[yes],${extra_flags})])
+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")
+
+# 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],
+    [*24.4*|*24.5*],[build_mu4e=yes],
+    [*25*|*26*|*27*|*28*],[build_mu4e=yes],
+    [AC_WARN([emacs is too old to build mu4e (need emacs >= 24.4)])])
+])
+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],
+    AC_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],
+    AC_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])
+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.38 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)"
+
+# gmime, version 3.0 or higher
+PKG_CHECK_MODULES(JSON_GLIB,json-glib-1.0 >= 1.4,[have_json_glib=yes],[have_json_glib=no])
+AS_IF([test "x$have_json_glib" = "xyes"],[
+  json_glib_version="$($PKG_CONFIG --modversion json-glib-1.0)"
+  AC_DEFINE(HAVE_JSON_GLIB,[1], [Do we support json-glib?])
+])
+AM_CONDITIONAL(HAVE_JSON_GLIB,[test "x$have_json_glib" = "xyes"])
+
+# 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 version
+AC_DEFINE(MU_STORE_SCHEMA_VERSION,["451"],['Schema' version of the database])
+###############################################################################
+
+###############################################################################
+# we need GTK+3 for some of the graphical tools
+# use --without-gtk to disable it
+AC_ARG_ENABLE([gtk],AS_HELP_STRING([--disable-gtk],[Disable GTK+]))
+AS_IF([test "x$enable_gtk" != "xno"],[
+     PKG_CHECK_MODULES(GTK,gtk+-3.0,[have_gtk=yes],[have_gtk=no])
+     gtk_version="$($PKG_CONFIG --modversion gtk+-3.0)"
+])
+AM_CONDITIONAL(HAVE_GTK,[test "x$have_gtk" = "xyes"])
+
+# webkit? needed for the fancy web widget
+# use --disable-webkit to disable it, even if you have it
+#
+# and note this is just a toy, not for distribution.
+AC_ARG_ENABLE([webkit],AS_HELP_STRING([--disable-webkit],[Disable webkit]))
+AS_IF([test "x$enable_webkit" != "xno"],[
+       PKG_CHECK_MODULES(WEBKIT,webkit2gtk-4.0 >= 2.0, [have_webkit=yes],[have_webkit=no])
+       AS_IF([test "x$have_webkit" = "xyes"],[
+        webkit_version="$($PKG_CONFIG --modversion webkit2gtk-4.0)"])
+])
+AM_CONDITIONAL(HAVE_WEBKIT, [test "x$have_webkit" = "xyes"])
+AM_CONDITIONAL(BUILD_GUI,[test "x$have_webkit" = "xyes" -a "x$have_gtk" = "xyes"])
+###############################################################################
+
+###############################################################################
+# build with guile2.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(GUILE22, guile-2.2, [have_guile22=yes],[have_guile22=no])
+  # this is a bit hacky; GUILE_PKG
+  AS_IF([test "x$have_guile22" = "xyes"],[
+    GUILE_PKG([2.2])
+    GUILE_PROGS
+    GUILE_FLAGS
+    AC_DEFINE_UNQUOTED([GUILE_BINARY],"$GUILE",[guile binary])
+    AC_DEFINE(BUILD_GUILE,[1], [Do we support Guile?])
+    AC_SUBST(GUILE_SNARF, [guile-snarf])
+    guile_version=$($PKG_CONFIG guile-2.2 --modversion)
+  ])
+])
+AM_CONDITIONAL(BUILD_GUILE,[test "x$have_guile22" = "xyes"])
+###############################################################################
+
+###############################################################################
+# optional readline
+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
+mu/mu-memcheck
+lib/Makefile
+lib/doxyfile
+lib/utils/Makefile
+lib/query/Makefile
+mu4e/Makefile
+mu4e/mu4e-meta.el
+guile/Makefile
+guile/texi.texi
+guile/mu/Makefile
+guile/examples/Makefile
+guile/tests/Makefile
+guile/scripts/Makefile
+toys/Makefile
+toys/mug/Makefile
+man/Makefile
+m4/Makefile
+contrib/Makefile
+],[
+ [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([HAVE_JSON_GLIB],[
+echo "Json-Glib version                    : $json_glib_version"
+])
+
+AM_COND_IF([BUILD_GUI],[
+echo "GTK+ version                         : $gtk_version"
+echo "Webkit2/GTK+ version                 : $webkit_version"
+])
+
+AM_COND_IF([BUILD_GUILE],[
+echo "Guile version                        : $guile_version"
+])
+
+if test "x$build_mu4e" = "xyes"; then
+echo "Emacs version                        : $emacs_version"
+fi
+
+echo
+echo "Have wordexp                         : $ac_cv_header_wordexp_h"
+echo "Build mu4e emacs frontend            : $build_mu4e"
+
+AM_COND_IF([BUILD_GUI],[
+echo "Build 'mug' toy-ui (gtk+/webkit)     : yes"],[
+echo "Build 'mug' toy-ui (gtk+/webkit)     : no"
+])
+
+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
+
+# gui
+AS_IF([test "x$buildgui" = "xyes"],[
+     echo "* The demo UI will be built in toys/mug"
+     echo
+])
+
+# 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
+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..faaf7cc
--- /dev/null
@@ -0,0 +1,31 @@
+## 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
+
+noinst_PROGRAMS=gmime-test
+gmime_test_SOURCES=gmime-test.c
+gmime_test_LDADD=$(GMIME_LIBS) $(GLIB_LIBS)
+
+
+EXTRA_DIST=                    \
+       mu-completion.zsh       \
+       mu-sexp-convert         \
+       mu.spec
+
diff --git a/contrib/gmime-test.c b/contrib/gmime-test.c
new file mode 100644 (file)
index 0000000..5c59ed2
--- /dev/null
@@ -0,0 +1,275 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** Copyright (C) 2011-2017 Dirk-Jan C. Binnema <djcb@cthulhu>
+**
+** 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.
+**
+*/
+
+/* gmime-test; compile with:
+       gcc -o gmime-test gmime-test.c -Wall -O0 -ggdb \
+          `pkg-config --cflags --libs gmime-2.6`
+ */
+
+#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,
+                          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/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..319b079
--- /dev/null
@@ -0,0 +1,79 @@
+## 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 tests
+
+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}
+AM_CXXFLAGS=$(ASAN_CXXFLAGS) ${WARN_CXXFLAGS}
+
+lib_LTLIBRARIES=                                       \
+       libguile-mu.la
+
+libguile_mu_la_SOURCES=                                        \
+       mu-guile.c                                      \
+       mu-guile.h                                      \
+       mu-guile-message.c                              \
+       mu-guile-message.h
+
+libguile_mu_la_LIBADD=                                 \
+       ${top_builddir}/lib/libmu.la                    \
+       ${GUILE_LIBS}
+
+libguile_mu_la_LDFLAGS=                                \
+       $(ASAN_LDFLAGS)                                 \
+       -shared                                         \
+       -export-dynamic                                 \
+       -Wl,-z,muldefs
+
+XFILES=                                                        \
+       mu-guile.x                                      \
+       mu-guile-message.x
+
+info_TEXINFOS=                                         \
+       mu-guile.texi
+mu_guile_TEXINFOS=                                     \
+       fdl.texi
+
+BUILT_SOURCES=$(XFILES)
+
+snarfcppopts= $(DEFS) $(AM_CPPFLAGS) $(CPPFLAGS) $(CFLAGS) $(AM_CPPFLAGS)
+SUFFIXES = .x .doc
+.c.x:
+       $(GUILE_SNARF) -o $@ $< $(snarfcppopts)
+
+# FIXME: GUILE_SITEDIR would be better, but that
+# breaks 'make distcheck'
+scmdir=${prefix}/share/guile/site/2.2/
+scm_DATA=mu.scm
+
+EXTRA_DIST=$(scm_DATA)
+
+## Add -MG to make the .x magic work with auto-dep code.
+MKDEP = $(CC) -M -MG $(snarfcppopts)
+
+CLEANFILES=$(XFILES)
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/mu-guile-message.c b/guile/mu-guile-message.c
new file mode 100644 (file)
index 0000000..04a1669
--- /dev/null
@@ -0,0 +1,627 @@
+/*
+** 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.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib-object.h>
+#include <libguile.h>
+
+#include "mu-guile.h"
+
+#include <mu-runtime.h>
+#include <mu-store.hh>
+#include <mu-query.h>
+#include <mu-msg.h>
+#include <mu-msg-part.h>
+
+/* pseudo field, not in Xapian */
+#define MU_GUILE_MSG_FIELD_ID_TIMESTAMP (MU_MSG_FIELD_ID_NUM + 1)
+
+/* some symbols */
+static SCM SYMB_PRIO_LOW, SYMB_PRIO_NORMAL, SYMB_PRIO_HIGH;
+static SCM SYMB_FLAG_NEW, SYMB_FLAG_PASSED, SYMB_FLAG_REPLIED,
+       SYMB_FLAG_SEEN, SYMB_FLAG_TRASHED, SYMB_FLAG_DRAFT,
+       SYMB_FLAG_FLAGGED, SYMB_FLAG_SIGNED, SYMB_FLAG_ENCRYPTED,
+       SYMB_FLAG_HAS_ATTACH, SYMB_FLAG_UNREAD;
+static SCM SYMB_CONTACT_TO, SYMB_CONTACT_CC, SYMB_CONTACT_BCC,
+       SYMB_CONTACT_FROM;
+
+struct _MuMsgWrapper {
+       MuMsg   *_msg;
+       gboolean _unrefme;
+};
+typedef struct _MuMsgWrapper MuMsgWrapper;
+static long MSG_TAG;
+
+static gboolean
+mu_guile_scm_is_msg (SCM scm)
+{
+       return SCM_NIMP(scm) && (long)SCM_CAR(scm) == MSG_TAG;
+}
+
+SCM
+mu_guile_msg_to_scm (MuMsg *msg)
+{
+       MuMsgWrapper *msgwrap;
+
+       g_return_val_if_fail (msg, SCM_UNDEFINED);
+
+       msgwrap = scm_gc_malloc (sizeof (MuMsgWrapper), "msg");
+       msgwrap->_msg = msg;
+       msgwrap->_unrefme = FALSE;
+
+       SCM_RETURN_NEWSMOB (MSG_TAG, msgwrap);
+}
+
+struct _FlagData {
+       MuFlags flags;
+       SCM lst;
+};
+typedef struct _FlagData FlagData;
+
+
+#define MU_GUILE_INITIALIZED_OR_ERROR                                  \
+       do { if (!(mu_guile_initialized()))                             \
+                    return mu_guile_error (FUNC_NAME, 0,               \
+                            "mu not initialized; call mu:initialize",  \
+                                    SCM_UNDEFINED);                    \
+       } while (0)
+
+
+static void
+check_flag (MuFlags flag, FlagData *fdata)
+{
+       SCM flag_scm;
+
+       if (!(fdata->flags & flag))
+               return;
+
+       switch (flag) {
+       case MU_FLAG_NEW:        flag_scm = SYMB_FLAG_NEW; break;
+       case MU_FLAG_PASSED:     flag_scm = SYMB_FLAG_PASSED; break;
+       case MU_FLAG_REPLIED:    flag_scm = SYMB_FLAG_REPLIED; break;
+       case MU_FLAG_SEEN:       flag_scm = SYMB_FLAG_SEEN; break;
+       case MU_FLAG_TRASHED:    flag_scm = SYMB_FLAG_TRASHED; break;
+       case MU_FLAG_SIGNED:     flag_scm = SYMB_FLAG_SIGNED; break;
+       case MU_FLAG_DRAFT:      flag_scm = SYMB_FLAG_DRAFT; break;
+       case MU_FLAG_FLAGGED:    flag_scm = SYMB_FLAG_FLAGGED; break;
+       case MU_FLAG_ENCRYPTED:  flag_scm = SYMB_FLAG_ENCRYPTED; break;
+       case MU_FLAG_HAS_ATTACH: flag_scm = SYMB_FLAG_HAS_ATTACH; break;
+       case MU_FLAG_UNREAD:     flag_scm = SYMB_FLAG_UNREAD; break;
+       default: flag_scm = SCM_UNDEFINED;
+       }
+
+       fdata->lst = scm_append_x
+               (scm_list_2(fdata->lst,
+                           scm_list_1 (flag_scm)));
+}
+
+static SCM
+get_flags_scm (MuMsg *msg)
+{
+       FlagData fdata;
+
+       fdata.flags = mu_msg_get_flags (msg);
+       fdata.lst   = SCM_EOL;
+
+       mu_flags_foreach ((MuFlagsForeachFunc)check_flag, &fdata);
+
+       return fdata.lst;
+}
+
+
+static SCM
+get_prio_scm (MuMsg *msg)
+{
+       switch (mu_msg_get_prio (msg)) {
+
+       case MU_MSG_PRIO_LOW:    return  SYMB_PRIO_LOW;
+       case MU_MSG_PRIO_NORMAL: return  SYMB_PRIO_NORMAL;
+       case MU_MSG_PRIO_HIGH:   return  SYMB_PRIO_HIGH;
+
+       default:
+               g_return_val_if_reached (SCM_UNDEFINED);
+       }
+}
+
+static SCM
+msg_string_list_field (MuMsg *msg, MuMsgFieldId mfid)
+{
+       SCM scmlst;
+       const GSList *lst;
+
+       lst = mu_msg_get_field_string_list (msg, mfid);
+
+       for (scmlst = SCM_EOL; lst;
+            lst = g_slist_next(lst)) {
+               SCM item;
+               item = scm_list_1
+                       (mu_guile_scm_from_str((const char*)lst->data));
+               scmlst = scm_append_x (scm_list_2(scmlst, item));
+       }
+
+       return scmlst;
+}
+
+
+static SCM
+get_body (MuMsg *msg, gboolean html)
+{
+       SCM data;
+       const char* body;
+       MuMsgOptions opts;
+
+       opts = MU_MSG_OPTION_NONE;
+
+       if (html)
+               body = mu_msg_get_body_html (msg, opts);
+       else
+               body = mu_msg_get_body_text (msg, opts);
+
+       if (body)
+               data = mu_guile_scm_from_str (body);
+       else
+               data = SCM_BOOL_F;
+
+       /* explicitly close the file backend, so we won't run of fds */
+       mu_msg_unload_msg_file (msg);
+
+       return data;
+}
+
+
+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
+{
+       MuMsgWrapper *msgwrap;
+       MuMsgFieldId mfid;
+       msgwrap = (MuMsgWrapper*) SCM_CDR(MSG);
+
+       MU_GUILE_INITIALIZED_OR_ERROR;
+
+       SCM_ASSERT (mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+       SCM_ASSERT (scm_integer_p(FIELD), FIELD, SCM_ARG2, FUNC_NAME);
+
+       mfid = scm_to_int (FIELD);
+       SCM_ASSERT (mfid < MU_MSG_FIELD_ID_NUM ||
+                   mfid == MU_GUILE_MSG_FIELD_ID_TIMESTAMP,
+                   FIELD, SCM_ARG2, FUNC_NAME);
+
+       switch (mfid) {
+       case MU_MSG_FIELD_ID_PRIO:  return get_prio_scm (msgwrap->_msg);
+       case MU_MSG_FIELD_ID_FLAGS: return get_flags_scm (msgwrap->_msg);
+
+       case MU_MSG_FIELD_ID_BODY_HTML:
+               return get_body (msgwrap->_msg, TRUE);
+       case MU_MSG_FIELD_ID_BODY_TEXT:
+               return get_body (msgwrap->_msg, FALSE);
+
+       /* our pseudo-field; we get it from the message file */
+       case MU_GUILE_MSG_FIELD_ID_TIMESTAMP:
+               return scm_from_uint (
+                       (unsigned)mu_msg_get_timestamp(msgwrap->_msg));
+       default: break;
+       }
+
+       switch (mu_msg_field_type (mfid)) {
+       case MU_MSG_FIELD_TYPE_STRING:
+               return mu_guile_scm_from_str
+                       (mu_msg_get_field_string(msgwrap->_msg, mfid));
+       case MU_MSG_FIELD_TYPE_BYTESIZE:
+       case MU_MSG_FIELD_TYPE_TIME_T:
+               return scm_from_uint (
+                       mu_msg_get_field_numeric (msgwrap->_msg, mfid));
+       case MU_MSG_FIELD_TYPE_INT:
+               return scm_from_int (
+                       mu_msg_get_field_numeric (msgwrap->_msg, mfid));
+       case MU_MSG_FIELD_TYPE_STRING_LIST:
+               return msg_string_list_field (msgwrap->_msg, mfid);
+       default:
+               SCM_ASSERT (0, FIELD, SCM_ARG2, FUNC_NAME);
+       }
+}
+#undef FUNC_NAME
+
+
+
+struct _EachContactData {
+       SCM lst;
+       MuMsgContactType ctype;
+};
+typedef struct _EachContactData EachContactData;
+
+static void
+contacts_to_list (MuMsgContact *contact, EachContactData *ecdata)
+{
+       SCM item;
+
+       if (ecdata->ctype != MU_MSG_CONTACT_TYPE_ALL &&
+           mu_msg_contact_type (contact) != ecdata->ctype)
+               return;
+
+       item = scm_list_1
+               (scm_cons
+                (mu_guile_scm_from_str(mu_msg_contact_name (contact)),
+                 mu_guile_scm_from_str(mu_msg_contact_email (contact))));
+
+       ecdata->lst = scm_append_x (scm_list_2(ecdata->lst, item));
+}
+
+
+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
+{
+       MuMsgWrapper *msgwrap;
+       EachContactData ecdata;
+
+       MU_GUILE_INITIALIZED_OR_ERROR;
+
+       SCM_ASSERT (mu_guile_scm_is_msg(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 */
+       else if (CONTACT_TYPE == SCM_BOOL_T)
+               ecdata.ctype = MU_MSG_CONTACT_TYPE_ALL;
+       else {
+               if (scm_is_eq (CONTACT_TYPE, SYMB_CONTACT_TO))
+                       ecdata.ctype = MU_MSG_CONTACT_TYPE_TO;
+               else if (scm_is_eq (CONTACT_TYPE, SYMB_CONTACT_CC))
+                       ecdata.ctype = MU_MSG_CONTACT_TYPE_CC;
+               else if (scm_is_eq (CONTACT_TYPE, SYMB_CONTACT_BCC))
+                       ecdata.ctype = MU_MSG_CONTACT_TYPE_BCC;
+               else if (scm_is_eq (CONTACT_TYPE, SYMB_CONTACT_FROM))
+                       ecdata.ctype = MU_MSG_CONTACT_TYPE_FROM;
+               else
+                       return mu_guile_error (FUNC_NAME, 0,
+                                              "invalid contact type",
+                                              SCM_UNDEFINED);
+       }
+
+       ecdata.lst = SCM_EOL;
+       msgwrap = (MuMsgWrapper*) SCM_CDR(MSG);
+       mu_msg_contact_foreach (msgwrap->_msg,
+                               (MuMsgContactForeachFunc)contacts_to_list,
+                               &ecdata);
+       /* explicitly close the file backend, so we won't run out of fds */
+       mu_msg_unload_msg_file (msgwrap->_msg);
+
+       return ecdata.lst;
+}
+#undef FUNC_NAME
+
+struct _AttInfo {
+       SCM      attlist;
+       gboolean attachments_only;
+};
+typedef struct _AttInfo AttInfo;
+
+static void
+each_part (MuMsg *msg, MuMsgPart *part, AttInfo *attinfo)
+{
+       char *mime_type, *filename;
+       SCM elm;
+
+       if (!part->type)
+               return;
+       if (attinfo->attachments_only &&
+           !mu_msg_part_maybe_attachment (part))
+               return;
+
+       mime_type = g_strdup_printf ("%s/%s", part->type, part->subtype);
+       filename  = mu_msg_part_get_filename (part, FALSE);
+
+       elm = scm_list_5 (
+               /* msg */
+               mu_guile_scm_from_str (mu_msg_get_path(msg)),
+               /* index */
+               scm_from_uint(part->index),
+               /* filename or #f */
+               filename ? mu_guile_scm_from_str (filename) : SCM_BOOL_F,
+               /* mime-type */
+               mime_type ? mu_guile_scm_from_str (mime_type): SCM_BOOL_F,
+               /* size */
+               part->size > 0 ? scm_from_uint (part->size) : SCM_BOOL_F);
+
+       g_free (mime_type);
+       g_free (filename);
+
+       attinfo->attlist = scm_cons (elm, attinfo->attlist);
+}
+
+
+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
+{
+       MuMsgWrapper *msgwrap;
+       AttInfo attinfo;
+
+       MU_GUILE_INITIALIZED_OR_ERROR;
+
+       SCM_ASSERT (mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+       SCM_ASSERT (scm_is_bool(ATTS_ONLY), ATTS_ONLY, SCM_ARG2, FUNC_NAME);
+
+       attinfo.attlist          = SCM_EOL;     /* empty list */
+       attinfo.attachments_only = ATTS_ONLY == SCM_BOOL_T ? TRUE : FALSE;
+
+       msgwrap = (MuMsgWrapper*) SCM_CDR(MSG);
+       mu_msg_part_foreach (msgwrap->_msg, MU_MSG_OPTION_NONE,
+                            (MuMsgPartForeachFunc)each_part,
+                            &attinfo);
+
+       /* explicitly close the file backend, so we won't run of fds */
+       mu_msg_unload_msg_file (msgwrap->_msg);
+
+       return attinfo.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
+{
+       MuMsgWrapper *msgwrap;
+       char *header;
+       SCM val;
+
+       MU_GUILE_INITIALIZED_OR_ERROR;
+
+       SCM_ASSERT (mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+       SCM_ASSERT (scm_is_string (HEADER)||HEADER==SCM_UNDEFINED,
+                   HEADER, SCM_ARG2, FUNC_NAME);
+
+       msgwrap = (MuMsgWrapper*) SCM_CDR(MSG);
+       header  =  scm_to_utf8_string (HEADER);
+       val     = mu_guile_scm_from_str
+               (mu_msg_get_header(msgwrap->_msg, header));
+       free (header);
+
+       /* explicitly close the file backend, so we won't run of fds */
+       mu_msg_unload_msg_file (msgwrap->_msg);
+
+       return val;
+}
+#undef FUNC_NAME
+
+
+static void
+call_func (SCM FUNC, MuMsgIter *iter, const char* func_name)
+{
+       SCM msgsmob;
+       MuMsg *msg;
+
+       msg = mu_msg_iter_get_msg_floating (iter); /* don't unref */
+
+       msgsmob = mu_guile_msg_to_scm (mu_msg_ref(msg));
+       scm_call_1 (FUNC, msgsmob);
+}
+
+
+static MuMsgIter*
+get_query_iter (MuQuery *query, const char* expr, int maxnum)
+{
+       MuMsgIter *iter;
+       GError *err;
+
+       err = NULL;
+       iter = mu_query_run (query, expr, MU_MSG_FIELD_ID_NONE, maxnum,
+                            MU_QUERY_FLAG_NONE, &err);
+       if (!iter) {
+               mu_guile_g_error ("<internal error>", err);
+               g_clear_error (&err);
+       }
+
+       return iter;
+}
+
+
+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
+{
+       MuMsgIter *iter;
+       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);
+
+       iter = get_query_iter (mu_guile_instance()->query, expr,
+                              scm_to_int(MAXNUM));
+       free (expr);
+       if (!iter)
+               return SCM_UNSPECIFIED;
+
+       while (!mu_msg_iter_is_done(iter)) {
+               call_func (FUNC, iter, FUNC_NAME);
+               mu_msg_iter_next (iter);
+       }
+
+       mu_msg_iter_destroy (iter);
+
+       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");
+
+       SYMB_FLAG_NEW           = register_symbol ("mu:flag:new");
+       SYMB_FLAG_PASSED        = register_symbol ("mu:flag:passed");
+       SYMB_FLAG_REPLIED       = register_symbol ("mu:flag:replied");
+       SYMB_FLAG_SEEN          = register_symbol ("mu:flag:seen");
+       SYMB_FLAG_TRASHED       = register_symbol ("mu:flag:trashed");
+       SYMB_FLAG_DRAFT         = register_symbol ("mu:flag:draft");
+       SYMB_FLAG_FLAGGED       = register_symbol ("mu:flag:flagged");
+       SYMB_FLAG_SIGNED        = register_symbol ("mu:flag:signed");
+       SYMB_FLAG_ENCRYPTED     = register_symbol ("mu:flag:encrypted");
+       SYMB_FLAG_HAS_ATTACH    = register_symbol ("mu:flag:has-attach");
+       SYMB_FLAG_UNREAD        = register_symbol ("mu:flag:unread");
+}
+
+
+static struct  {
+       const char* name;
+       unsigned val;
+} VAR_PAIRS[] = {
+
+       { "mu:field:bcc",       MU_MSG_FIELD_ID_BCC },
+       { "mu:field:body-html", MU_MSG_FIELD_ID_BODY_HTML },
+       { "mu:field:body-txt",  MU_MSG_FIELD_ID_BODY_TEXT },
+       { "mu:field:cc",        MU_MSG_FIELD_ID_CC },
+       { "mu:field:date",      MU_MSG_FIELD_ID_DATE },
+       { "mu:field:flags",     MU_MSG_FIELD_ID_FLAGS },
+       { "mu:field:from",      MU_MSG_FIELD_ID_FROM },
+       { "mu:field:maildir",   MU_MSG_FIELD_ID_MAILDIR },
+       { "mu:field:message-id",MU_MSG_FIELD_ID_MSGID },
+       { "mu:field:path",      MU_MSG_FIELD_ID_PATH },
+       { "mu:field:prio",      MU_MSG_FIELD_ID_PRIO },
+       { "mu:field:refs",      MU_MSG_FIELD_ID_REFS },
+       { "mu:field:size",      MU_MSG_FIELD_ID_SIZE },
+       { "mu:field:subject",   MU_MSG_FIELD_ID_SUBJECT },
+       { "mu:field:tags",      MU_MSG_FIELD_ID_TAGS },
+       { "mu:field:to",        MU_MSG_FIELD_ID_TO },
+
+       /* non-Xapian field: timestamp */
+       { "mu:field:timestamp",  MU_GUILE_MSG_FIELD_ID_TIMESTAMP }
+};
+
+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);
+       }
+}
+
+
+static SCM
+msg_mark (SCM msg_smob)
+{
+       MuMsgWrapper *msgwrap;
+       msgwrap = (MuMsgWrapper*) SCM_CDR(msg_smob);
+
+       msgwrap->_unrefme = TRUE;
+
+       return SCM_UNSPECIFIED;
+}
+
+static size_t
+msg_free (SCM msg_smob)
+{
+       MuMsgWrapper *msgwrap;
+       msgwrap = (MuMsgWrapper*) SCM_CDR(msg_smob);
+
+       if (msgwrap->_unrefme)
+               mu_msg_unref (msgwrap->_msg);
+
+       return sizeof (MuMsgWrapper);
+}
+
+static int
+msg_print (SCM msg_smob, SCM port, scm_print_state * pstate)
+{
+       MuMsgWrapper *msgwrap;
+       msgwrap = (MuMsgWrapper*) SCM_CDR(msg_smob);
+
+       scm_puts ("#<msg ", port);
+
+       if (msg_smob == SCM_BOOL_F)
+               scm_puts ("#f", port);
+       else
+               scm_puts (mu_msg_get_path(msgwrap->_msg),
+                         port);
+
+       scm_puts (">", port);
+
+       return 1;
+}
+
+
+void*
+mu_guile_message_init (void *data)
+{
+       MSG_TAG = scm_make_smob_type ("msg", sizeof(MuMsgWrapper));
+
+       scm_set_smob_mark  (MSG_TAG, msg_mark);
+       scm_set_smob_free  (MSG_TAG, msg_free);
+       scm_set_smob_print (MSG_TAG, msg_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.h b/guile/mu-guile-message.h
new file mode 100644 (file)
index 0000000..411644e
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_GUILE_MESSAGE_H__
+#define __MU_GUILE_MESSAGE_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+/**
+ * Initialize this mu guile module.
+ *
+ * @param data
+ *
+ * @return
+ */
+void* mu_guile_message_init (void *data);
+
+
+G_END_DECLS
+
+#endif /*__MU_GUILE_MESSAGE_H__*/
diff --git a/guile/mu-guile.c b/guile/mu-guile.c
new file mode 100644 (file)
index 0000000..dcd7beb
--- /dev/null
@@ -0,0 +1,256 @@
+/*
+** 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.
+**
+*/
+
+#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.h>
+#include <mu-query.h>
+#include <mu-runtime.h>
+#include <mu-store.hh>
+#include <mu-query.h>
+#include <mu-msg.h>
+
+#include "mu-guile.h"
+
+
+SCM
+mu_guile_scm_from_str (const char *str)
+{
+       if (!str)
+               return SCM_BOOL_F;
+       else
+               return scm_from_stringn (str, strlen(str), "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 MuGuile *_singleton = NULL;
+
+static gboolean
+mu_guile_init_instance (const char *muhome)
+{
+       MuStore *store;
+       MuQuery *query;
+       GError *err;
+
+       setlocale (LC_ALL, "");
+
+       if (!mu_runtime_init (muhome, "guile"))
+               return FALSE;
+
+       err = NULL;
+       store = mu_store_new_readable
+               (mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB),
+                &err);
+       if (!store)
+               goto errexit;
+
+       query = mu_query_new (store, &err);
+       mu_store_unref (store);
+       if (!query)
+               goto errexit;
+
+       _singleton        = g_new0 (MuGuile, 1);
+       _singleton->query = query;
+
+       return TRUE;
+
+errexit:
+       mu_guile_g_error (__func__, err);
+       g_clear_error (&err);
+       return FALSE;
+}
+
+static void
+mu_guile_uninit_instance (void)
+{
+       g_return_if_fail (_singleton);
+
+       mu_query_destroy (_singleton->query);
+       g_free (_singleton);
+
+       _singleton = NULL;
+
+       mu_runtime_uninit ();
+}
+
+
+MuGuile*
+mu_guile_instance (void)
+{
+       g_return_val_if_fail (_singleton, NULL);
+       return _singleton;
+}
+
+gboolean
+mu_guile_initialized (void)
+{
+       return _singleton != NULL;
+}
+
+
+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;
+       gboolean rv;
+
+       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);
+
+       rv = mu_guile_init_instance (muhome);
+       free (muhome);
+
+       if (!rv)
+               return mu_guile_error (FUNC_NAME, 0, "Failed to initialize mu",
+                                      SCM_UNSPECIFIED);
+
+       /* 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, 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.h b/guile/mu-guile.h
new file mode 100644 (file)
index 0000000..fd230a5
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_GUILE_H__
+#define __MU_GUILE_H__
+
+#include <glib.h>
+#include <mu-query.h>
+
+G_BEGIN_DECLS
+
+
+struct _MuGuile {
+       MuQuery *query;
+};
+typedef struct _MuGuile MuGuile;
+
+/**
+ * get the single MuGuile instance
+ *
+ * @return the instance or NULL in case of error
+ */
+MuGuile *mu_guile_instance (void);
+
+
+/**
+ * whether mu-guile is initialized
+ *
+ * @return TRUE if MuGuile is Initialized, FALSE otherwise
+ */
+gboolean mu_guile_initialized (void);
+
+
+/**
+ * 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 const char* into an SCM -- either a string or, if str ==
+ * NULL, #f. It assumes str is in UTF8 encoding, and replace
+ * characters with '?' if needed.
+ *
+ * @param str a string or NULL
+ *
+ * @return a guile string or #f
+ */
+SCM mu_guile_scm_from_str (const char *str);
+
+
+/**
+ * Initialize this mu guile module.
+ *
+ * @param data
+ *
+ * @return
+ */
+void* mu_guile_init (void *data);
+
+
+G_END_DECLS
+
+#endif /*__MU_GUILE_H__*/
diff --git a/guile/mu-guile.texi b/guile/mu-guile.texi
new file mode 100644 (file)
index 0000000..c52213e
--- /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 texi.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{mu-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.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..f531822
--- /dev/null
@@ -0,0 +1,28 @@
+## 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
+
+# FIXME: GUILE_SITEDIR would be better, but that
+# breaks 'make distcheck'
+scmdir=${prefix}/share/guile/site/2.2/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/Makefile.am b/guile/tests/Makefile.am
new file mode 100644 (file)
index 0000000..8df87fb
--- /dev/null
@@ -0,0 +1,52 @@
+# 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.
+
+include $(top_srcdir)/gtest.mk
+
+AM_CPPFLAGS=$(XAPIAN_CXXFLAGS)                                         \
+        $(GMIME_CFLAGS)                                                \
+        $(GLIB_CFLAGS)                                                 \
+       -I${top_srcdir}                                                 \
+       -I${top_srcdir}/lib                                             \
+       -DMU_TESTMAILDIR=\"${top_srcdir}/lib/testdir\"                  \
+       -DMU_TESTMAILDIR2=\"${top_srcdir}/lib/testdir2\"                \
+       -DMU_TESTMAILDIR3=\"${top_srcdir}/lib/testdir3\"                \
+       -DMU_PROGRAM=\"${abs_top_builddir}/mu/mu\"                      \
+       -DMU_GUILE_MODULE_PATH=\"${abs_top_srcdir}/guile/\"             \
+       -DMU_GUILE_LIBRARY_PATH=\"${abs_top_builddir}/guile/.libs\"     \
+       -DABS_CURDIR=\"${abs_builddir}\"                                \
+       -DABS_SRCDIR=\"${abs_srcdir}\"
+
+# 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}
+AM_CXXFLAGS=$(ASAN_CXXFLAGS) ${WARN_CXXFLAGS}
+AM_LDFLAGS=$(ASAN_LDFLAGS)
+
+noinst_PROGRAMS= $(TEST_PROGS)
+
+TEST_PROGS += test-mu-guile
+test_mu_guile_SOURCES= test-mu-guile.c dummy.cc
+test_mu_guile_LDADD=${top_builddir}/lib/libtestmucommon.la
+
+# we need to use dummy.cc to enforce c++ linking...
+BUILT_SOURCES=                                                         \
+       dummy.cc
+dummy.cc:
+       touch dummy.cc
+
+EXTRA_DIST=test-mu-guile.scm
diff --git a/guile/tests/test-mu-guile.c b/guile/tests/test-mu-guile.c
new file mode 100644 (file)
index 0000000..50199eb
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+** 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 <glib.h>
+#include <glib/gstdio.h>
+
+#include <lib/mu-query.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "test-mu-common.h"
+#include <lib/mu-store.hh>
+
+
+/* tests for the command line interface, uses testdir2 */
+
+static gchar*
+fill_database (void)
+{
+       gchar *cmdline, *tmpdir;
+       GError *err;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       cmdline = g_strdup_printf (
+               "/bin/sh -c '"
+               "%s init  --muhome=%s --maildir=%s --quiet; "
+               "%s index --muhome=%s  --quiet'",
+               MU_PROGRAM,  tmpdir, MU_TESTMAILDIR2,
+               MU_PROGRAM,  tmpdir);
+
+       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);
+       return tmpdir;
+}
+
+
+static void
+test_something (const char *what)
+{
+       char *dir, *cmdline;
+       gint result;
+
+       dir = fill_database ();
+       cmdline = g_strdup_printf (
+               "LD_LIBRARY_PATH=%s %s -q -L %s -e main %s/test-mu-guile.scm "
+               "--muhome=%s --test=%s",
+               MU_GUILE_LIBRARY_PATH,
+               GUILE_BINARY,
+               MU_GUILE_MODULE_PATH,
+               ABS_SRCDIR,
+               dir,
+               what);
+
+       if (g_test_verbose ())
+               g_print ("cmdline: %s\n", cmdline);
+
+       result = system (cmdline);
+       g_assert (result == 0);
+
+       g_free (dir);
+       g_free (cmdline);
+}
+
+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;
+       g_test_init (&argc, &argv, NULL);
+
+       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);
+
+       g_log_set_handler (NULL,
+                          G_LOG_LEVEL_MASK | G_LOG_LEVEL_WARNING|
+                          G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       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..8281518
--- /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/guile/texi.texi.in b/guile/texi.texi.in
new file mode 100644 (file)
index 0000000..716d9a9
--- /dev/null
@@ -0,0 +1,3 @@
+@c the version for mu for including in texinfo docs
+@set mu-version @VERSION@
+
diff --git a/lib/Makefile.am b/lib/Makefile.am
new file mode 100644 (file)
index 0000000..405506e
--- /dev/null
@@ -0,0 +1,285 @@
+## 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.
+
+# enforce compiling guile (optionally) first,then this dir first
+# before descending into tests/
+include $(top_srcdir)/gtest.mk
+
+SUBDIRS= utils query
+
+if HAVE_JSON_GLIB
+json_srcs=                                                      \
+       mu-msg-json.c
+json_flag="-DHAVE_JSON_GLIB"
+endif
+
+AM_CFLAGS=                                                      \
+       $(WARN_CFLAGS)                                          \
+       $(GMIME_CFLAGS)                                         \
+       $(GLIB_CFLAGS)                                          \
+       $(GUILE_CFLAGS)                                         \
+       $(JSON_GLIB_CFLAGS)                                     \
+       $(ASAN_CFLAGS)                                          \
+       $(json_flag)                                            \
+       $(CODE_COVERAGE_CFLAGS)                                 \
+       -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\"              \
+       -DMU_TESTMAILDIR2=\"${abs_srcdir}/testdir2\"            \
+       -DMU_TESTMAILDIR3=\"${abs_srcdir}/testdir3\"            \
+       -DMU_TESTMAILDIR4=\"${abs_srcdir}/testdir4\"            \
+       -DABS_CURDIR=\"${abs_builddir}\"                        \
+       -DABS_SRCDIR=\"${abs_srcdir}\"                          \
+       -Wno-format-nonliteral                                  \
+       -Wno-switch-enum                                        \
+       -Wno-deprecated-declarations                            \
+       -Wno-inline
+
+AM_CXXFLAGS=                                                    \
+       $(GMIME_CFLAGS)                                         \
+       $(GLIB_CFLAGS)                                          \
+       $(GUILE_CFLAGS)                                         \
+       $(JSON_GLIB_CFLAGS)                                     \
+       $(json_flag)                                            \
+       $(WARN_CXXFLAGS)                                        \
+       $(XAPIAN_CXXFLAGS)                                      \
+       $(ASAN_CXXFLAGS)                                        \
+       $(CODE_COVERAGE_CFLAGS)                                 \
+       -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\"
+
+AM_CPPFLAGS=                                                    \
+       $(CODE_COVERAGE_CPPFLAGS)
+
+# 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=-Wall -Wextra -Wno-unused-parameter                 \
+# -Wdeclaration-after-statement -Wno-variadic-macros
+# AM_CXXFLAGS=-Wall -Wextra -Wno-unused-parameter
+
+noinst_LTLIBRARIES=                                             \
+       libmu.la
+
+libmu_la_SOURCES=                                               \
+       mu-bookmarks.c                                          \
+       mu-bookmarks.h                                          \
+       mu-contacts.cc                                          \
+       mu-contacts.hh                                          \
+       mu-container.c                                          \
+       mu-container.h                                          \
+       mu-flags.h                                              \
+       mu-flags.c                                              \
+       mu-index.c                                              \
+       mu-index.h                                              \
+       mu-maildir.c                                            \
+       mu-maildir.h                                            \
+       mu-msg-crypto.c                                         \
+       mu-msg-doc.cc                                           \
+       mu-msg-doc.h                                            \
+       mu-msg-fields.c                                         \
+       mu-msg-fields.h                                         \
+       mu-msg-file.c                                           \
+       mu-msg-file.h                                           \
+       mu-msg-iter.cc                                          \
+       mu-msg-iter.h                                           \
+       $(json_srcs)                                            \
+       mu-msg-part.c                                           \
+       mu-msg-part.h                                           \
+       mu-msg-prio.c                                           \
+       mu-msg-prio.h                                           \
+       mu-msg-priv.h                                           \
+       mu-msg-sexp.c                                           \
+       mu-msg.c                                                \
+       mu-msg.h                                                \
+       mu-msg.h                                                \
+       mu-query.cc                                             \
+       mu-query.h                                              \
+       mu-runtime.cc                                           \
+       mu-runtime.h                                            \
+       mu-script.c                                             \
+       mu-script.h                                             \
+       mu-store.cc                                             \
+       mu-store.hh                                             \
+       mu-threader.c                                           \
+       mu-threader.h
+
+libmu_la_LIBADD=                                                \
+       $(XAPIAN_LIBS)                                          \
+       $(GMIME_LIBS)                                           \
+       $(GLIB_LIBS)                                            \
+       $(GUILE_LIBS)                                           \
+       $(JSON_GLIB_LIBS)                                       \
+       ${builddir}/utils/libmu-utils.la                        \
+       ${builddir}/query/libmu-query.la                        \
+       $(CODE_COVERAGE_LIBS)
+
+libmu_la_LDFLAGS=                                               \
+       $(ASAN_LDFLAGS)
+
+EXTRA_DIST=                                                     \
+       mu-msg-crypto.c                                         \
+       doxyfile.in
+
+noinst_PROGRAMS= $(TEST_PROGS)
+
+noinst_LTLIBRARIES+=                                            \
+       libtestmucommon.la
+
+TEST_PROGS += test-mu-maildir
+test_mu_maildir_SOURCES= test-mu-maildir.c dummy.cc
+test_mu_maildir_LDADD=  libtestmucommon.la
+
+TEST_PROGS += test-mu-msg-fields
+test_mu_msg_fields_SOURCES= test-mu-msg-fields.c dummy.cc
+test_mu_msg_fields_LDADD=  libtestmucommon.la
+
+TEST_PROGS += test-mu-msg
+test_mu_msg_SOURCES= test-mu-msg.c dummy.cc
+test_mu_msg_LDADD=  libtestmucommon.la
+
+TEST_PROGS += test-mu-store
+test_mu_store_SOURCES= test-mu-store.c dummy.cc
+test_mu_store_LDADD= libtestmucommon.la
+
+TEST_PROGS += test-mu-flags
+test_mu_flags_SOURCES= test-mu-flags.c dummy.cc
+test_mu_flags_LDADD=  libtestmucommon.la
+
+TEST_PROGS += test-mu-container
+test_mu_container_SOURCES= test-mu-container.c dummy.cc
+test_mu_container_LDADD= libtestmucommon.la
+
+TEST_PROGS += test-mu-contacts
+test_mu_contacts_SOURCES= test-mu-contacts.cc
+test_mu_contacts_LDADD= libtestmucommon.la
+
+# we need to use dummy.cc to enforce c++ linking...
+BUILT_SOURCES=                                                  \
+       dummy.cc
+
+dummy.cc:
+       touch dummy.cc
+
+libtestmucommon_la_SOURCES=                                     \
+       test-mu-common.c                                        \
+       test-mu-common.h
+libtestmucommon_la_LIBADD=                                      \
+       libmu.la
+
+# note the question marks; make does not like files with ':', so we
+# use the (also supported) version with '!' instead. We could escape
+# the : with \: but automake does not recognize that....
+
+# test messages, the '.ignore' message should be ignored
+# when indexing
+EXTRA_DIST+=                                                    \
+       testdir/tmp/1220863087.12663.ignore                     \
+       testdir/new/1220863087.12663_9.mindcrime                \
+       testdir/new/1220863087.12663_25.mindcrime               \
+       testdir/new/1220863087.12663_21.mindcrime               \
+       testdir/new/1220863087.12663_23.mindcrime               \
+       testdir/cur/1220863087.12663_5.mindcrime!2,S            \
+       testdir/cur/1220863087.12663_7.mindcrime!2,RS           \
+       testdir/cur/1220863087.12663_15.mindcrime!2,PS          \
+       testdir/cur/1220863087.12663_19.mindcrime!2,S           \
+       testdir/cur/1220863042.12663_1.mindcrime!2,S            \
+       testdir/cur/1220863060.12663_3.mindcrime!2,S            \
+       testdir/cur/1283599333.1840_11.cthulhu!2,               \
+       testdir/cur/1305664394.2171_402.cthulhu!2,              \
+       testdir/cur/1252168370_3.14675.cthulhu!2,S              \
+       testdir/cur/encrypted!2,S                               \
+       testdir/cur/multimime!2,FS                              \
+       testdir/cur/signed!2,S                                  \
+       testdir/cur/signed-encrypted!2,S                        \
+       testdir/cur/special!2,Sabc                              \
+       testdir/cur/multirecip!2,S                              \
+       testdir2/bar/cur/mail1                                  \
+       testdir2/bar/cur/mail2                                  \
+       testdir2/bar/cur/mail3                                  \
+       testdir2/bar/cur/mail4                                  \
+       testdir2/bar/cur/mail5                                  \
+       testdir2/bar/cur/181736.eml                             \
+       testdir2/bar/cur/mail6                                  \
+       testdir2/bar/tmp/.noindex                               \
+       testdir2/bar/new/.noindex                               \
+       testdir2/Foo/cur/mail5                                  \
+       testdir2/Foo/cur/arto.eml                               \
+       testdir2/Foo/cur/fraiche.eml                            \
+       testdir2/Foo/tmp/.noindex                               \
+       testdir2/Foo/new/.noindex                               \
+       testdir2/wom_bat/cur/atomic                             \
+       testdir2/wom_bat/cur/rfc822.1                           \
+       testdir2/wom_bat/cur/rfc822.2                           \
+       testdir3/cycle                                          \
+       testdir3/cycle/new/.noindex                             \
+       testdir3/cycle/cur/rogue0                               \
+       testdir3/cycle/cur/cycle0                               \
+       testdir3/cycle/cur/cycle0.0                             \
+       testdir3/cycle/cur/cycle0.0.0                           \
+       testdir3/cycle/tmp/.noindex                             \
+       testdir3/tree/new/.noindex                              \
+       testdir3/tree/cur/child0.0                              \
+       testdir3/tree/cur/child4.0                              \
+       testdir3/tree/cur/root2                                 \
+       testdir3/tree/cur/root1                                 \
+       testdir3/tree/cur/child3.0.0.0.0                        \
+       testdir3/tree/cur/root0                                 \
+       testdir3/tree/cur/child2.0.0                            \
+       testdir3/tree/cur/child0.1                              \
+       testdir3/tree/cur/child0.1.0                            \
+       testdir3/tree/cur/child4.1                              \
+       testdir3/tree/tmp/.noindex                              \
+       testdir3/sort/1st-child-promotes-thread/cur/A           \
+       testdir3/sort/1st-child-promotes-thread/cur/B           \
+       testdir3/sort/1st-child-promotes-thread/cur/C           \
+       testdir3/sort/1st-child-promotes-thread/cur/D           \
+       testdir3/sort/2nd-child-promotes-thread/cur/A           \
+       testdir3/sort/2nd-child-promotes-thread/cur/B           \
+       testdir3/sort/2nd-child-promotes-thread/cur/C           \
+       testdir3/sort/2nd-child-promotes-thread/cur/D           \
+       testdir3/sort/2nd-child-promotes-thread/cur/E           \
+       testdir3/sort/child-does-not-promote-thread/cur/A       \
+       testdir3/sort/child-does-not-promote-thread/cur/X       \
+       testdir3/sort/child-does-not-promote-thread/cur/Y       \
+       testdir3/sort/child-does-not-promote-thread/cur/Z       \
+       testdir3/sort/grandchild-promotes-only-subthread/cur/A  \
+       testdir3/sort/grandchild-promotes-only-subthread/cur/B  \
+       testdir3/sort/grandchild-promotes-only-subthread/cur/C  \
+       testdir3/sort/grandchild-promotes-only-subthread/cur/D  \
+       testdir3/sort/grandchild-promotes-only-subthread/cur/E  \
+       testdir3/sort/grandchild-promotes-only-subthread/cur/F  \
+       testdir3/sort/grandchild-promotes-only-subthread/cur/G  \
+       testdir3/sort/grandchild-promotes-thread/cur/A          \
+       testdir3/sort/grandchild-promotes-thread/cur/B          \
+       testdir3/sort/grandchild-promotes-thread/cur/C          \
+       testdir3/sort/grandchild-promotes-thread/cur/D          \
+       testdir3/sort/grandchild-promotes-thread/cur/E          \
+       testdir4/1220863087.12663_19.mindcrime!2,S              \
+       testdir4/1220863042.12663_1.mindcrime!2,S               \
+       testdir4/1283599333.1840_11.cthulhu!2,                  \
+       testdir4/1305664394.2171_402.cthulhu!2,                 \
+       testdir4/1252168370_3.14675.cthulhu!2,S                 \
+       testdir4/mail1                                          \
+       testdir4/mail5                                          \
+       testdir4/181736.eml                                     \
+       testdir4/encrypted!2,S                                  \
+       testdir4/multimime!2,FS                                 \
+       testdir4/signed!2,S                                     \
+       testdir4/signed-bad!2,S                                 \
+       testdir4/signed-encrypted!2,S                           \
+       testdir4/special!2,Sabc
+
+TESTS=$(TEST_PROGS)
+
+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/mu-bookmarks.c b/lib/mu-bookmarks.c
new file mode 100644 (file)
index 0000000..2f1ebac
--- /dev/null
@@ -0,0 +1,146 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+/*
+** Copyright (C) 2010-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.
+**
+*/
+
+#include <glib.h>
+#include "mu-bookmarks.h"
+
+#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 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.h b/lib/mu-bookmarks.h
new file mode 100644 (file)
index 0000000..2f1d99f
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_BOOKMARKS_H__
+#define __MU_BOOKMARKS_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+/**
+ * @addtogroup MuBookmarks
+ * Functions for dealing with bookmarks
+ * @{
+ */
+
+struct _MuBookmarks;
+/*! \struct MuBookmarks
+ * \brief Opaque structure representing a sequence of bookmarks
+ */
+typedef struct _MuBookmarks 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);
+
+/** @} */
+
+G_END_DECLS
+
+#endif /*__MU_BOOKMARKS_H__*/
diff --git a/lib/mu-contacts.cc b/lib/mu-contacts.cc
new file mode 100644 (file)
index 0000000..e5bb21f
--- /dev/null
@@ -0,0 +1,299 @@
+/*
+** Copyright (C) 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.
+**
+*/
+
+#include "mu-contacts.hh"
+
+#include <mutex>
+#include <unordered_map>
+#include <set>
+#include <sstream>
+#include <functional>
+#include <algorithm>
+
+#include <utils/mu-utils.hh>
+#include <glib.h>
+
+using namespace Mu;
+
+ContactInfo::ContactInfo (const std::string& _full_address,
+                          const std::string& _email,
+                          const std::string& _name,
+                          bool _personal, time_t _last_seen, size_t _freq):
+        full_address{_full_address},
+        email{_email},
+        name{_name},
+        personal{_personal},
+        last_seen{_last_seen},
+        freq{_freq},
+        tstamp{g_get_monotonic_time()} {}
+
+
+struct EmailHash {
+        std::size_t operator()(const std::string& email) const {
+                std::size_t djb = 5381; // djb hash
+                for (const auto c : email)
+                        djb = ((djb << 5) + djb) + g_ascii_tolower(c);
+                return djb;
+        }
+};
+
+struct EmailEqual {
+        bool operator()(const std::string& email1, const std::string& email2) const {
+                return g_ascii_strcasecmp(email1.c_str(), email2.c_str()) == 0;
+        }
+};
+
+struct ContactInfoHash {
+        std::size_t operator()(const ContactInfo& ci) const {
+                std::size_t djb = 5381; // djb hash
+                for (const auto c : ci.email)
+                        djb = ((djb << 5) + djb) + g_ascii_tolower(c);
+                return djb;
+        }
+};
+
+struct ContactInfoEqual {
+        bool operator()(const Mu::ContactInfo& ci1, const Mu::ContactInfo& ci2) const {
+                return g_ascii_strcasecmp(ci1.email.c_str(), ci2.email.c_str()) == 0;
+        }
+};
+
+struct ContactInfoLessThan {
+        bool operator()(const Mu::ContactInfo& ci1, const Mu::ContactInfo& ci2) const {
+
+                if (ci1.personal != ci2.personal)
+                        return ci1.personal; // personal comes first
+
+                if (ci1.last_seen != ci2.last_seen) // more recent comes first
+                        return ci1.last_seen > ci2.last_seen;
+
+                if (ci1.freq != ci2.freq) // more frequent comes first
+                        return ci1.freq > ci2.freq;
+
+                return g_ascii_strcasecmp(ci1.email.c_str(), ci2.email.c_str()) < 0;
+        }
+};
+
+using ContactUMap = std::unordered_map<const std::string, ContactInfo, EmailHash, EmailEqual>;
+//using ContactUSet = std::unordered_set<ContactInfo, ContactInfoHash, ContactInfoEqual>;
+using ContactSet  = std::set<std::reference_wrapper<const ContactInfo>, ContactInfoLessThan>;
+
+struct Contacts::Private {
+        Private(const std::string& serialized):
+                contacts_{deserialize(serialized)}
+        {}
+
+        ContactUMap deserialize(const std::string&) const;
+        std::string serialize() const;
+
+        ContactUMap contacts_;
+        std::mutex  mtx_;
+};
+
+constexpr auto Separator = "\xff"; // Invalid in UTF-8
+
+ContactUMap
+Contacts::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;
+                }
+
+                ContactInfo ci(std::move(parts[0]), // full address
+                               parts[1], // email
+                               std::move(parts[2]), // name
+                               parts[3][0] == '1' ? true : false, // personal
+                               (time_t)g_ascii_strtoll(parts[4].c_str(), NULL, 10), // last_seen
+                               (std::size_t)g_ascii_strtoll(parts[5].c_str(), NULL, 10)); // freq
+
+                contacts.emplace(std::move(parts[1]), std::move(ci));
+
+        }
+
+        return contacts;
+}
+
+
+Contacts::Contacts (const std::string& serialized) :
+        priv_{std::make_unique<Private>(serialized)}
+{}
+
+Contacts::~Contacts() = default;
+
+std::string
+Contacts::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.full_address.c_str(), Separator,
+                                 ci.email.c_str(), Separator,
+                                 ci.name.c_str(), Separator,
+                                 ci.personal ? 1 : 0, Separator,
+                                 (gint64)ci.last_seen, Separator,
+                                 (gint64)ci.freq);
+        }
+
+        return s;
+}
+
+
+// for now, we only care about _not_ having newlines.
+static void
+wash (std::string& str)
+{
+        str.erase(std::remove(str.begin(), str.end(), '\n'), str.end());
+}
+
+
+void
+Contacts::add (ContactInfo&& ci)
+{
+        std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+        auto down = g_ascii_strdown (ci.email.c_str(), -1);
+        std::string email{down};
+        g_free(down);
+
+        auto it = priv_->contacts_.find(email);
+        if (it != priv_->contacts_.end()) {
+                auto& ci2 = it->second;
+                ++ci2.freq;
+                if (ci.last_seen > ci2.last_seen) {
+                        ci2.last_seen = ci.last_seen;
+                        wash(ci.email);
+                        ci2.email = std::move(ci.email);
+                        if (!ci.name.empty()) {
+                                wash(ci.name);
+                                ci2.name = std::move(ci.name);
+                        }
+                }
+        }
+
+        wash(ci.name);
+        wash(ci.email);
+        wash(ci.full_address);
+
+        priv_->contacts_.emplace(
+                        ContactUMap::value_type(std::move(email), std::move(ci)));
+}
+
+
+const ContactInfo*
+Contacts::_find (const std::string& email) const
+{
+        std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+        ContactInfo ci{"", email, "", false, 0};
+        const auto it = priv_->contacts_.find(ci.email);
+        if (it == priv_->contacts_.end())
+                return {};
+        else
+                return &it->second;
+}
+
+
+void
+Contacts::clear()
+{
+        std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+        priv_->contacts_.clear();
+}
+
+
+std::size_t
+Contacts::size() const
+{
+        std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+        return priv_->contacts_.size();
+}
+
+
+void
+Contacts::for_each(const EachContactFunc& each_contact) const
+{
+        std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+        if (!each_contact)
+                return; // nothing to do
+
+        // first sort them for 'rank'
+        ContactSet sorted;
+        for (const auto& item: priv_->contacts_)
+                sorted.emplace(item.second);
+
+        for (const auto& ci: sorted)
+                each_contact (ci);
+}
+
+/// C binding
+
+size_t
+mu_contacts_count (const MuContacts *self)
+{
+        g_return_val_if_fail (self, 0);
+
+        auto myself = reinterpret_cast<const Mu::Contacts*>(self);
+
+        return myself->size();
+}
+
+gboolean
+mu_contacts_foreach (const MuContacts *self, MuContactsForeachFunc func,
+                     gpointer user_data)
+{
+        g_return_val_if_fail (self, FALSE);
+        g_return_val_if_fail (func, FALSE);
+
+        auto myself = reinterpret_cast<const Mu::Contacts*>(self);
+
+        myself->for_each([&](const ContactInfo& ci) {
+                 g_return_if_fail (!ci.email.empty());
+                 func(ci.full_address.c_str(),
+                      ci.email.c_str(),
+                      ci.name.empty() ? NULL : ci.name.c_str(),
+                      ci.personal,
+                      ci.last_seen,
+                      ci.freq,
+                      ci.tstamp,
+                      user_data);
+         });
+
+        return TRUE;
+}
+
+struct _MuContacts :  public Mu::Contacts {}; /**< c-compat */
diff --git a/lib/mu-contacts.hh b/lib/mu-contacts.hh
new file mode 100644 (file)
index 0000000..7873cd6
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+** 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_CONTACTS_HH__
+#define __MU_CONTACTS_HH__
+
+#include <glib.h>
+#include <time.h>
+
+struct _MuContacts;
+typedef struct _MuContacts MuContacts;
+
+#ifdef __cplusplus
+
+#include <memory>
+#include <functional>
+#include <chrono>
+#include <string>
+#include <time.h>
+#include <inttypes.h>
+
+namespace Mu {
+
+/// Data-structure representing information about some contact.
+
+struct ContactInfo {
+        /**
+         * Construct a new ContactInfo
+         *
+         * @param _full_address the full email address + name.
+         * @param _email email address
+         * @param _name name or empty
+         * @param _personal is this a personal contact?
+         * @param _last_seen when was this contact last seen?
+         * @param _freq how often was this contact seen?
+         *
+         * @return
+         */
+        ContactInfo (const std::string& _full_address,
+                     const std::string& _email,
+                     const std::string& _name,
+                     bool _personal, time_t _last_seen, size_t _freq=1);
+
+        std::string full_address; /**< Full name <email> */
+        std::string email;        /**< email address */
+        std::string name;         /**< name (or empty) */
+        bool        personal;     /**< is this a personal contact? */
+        time_t      last_seen;    /**< when was this contact last seen? */
+        std::size_t freq;         /**< how often was this contact seen? */
+
+        int64_t     tstamp;       /**< Time-stamp, as per g_get_monotonic_time */
+};
+
+/// All contacts
+class Contacts {
+public:
+        /**
+         * Construct a new contacts objects
+         *
+         * @param serialized serialized contacts
+         */
+        Contacts (const std::string& serialized = "");
+
+        /**
+         * DTOR
+         *
+         */
+        ~Contacts ();
+
+        /**
+         * Add a contact
+         *
+         * @param ci A contact-info object
+         */
+        void add(ContactInfo&& ci);
+
+        /**
+         * 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.
+         *
+         * @return serialized contacts
+         */
+        std::string serialize() 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 ContactInfo* _find (const std::string& email) const;
+
+        /**
+         * Prototype for a callable that receives a contact
+         *
+         * @param contact some contact
+         */
+        using EachContactFunc = std::function<void (const ContactInfo& contact_info)>;
+
+        /**
+         * Invoke some callable for each contact, in order of rank.
+         *
+         * @param each_contact
+         */
+        void for_each (const EachContactFunc& each_contact) const;
+
+        /**
+         * For C compatiblityy
+         *
+         * @return a MuContacts* referring to this.
+         */
+        const MuContacts* mu_contacts() const {
+                return reinterpret_cast<const MuContacts*>(this);
+        }
+
+
+
+private:
+        struct                   Private;
+        std::unique_ptr<Private> priv_;
+};
+
+} // namespace Mu
+
+#endif /*__cplusplus*/
+
+G_BEGIN_DECLS
+
+
+/**
+ * return the number of contacts
+ *
+ * @param self a contacts object
+ *
+ * @return the number of contacts
+ */
+size_t mu_contacts_count (const MuContacts *self);
+
+/**
+ * Function called for mu_contacts_foreach; returns the e-mail address, name
+ * (which may be NULL) , whether the message is 'personal', the timestamp for
+ * the address (when it was last seen), and the frequency (in how many message
+ * did this contact participate) and the tstamp (last modification)
+ *
+ */
+typedef void (*MuContactsForeachFunc) (const char *full_address,
+                                       const char *email, const char *name,
+                                       gboolean personal,
+                                       time_t last_seen, unsigned freq,
+                                       gint64 tstamp, gpointer user_data);
+
+/**
+ * call a function for either each contact, or each contact satisfying
+ * a regular expression,
+ *
+ * @param self contacts object
+ * @param func callback function to be called for each
+ * @param user_data user data to pass to the callback
+ *
+ * @return TRUE if the function succeeded, or FALSE if the provide regular
+ * expression was invalid (and not NULL)
+ */
+gboolean mu_contacts_foreach (const MuContacts *self,
+                              MuContactsForeachFunc func,
+                              gpointer user_data);
+
+G_END_DECLS
+
+#endif /* __MU_CONTACTS_HH__ */
diff --git a/lib/mu-container.c b/lib/mu-container.c
new file mode 100644 (file)
index 0000000..a7e07e3
--- /dev/null
@@ -0,0 +1,691 @@
+/*
+** 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.
+**
+*/
+
+#include <string.h> /* for memset */
+#include <math.h> /* for log, ceil */
+
+#include "mu-container.h"
+#include "mu-msg.h"
+#include "mu-msg-iter.h"
+
+
+/*
+ * path data structure, to determine the thread paths mentioned above;
+ * the path is filled as we're traversing the tree of MuContainers
+ * (messages)
+ */
+struct _Path {
+       int *_data;
+       guint _len;
+};
+typedef struct _Path Path;
+
+static Path* path_new (guint initial);
+static void  path_destroy (Path *p);
+static void  path_inc (Path *p, guint index);
+static gchar* path_to_string (Path *p, const char* frmt);
+
+MuContainer*
+mu_container_new (MuMsg *msg, guint docid, const char *msgid)
+{
+       MuContainer *c;
+
+       g_return_val_if_fail (!msg || docid != 0, NULL);
+
+       c = g_slice_new0 (MuContainer);
+       if (msg)
+               c->msg = mu_msg_ref (msg);
+
+       c->leader = c;
+       c->docid = docid;
+       c->msgid = msgid;
+
+       return c;
+}
+
+void
+mu_container_destroy (MuContainer *c)
+{
+       if (!c)
+               return;
+
+       if (c->msg)
+               mu_msg_unref (c->msg);
+
+       g_slice_free (MuContainer, c);
+}
+
+
+static void
+set_parent (MuContainer *c, MuContainer *parent)
+{
+       while (c) {
+               c->parent = parent;
+               c = c->next;
+       }
+}
+
+
+G_GNUC_UNUSED static gboolean
+check_dup (MuContainer *c, GHashTable *hash)
+{
+       if (g_hash_table_lookup (hash, c)) {
+               g_warning ("ALREADY!!");
+               mu_container_dump (c, TRUE);
+               g_assert (0);
+       } else
+               g_hash_table_insert (hash, c, GUINT_TO_POINTER(TRUE));
+
+       return TRUE;
+}
+
+
+G_GNUC_UNUSED static void
+assert_no_duplicates (MuContainer *c)
+{
+       GHashTable *hash;
+
+       hash = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+       mu_container_foreach (c,
+                          (MuContainerForeachFunc)check_dup,
+                          hash);
+
+       g_hash_table_destroy (hash);
+}
+
+
+MuContainer*
+mu_container_append_siblings (MuContainer *c, MuContainer *sibling)
+{
+       g_assert (c);
+
+       g_return_val_if_fail (c, NULL);
+       g_return_val_if_fail (sibling, NULL);
+       g_return_val_if_fail (c != sibling, NULL);
+
+       /* assert_no_duplicates (c); */
+
+       set_parent (sibling, c->parent);
+
+       /* find the last sibling and append; first we try our cache
+        * 'last', otherwise we need to walk the chain. We use a
+        * cached last as to avoid walking the chain (which is
+        * O(n*n)) */
+       if (c->last)
+               c->last->next = sibling;
+       else {
+               /* no 'last' cached, so walk the chain */
+               MuContainer *c2;
+               for (c2 = c; c2 && c2->next; c2 = c2->next);
+               c2->next = sibling;
+       }
+       /* update the cached last */
+       c->last = sibling->last ? sibling->last : sibling;
+
+       /* assert_no_duplicates (c); */
+
+       return c;
+}
+
+MuContainer*
+mu_container_remove_sibling (MuContainer *c, MuContainer *sibling)
+{
+       MuContainer *cur, *prev;
+
+       g_return_val_if_fail (c, NULL);
+       g_return_val_if_fail (sibling, NULL);
+
+       for (prev = NULL, cur = c; cur; cur = cur->next) {
+
+               if (cur == sibling) {
+                       if (!prev)
+                               c = cur->next;
+                       else
+                               prev->next = cur->next;
+                       break;
+               }
+               prev = cur;
+       }
+
+       /* unset the cached last; it's not valid anymore
+        *
+        * TODO: we could actually do a better job updating last
+        * rather than invalidating it. */
+       if (c)
+               c->last = NULL;
+
+       return c;
+}
+
+MuContainer*
+mu_container_append_children (MuContainer *c, MuContainer *child)
+{
+       g_return_val_if_fail (c, NULL);
+       g_return_val_if_fail (child, NULL);
+       g_return_val_if_fail (c != child, NULL);
+
+       /* assert_no_duplicates (c); */
+
+       set_parent (child, c);
+       if (!c->child)
+               c->child = child;
+       else
+               c->child = mu_container_append_siblings (c->child, child);
+
+       /* assert_no_duplicates (c->child); */
+
+       return c;
+}
+
+
+MuContainer*
+mu_container_remove_child (MuContainer *c, MuContainer *child)
+{
+       g_return_val_if_fail (c, NULL);
+       g_return_val_if_fail (child, NULL);
+
+       /* g_assert (!child->child); */
+       /* g_return_val_if_fail (!child->child, NULL); */
+       g_return_val_if_fail (c != child, NULL);
+
+       c->child = mu_container_remove_sibling (c->child, child);
+
+       return c;
+}
+
+typedef void (*MuContainerPathForeachFunc) (MuContainer*, gpointer, Path*);
+
+static void
+mu_container_path_foreach_real (MuContainer *c, guint level, Path *path,
+                               MuContainerPathForeachFunc func,
+                               gpointer user_data)
+{
+       if (!c)
+               return;
+
+       path_inc (path, level);
+       func (c, user_data, path);
+
+       /* children */
+       mu_container_path_foreach_real (c->child, level + 1, path,
+                                       func, user_data);
+
+       /* siblings */
+       mu_container_path_foreach_real (c->next, level, path, func, user_data);
+}
+
+static void
+mu_container_path_foreach (MuContainer *c, MuContainerPathForeachFunc func,
+                       gpointer user_data)
+{
+       Path *path;
+
+       path = path_new (100);
+
+       mu_container_path_foreach_real (c, 0, path, func, user_data);
+
+       path_destroy (path);
+}
+
+
+gboolean
+mu_container_foreach (MuContainer *c, MuContainerForeachFunc func,
+                     gpointer user_data)
+{
+       g_return_val_if_fail (func, FALSE);
+
+       if (!c)
+               return TRUE;
+
+       if (!mu_container_foreach (c->child, func, user_data))
+               return FALSE; /* recurse into children */
+
+       /* recurse into siblings */
+       if (!mu_container_foreach (c->next, func, user_data))
+               return FALSE;
+
+       return func (c, user_data);
+}
+
+MuContainer*
+mu_container_splice_children (MuContainer *c, MuContainer *sibling)
+{
+       MuContainer *children;
+
+       g_return_val_if_fail (c, NULL);
+       g_return_val_if_fail (sibling, NULL);
+
+       children = sibling->child;
+       sibling->child = NULL;
+
+       return mu_container_append_siblings (c, children);
+}
+
+MuContainer*
+mu_container_splice_grandchildren (MuContainer *parent, MuContainer *child)
+{
+       MuContainer *newchild;
+
+       g_return_val_if_fail (parent, NULL);
+       g_return_val_if_fail (child, NULL);
+       g_return_val_if_fail (parent != child, NULL);
+
+       newchild = child->child;
+       child->child=NULL;
+
+       return mu_container_append_children (parent, newchild);
+}
+
+
+static GSList*
+mu_container_to_list (MuContainer *c)
+{
+       GSList *lst;
+
+       for (lst = NULL; c; c = c->next)
+               lst = g_slist_prepend (lst, c);
+
+       return lst;
+}
+
+static gpointer
+list_last_data (GSList *lst)
+{
+       GSList *tail;
+
+       tail = g_slist_last (lst);
+
+       return tail->data;
+}
+
+static MuContainer*
+mu_container_from_list (GSList *lst)
+{
+       MuContainer *c, *cur, *tail;
+
+       if (!lst)
+               return NULL;
+
+       tail = list_last_data (lst);
+       for (c = cur = (MuContainer*)lst->data; cur; lst = g_slist_next(lst)) {
+               cur->next = lst ? (MuContainer*)lst->data : NULL;
+               cur->last = tail;
+               cur=cur->next;
+       }
+
+       return c;
+}
+
+struct _SortFuncData {
+       MuMsgFieldId         mfid;
+       gboolean             descending;
+       gpointer             user_data;
+};
+typedef struct _SortFuncData SortFuncData;
+
+static int
+container_cmp (MuContainer *a, MuContainer *b, MuMsgFieldId mfid)
+{
+       if (a == b)
+               return 0;
+       else if (!a->msg)
+               return -1;
+       else if (!b->msg)
+               return 1;
+
+       return mu_msg_cmp (a->msg, b->msg, mfid);
+}
+
+static int
+sort_func_root (MuContainer *a, MuContainer *b, SortFuncData *data)
+{
+       if (data->descending)
+               return container_cmp (b->leader, a->leader, data->mfid);
+       else
+               return container_cmp (a->leader, b->leader, data->mfid);
+}
+
+static int
+sort_func_child (MuContainer *a, MuContainer *b, SortFuncData *data)
+{
+       return container_cmp (a, b, data->mfid);
+}
+
+static MuContainer*
+container_sort(MuContainer *c, GCompareDataFunc func, SortFuncData *sfdata)
+{
+       GSList *lst;
+
+       lst = mu_container_to_list (c);
+       lst = g_slist_sort_with_data (lst, func, sfdata);
+       c = mu_container_from_list (lst);
+       g_slist_free (lst);
+
+       return c;
+}
+
+static MuContainer*
+container_sort_child (MuContainer *c, SortFuncData *sfdata)
+{
+       MuContainer *cur, *leader;
+
+       if (!c)
+               return NULL;
+
+       /* find leader */
+       leader = c->leader;
+       for (cur = c; cur; cur = cur->next) {
+               if (cur->child)
+                       cur->child = container_sort_child (cur->child, sfdata);
+               if (container_cmp (cur->leader, leader, sfdata->mfid) > 0)
+                       leader = cur->leader;
+       }
+
+       c = container_sort(c, (GCompareDataFunc)sort_func_child, sfdata);
+
+       /* set parent's leader to the one found */
+       c->parent->leader = leader;
+
+       return c;
+}
+
+static MuContainer*
+container_sort_root (MuContainer *c, SortFuncData *sfdata)
+{
+       MuContainer *cur;
+
+       if (!c)
+               return NULL;
+
+       for (cur = c; cur; cur = cur->next) {
+               if (cur->child)
+                       cur->child = container_sort_child (cur->child, sfdata);
+       }
+
+       return container_sort (c, (GCompareDataFunc)sort_func_root, sfdata);
+}
+
+MuContainer*
+mu_container_sort (MuContainer *c, MuMsgFieldId mfid, gboolean descending,
+                  gpointer user_data)
+{
+       SortFuncData sfdata;
+
+       sfdata.mfid       = mfid;
+       sfdata.descending = descending;
+       sfdata.user_data  = user_data;
+
+       g_return_val_if_fail (c, NULL);
+       g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), NULL);
+
+       return container_sort_root (c, &sfdata);
+}
+
+
+static gboolean
+unequal (MuContainer *a, MuContainer *b)
+{
+       return a == b ? FALSE : TRUE;
+}
+
+
+gboolean
+mu_container_reachable (MuContainer *haystack, MuContainer *needle)
+{
+       g_return_val_if_fail (haystack, FALSE);
+       g_return_val_if_fail (needle, FALSE);
+
+       if (!mu_container_foreach
+           (haystack, (MuContainerForeachFunc)unequal, needle))
+               return TRUE;
+
+       return FALSE;
+}
+
+
+static gboolean
+dump_container (MuContainer *c)
+{
+       const gchar* subject;
+
+       if (!c) {
+               g_print ("<empty>\n");
+               return TRUE;
+       }
+
+       subject =  (c->msg) ? mu_msg_get_subject (c->msg) : "<none>";
+
+       g_print ("[%s][%s m:%p p:%p docid:%u %s]\n",c->msgid, subject, (void*)c,
+                (void*)c->parent, c->docid,
+                c->msg ? mu_msg_get_path (c->msg) : "");
+
+       return TRUE;
+}
+
+
+void
+mu_container_dump (MuContainer *c, gboolean recursive)
+{
+       g_return_if_fail (c);
+
+       if (!recursive)
+               dump_container (c);
+       else
+               mu_container_foreach
+                       (c,
+                        (MuContainerForeachFunc)dump_container,
+                        NULL);
+}
+
+
+
+static Path*
+path_new (guint initial)
+{
+       Path *p;
+
+       p = g_slice_new0 (Path);
+
+       p->_data = g_new0 (int, initial);
+       p->_len  = initial;
+
+       return p;
+}
+
+static void
+path_destroy (Path *p)
+{
+       if (!p)
+               return;
+
+       g_free (p->_data);
+       g_slice_free (Path, p);
+}
+
+static void
+path_inc (Path *p, guint index)
+{
+       if (index + 1 >= p->_len) {
+               p->_data = g_renew (int, p->_data, 2 * p->_len);
+               memset (&p->_data[p->_len], 0, p->_len);
+               p->_len *= 2;
+       }
+
+       ++p->_data[index];
+       p->_data[index + 1] = 0;
+}
+
+
+static gchar*
+path_to_string (Path *p, const char* frmt)
+{
+       char *str;
+       guint u;
+
+       if (!p->_data)
+               return NULL;
+
+       for (u = 0, str = NULL; p->_data[u] != 0; ++u) {
+
+               char segm[16];
+               g_snprintf (segm, sizeof(segm), frmt, p->_data[u] - 1);
+
+               if (!str)
+                       str = g_strdup (segm);
+               else {
+                       gchar *tmp;
+                       tmp = g_strdup_printf ("%s:%s", str, segm);
+                       g_free (str);
+                       str = tmp;
+               }
+       }
+
+       return str;
+}
+
+static unsigned
+count_colons (const char *str)
+{
+       unsigned num;
+
+       num = 0;
+       while (str++ && *str)
+               if (*str == ':')
+                       ++num;
+
+       return num;
+}
+
+
+
+static MuMsgIterThreadInfo*
+thread_info_new (gchar *threadpath, gboolean root, gboolean first_child,
+                gboolean last_child, gboolean empty_parent,
+                gboolean has_child, gboolean is_dup)
+{
+       MuMsgIterThreadInfo *ti;
+
+       ti                   = g_slice_new (MuMsgIterThreadInfo);
+       ti->threadpath       = threadpath;
+       ti->level            = count_colons (threadpath); /* hacky... */
+
+       ti->prop  = MU_MSG_ITER_THREAD_PROP_NONE;
+       ti->prop |= root         ? MU_MSG_ITER_THREAD_PROP_ROOT         : 0;
+       ti->prop |= first_child  ? MU_MSG_ITER_THREAD_PROP_FIRST_CHILD  : 0;
+       ti->prop |= last_child   ? MU_MSG_ITER_THREAD_PROP_LAST_CHILD   : 0;
+       ti->prop |= empty_parent ? MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT : 0;
+       ti->prop |= is_dup       ? MU_MSG_ITER_THREAD_PROP_DUP          : 0;
+       ti->prop |= has_child    ? MU_MSG_ITER_THREAD_PROP_HAS_CHILD    : 0;
+
+       return ti;
+}
+
+static void
+thread_info_destroy (MuMsgIterThreadInfo *ti)
+{
+       if (ti) {
+               g_free (ti->threadpath);
+               g_slice_free (MuMsgIterThreadInfo, ti);
+       }
+}
+
+
+struct _ThreadInfo {
+       GHashTable *hash;
+       const char *format;
+};
+typedef struct _ThreadInfo ThreadInfo;
+
+
+static void
+add_to_thread_info_hash (GHashTable *thread_info_hash, MuContainer *c,
+                        char *threadpath)
+{
+       gboolean is_root, first_child, last_child, empty_parent, is_dup, has_child;
+
+       /* 'root' means we're a child of the dummy root-container */
+       is_root = (c->parent == NULL);
+
+       first_child  = is_root ? FALSE : (c->parent->child == c);
+       last_child  = is_root ? FALSE : (c->next == NULL);
+       empty_parent = is_root ? FALSE : (!c->parent->msg);
+       is_dup       = c->flags & MU_CONTAINER_FLAG_DUP;
+       has_child    = c->child ? TRUE : FALSE;
+
+       g_hash_table_insert (thread_info_hash,
+                            GUINT_TO_POINTER(c->docid),
+                            thread_info_new (threadpath,
+                                             is_root,
+                                             first_child,
+                                             last_child,
+                                             empty_parent,
+                                             has_child,
+                                             is_dup));
+}
+
+/* device a format string that is the minimum size to fit up to
+ * matchnum matches -- returns static memory */
+static const char*
+thread_segment_format_string (size_t matchnum)
+{
+       unsigned digitnum;
+       static char frmt[16];
+
+       /* get the number of digits needed in a hex-representation of
+        * matchnum */
+       digitnum = (unsigned) (ceil (log(matchnum)/log(16)));
+       g_snprintf (frmt, sizeof(frmt), "%%0%ux", digitnum);
+
+       return frmt;
+}
+
+static gboolean
+add_thread_info (MuContainer *c, ThreadInfo *ti, Path *path)
+{
+       gchar *pathstr;
+
+       pathstr = path_to_string (path, ti->format);
+       add_to_thread_info_hash (ti->hash, c, pathstr);
+
+       return TRUE;
+}
+
+
+GHashTable*
+mu_container_thread_info_hash_new (MuContainer *root_set, size_t matchnum)
+{
+       ThreadInfo ti;
+
+       g_return_val_if_fail (root_set, NULL);
+       g_return_val_if_fail (matchnum > 0, NULL);
+
+       /* create hash docid => thread-info */
+       ti.hash = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+                                        NULL,
+                                        (GDestroyNotify)thread_info_destroy);
+
+       ti.format     = thread_segment_format_string (matchnum);
+
+       mu_container_path_foreach (root_set,
+                               (MuContainerPathForeachFunc)add_thread_info,
+                               &ti);
+
+       return ti.hash;
+}
diff --git a/lib/mu-container.h b/lib/mu-container.h
new file mode 100644 (file)
index 0000000..69c1950
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_CONTAINER_H__
+#define __MU_CONTAINER_H__
+
+#include <glib.h>
+#include <mu-msg.h>
+
+enum _MuContainerFlag {
+       MU_CONTAINER_FLAG_NONE    = 0,
+       MU_CONTAINER_FLAG_DELETE  = 1 << 0,
+       MU_CONTAINER_FLAG_SPLICE  = 1 << 1,
+       MU_CONTAINER_FLAG_DUP     = 1 << 2
+};
+typedef enum _MuContainerFlag MuContainerFlag;
+
+/*
+ * MuContainer data structure, as seen in JWZs document:
+ *     http://www.jwz.org/doc/threading.html
+ */
+struct _MuContainer {
+       struct _MuContainer *parent, *child, *next;
+
+       /* note: we cache the last of the string of next->next->...
+        * `mu_container_append_siblings' shows up high in the
+        * profiles since it needs to walk to the end, and this give
+        * O(n*n) behavior.
+        * */
+       struct _MuContainer *last;
+
+       /* Node in the subtree rooted at this node which comes first
+        * in the descending sort order, e.g. the latest message if
+        * sorting by date. We compare the leaders when ordering
+        * subtrees. */
+       struct _MuContainer *leader;
+
+       MuMsg               *msg;
+       const char          *msgid;
+
+       unsigned            docid;
+       MuContainerFlag     flags;
+
+};
+typedef struct _MuContainer MuContainer;
+
+
+/**
+ * create a new Container object
+ *
+ * @param msg a MuMsg, or NULL; when it's NULL, docid should be 0
+ * @param docid a Xapian docid, or 0
+ * @param msgid a message id, or NULL
+ *
+ * @return a new Container instance, or NULL in case of error; free
+ * with mu_container_destroy
+ */
+MuContainer* mu_container_new (MuMsg *msg, guint docid, const char* msgid);
+
+
+/**
+ * free a Container object
+ *
+ * @param c a Container object, or NULL
+ */
+void       mu_container_destroy (MuContainer *c);
+
+
+
+/**
+ * append new child(ren) to this container; the child(ren) container's
+ * parent pointer will point to this one
+ *
+ * @param c a Container instance
+ * @param child a child
+ *
+ * @return the Container instance with a child added
+ */
+MuContainer* mu_container_append_children (MuContainer *c, MuContainer *child);
+
+/**
+ * append a new sibling to this (list of) containers; all the siblings
+ * will get the same parent that @c has
+ *
+ * @param c a container instance
+ * @param sibling a sibling
+ *
+ * @return the container (list) with the sibling(s) appended
+ */
+MuContainer* mu_container_append_siblings (MuContainer *c, MuContainer *sibling);
+
+/**
+ * remove a _single_ child container from a container
+ *
+ * @param c a container instance
+ * @param child the child container to remove
+ *
+ * @return the container with the child removed; if the container did
+ * have this child, nothing changes
+ */
+MuContainer* mu_container_remove_child (MuContainer *c, MuContainer *child);
+
+/**
+ * remove a _single_ sibling container from a container
+ *
+ * @param c a container instance
+ * @param sibling the sibling container to remove
+ *
+ * @return the container with the sibling removed; if the container did
+ * have this sibling, nothing changes
+ */
+MuContainer* mu_container_remove_sibling (MuContainer *c, MuContainer *sibling);
+
+/**
+ * promote sibling's children to be this container's siblings
+ *
+ * @param c a container instance
+ * @param sibling a sibling of this container
+ *
+ * @return the container with the sibling's children promoted
+ */
+
+MuContainer* mu_container_splice_children (MuContainer *c,
+                                           MuContainer *sibling);
+
+/**
+ * promote child's children to be parent's children
+ *
+ * @param parent a container instance
+ * @param child a child of this container
+ *
+ * @return the new container with it's children's children promoted
+ */
+MuContainer* mu_container_splice_grandchildren (MuContainer *parent,
+                                                MuContainer *child);
+
+typedef gboolean (*MuContainerForeachFunc) (MuContainer*, gpointer);
+
+/**
+ * execute some function on all siblings an children of some container
+ * (recursively) until all children have been visited or the callback
+ * function returns FALSE
+ *
+ * @param c a container
+ * @param func a function to call for each container
+ * @param user_data a pointer to pass to the callback function
+ *
+ * @return
+ */
+gboolean   mu_container_foreach (MuContainer *c,
+                                MuContainerForeachFunc func,
+                                gpointer user_data);
+
+/**
+ * check whether container needle is a child or sibling (recursively)
+ * of container haystack
+ *
+ * @param haystack a container
+ * @param needle a container
+ *
+ * @return TRUE if needle is reachable from haystack, FALSE otherwise
+ */
+gboolean   mu_container_reachable (MuContainer *haystack, MuContainer *needle);
+
+
+/**
+ * dump the container to stdout (for debugging)
+ *
+ * @param c a container
+ * @param recursive whether to include siblings, children
+ */
+void       mu_container_dump (MuContainer *c, gboolean recursive);
+
+
+typedef int (*MuContainerCmpFunc) (MuContainer *c1, MuContainer *c2,
+                                  gpointer user_data);
+
+/**
+ * sort the tree of MuContainers, recursively; ie. each of the list of
+ * siblings (children) will be sorted according to @func; if the
+ * container is empty, the first non-empty 'leftmost' child is used.
+ *
+ * @param c a container
+ * @param mfid the field to sort by
+ * @param revert if TRUE, revert the sorting order *
+ * @param user_data a user pointer to pass to the sorting function
+ *
+ * @return a sorted container
+ */
+MuContainer* mu_container_sort (MuContainer *c, MuMsgFieldId mfid,
+                               gboolean revert,
+                               gpointer user_data);
+
+
+/**
+ * create a hashtable with maps document-ids to information about them,
+ * ie. Xapian docid => MuMsgIterThreadInfo
+ *
+ * @param root_set the containers @param matchnum the number of
+ * matches in the list (this is needed to determine the shortest
+ * possible collation keys ('threadpaths') for the messages
+ *
+ * @return a hash; free with g_hash_table_destroy
+ */
+GHashTable* mu_container_thread_info_hash_new (MuContainer *root_set,
+                                              size_t matchnum);
+
+#endif /*__MU_CONTAINER_H__*/
diff --git a/lib/mu-flags.c b/lib/mu-flags.c
new file mode 100644 (file)
index 0000000..330fa31
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+** Copyright (C) 2011-2012  <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.h>
+#include "mu-flags.h"
+
+struct _FlagInfo {
+       MuFlags          flag;
+       char             kar;
+       const char      *name;
+       MuFlagType       flag_type;
+};
+typedef struct _FlagInfo FlagInfo;
+
+static const FlagInfo FLAG_INFO[] = {
+
+       /* NOTE: order of this is significant, due to optimizations
+        * below */
+
+       { MU_FLAG_DRAFT,      'D', "draft",     MU_FLAG_TYPE_MAILFILE },
+       { MU_FLAG_FLAGGED,    'F', "flagged",   MU_FLAG_TYPE_MAILFILE },
+       { MU_FLAG_PASSED,     'P', "passed",    MU_FLAG_TYPE_MAILFILE },
+       { MU_FLAG_REPLIED,    'R', "replied",   MU_FLAG_TYPE_MAILFILE },
+       { MU_FLAG_SEEN,       'S', "seen",      MU_FLAG_TYPE_MAILFILE },
+       { MU_FLAG_TRASHED,    'T', "trashed",   MU_FLAG_TYPE_MAILFILE },
+
+       { MU_FLAG_NEW,        'N', "new",       MU_FLAG_TYPE_MAILDIR  },
+
+       { MU_FLAG_SIGNED,     'z', "signed",    MU_FLAG_TYPE_CONTENT  },
+       { MU_FLAG_ENCRYPTED,  'x', "encrypted", MU_FLAG_TYPE_CONTENT  },
+       { MU_FLAG_HAS_ATTACH, 'a', "attach",    MU_FLAG_TYPE_CONTENT  },
+       { MU_FLAG_LIST,       'l', "list",      MU_FLAG_TYPE_CONTENT  },
+
+
+       { MU_FLAG_UNREAD,     'u', "unread",    MU_FLAG_TYPE_PSEUDO  }
+};
+
+
+MuFlagType
+mu_flag_type (MuFlags flag)
+{
+       unsigned u;
+
+       for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u)
+               if (FLAG_INFO[u].flag == flag)
+                       return FLAG_INFO[u].flag_type;
+
+       return MU_FLAG_TYPE_INVALID;
+}
+
+
+char
+mu_flag_char (MuFlags flag)
+{
+       unsigned u;
+
+       for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u)
+               if (FLAG_INFO[u].flag == flag)
+                       return FLAG_INFO[u].kar;
+       return 0;
+}
+
+
+MuFlags
+mu_flag_char_from_name (const char *str)
+{
+       unsigned u;
+
+       g_return_val_if_fail (str, MU_FLAG_INVALID);
+
+       for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u)
+               if (g_strcmp0(FLAG_INFO[u].name, str) == 0)
+                       return FLAG_INFO[u].kar;
+
+       return 0;
+}
+
+
+static MuFlags
+mu_flag_from_char (char kar)
+{
+       unsigned u;
+
+       for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u)
+               if (FLAG_INFO[u].kar == kar)
+                       return FLAG_INFO[u].flag;
+
+       return MU_FLAG_INVALID;
+}
+
+
+const char*
+mu_flag_name (MuFlags flag)
+{
+       unsigned u;
+
+       for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u)
+               if (FLAG_INFO[u].flag == flag)
+                       return FLAG_INFO[u].name;
+
+       return NULL;
+}
+
+
+const char*
+mu_flags_to_str_s (MuFlags flags, MuFlagType types)
+{
+       unsigned u,v;
+       static char str[sizeof(FLAG_INFO) + 1];
+
+       for (u = 0, v = 0; u != G_N_ELEMENTS(FLAG_INFO); ++u)
+               if (flags & FLAG_INFO[u].flag &&
+                   types & FLAG_INFO[u].flag_type)
+                       str[v++] = FLAG_INFO[u].kar;
+       str[v] = '\0';
+
+       return str;
+}
+
+
+MuFlags
+mu_flags_from_str (const char *str, MuFlagType types,
+                  gboolean ignore_invalid)
+{
+       const char      *cur;
+       MuFlags          flag;
+
+       g_return_val_if_fail (str, MU_FLAG_INVALID);
+
+       for (cur = str, flag = MU_FLAG_NONE; *cur; ++cur) {
+
+               MuFlags f;
+
+               f = mu_flag_from_char (*cur);
+
+               if (f == MU_FLAG_INVALID) {
+                       if (ignore_invalid)
+                               continue;
+                       return MU_FLAG_INVALID;
+               }
+
+               if (mu_flag_type (f) & types)
+                       flag |= f;
+       }
+
+       return flag;
+}
+
+
+
+char*
+mu_flags_custom_from_str (const char *str)
+{
+       char *custom;
+       const char* cur;
+       unsigned u;
+
+       g_return_val_if_fail (str, NULL);
+
+       for (cur = str, u = 0, custom = NULL; *cur; ++cur) {
+
+               MuFlags flag;
+               flag = mu_flag_from_char (*cur);
+
+               /* if it's a valid file flag, ignore it */
+               if (flag != MU_FLAG_INVALID &&
+                   mu_flag_type (flag) == MU_FLAG_TYPE_MAILFILE)
+                       continue;
+
+               /* otherwise, add it to our custom string */
+               if (!custom)
+                       custom = g_new0 (char, strlen(str) + 1);
+               custom[u++] = *cur;
+       }
+
+       return custom;
+}
+
+
+
+void
+mu_flags_foreach (MuFlagsForeachFunc func, gpointer user_data)
+{
+       unsigned u;
+
+       g_return_if_fail (func);
+
+       for (u = 0; u != G_N_ELEMENTS(FLAG_INFO); ++u)
+               func (FLAG_INFO[u].flag, user_data);
+}
+
+
+MuFlags
+mu_flags_from_str_delta (const char *str, MuFlags oldflags,
+                        MuFlagType types)
+{
+       const char      *cur;
+       MuFlags          newflags;
+
+       g_return_val_if_fail (str, MU_FLAG_INVALID);
+
+       for (cur = str, newflags = oldflags; *cur; ++cur) {
+
+               MuFlags f;
+               if (*cur == '+' || *cur == '-') {
+                       f = mu_flag_from_char (cur[1]);
+                       if (f == 0)
+                               goto error;
+                       if (*cur == '+')
+                               newflags  |= f;
+                       else
+                               newflags  &= ~f;
+                       ++cur;
+                       continue;
+               }
+
+               goto error;
+       }
+
+       return newflags;
+error:
+       g_warning ("invalid flag string");
+       return MU_FLAG_INVALID;
+
+}
diff --git a/lib/mu-flags.h b/lib/mu-flags.h
new file mode 100644 (file)
index 0000000..9d892f3
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+** 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.
+**
+*/
+
+
+#ifndef __MU_FLAGS_H__
+#define __MU_FLAGS_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+enum _MuFlags {
+       MU_FLAG_NONE            = 0,
+
+       /* 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) */
+       MU_FLAG_DRAFT           = 1 << 0,
+       MU_FLAG_FLAGGED         = 1 << 1,
+       MU_FLAG_PASSED          = 1 << 2,
+       MU_FLAG_REPLIED         = 1 << 3,
+       MU_FLAG_SEEN            = 1 << 4,
+       MU_FLAG_TRASHED         = 1 << 5,
+
+       /* decides on cur/ or new/ in the maildir */
+       MU_FLAG_NEW             = 1 << 6,
+
+       /* content flags -- not visible in the filename, but used for
+        * searching */
+       MU_FLAG_SIGNED          = 1 << 7,
+       MU_FLAG_ENCRYPTED       = 1 << 8,
+       MU_FLAG_HAS_ATTACH      = 1 << 9,
+
+       /* pseudo-flag, only for queries, so we can search for
+        * flag:unread, which is equivalent to 'flag:new OR NOT
+        * flag:seen' */
+       MU_FLAG_UNREAD          = 1 << 10,
+
+       /* other content flags */
+       MU_FLAG_LIST            = 1 << 11
+};
+typedef enum _MuFlags MuFlags;
+
+#define MU_FLAG_INVALID ((MuFlags)-1)
+
+enum _MuFlagType {
+       MU_FLAG_TYPE_MAILFILE    = 1 << 0,
+       MU_FLAG_TYPE_MAILDIR     = 1 << 1,
+       MU_FLAG_TYPE_CONTENT     = 1 << 2,
+       MU_FLAG_TYPE_PSEUDO      = 1 << 3
+};
+typedef enum _MuFlagType MuFlagType;
+
+#define MU_FLAG_TYPE_ANY ((MuFlagType)-1)
+#define MU_FLAG_TYPE_INVALID ((MuFlagType)-1)
+
+
+/**
+ * Get the type of flag (mailfile, maildir, pseudo or content)
+ *
+ * @param flag a MuFlag
+ *
+ * @return the flag type or MU_FLAG_TYPE_INVALID in case of error
+ */
+MuFlagType mu_flag_type (MuFlags flag) G_GNUC_CONST;
+
+
+/**
+ * Get the flag character
+ *
+ * @param flag a MuFlag (single)
+ *
+ * @return the character, or 0 if it's not a valid flag
+ */
+char mu_flag_char (MuFlags flag) G_GNUC_CONST;
+
+
+/**
+ * Get the flag name
+ *
+ * @param flag a single MuFlag
+ *
+ * @return the name (don't free) as string or NULL in case of error
+ */
+const char* mu_flag_name (MuFlags flag) G_GNUC_CONST;
+
+
+/**
+ * Get the string representation of an OR'ed set of flags
+ *
+ * @param flags MuFlag (OR'ed)
+ * @param types allowable types (OR'ed) for the result; the rest is ignored
+ *
+ * @return The string representation (static, don't free), or NULL in
+ * case of error
+ */
+const char* mu_flags_to_str_s (MuFlags flags, MuFlagType types);
+
+
+/**
+ * Get the (OR'ed) flags corresponding to a string representation
+ *
+ * @param str the file info string
+ * @param types the flag types to accept (other will be ignored)
+ * @param ignore invalid if TRUE, ignore invalid flags, otherwise return
+ * MU_FLAG_INVALID if an invalid flag is encountered
+ *
+ * @return the (OR'ed) flags
+ */
+MuFlags mu_flags_from_str (const char *str, MuFlagType types,
+                          gboolean ignore_invalid);
+
+
+
+
+/**
+ * Get the MuFlag char for some flag name
+ *
+ * @param str a flag name
+ *
+ * @return a flag character, or 0
+ */
+MuFlags mu_flag_char_from_name (const char *str);
+
+
+/**
+ * return the concatenation of all non-standard file flags in str
+ * (ie., characters other than DFPRST) as a newly allocated string.
+ *
+ * @param str the file info string
+ *
+ * @return concatenation of all non-standard flags, as a string; free
+ * with g_free when done. If there are no such flags, return NULL.
+ */
+char* mu_flags_custom_from_str (const char *str) G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * Update #oldflags with the flags in #str, where #str consists of the
+ * the normal flag characters, but prefixed with either '+' or '-',
+ * which means resp. "add this flag" or "remove this flag" from
+ * oldflags.  So, e.g. "-N+S" would unset the NEW flag and set the
+ * SEEN flag, without affecting other flags.
+ *
+ * @param str the string representation
+ * @param old flags to update
+ * @param types the flag types to accept (other will be ignored)
+ *
+ * @return
+ */
+MuFlags mu_flags_from_str_delta (const char *str, MuFlags oldflags,
+                                MuFlagType types);
+
+
+typedef void (*MuFlagsForeachFunc) (MuFlags flag, gpointer user_data);
+
+/**
+ * call a function for each available flag
+ *
+ * @param func a function to call
+ * @param user_data a user pointer to pass to the function
+ */
+void mu_flags_foreach (MuFlagsForeachFunc func, gpointer user_data);
+
+G_END_DECLS
+
+#endif /*__MU_FLAGS_H__*/
diff --git a/lib/mu-index.c b/lib/mu-index.c
new file mode 100644 (file)
index 0000000..33aaffe
--- /dev/null
@@ -0,0 +1,475 @@
+/*
+** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This program is free software; you can redistribute it and/or modify
+1** 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"
+#include "mu-index.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <errno.h>
+
+#include "mu-maildir.h"
+
+#define        MU_LAST_USED_MAILDIR_KEY "last_used_maildir"
+#define MU_INDEX_MAX_FILE_SIZE (500*1000*1000) /* 500 Mb */
+/* apparently, people are getting really big mails, so let us index those (by
+ * default)*/
+
+struct _MuIndex {
+       MuStore         *_store;
+       gboolean         _needs_reindex;
+       guint            _max_filesize;
+};
+
+MuIndex*
+mu_index_new (MuStore *store, GError **err)
+{
+       MuIndex *index;
+       unsigned count;
+
+       g_return_val_if_fail (store, NULL);
+       g_return_val_if_fail (!mu_store_is_read_only(store), NULL);
+
+       index = g_new0 (MuIndex, 1);
+
+       index->_store = mu_store_ref (store);
+
+       /* set the default max file size */
+       index->_max_filesize = MU_INDEX_MAX_FILE_SIZE;
+
+       count = mu_store_count (store, err);
+       if (count == (unsigned)-1)
+               return NULL;
+       else if (count  == 0)
+               index->_needs_reindex = TRUE;
+
+       return index;
+}
+
+void
+mu_index_destroy (MuIndex *index)
+{
+       if (!index)
+               return;
+
+       mu_store_unref (index->_store);
+       g_free (index);
+}
+
+
+struct _MuIndexCallbackData {
+       MuIndexMsgCallback              _idx_msg_cb;
+       MuIndexDirCallback              _idx_dir_cb;
+       MuStore*                        _store;
+       void*                           _user_data;
+       MuIndexStats*                   _stats;
+       gboolean                        _reindex;
+       gboolean                        _lazy_check;
+       time_t                          _dirstamp;
+       guint                           _max_filesize;
+};
+typedef struct _MuIndexCallbackData    MuIndexCallbackData;
+
+
+/* checks to determine if we need to (re)index this message note:
+ * simply checking timestamps is not good enough because message may
+ * be moved from other dirs (e.g. from 'new' to 'cur') and the time
+ * stamps won't change. */
+static inline gboolean
+needs_index (MuIndexCallbackData *data, const char *fullpath,
+            time_t filestamp)
+{
+       /* unconditionally reindex */
+       if (data->_reindex)
+               return TRUE;
+
+       /* it's not in the database yet */
+       if (!mu_store_contains_message (data->_store, fullpath))
+               return TRUE;
+
+       /* it's there, but it's not up to date */
+       if ((unsigned)filestamp >= (unsigned)data->_dirstamp)
+               return TRUE;
+
+       return FALSE; /* index not needed */
+}
+
+
+static MuError
+insert_or_update_maybe (const char *fullpath, const char *mdir,
+                       time_t filestamp, MuIndexCallbackData *data,
+                       gboolean *updated)
+{
+       MuMsg           *msg;
+       GError          *err;
+       gboolean         rv;
+
+       *updated = FALSE;
+       if (!needs_index (data, fullpath, filestamp))
+               return MU_OK; /* nothing to do for this one */
+
+       err = NULL;
+       msg = mu_msg_new_from_file (fullpath, mdir, &err);
+       if (!msg) {
+               if (!err)
+                       g_warning ("error creating message object: %s",
+                                  fullpath);
+               else {
+                       g_warning ("%s", err->message);
+                       g_clear_error (&err);
+               }
+               /* warn, then simply continue */
+               return MU_OK;
+       }
+
+       /* we got a valid id; scan the message contents as well */
+       rv = mu_store_add_msg (data->_store, msg, &err);
+       mu_msg_unref (msg);
+
+       if (!rv) {
+               g_warning ("error storing message object: %s",
+                          err ? err->message : "cause unknown");
+               g_clear_error (&err);
+               return MU_ERROR;
+       }
+
+       *updated = TRUE;
+       return MU_OK;
+}
+
+
+static MuError
+run_msg_callback_maybe (MuIndexCallbackData *data)
+{
+       MuError result;
+
+       if (!data || !data->_idx_msg_cb)
+               return MU_OK;
+
+       result = data->_idx_msg_cb (data->_stats, data->_user_data);
+       if (G_UNLIKELY(result != MU_OK && result != MU_STOP))
+               g_warning ("error in callback");
+
+       return result;
+}
+
+
+static MuError
+on_run_maildir_msg (const char *fullpath, const char *mdir,
+                   struct stat *statbuf, MuIndexCallbackData *data)
+{
+       MuError result;
+       gboolean updated;
+
+       /* protect against too big messages */
+       if (G_UNLIKELY(statbuf->st_size > data->_max_filesize)) {
+               g_warning ("ignoring because bigger than %u bytes: %s",
+                          data->_max_filesize, fullpath);
+               return MU_OK; /* not an error */
+       }
+
+       result = run_msg_callback_maybe (data);
+       if (result != MU_OK)
+               return result;
+
+       /* see if we need to update/insert anything...
+        * use the ctime, so any status change will be visible (perms,
+        * filename etc.)*/
+       result = insert_or_update_maybe (fullpath, mdir, statbuf->st_ctime,
+                                        data, &updated);
+
+       if (result == MU_OK && data && data->_stats) {  /* update statistics */
+               ++data->_stats->_processed;
+               updated ? ++data->_stats->_updated : ++data->_stats->_uptodate;
+       }
+
+       return result;
+}
+
+static time_t
+get_dir_timestamp (const char *path)
+{
+       struct stat statbuf;
+
+       if (stat (path, &statbuf) != 0) {
+               g_warning ("failed to stat %s: %s",
+                          path, strerror(errno));
+               return 0;
+       }
+
+       return statbuf.st_ctime;
+}
+
+static MuError
+on_run_maildir_dir (const char* fullpath, gboolean enter,
+                   MuIndexCallbackData *data)
+{
+       GError *err;
+
+       err = NULL;
+
+       /* xapian stores a per-dir timestamp; we use this timestamp to determine
+        * whether a message is up-to-date
+        */
+       if (enter) {
+               data->_dirstamp =
+                       mu_store_get_dirstamp (data->_store, fullpath, &err);
+               /* in 'lazy' mode, we only check the dir timestamp, and if it's
+                * up to date, we don't bother with this dir. This fails to
+                * account for messages below this dir that have merely
+                * _changed_ though */
+               if (data->_lazy_check && mu_maildir_is_leaf_dir(fullpath)) {
+                       time_t dirstamp;
+                       dirstamp = get_dir_timestamp (fullpath);
+                       if (dirstamp <= data->_dirstamp) {
+                               g_debug ("ignore %s (up-to-date)", fullpath);
+                               return MU_IGNORE;
+                       }
+               }
+               g_debug ("entering %s", fullpath);
+       } else {
+               mu_store_set_dirstamp (data->_store, fullpath,
+                                       time(NULL), &err);
+               g_debug ("leaving %s", fullpath);
+       }
+
+       if (data->_idx_dir_cb)
+               return data->_idx_dir_cb (fullpath, enter,
+                                         data->_user_data);
+
+       if (err) {
+               MU_WRITE_LOG ("%s: %s", __func__, err->message);
+               g_clear_error(&err);
+       }
+
+       return MU_OK;
+}
+
+static gboolean
+check_path (const char *path)
+{
+       g_return_val_if_fail (path, FALSE);
+
+       if (!g_path_is_absolute (path)) {
+               g_warning ("%s: not an absolute path: '%s'", __func__, path);
+               return FALSE;
+       }
+
+       if (access (path, R_OK) != 0) {
+               g_warning ("%s: cannot open '%s': %s",
+                          __func__, path, strerror (errno));
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static void
+init_cb_data (MuIndexCallbackData *cb_data, MuStore  *xapian,
+             gboolean reindex, gboolean lazycheck,
+             guint max_filesize, MuIndexStats *stats,
+             MuIndexMsgCallback msg_cb, MuIndexDirCallback dir_cb,
+             void *user_data)
+{
+       cb_data->_idx_msg_cb    = msg_cb;
+       cb_data->_idx_dir_cb    = dir_cb;
+
+       cb_data->_user_data     = user_data;
+       cb_data->_store         = xapian;
+
+       cb_data->_reindex      = reindex;
+       cb_data->_lazy_check   = lazycheck;
+       cb_data->_dirstamp     = 0;
+       cb_data->_max_filesize = max_filesize;
+
+       cb_data->_stats         = stats;
+       if (cb_data->_stats)
+               memset (cb_data->_stats, 0, sizeof(MuIndexStats));
+}
+
+
+void
+mu_index_set_max_msg_size (MuIndex *index, guint max_size)
+{
+       g_return_if_fail (index);
+
+       if (max_size == 0)
+               index->_max_filesize = MU_INDEX_MAX_FILE_SIZE;
+       else
+               index->_max_filesize = max_size;
+}
+
+
+MuError
+mu_index_run (MuIndex *index,  gboolean reindex, gboolean lazycheck,
+             MuIndexStats *stats,
+             MuIndexMsgCallback msg_cb, MuIndexDirCallback dir_cb,
+             void *user_data)
+{
+       MuIndexCallbackData      cb_data;
+       MuError                  rv;
+       const char              *path;
+
+       g_return_val_if_fail (index && index->_store, MU_ERROR);
+       g_return_val_if_fail (msg_cb, MU_ERROR);
+
+       path = mu_store_root_maildir (index->_store);
+       if (!check_path (path))
+               return MU_ERROR;
+
+       if (index->_needs_reindex)
+                reindex = TRUE;
+
+       init_cb_data (&cb_data, index->_store, reindex, lazycheck,
+                     index->_max_filesize, stats,
+                     msg_cb, dir_cb, user_data);
+
+       rv = mu_maildir_walk (path,
+                             (MuMaildirWalkMsgCallback)on_run_maildir_msg,
+                             (MuMaildirWalkDirCallback)on_run_maildir_dir,
+                             reindex, /* re-index, ie. do a full update */
+                             &cb_data);
+
+       mu_store_flush (index->_store);
+
+       return rv;
+}
+
+static MuError
+on_stats_maildir_file (const char *fullpath, const char *mdir,
+                      struct stat *statbuf,
+                      MuIndexCallbackData *cb_data)
+{
+       MuError result;
+
+       if (cb_data && cb_data->_idx_msg_cb)
+               result = cb_data->_idx_msg_cb (cb_data->_stats,
+                                              cb_data->_user_data);
+       else
+               result = MU_OK;
+
+       if (result == MU_OK) {
+               if (cb_data->_stats)
+                       ++cb_data->_stats->_processed;
+               return MU_OK;
+       }
+
+       return result; /* MU_STOP or MU_OK */
+}
+
+
+MuError
+mu_index_stats (MuIndex *index,
+               MuIndexStats *stats, MuIndexMsgCallback cb_msg,
+               MuIndexDirCallback cb_dir, void *user_data)
+{
+       const char              *path;
+       MuIndexCallbackData      cb_data;
+
+       g_return_val_if_fail (index, MU_ERROR);
+       g_return_val_if_fail (cb_msg, MU_ERROR);
+
+       path = mu_store_root_maildir (index->_store);
+       if (!check_path (path))
+               return MU_ERROR;
+
+       if (stats)
+               memset (stats, 0, sizeof(MuIndexStats));
+
+       cb_data._idx_msg_cb = cb_msg;
+       cb_data._idx_dir_cb = cb_dir;
+
+       cb_data._stats     = stats;
+       cb_data._user_data = user_data;
+
+       cb_data._dirstamp = 0;
+
+       return mu_maildir_walk (path,
+                               (MuMaildirWalkMsgCallback)on_stats_maildir_file,
+                               NULL, FALSE, &cb_data);
+}
+
+struct _CleanupData {
+       MuStore                         *_store;
+       MuIndexStats                    *_stats;
+       MuIndexCleanupDeleteCallback     _cb;
+       void                            *_user_data;
+
+};
+typedef struct _CleanupData CleanupData;
+
+
+static MuError
+foreach_doc_cb (const char* path, CleanupData *cudata)
+{
+       if (access (path, R_OK) != 0) {
+               if (errno != EACCES)
+                       g_debug ("cannot access %s: %s", path, strerror(errno));
+               if (!mu_store_remove_path (cudata->_store, path))
+                       return MU_ERROR; /* something went wrong... bail out */
+               if (cudata->_stats)
+                       ++cudata->_stats->_cleaned_up;
+       }
+
+       if (cudata->_stats)
+               ++cudata->_stats->_processed;
+
+       if (!cudata->_cb)
+               return MU_OK;
+
+       return cudata->_cb (cudata->_stats, cudata->_user_data);
+}
+
+
+MuError
+mu_index_cleanup (MuIndex *index, MuIndexStats *stats,
+                 MuIndexCleanupDeleteCallback cb,
+                 void *user_data, GError **err)
+{
+       MuError         rv;
+       CleanupData     cudata;
+
+       g_return_val_if_fail (index, MU_ERROR);
+
+       cudata._store     = index->_store;
+       cudata._stats     = stats;
+       cudata._cb        = cb;
+       cudata._user_data = user_data;
+
+       rv = mu_store_foreach (index->_store,
+                              (MuStoreForeachFunc)foreach_doc_cb,
+                              &cudata, err);
+
+       mu_store_flush (index->_store);
+
+       return rv;
+}
+
+gboolean
+mu_index_stats_clear (MuIndexStats *stats)
+{
+       if (!stats)
+               return FALSE;
+
+       memset (stats, 0, sizeof(MuIndexStats));
+       return TRUE;
+}
diff --git a/lib/mu-index.h b/lib/mu-index.h
new file mode 100644 (file)
index 0000000..c0faba8
--- /dev/null
@@ -0,0 +1,193 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_INDEX_H__
+#define __MU_INDEX_H__
+
+#include <stdlib.h>
+#include <glib.h>
+#include <utils/mu-util.h>
+#include <mu-store.hh>
+
+G_BEGIN_DECLS
+
+/* opaque structure */
+struct _MuIndex;
+typedef struct _MuIndex MuIndex;
+
+struct _MuIndexStats {
+       unsigned _processed;     /* number of msgs processed or counted */
+       unsigned _updated;       /* number of msgs new or updated */
+       unsigned _cleaned_up;    /* number of msgs cleaned up */
+       unsigned _uptodate;      /* number of msgs already up-to-date */
+};
+typedef struct _MuIndexStats MuIndexStats;
+
+/**
+ * create a new MuIndex instance. NOTE: the database does not have
+ * to exist yet, but the directory must already exist; NOTE(2): before
+ * doing anything with the returned Index object, make sure you haved
+ * called mu_msg_init somewhere in your code.
+ *
+ * @param store a writable MuStore object
+ * @param err to receive error or NULL; there are only errors when this
+ * function returns NULL. Possible errors: see mu-error.h
+ *
+ * @return a new MuIndex instance, or NULL in case of error
+ */
+MuIndex* mu_index_new (MuStore *store, GError **err)
+    G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * destroy the index instance
+ *
+ * @param index a MuIndex instance, or NULL
+ */
+void mu_index_destroy (MuIndex *index);
+
+
+/**
+ * change the maximum file size that mu-index considers from its
+ * default (MU_INDEX_MAX_FILE_SIZE). Note that the maximum size is a
+ * protection against mu (or the libraries it uses) allocating too
+ * much memory, which can lead to problems
+ *
+ * @param index a mu index object
+ * @param max_size the maximum msg size, or 0 to reset to the default
+ */
+void mu_index_set_max_msg_size (MuIndex *index, guint max_size);
+
+
+/**
+ * callback function for mu_index_(run|stats|cleanup), for each message
+ *
+ * @param stats pointer to structure to receive statistics data
+ * @param user_data pointer to user data
+ *
+ * @return  MU_OK to continue, MU_STOP to stop, or MU_ERROR in
+ * case of some error.
+ */
+typedef MuError (*MuIndexMsgCallback) (MuIndexStats* stats, void *user_data);
+
+
+/**
+ * callback function for mu_index_(run|stats|cleanup), for each dir enter/leave
+ *
+ * @param path dirpath we just entered / left
+ * @param enter did we enter (TRUE) or leave(FALSE) the dir?
+ * @param user_data pointer to user data
+ *
+ * @return  MU_OK to continue, MU_STOP to stopd or MU_ERROR in
+ * case of some error.
+ */
+typedef MuError (*MuIndexDirCallback) (const char* path, gboolean enter,
+                                      void *user_data);
+
+/**
+ * start the indexing process
+ *
+ * @param index a valid MuIndex instance
+ * @param force if != 0, force re-indexing already index messages; this is
+ *         obviously a lot slower than only indexing new/changed messages
+ * @param lazycheck whether ignore subdirectoryies that have up-to-date
+ * timestamps.
+ * @param stats a structure with some statistics about the results;
+ * note that this function does *not* reset the struct values to allow
+ * for cumulative stats from multiple calls. If needed, you can use
+ * @mu_index_stats_clear before calling this function
+ * @param cb_msg a callback function called for every msg indexed;
+ * @param cb_dir a callback function called for every dir entered/left or NULL
+ * @param user_data a user pointer that will be passed to the callback function
+ *
+ * @return MU_OK if the stats gathering was completed successfully,
+ * MU_STOP if the user stopped or MU_ERROR in
+ * case of some error.
+ */
+MuError mu_index_run (MuIndex *index, gboolean force,
+                     gboolean lazycheck, MuIndexStats *stats,
+                     MuIndexMsgCallback msg_cb,
+                     MuIndexDirCallback dir_cb, void *user_data);
+
+/**
+ * gather some statistics about the Maildir; this is usually much faster than
+ * mu_index_run, and can thus be used to provide some information to the user
+ * note though that the statistics may be different from the reality that
+ * mu_index_run sees, when there are updates in the Maildir
+ *
+ * @param index a valid MuIndex instance
+ * @param stats a structure with some statistics about the results;
+ * note that this function does *not* reset the struct values to allow
+ * for cumulative stats from multiple calls. If needed, you can use
+ * @mu_index_stats_clear before calling this function
+ * @param msg_cb a callback function which will be called for every msg;
+ * @param dir_cb a callback function which will be called for every dir or NULL
+ * @param user_data a user pointer that will be passed to the callback function
+ * xb
+ * @return MU_OK if the stats gathering was completed successfully,
+ * MU_STOP if the user stopped or MU_ERROR in
+ * case of some error.
+ */
+MuError mu_index_stats (MuIndex *index, MuIndexStats *stats,
+                       MuIndexMsgCallback msg_cb, MuIndexDirCallback dir_cb,
+                       void *user_data);
+
+/**
+ * callback function called for each message
+ *
+ * @param MuIndexCleanupCallback
+ *
+ * @return a MuResult
+ */
+typedef MuError (*MuIndexCleanupDeleteCallback) (MuIndexStats *stats,
+                                                void *user_data);
+
+/**
+ * cleanup the database; ie. remove entries for which no longer a corresponding
+ * file exists in the maildir
+ *
+ * @param index a valid MuIndex instance
+ * @param stats a structure with some statistics about the results;
+ * note that this function does *not* reset the struct values to allow
+ * for cumulative stats from multiple calls. If needed, you can use
+ * @mu_index_stats_clear before calling this function
+ * @param cb a callback function which will be called for every msg;
+ * @param user_data a user pointer that will be passed to the callback function
+ * @param err to receive error info or NULL. err->code is MuError value
+ *
+ * @return MU_OK if the stats gathering was completed successfully,
+ * MU_STOP if the user stopped or MU_ERROR in
+ * case of some error.
+ */
+MuError mu_index_cleanup (MuIndex *index, MuIndexStats *stats,
+                         MuIndexCleanupDeleteCallback cb,
+                         void *user_data, GError **err);
+
+/**
+ * clear the stats structure
+ *
+ * @param stats a MuIndexStats object
+ *
+ * @return TRUE if stats != NULL, FALSE otherwise
+ */
+gboolean mu_index_stats_clear (MuIndexStats *stats);
+
+G_END_DECLS
+
+#endif /*__MU_INDEX_H__*/
diff --git a/lib/mu-maildir.c b/lib/mu-maildir.c
new file mode 100644 (file)
index 0000000..c8c74f4
--- /dev/null
@@ -0,0 +1,943 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** Copyright (C) 2008-2016 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.
+**
+*/
+
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#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 "mu-maildir.h"
+#include "utils/mu-str.h"
+
+#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
+ */
+#ifdef HAVE_STRUCT_DIRENT_D_TYPE
+#define GET_DTYPE(DE,FP)                                                  \
+       ((DE)->d_type == DT_UNKNOWN ? mu_util_get_dtype_with_lstat((FP)) : \
+        (DE)->d_type)
+#else
+#define GET_DTYPE(DE,FP)                                                  \
+       mu_util_get_dtype_with_lstat((FP))
+#endif /*HAVE_STRUCT_DIRENT_D_TYPE*/
+
+
+static gboolean
+create_maildir (const char *path, mode_t mode, GError **err)
+{
+       int i;
+       const gchar* subdirs[] = {"new", "cur", "tmp"};
+
+       for (i = 0; i != G_N_ELEMENTS(subdirs); ++i) {
+
+               const char *fullpath;
+               int rv;
+
+               /* static buffer */
+               fullpath = mu_str_fullpath_s (path, subdirs[i]);
+
+               /* if subdir already exists, don't try to re-create
+                * it */
+               if (mu_util_check_dir (fullpath, TRUE, TRUE))
+                       continue;
+
+               rv = g_mkdir_with_parents (fullpath, (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, TRUE, TRUE))
+                       return mu_util_g_set_error
+                               (err,MU_ERROR_FILE_CANNOT_MKDIR,
+                                "creating dir failed for %s: %s",
+                                fullpath, strerror (errno));
+       }
+
+       return TRUE;
+}
+
+static gboolean
+create_noindex (const char *path, GError **err)
+{
+       /* create a noindex file if requested */
+       int fd;
+       const char *noindexpath;
+
+       /* static buffer */
+       noindexpath = mu_str_fullpath_s (path, MU_MAILDIR_NOINDEX_FILE);
+
+       fd = creat (noindexpath, 0644);
+
+       /* note, if the 'close' failed, creation may still have
+        * succeeded...*/
+       if (fd < 0 || close (fd) != 0)
+               return mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_CREATE,
+                                           "error in create_noindex: %s",
+                                           strerror (errno));
+       return TRUE;
+}
+
+gboolean
+mu_maildir_mkdir (const char* path, mode_t mode, gboolean noindex, GError **err)
+{
+       g_return_val_if_fail (path, FALSE);
+
+       MU_WRITE_LOG ("%s (%s, %o, %s)", __func__,
+                     path, mode, noindex ? "TRUE" : "FALSE");
+
+       if (!create_maildir (path, mode, err))
+               return FALSE;
+
+       if (noindex && !create_noindex (path, err))
+               return FALSE;
+
+       return TRUE;
+}
+
+/* determine whether the source message is in 'new' or in 'cur';
+ * we ignore messages in 'tmp' for obvious reasons */
+static gboolean
+check_subdir (const char *src, gboolean *in_cur, GError **err)
+{
+       gboolean rv;
+       gchar *srcpath;
+
+       srcpath = g_path_get_dirname (src);
+       *in_cur = FALSE;
+       rv = TRUE;
+
+       if (g_str_has_suffix (srcpath, "cur"))
+               *in_cur = TRUE;
+       else if (!g_str_has_suffix (srcpath, "new"))
+               rv = mu_util_g_set_error (err,
+                                         MU_ERROR_FILE_INVALID_SOURCE,
+                                         "invalid source message '%s'",
+                                         src);
+       g_free (srcpath);
+       return rv;
+}
+
+static gchar*
+get_target_fullpath (const char* src, const gchar *targetpath, GError **err)
+{
+       gchar *targetfullpath, *srcfile;
+       gboolean in_cur;
+
+       if (!check_subdir (src, &in_cur, err))
+               return NULL;
+
+       srcfile = g_path_get_basename (src);
+
+       /* 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)
+        */
+       targetfullpath = g_strdup_printf ("%s%c%s%c%u_%s",
+                                         targetpath,
+                                         G_DIR_SEPARATOR,
+                                         in_cur ? "cur" : "new",
+                                         G_DIR_SEPARATOR,
+                                         g_str_hash(src),
+                                         srcfile);
+       g_free (srcfile);
+
+       return targetfullpath;
+}
+
+
+gboolean
+mu_maildir_link (const char* src, const char *targetpath, GError **err)
+{
+       gchar *targetfullpath;
+       int rv;
+
+       g_return_val_if_fail (src, FALSE);
+       g_return_val_if_fail (targetpath, FALSE);
+
+       targetfullpath = get_target_fullpath (src, targetpath, err);
+       if (!targetfullpath)
+               return FALSE;
+
+       rv = symlink (src, targetfullpath);
+
+       if (rv != 0)
+               mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_LINK,
+                                    "error creating link %s => %s: %s",
+                                    targetfullpath, src, strerror (errno));
+       g_free (targetfullpath);
+
+       return rv == 0 ? TRUE: FALSE;
+}
+
+
+static MuError
+process_dir (const char* path, const gchar *mdir,
+            MuMaildirWalkMsgCallback msg_cb,
+            MuMaildirWalkDirCallback dir_cb, gboolean full,
+            void *data);
+
+static MuError
+process_file (const char* fullpath, const gchar* mdir,
+             MuMaildirWalkMsgCallback msg_cb, void *data)
+{
+       MuError result;
+       struct stat statbuf;
+
+       if (!msg_cb)
+               return MU_OK;
+
+       if (G_UNLIKELY(access(fullpath, R_OK) != 0)) {
+               g_warning ("cannot access %s: %s", fullpath,
+                          strerror(errno));
+               return MU_ERROR;
+       }
+
+       if (G_UNLIKELY(stat (fullpath, &statbuf) != 0)) {
+               g_warning ("cannot stat %s: %s", fullpath, strerror(errno));
+               return MU_ERROR;
+       }
+
+       result = (msg_cb)(fullpath, mdir, &statbuf, data);
+       if (result == MU_STOP)
+               g_debug ("callback said 'MU_STOP' for %s", fullpath);
+       else if (result == MU_ERROR)
+               g_warning ("%s: error in callback (%s)",
+                          __func__, fullpath);
+
+       return result;
+}
+
+
+/*
+ * determine if path is a maildir leaf-dir; ie. if it's 'cur' or 'new'
+ * (we're skipping 'tmp' for obvious reasons)
+ */
+gboolean
+mu_maildir_is_leaf_dir (const char *path)
+{
+       size_t len;
+
+       /* path is the full path; it cannot possibly be shorter
+        * than 4 for a maildir (/cur or /new) */
+       len = path ? strlen (path) : 0;
+       if (G_UNLIKELY(len < 4))
+               return FALSE;
+
+       /* optimization; one further idea would be cast the 4 bytes to an
+        * integer and compare that -- need to think about alignment,
+        * endianness */
+
+       if (path[len - 4] == G_DIR_SEPARATOR &&
+           path[len - 3] == 'c' &&
+           path[len - 2] == 'u' &&
+           path[len - 1] == 'r')
+               return TRUE;
+
+       if (path[len - 4] == G_DIR_SEPARATOR &&
+           path[len - 3] == 'n' &&
+           path[len - 2] == 'e' &&
+           path[len - 1] == 'w')
+               return TRUE;
+
+       return FALSE;
+}
+
+
+/* check if there path contains file; used for checking if there is
+ * MU_MAILDIR_NOINDEX_FILE or MU_MAILDIR_NOUPDATE_FILE in this
+ * dir; */
+static gboolean
+dir_contains_file (const char *path, const char *file)
+{
+       const char* fullpath;
+
+       /* static buffer */
+       fullpath = mu_str_fullpath_s (path, file);
+
+       if (access (fullpath, F_OK) == 0)
+               return TRUE;
+       else if (G_UNLIKELY(errno != ENOENT && errno != EACCES))
+               g_warning ("error testing for %s/%s: %s",
+                          fullpath, file, strerror(errno));
+       return FALSE;
+}
+
+static gboolean
+is_dotdir_to_ignore (const char* dir)
+{
+       int i;
+       const char* ignore[] = {
+               ".notmuch",
+               ".nnmaildir",
+               ".#evolution"
+       }; /* when adding names, check the optimization below */
+
+       if (dir[0] != '.')
+               return FALSE; /* not a dotdir */
+
+       if (dir[1] == '\0' || (dir[1] == '.' && dir[2] == '\0'))
+               return TRUE; /* ignore '.' and '..' */
+
+       /* optimization: special dirs have 'n' or '#' in pos 1 */
+       if (dir[1] != 'n' && dir[1] != '#')
+               return FALSE; /* not special: don't ignore */
+
+       for (i = 0; i != G_N_ELEMENTS(ignore); ++i)
+               if (strcmp(dir, ignore[i]) == 0)
+                       return TRUE;
+
+       return FALSE; /* don't ignore */
+}
+
+static gboolean
+ignore_dir_entry (struct dirent *entry, unsigned char d_type)
+{
+       if (G_LIKELY(d_type == DT_REG)) {
+
+               guint u;
+
+               /* ignore emacs tempfiles */
+               if (entry->d_name[0] == '#')
+                       return TRUE;
+               /* ignore dovecot metadata */
+               if (entry->d_name[0] == 'd' &&
+                   strncmp (entry->d_name, "dovecot", 7) == 0)
+                       return TRUE;
+               /* ignore special files */
+               if (entry->d_name[0] == '.')
+                       return TRUE;
+               /* ignore core files */
+               if (entry->d_name[0] == 'c' &&
+                   strncmp (entry->d_name, "core", 4) == 0)
+                       return TRUE;
+               /* ignore tmp/backup files; find the last char */
+               for (u = 0; entry->d_name[u] != '\0'; ++u) {
+                       switch (entry->d_name[u]) {
+                       case '#':
+                       case '~':
+                               /* looks like a backup / tempsave file */
+                               if (entry->d_name[u + 1] == '\0')
+                                       return TRUE;
+                               continue;
+                       default:
+                               continue;
+                       }
+               }
+               return FALSE; /* other files: don't ignore */
+
+       } else if (d_type == DT_DIR)
+               return is_dotdir_to_ignore (entry->d_name);
+       else
+               return TRUE; /* ignore non-normal files, non-dirs */
+}
+
+/*
+ * return the maildir value for the the path - this is the directory
+ * for the message (with the top-level dir as "/"), and without the
+ * leaf "/cur" or "/new". In other words, contatenate old_mdir + "/" + dir,
+ * unless dir is either 'new' or 'cur'. The value will be used in queries.
+ */
+static gchar*
+get_mdir_for_path (const gchar *old_mdir, const gchar *dir)
+{
+       /* if the current dir is not 'new' or 'cur', contatenate
+        * old_mdir an dir */
+       if ((dir[0] == 'n' && strcmp(dir, "new") == 0) ||
+           (dir[0] == 'c' && strcmp(dir, "cur") == 0) ||
+           (dir[0] == 't' && strcmp(dir, "tmp") == 0))
+               return strdup (old_mdir ? old_mdir : G_DIR_SEPARATOR_S);
+       else
+               return g_strconcat (old_mdir ? old_mdir : "",
+                                   G_DIR_SEPARATOR_S, dir, NULL);
+
+}
+
+
+static MuError
+process_dir_entry (const char* path, const char* mdir, struct dirent *entry,
+                  MuMaildirWalkMsgCallback cb_msg,
+                  MuMaildirWalkDirCallback cb_dir,
+                  gboolean full, void *data)
+{
+       const char *fp;
+       char* fullpath;
+       unsigned char d_type;
+
+       /* we have to copy the buffer from fullpath_s, because it
+        * returns a static buffer, and we maybe called reentrantly */
+       fp = mu_str_fullpath_s (path, entry->d_name);
+       fullpath = g_newa (char, strlen(fp) + 1);
+       strcpy (fullpath, fp);
+
+       d_type = GET_DTYPE(entry, fullpath);
+
+       /* ignore special files/dirs */
+       if (ignore_dir_entry (entry, d_type)) {
+               /* g_debug ("ignoring %s\n", entry->d_name); */
+               return MU_OK;
+       }
+
+       switch (d_type) {
+       case DT_REG: /* we only want files in cur/ and new/ */
+               if (!mu_maildir_is_leaf_dir (path))
+                       return MU_OK;
+
+               return process_file (fullpath, mdir, cb_msg, data);
+
+       case DT_DIR: {
+               char *my_mdir;
+               MuError rv;
+               /* my_mdir is the search maildir (the dir starting
+                * with the top-level maildir as /, and without the
+                * /tmp, /cur, /new  */
+               my_mdir = get_mdir_for_path (mdir, entry->d_name);
+               rv = process_dir (fullpath, my_mdir, cb_msg, cb_dir, full, data);
+               g_free (my_mdir);
+
+               return rv;
+       }
+
+       default:
+               return MU_OK; /* ignore other types */
+       }
+}
+
+
+static const size_t DIRENT_ALLOC_SIZE =
+       offsetof (struct dirent, d_name) + PATH_MAX;
+
+static struct dirent*
+dirent_new (void)
+{
+       return (struct dirent*) g_slice_alloc (DIRENT_ALLOC_SIZE);
+}
+
+
+static void
+dirent_destroy (struct dirent *entry)
+{
+       g_slice_free1 (DIRENT_ALLOC_SIZE, entry);
+}
+
+#ifdef HAVE_STRUCT_DIRENT_D_INO
+static int
+dirent_cmp (struct dirent *d1, struct dirent *d2)
+{
+       /* we do it his way instead of a simple d1->d_ino - d2->d_ino
+        * because this way, we don't need 64-bit numbers for the
+        * actual sorting */
+       if (d1->d_ino < d2->d_ino)
+               return -1;
+       else if (d1->d_ino > d2->d_ino)
+               return 1;
+       else
+               return 0;
+}
+#endif /*HAVE_STRUCT_DIRENT_D_INO*/
+
+static MuError
+process_dir_entries (DIR *dir, const char* path, const char* mdir,
+                    MuMaildirWalkMsgCallback msg_cb,
+                    MuMaildirWalkDirCallback dir_cb,
+                    gboolean full, void *data)
+{
+       MuError result;
+       GSList *lst, *c;
+
+       for (lst = NULL;;) {
+               int rv;
+               struct dirent *entry, *res;
+               entry = dirent_new ();
+               rv = readdir_r (dir, entry, &res);
+               if (rv == 0) {
+                       if (res)
+                               lst = g_slist_prepend (lst, entry);
+                       else {
+                               dirent_destroy (entry);
+                               break; /* last direntry reached */
+                       }
+               } else {
+                       dirent_destroy (entry);
+                       g_warning ("error scanning dir: %s", strerror(rv));
+                       return MU_ERROR_FILE;
+               }
+       }
+
+       /* we sort by inode; this makes things much faster on
+        * extfs2,3 */
+#if HAVE_STRUCT_DIRENT_D_INO
+       c = lst = g_slist_sort (lst, (GCompareFunc)dirent_cmp);
+#endif /*HAVE_STRUCT_DIRENT_D_INO*/
+
+       for (c = lst, result = MU_OK; c && result == MU_OK; c = g_slist_next(c))
+               result = process_dir_entry (path, mdir, (struct dirent*)c->data,
+                                           msg_cb, dir_cb, full, data);
+
+       g_slist_foreach (lst, (GFunc)dirent_destroy, NULL);
+       g_slist_free (lst);
+
+       return result;
+}
+
+
+static MuError
+process_dir (const char* path, const char* mdir,
+            MuMaildirWalkMsgCallback msg_cb, MuMaildirWalkDirCallback dir_cb,
+            gboolean full, void *data)
+{
+       MuError result;
+       DIR*    dir;
+
+       /* if it has a noindex file, we ignore this dir */
+       if (dir_contains_file (path, MU_MAILDIR_NOINDEX_FILE) ||
+           (!full && dir_contains_file (path, MU_MAILDIR_NOUPDATE_FILE))) {
+               g_debug ("found noindex/noupdate: ignoring dir %s", path);
+               return MU_OK;
+       }
+
+       if (dir_cb) {
+               MuError rv;
+               rv = dir_cb (path, TRUE/*enter*/, data);
+               /* ignore this dir; not necessarily an _error_, dir might
+                * be up-to-date and return MU_IGNORE */
+               if (rv == MU_IGNORE)
+                       return MU_OK;
+               else if (rv != MU_OK)
+                       return rv;
+       }
+
+       dir = opendir (path);
+       if (!dir) {
+               g_warning ("cannot access %s: %s", path, strerror(errno));
+               return MU_OK;
+       }
+
+       result = process_dir_entries (dir, path, mdir, msg_cb, dir_cb,
+                                     full, data);
+       closedir (dir);
+
+       /* only run dir_cb if it exists and so far, things went ok */
+       if (dir_cb && result == MU_OK)
+               return dir_cb (path, FALSE/*leave*/, data);
+
+       return result;
+}
+
+
+MuError
+mu_maildir_walk (const char *path, MuMaildirWalkMsgCallback cb_msg,
+                MuMaildirWalkDirCallback cb_dir, gboolean full,
+                void *data)
+{
+       MuError rv;
+       char *mypath;
+
+       g_return_val_if_fail (path && cb_msg, MU_ERROR);
+       g_return_val_if_fail (mu_util_check_dir(path, TRUE, FALSE), MU_ERROR);
+
+       /* strip the final / or \ */
+       mypath = g_strdup (path);
+       if (mypath[strlen(mypath)-1] == G_DIR_SEPARATOR)
+               mypath[strlen(mypath)-1] = '\0';
+
+       rv = process_dir (mypath, NULL, cb_msg, cb_dir, full, data);
+       g_free (mypath);
+
+       return rv;
+}
+
+
+static gboolean
+clear_links (const char *path, DIR *dir)
+{
+       gboolean         rv;
+       struct dirent   *dentry;
+
+       rv    = TRUE;
+       errno = 0;
+
+       while ((dentry = readdir (dir))) {
+
+               guint8   d_type;
+               char    *fullpath;
+
+               if (dentry->d_name[0] == '.')
+                       continue; /* ignore .,.. other dotdirs */
+
+               fullpath = g_build_path ("/", path, dentry->d_name, NULL);
+               d_type   = GET_DTYPE (dentry, fullpath);
+
+               if (d_type == DT_LNK) {
+                       if (unlink (fullpath) != 0 ) {
+                               g_warning ("error unlinking %s: %s",
+                                          fullpath, strerror(errno));
+                               rv = FALSE;
+                       }
+               } else if (d_type == DT_DIR) {
+                       DIR *subdir;
+                       subdir = opendir (fullpath);
+                       if (!subdir) {
+                               g_warning ("failed to open dir %s: %s",
+                                          fullpath, strerror(errno));
+                               rv = FALSE;
+                               goto next;
+                       }
+
+                       if (!clear_links (fullpath, subdir))
+                               rv = FALSE;
+
+                       closedir (subdir);
+               }
+
+       next:
+               g_free (fullpath);
+       }
+
+       return rv;
+}
+
+gboolean
+mu_maildir_clear_links (const char *path, GError **err)
+{
+       DIR             *dir;
+       gboolean         rv;
+
+       g_return_val_if_fail (path, FALSE);
+
+       dir = opendir (path);
+       if (!dir) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE_CANNOT_OPEN,
+                            "failed to open %s: %s", path, strerror(errno));
+               return FALSE;
+       }
+
+       rv = clear_links (path, dir);
+
+       closedir (dir);
+
+       return rv;
+}
+
+
+
+
+MuFlags
+mu_maildir_get_flags_from_path (const char *path)
+{
+       g_return_val_if_fail (path, MU_FLAG_INVALID);
+
+       /* try to find the info part */
+       /* note that we can use either the ':' or '!' as separator;
+        * the former is the official, but as it does not work on e.g. VFAT
+        * file systems, some Maildir implementations use the latter instead
+        * (or both). For example, Tinymail/modest does this. The python
+        * documentation at http://docs.python.org/lib/mailbox-maildir.html
+        * mentions the '!' as well as a 'popular choice'
+        */
+
+       /* we check the dir -- */
+       if (strstr (path, G_DIR_SEPARATOR_S "new" G_DIR_SEPARATOR_S)) {
+
+               char *dir, *dir2;
+               MuFlags flags;
+
+               dir  = g_path_get_dirname (path);
+               dir2 = g_path_get_basename (dir);
+
+               flags = MU_FLAG_NONE;
+
+               if (g_strcmp0 (dir2, "new") == 0)
+                       flags = MU_FLAG_NEW;
+
+               g_free (dir);
+               g_free (dir2);
+
+               /* NOTE: new/ message should not have :2,-stuff, as
+                * per http://cr.yp.to/proto/maildir.html. If they, do
+                * we ignore it
+                */
+               if (flags == MU_FLAG_NEW)
+                       return flags;
+       }
+
+       /*  get the file flags */
+       {
+               char *info;
+
+               info = strrchr (path, '2');
+               if (!info || info == path ||
+                   (info[-1] != ':' && info[-1] != '!') ||
+                   (info[1] != ','))
+                       return MU_FLAG_NONE;
+               else
+                       return mu_flags_from_str
+                               (&info[2], MU_FLAG_TYPE_MAILFILE,
+                                TRUE /*ignore invalid */);
+       }
+}
+
+
+/*
+ * take an existing message path, and return a new path, based on
+ * whether it should be in 'new' or 'cur'; ie.
+ *
+ * /home/user/Maildir/foo/bar/cur/abc:2,F  and flags == MU_FLAG_NEW
+ *     => /home/user/Maildir/foo/bar/new
+ * and
+ * /home/user/Maildir/foo/bar/new/abc  and flags == MU_FLAG_REPLIED
+ *    => /home/user/Maildir/foo/bar/cur
+ *
+ * so the difference is whether MU_FLAG_NEW is set or not; and in the
+ * latter case, no other flags are allowed.
+ *
+ */
+static gchar*
+get_new_path (const char *mdir, const char *mfile, MuFlags flags,
+             const char* custom_flags)
+{
+       if (flags & MU_FLAG_NEW)
+               return g_strdup_printf ("%s%cnew%c%s",
+                                       mdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR,
+                                       mfile);
+       else {
+               const char *flagstr;
+               flagstr = mu_flags_to_str_s (flags, MU_FLAG_TYPE_MAILFILE);
+
+               return g_strdup_printf ("%s%ccur%c%s:2,%s%s",
+                                       mdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR,
+                                       mfile, flagstr,
+                                       custom_flags ? custom_flags : "");
+       }
+}
+
+
+char*
+mu_maildir_get_maildir_from_path (const char* path)
+{
+       gchar *mdir;
+
+       /* determine the maildir */
+       mdir = g_path_get_dirname (path);
+       if (!g_str_has_suffix (mdir, "cur") &&
+           !g_str_has_suffix (mdir, "new")) {
+               g_warning ("%s: not a valid maildir path: %s",
+                          __func__, path);
+               g_free (mdir);
+               return NULL;
+       }
+
+       /* remove the 'cur' or 'new' */
+       mdir[strlen(mdir) - 4] = '\0';
+
+       return mdir;
+}
+
+
+static char*
+get_new_basename (void)
+{
+       return g_strdup_printf ("%u.%08x%08x.%s",
+                               (guint)time(NULL),
+                               g_random_int(),
+                               (gint32)g_get_monotonic_time (),
+                               g_get_host_name ());
+}
+
+
+char*
+mu_maildir_get_new_path (const char *oldpath, const char *new_mdir,
+                        MuFlags newflags, gboolean new_name)
+{
+       char *mfile, *mdir, *custom_flags, *newpath;
+
+       g_return_val_if_fail (oldpath, NULL);
+
+       mfile = newpath = custom_flags = NULL;
+
+       /* determine the maildir */
+       mdir = mu_maildir_get_maildir_from_path (oldpath);
+       if (!mdir)
+               return NULL;
+
+       if (new_name)
+               mfile = get_new_basename ();
+       else {
+               /* determine the name of the mailfile, stripped of its flags, as
+                * well as any custom (non-standard) flags */
+               char *cur;
+               mfile = g_path_get_basename (oldpath);
+               for (cur = &mfile[strlen(mfile)-1]; cur > mfile; --cur) {
+                       if ((*cur == ':' || *cur == '!') &&
+                           (cur[1] == '2' && cur[2] == ',')) {
+                               /* get the custom flags (if any) */
+                               custom_flags =
+                                       mu_flags_custom_from_str (cur + 3);
+                               cur[0] = '\0'; /* strip the flags */
+                               break;
+                       }
+               }
+       }
+
+       newpath = get_new_path (new_mdir ? new_mdir : mdir,
+                               mfile, newflags, custom_flags);
+       g_free (mfile);
+       g_free (mdir);
+       g_free (custom_flags);
+
+       return newpath;
+}
+
+
+static gint64
+get_file_size (const char* path)
+{
+       int             rv;
+       struct stat     statbuf;
+
+       rv = stat (path, &statbuf);
+       if (rv != 0) {
+               /* g_warning ("error: %s", strerror (errno)); */
+               return -1;
+       }
+
+       return (gint64)statbuf.st_size;
+}
+
+
+static gboolean
+msg_move_check_pre (const gchar *src, const gchar *dst, GError **err)
+{
+       gint size1, size2;
+
+       if (!g_path_is_absolute(src))
+               return mu_util_g_set_error
+                       (err, MU_ERROR_FILE,
+                        "source is not an absolute path: '%s'", src);
+
+       if (!g_path_is_absolute(dst))
+               return mu_util_g_set_error
+                       (err, MU_ERROR_FILE,
+                        "target is not an absolute path: '%s'", dst);
+
+       if (access (src, R_OK) != 0)
+               return mu_util_g_set_error (err, MU_ERROR_FILE,
+                                           "cannot read %s",  src);
+
+       if (access (dst, F_OK) != 0)
+               return TRUE;
+
+       /* target exist; we simply overwrite it, unless target has a different
+        * size. ignore the exceedingly rare case where have duplicate message
+        * file names with different content yet the same length. (md5 etc. is a
+        * bit slow) */
+       size1 = get_file_size (src);
+       size2 = get_file_size (dst);
+       if (size1 != size2)
+               return mu_util_g_set_error (err, MU_ERROR_FILE,
+                                           "%s already exists", dst);
+
+       return TRUE;
+}
+
+static gboolean
+msg_move_check_post (const char *src, const char *dst, GError **err)
+{
+       /* double check -- is the target really there? */
+       if (access (dst, F_OK) != 0)
+               return mu_util_g_set_error
+                       (err, MU_ERROR_FILE, "can't find target (%s)",  dst);
+
+       if (access (src, F_OK) == 0)
+               return mu_util_g_set_error
+                       (err, MU_ERROR_FILE, "source still there (%s)", src);
+
+       return TRUE;
+}
+
+
+static gboolean
+msg_move (const char* src, const char *dst, GError **err)
+{
+       if (!msg_move_check_pre (src, dst, err))
+               return FALSE;
+
+       if (rename (src, dst) != 0)
+               return mu_util_g_set_error
+                       (err, MU_ERROR_FILE,"error moving %s to %s", src, dst);
+
+       return msg_move_check_post (src, dst, err);
+}
+
+gchar*
+mu_maildir_move_message (const char* oldpath, const char* targetmdir,
+                        MuFlags newflags, gboolean ignore_dups,
+                        gboolean new_name, GError **err)
+{
+       char *newfullpath;
+       gboolean rv;
+       gboolean src_is_target;
+
+       g_return_val_if_fail (oldpath, FALSE);
+
+       newfullpath = mu_maildir_get_new_path (oldpath, targetmdir,
+                                              newflags, new_name);
+       if (!newfullpath) {
+               mu_util_g_set_error (err, MU_ERROR_FILE,
+                                    "failed to determine targetpath");
+               return NULL;
+       }
+
+       src_is_target = (g_strcmp0 (oldpath, newfullpath) == 0);
+
+       if (!ignore_dups && src_is_target) {
+               mu_util_g_set_error (err, MU_ERROR_FILE_TARGET_EQUALS_SOURCE,
+                                    "target equals source");
+               return NULL;
+       }
+
+       if (!src_is_target) {
+               rv = msg_move (oldpath, newfullpath, err);
+               if (!rv) {
+                       g_free (newfullpath);
+                       return NULL;
+               }
+       }
+
+       return newfullpath;
+}
diff --git a/lib/mu-maildir.h b/lib/mu-maildir.h
new file mode 100644 (file)
index 0000000..790c345
--- /dev/null
@@ -0,0 +1,223 @@
+/*
+** Copyright (C) 2008-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.
+**
+*/
+
+#ifndef __MU_MAILDIR_H__
+#define __MU_MAILDIR_H__
+
+#include <glib.h>
+#include <time.h>
+#include <sys/types.h>          /* for mode_t */
+#include <utils/mu-util.h>
+#include <mu-flags.h>
+
+
+G_BEGIN_DECLS
+
+/**
+ * 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')
+ * @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'
+ * @param err if function returns FALSE, receives error
+ * information. err may be NULL.
+ *
+ * @return TRUE if creation succeeded (or already existed), FALSE otherwise
+ */
+gboolean mu_maildir_mkdir (const char* path, mode_t mode, gboolean noindex,
+                          GError **err);
+
+
+/**
+ * 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 err if function returns FALSE, err may contain extra
+ * information. if err is NULL, does nothing
+ *
+ * @return
+ */
+gboolean mu_maildir_link   (const char* src, const char *targetpath,
+                           GError **err);
+
+/**
+ * MuMaildirWalkMsgCallback -- callback function for
+ * mu_path_walk_maildir; see the documentation there. It will be
+ * called for each message found, with fullpath containing the full
+ * path to the message, mdir containing the maildir -- that is, when
+ * indexing ~/Maildir, a message ~/Maildir/foo/bar/cur/msg would have
+ * the maildir "foo/bar". Then, the information from 'stat' of this
+ * file (see stat(3)), and a user_data pointer
+ */
+typedef MuError (*MuMaildirWalkMsgCallback)
+  (const char* fullpath, const char* mdir, struct stat *statinfo,
+   void *user_data);
+
+/**
+ * MuPathWalkDirCallback -- callback function for mu_path_walk_maildir; see the
+ * documentation there. It will be called each time a dir is entered or left,
+ * with 'enter' being TRUE upon entering, FALSE otherwise
+ */
+typedef MuError (*MuMaildirWalkDirCallback)
+     (const char* fullpath, gboolean enter, void *user_data);
+
+/**
+ * start a recursive walk of a maildir; for each file found, we call
+ * callback with the path (with the Maildir path of scanner_new as
+ * root), the filename, the timestamp (mtime) of the file,and the
+ * *data pointer, for user data.  dot-files are ignored, as well as
+ * files outside cur/ and new/ dirs and unreadable files; however,
+ * dotdirs are visited (ie. '.dotdir/cur'), so this enables Maildir++.
+ * (http://www.inter7.com/courierimap/README.maildirquota.html, search
+ * for 'Mission statement'). In addition, dirs containing a file named
+ * '.noindex' are ignored, as are their subdirectories, and dirs
+ * containing a file called '.noupdate' are ignored, unless @param
+ * full is TRUE.
+ *
+ * mu_walk_maildir stops if the callbacks return something different
+ * from MU_OK. For example, it can return MU_STOP to stop the scan, or
+ * some error.
+ *
+ * @param path the maildir path to scan
+ * @param cb_msg the callback function called for each msg
+ * @param cb_dir the callback function called for each dir
+ * @param full whether do a full scan, i.e., to ignore .noupdate files
+ * @param data user data pointer
+ *
+ * @return a scanner result; MU_OK if everything went ok,
+ * MU_STOP if we want to stop, or MU_ERROR in
+ * case of error
+ */
+MuError mu_maildir_walk (const char *path, MuMaildirWalkMsgCallback cb_msg,
+                        MuMaildirWalkDirCallback cb_dir, gboolean full,
+                        void *data);
+/**
+ * recursively delete all the symbolic links in a directory tree
+ *
+ * @param dir top dir
+ * @param err if function returns FALSE, err may contain extra
+ * information. if err is NULL, does nothing
+ *
+ * @return TRUE if it worked, FALSE in case of error
+ */
+gboolean mu_maildir_clear_links (const gchar* dir, GError **err);
+
+
+
+/**
+ * whether the directory path ends in '/cur/' or '/new/'
+ *
+ * @param path some path
+ */
+gboolean mu_maildir_is_leaf_dir (const char *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
+ * MU_MSG_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 flags, or MU_MSG_FILE_FLAG_UNKNOWN in case of error
+ */
+MuFlags mu_maildir_get_flags_from_path (const char* pathname);
+
+/**
+ * get the new pathname for a message, based on the old path and the
+ * new flags and (optionally) a new maildir. Note that
+ * setting/removing the MU_FLAG_NEW will change the directory in which
+ * a message lives. The flags are as specified in
+ * http://cr.yp.to/proto/maildir.html, plus MU_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 oldpath the old (current) full path to the message
+ * (including the filename)
+ * @param new_mdir the new maildir for this message, or NULL to keep
+ * it in the current one. The maildir is the absolute file system
+ * path, without the 'cur' or 'new'
+ * @param new_flags the new flags for this message
+ * @param new_name whether to create a new unique name, or keep the
+ * old one
+ *
+ * @return a new path name; use g_free when done with. NULL in case of
+ * error.
+ */
+char* mu_maildir_get_new_path (const char *oldpath, const char *new_mdir,
+                              MuFlags new_flags, gboolean new_name)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * get the maildir for a certain message path, ie, the path *before*
+ * cur/ or new/
+ *
+ * @param path path for some message
+ *
+ * @return the maildir (free with g_free), or NULL in case of error
+ */
+char* mu_maildir_get_maildir_from_path (const char* path)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * move a message file to another maildir; the function returns the full path to
+ * the new message. if the target file already exists, it is overwritten.
+ *
+ * @param msgpath an absolute file system path to an existing message in an
+ * actual maildir
+ * @param targetmdir 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. If you specify NULL for targetmdir, only the flags
+ * of the message are affected; note that this may still involve a
+ * moved to another directory (say, from new/ to cur/)
+ * @param flags to set for the target (influences the filename, path)
+ * @param ignore_dups whether to silently ignore the src=target case
+ * (and return TRUE)
+ * @param new_name whether to create a new unique name, or keep the
+ * old one
+ * @param err receives error information
+ *
+ * @return return the full path name of the target file (g_free) if
+ * the move succeeded, NULL otherwise
+ */
+gchar* mu_maildir_move_message (const char* oldpath, const char* targetmdir,
+                               MuFlags newflags, gboolean ignore_dups,
+                               gboolean new_name, GError **err)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+G_END_DECLS
+
+#endif /*__MU_MAILDIR_H__*/
diff --git a/lib/mu-msg-crypto.c b/lib/mu-msg-crypto.c
new file mode 100644 (file)
index 0000000..c7ca8ba
--- /dev/null
@@ -0,0 +1,374 @@
+/*
+** Copyright (C) 2012-2018 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 <string.h>
+
+#include "mu-msg.h"
+#include "mu-msg-priv.h"
+#include "mu-msg-part.h"
+#include "utils/mu-date.h"
+
+#include <gmime/gmime.h>
+#include <gmime/gmime-multipart-signed.h>
+
+
+static const char*
+get_pubkey_algo_name (GMimePubKeyAlgo algo)
+{
+       switch (algo) {
+       case GMIME_PUBKEY_ALGO_DEFAULT:
+               return "default";
+       case GMIME_PUBKEY_ALGO_RSA:
+               return "RSA";
+       case GMIME_PUBKEY_ALGO_RSA_E:
+               return "RSA (encryption only)";
+       case GMIME_PUBKEY_ALGO_RSA_S:
+               return "RSA (signing only)";
+       case GMIME_PUBKEY_ALGO_ELG_E:
+               return "ElGamal (encryption only)";
+       case GMIME_PUBKEY_ALGO_DSA:
+               return "DSA";
+       case GMIME_PUBKEY_ALGO_ELG:
+               return "ElGamal";
+       default:
+               return "unknown pubkey algorithm";
+       }
+}
+
+static const gchar*
+get_digestkey_algo_name (GMimeDigestAlgo algo)
+{
+       switch (algo) {
+       case GMIME_DIGEST_ALGO_DEFAULT:
+               return "default";
+       case GMIME_DIGEST_ALGO_MD5:
+               return "MD5";
+       case GMIME_DIGEST_ALGO_SHA1:
+               return "SHA-1";
+       case GMIME_DIGEST_ALGO_RIPEMD160:
+               return "RIPEMD160";
+       case GMIME_DIGEST_ALGO_MD2:
+               return "MD2";
+       case GMIME_DIGEST_ALGO_TIGER192:
+               return "TIGER-192";
+       case GMIME_DIGEST_ALGO_HAVAL5160:
+               return "HAVAL-5-160";
+       case GMIME_DIGEST_ALGO_SHA256:
+               return "SHA-256";
+       case GMIME_DIGEST_ALGO_SHA384:
+               return "SHA-384";
+       case GMIME_DIGEST_ALGO_SHA512:
+               return "SHA-512";
+       case GMIME_DIGEST_ALGO_SHA224:
+               return "SHA-224";
+       case GMIME_DIGEST_ALGO_MD4:
+               return "MD4";
+       default:
+               return "unknown digest algorithm";
+       }
+}
+
+
+/* get data from the 'certificate' */
+static char*
+get_cert_data (GMimeCertificate *cert)
+{
+       const char /**email,*/ *name, *digest_algo, *pubkey_algo,
+               *keyid, *trust;
+
+       /* email         =  g_mime_certificate_get_email (cert); */
+       name  = g_mime_certificate_get_name (cert);
+       keyid = g_mime_certificate_get_key_id (cert);
+
+       digest_algo  =  get_digestkey_algo_name
+               (g_mime_certificate_get_digest_algo (cert));
+       pubkey_algo  =  get_pubkey_algo_name
+               (g_mime_certificate_get_pubkey_algo (cert));
+
+       switch (g_mime_certificate_get_trust (cert)) {
+       case GMIME_TRUST_UNKNOWN:   trust = "unknown"; break;
+       case GMIME_TRUST_UNDEFINED: trust = "undefined"; break;
+       case GMIME_TRUST_NEVER:     trust = "never"; break;
+       case GMIME_TRUST_MARGINAL:  trust = "marginal"; break;
+       case GMIME_TRUST_FULL:      trust = "full"; break;
+       case GMIME_TRUST_ULTIMATE:  trust = "ultimate"; break;
+       default:
+               g_return_val_if_reached (NULL);
+       }
+
+       return g_strdup_printf (
+               "signer:%s, key:%s (%s,%s), trust:%s",
+               name ? name : "?",
+               /* email ? email : "?", */
+               keyid, pubkey_algo, digest_algo,
+               trust);
+}
+
+
+static char*
+get_signature_status (GMimeSignatureStatus status)
+{
+        size_t   n;
+        GString *descr;
+
+        struct {
+                GMimeSignatureStatus  status;
+                const char           *name;
+        } status_info[] = {
+                { GMIME_SIGNATURE_STATUS_VALID,         "valid" },
+                { GMIME_SIGNATURE_STATUS_GREEN,         "green" },
+                { GMIME_SIGNATURE_STATUS_RED,           "red" },
+                { GMIME_SIGNATURE_STATUS_KEY_REVOKED,   "key revoked" },
+                { GMIME_SIGNATURE_STATUS_KEY_EXPIRED,   "key expired" },
+                { GMIME_SIGNATURE_STATUS_SIG_EXPIRED,   "signature expired" },
+                { GMIME_SIGNATURE_STATUS_KEY_MISSING,   "key missing" },
+                { GMIME_SIGNATURE_STATUS_CRL_MISSING,   "crl missing" },
+                { GMIME_SIGNATURE_STATUS_CRL_TOO_OLD,   "crl too old" },
+                { GMIME_SIGNATURE_STATUS_BAD_POLICY,    "bad policy" },
+                { GMIME_SIGNATURE_STATUS_SYS_ERROR,     "system error" },
+                { GMIME_SIGNATURE_STATUS_TOFU_CONFLICT, "tofu conflict " },
+        };
+
+        descr = g_string_new("");
+        for (n = 0; n != G_N_ELEMENTS(status_info); ++n) {
+
+                if (!(status & status_info[n].status))
+                        continue;
+
+                g_string_append_printf (descr, "%s%s",
+                                        descr->len > 0 ? ", " : "",
+                                        status_info[n].name);
+        }
+
+        return g_string_free (descr, FALSE);
+}
+
+
+/* get a human-readable report about the signature */
+static char*
+get_verdict_report (GMimeSignature *msig)
+{
+       time_t                t;
+       const char           *created, *expires;
+       gchar                *certdata, *report, *status;
+       GMimeSignatureStatus  sigstat;
+
+       sigstat = g_mime_signature_get_status (msig);
+        status = get_signature_status(sigstat);
+
+       t = g_mime_signature_get_created (msig);
+       created = (t == 0 || t == (time_t)-1) ? "?" : mu_date_str_s ("%x", t);
+
+       t = g_mime_signature_get_expires (msig);
+       expires = (t == 0 || t == (time_t)-1) ? "?" : mu_date_str_s ("%x", t);
+
+       certdata = get_cert_data (g_mime_signature_get_certificate (msig));
+       report = g_strdup_printf ("%s; created:%s, expires:%s, %s",
+                                 status, created, expires,
+                                 certdata ? certdata : "?");
+       g_free (certdata);
+        g_free (status);
+
+       return report;
+}
+
+
+static char*
+get_signers (GHashTable *signerhash)
+{
+       GString         *gstr;
+       GHashTableIter   iter;
+       const char      *name;
+
+       if (!signerhash || g_hash_table_size(signerhash) == 0)
+               return NULL;
+
+       gstr = g_string_new (NULL);
+       g_hash_table_iter_init (&iter, signerhash);
+       while (g_hash_table_iter_next (&iter, (gpointer)&name, NULL)) {
+               if (gstr->len != 0)
+                       g_string_append_c (gstr, ',');
+               gstr = g_string_append (gstr, name);
+       }
+
+       return g_string_free (gstr, FALSE);
+}
+
+
+static MuMsgPartSigStatusReport*
+get_status_report (GMimeSignatureList *sigs)
+{
+       int                       i;
+       MuMsgPartSigStatus        status;
+       MuMsgPartSigStatusReport *status_report;
+       char                     *report;
+       GHashTable               *signerhash;
+
+       status     = MU_MSG_PART_SIG_STATUS_GOOD; /* let's start positive! */
+       signerhash = g_hash_table_new (g_str_hash, g_str_equal);
+
+       for (i = 0, report = NULL; i != g_mime_signature_list_length (sigs);
+            ++i) {
+
+               GMimeSignature          *msig;
+               GMimeCertificate        *cert;
+               GMimeSignatureStatus     sigstat;
+               gchar                   *rep;
+
+               msig = g_mime_signature_list_get_signature (sigs, i);
+               sigstat = g_mime_signature_get_status (msig);
+
+               /* downgrade our expectations */
+               if ((sigstat & GMIME_SIGNATURE_STATUS_ERROR_MASK) &&
+                   status != MU_MSG_PART_SIG_STATUS_ERROR)
+                       status = MU_MSG_PART_SIG_STATUS_ERROR;
+               else if ((sigstat & GMIME_SIGNATURE_STATUS_RED) &&
+                        status == MU_MSG_PART_SIG_STATUS_GOOD)
+                       status = MU_MSG_PART_SIG_STATUS_BAD;
+
+               rep  = get_verdict_report (msig);
+               report = g_strdup_printf ("%s%s%d: %s",
+                                         report ? report : "",
+                                         report ? "; " : "",  i + 1,
+                                         rep);
+               g_free (rep);
+
+               cert = g_mime_signature_get_certificate (msig);
+               if (cert && g_mime_certificate_get_name (cert))
+                       g_hash_table_add (
+                               signerhash,
+                               (gpointer)g_mime_certificate_get_name (cert));
+       }
+
+       status_report = g_slice_new0 (MuMsgPartSigStatusReport);
+
+       status_report->verdict = status;
+       status_report->report  = report;
+       status_report->signers = get_signers(signerhash);
+
+       g_hash_table_unref (signerhash);
+
+       return status_report;
+}
+
+void
+mu_msg_part_sig_status_report_destroy (MuMsgPartSigStatusReport *report)
+{
+       if (!report)
+               return;
+
+       g_free ((char*)report->report);
+       g_free ((char*)report->signers);
+
+       g_slice_free (MuMsgPartSigStatusReport, report);
+}
+
+
+static inline void
+tag_with_sig_status(GObject *part,
+                   MuMsgPartSigStatusReport *report)
+{
+       g_object_set_data_full
+               (part, SIG_STATUS_REPORT, report,
+                (GDestroyNotify)mu_msg_part_sig_status_report_destroy);
+}
+
+
+void
+mu_msg_crypto_verify_part (GMimeMultipartSigned *sig, MuMsgOptions opts,
+                          GError **err)
+{
+       /* the signature status */
+       MuMsgPartSigStatusReport *report;
+       GMimeSignatureList *sigs;
+
+       g_return_if_fail (GMIME_IS_MULTIPART_SIGNED(sig));
+
+       sigs = g_mime_multipart_signed_verify (sig, GMIME_VERIFY_NONE, err);
+       if (!sigs) {
+               if (err && !*err)
+                       mu_util_g_set_error (err, MU_ERROR_CRYPTO,
+                                            "verification failed");
+               return;
+       }
+
+       report = get_status_report (sigs);
+       g_clear_object (&sigs);
+
+       /* tag this part with the signature status check */
+       tag_with_sig_status(G_OBJECT(sig), report);
+}
+
+
+static inline void
+check_decrypt_result(GMimeMultipartEncrypted *part, GMimeDecryptResult *res,
+                    GError **err)
+{
+       GMimeSignatureList *sigs;
+       MuMsgPartSigStatusReport *report;
+
+       if (res) {
+               /* Check if the decrypted part had any embed signatures */
+               sigs = res->signatures;
+               if (sigs) {
+                       report = get_status_report (sigs);
+                       g_mime_signature_list_clear (sigs);
+
+                       /* tag this part with the signature status check */
+                       tag_with_sig_status(G_OBJECT(part), report);
+               }
+               else {
+                       if (err && !*err)
+                               mu_util_g_set_error (err, MU_ERROR_CRYPTO,
+                                                    "verification failed");
+               }
+               g_object_unref (res);
+       }
+
+}
+
+
+GMimeObject* /* this is declared in mu-msg-priv.h */
+mu_msg_crypto_decrypt_part (GMimeMultipartEncrypted *enc, MuMsgOptions opts,
+                           MuMsgPartPasswordFunc func, gpointer user_data,
+                           GError **err)
+{
+       GMimeObject *dec;
+       GMimeDecryptResult *res;
+
+       g_return_val_if_fail (GMIME_IS_MULTIPART_ENCRYPTED(enc), NULL);
+
+       res = NULL;
+       dec = g_mime_multipart_encrypted_decrypt (enc, GMIME_DECRYPT_NONE, NULL,
+                                                 &res, err);
+       check_decrypt_result(enc, res, err);
+
+       if (!dec) {
+               if (err && !*err)
+                       mu_util_g_set_error (err, MU_ERROR_CRYPTO,
+                                            "decryption failed");
+               return NULL;
+       }
+
+       return dec;
+}
diff --git a/lib/mu-msg-doc.cc b/lib/mu-msg-doc.cc
new file mode 100644 (file)
index 0000000..21c3c8b
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+** 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 <stdlib.h>
+#include <iostream>
+#include <string.h>
+#include <errno.h>
+#include <xapian.h>
+
+#include "mu-msg-fields.h"
+#include "mu-msg-doc.h"
+
+#include "utils/mu-util.h"
+#include "utils/mu-str.h"
+#include "utils/mu-date.h"
+#include "utils/mu-utils.hh"
+
+struct _MuMsgDoc {
+
+       _MuMsgDoc (Xapian::Document *doc): _doc (doc) { }
+       ~_MuMsgDoc () { delete _doc; }
+       const Xapian::Document doc() const { return *_doc; }
+private:
+       Xapian::Document *_doc;
+};
+
+
+MuMsgDoc*
+mu_msg_doc_new (XapianDocument *doc, GError **err)
+{
+       g_return_val_if_fail (doc, NULL);
+
+       try {
+               return new MuMsgDoc ((Xapian::Document*)doc);
+
+       } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN, NULL);
+
+       return FALSE;
+}
+
+void
+mu_msg_doc_destroy (MuMsgDoc *self)
+{
+       try {
+               delete self;
+
+       } MU_XAPIAN_CATCH_BLOCK;
+}
+
+
+gchar*
+mu_msg_doc_get_str_field (MuMsgDoc *self, MuMsgFieldId mfid)
+{
+       g_return_val_if_fail (self, NULL);
+       g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), NULL);
+
+       // disable this check:
+       //    g_return_val_if_fail (mu_msg_field_is_string(mfid), NULL);
+       // because it's useful to get numerical field as strings,
+       // for example when sorting (which is much faster if don't
+       // have to convert to numbers first, esp. when it's a date
+       // time_t)
+
+       try {
+               const std::string s (self->doc().get_value(mfid));
+               return s.empty() ? NULL : g_strdup (s.c_str());
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN(NULL);
+}
+
+
+GSList*
+mu_msg_doc_get_str_list_field (MuMsgDoc *self, MuMsgFieldId mfid)
+{
+       g_return_val_if_fail (self, NULL);
+       g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), NULL);
+       g_return_val_if_fail (mu_msg_field_is_string_list(mfid), NULL);
+
+       try {
+               /* return a comma-separated string as a GSList */
+               const std::string s (self->doc().get_value(mfid));
+               return s.empty() ? NULL : mu_str_to_list(s.c_str(),',',TRUE);
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN(NULL);
+}
+
+
+gint64
+mu_msg_doc_get_num_field (MuMsgDoc *self, MuMsgFieldId mfid)
+{
+       g_return_val_if_fail (self, -1);
+       g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), -1);
+       g_return_val_if_fail (mu_msg_field_is_numeric(mfid), -1);
+
+       try {
+               const std::string s (self->doc().get_value(mfid));
+               if (s.empty())
+                       return 0;
+               else if (mfid == MU_MSG_FIELD_ID_DATE ||
+                        mfid == MU_MSG_FIELD_ID_SIZE)
+                       return strtol (s.c_str(), NULL, 10);
+               else {
+                       return static_cast<gint64>
+                               (Xapian::sortable_unserialise(s));
+               }
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN(-1);
+}
diff --git a/lib/mu-msg-doc.h b/lib/mu-msg-doc.h
new file mode 100644 (file)
index 0000000..2f5fe5c
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_MSG_DOC_H__
+#define __MU_MSG_DOC_H__
+
+#include <glib.h>
+#include <utils/mu-util.h>
+
+G_BEGIN_DECLS
+
+struct _MuMsgDoc;
+typedef struct _MuMsgDoc MuMsgDoc;
+
+/**
+ * create a new MuMsgDoc instance
+ *
+ * @param doc a Xapian::Document* (you'll need to cast the
+ * Xapian::Document* to XapianDocument*, because only C (not C++) is
+ * allowed in this header file. MuMsgDoc takes _ownership_ of this pointer;
+ * don't touch it afterwards
+ * @param err receives error info, or NULL
+ *
+ * @return a new MuMsgDoc instance (free with mu_msg_doc_destroy), or
+ * NULL in case of error.
+ */
+MuMsgDoc* mu_msg_doc_new (XapianDocument *doc, GError **err)
+       G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * destroy a MuMsgDoc instance -- free all the resources. Note, after
+ * destroying, any strings returned from mu_msg_doc_get_str_field with
+ * do_free==FALSE are no longer valid
+ *
+ * @param self a MuMsgDoc instance
+ */
+void mu_msg_doc_destroy (MuMsgDoc *self);
+
+
+/**
+ * get a string parameter from the msgdoc
+ *
+ * @param self a MuMsgDoc instance
+ * @param mfid a MuMsgFieldId for a string field
+ *
+ * @return a string for the given field (see do_free), or NULL in case of error.
+ * free with g_free
+ */
+gchar* mu_msg_doc_get_str_field (MuMsgDoc *self, MuMsgFieldId mfid)
+          G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * get a string-list parameter from the msgdoc
+ *
+ * @param self a MuMsgDoc instance
+ * @param mfid a MuMsgFieldId for a string-list field
+ *
+ * @return a list for the given field (see do_free), or NULL in case
+ * of error. free with mu_str_free_list
+ */
+GSList* mu_msg_doc_get_str_list_field (MuMsgDoc *self, MuMsgFieldId mfid)
+    G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ *
+ * get a numeric parameter from the msgdoc
+ *
+ * @param self a MuMsgDoc instance
+ * @param mfid a MuMsgFieldId for a numeric field
+ *
+ * @return the numerical value, or -1 in case of error. You'll need to
+ * cast this value to the actual type (e.g. time_t for MU_MSG_FIELD_ID_DATE)
+ */
+gint64 mu_msg_doc_get_num_field (MuMsgDoc *self, MuMsgFieldId mfid);
+
+
+G_END_DECLS
+
+#endif /*__MU_MSG_DOC_H__*/
diff --git a/lib/mu-msg-fields.c b/lib/mu-msg-fields.c
new file mode 100644 (file)
index 0000000..daf3c2f
--- /dev/null
@@ -0,0 +1,425 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** 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 <string.h>
+#include "mu-msg-fields.h"
+
+/*
+ * 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)
+ */
+enum _FieldFlags {
+       FLAG_GMIME               = 1 << 0, /* field retrieved through
+                                           * gmime */
+       FLAG_XAPIAN_INDEX        = 1 << 1, /* field is indexed in
+                                           * xapian (i.e., the text
+                                           * is processed */
+       FLAG_XAPIAN_TERM         = 1 << 2, /* field stored as term in
+                                           * xapian (so it can be searched) */
+       FLAG_XAPIAN_VALUE        = 1 << 3, /* field stored as value in
+                                           * xapian (so the literal
+                                           * value can be
+                                           * retrieved) */
+       FLAG_XAPIAN_CONTACT      = 1 << 4, /* field contains one or more
+                                           * e-mail-addresses */
+       FLAG_XAPIAN_BOOLEAN      = 1 << 5, /* use 'add_boolean_prefix'
+                                           * for Xapian queries;
+                                           * wildcards do NOT WORK
+                                           * for such fields */
+       FLAG_DONT_CACHE          = 1 << 6,  /* don't cache this field in
+                                           * the MuMsg cache */
+       FLAG_RANGE_FIELD         = 1 << 7  /* whether this is a range field */
+
+};
+typedef enum _FieldFlags       FieldFlags;
+
+/*
+ * this struct describes the fields of an e-mail
+ /*/
+struct _MuMsgField {
+       MuMsgFieldId      _id;       /* the id of the field */
+       MuMsgFieldType    _type;     /* the type of the field */
+       const char       *_name;     /* the name of the field */
+       const char        _shortcut; /* the shortcut for use in
+                                     * --fields and sorting */
+       const char        _xprefix;  /* the Xapian-prefix  */
+       FieldFlags        _flags;    /* the flags that tells us
+                                     * what to do */
+};
+typedef struct _MuMsgField MuMsgField;
+
+/* the name and shortcut fields must be lower case, or they might be
+ * misinterpreted by the query-preprocesser which turns queries into
+ * lowercase */
+static const MuMsgField FIELD_DATA[] = {
+
+       {
+               MU_MSG_FIELD_ID_BCC,
+               MU_MSG_FIELD_TYPE_STRING,
+               "bcc" , 'h', 'H',  /* 'hidden */
+               FLAG_GMIME | FLAG_XAPIAN_CONTACT |
+               FLAG_XAPIAN_VALUE
+       },
+
+       {
+               MU_MSG_FIELD_ID_BODY_TEXT,
+               MU_MSG_FIELD_TYPE_STRING,
+               "body", 'b', 'B',
+               FLAG_GMIME | FLAG_XAPIAN_INDEX |
+               FLAG_DONT_CACHE
+       },
+
+       {
+               MU_MSG_FIELD_ID_BODY_HTML,
+               MU_MSG_FIELD_TYPE_STRING,
+               "bodyhtml", 0, 0,
+               FLAG_GMIME | FLAG_DONT_CACHE
+       },
+
+       {
+               MU_MSG_FIELD_ID_CC,
+               MU_MSG_FIELD_TYPE_STRING,
+               "cc", 'c', 'C',
+               FLAG_GMIME | FLAG_XAPIAN_CONTACT | FLAG_XAPIAN_VALUE
+       },
+
+       {
+               MU_MSG_FIELD_ID_DATE,
+               MU_MSG_FIELD_TYPE_TIME_T,
+               "date", 'd', 'D',
+               FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE |
+               FLAG_XAPIAN_BOOLEAN | FLAG_RANGE_FIELD
+       },
+
+       {
+               MU_MSG_FIELD_ID_EMBEDDED_TEXT,
+               MU_MSG_FIELD_TYPE_STRING,
+               "embed", 'e', 'E',
+               FLAG_GMIME | FLAG_XAPIAN_INDEX | FLAG_DONT_CACHE
+       },
+
+       {
+               MU_MSG_FIELD_ID_FILE,
+               MU_MSG_FIELD_TYPE_STRING,
+               "file" , 'j', 'J',
+               FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_DONT_CACHE
+       },
+
+
+       {
+               MU_MSG_FIELD_ID_FLAGS,
+               MU_MSG_FIELD_TYPE_INT,
+               "flag", 'g', 'G',  /* flaGs */
+               FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE
+       },
+
+       {
+               MU_MSG_FIELD_ID_FROM,
+               MU_MSG_FIELD_TYPE_STRING,
+               "from", 'f', 'F',
+               FLAG_GMIME | FLAG_XAPIAN_CONTACT | FLAG_XAPIAN_VALUE
+       },
+
+
+       {
+               MU_MSG_FIELD_ID_MAILDIR,
+               MU_MSG_FIELD_TYPE_STRING,
+               "maildir", 'm', 'M',
+               FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE
+       },
+
+
+       {
+               MU_MSG_FIELD_ID_MAILING_LIST,
+               MU_MSG_FIELD_TYPE_STRING,
+               "list", 'v', 'V',
+               FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE
+       },
+
+
+       {
+               MU_MSG_FIELD_ID_MIME,
+               MU_MSG_FIELD_TYPE_STRING,
+               "mime" , 'y', 'Y',
+               FLAG_XAPIAN_TERM
+       },
+
+
+       {
+               MU_MSG_FIELD_ID_MSGID,
+               MU_MSG_FIELD_TYPE_STRING,
+               "msgid", 'i', 'I',  /* 'i' for Id */
+               FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE
+       },
+
+
+       {
+               MU_MSG_FIELD_ID_PATH,
+               MU_MSG_FIELD_TYPE_STRING,
+               "path", 'l', 'L',   /* 'l' for location */
+               FLAG_GMIME | FLAG_XAPIAN_VALUE |
+               FLAG_XAPIAN_BOOLEAN
+       },
+
+
+       {
+               MU_MSG_FIELD_ID_PRIO,
+               MU_MSG_FIELD_TYPE_INT,
+               "prio", 'p', 'P',
+               FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE
+       },
+
+
+       {
+               MU_MSG_FIELD_ID_REFS,
+               MU_MSG_FIELD_TYPE_STRING_LIST,
+               "refs", 'r', 'R',
+               FLAG_GMIME | FLAG_XAPIAN_VALUE
+       },
+
+
+       {
+               MU_MSG_FIELD_ID_SIZE,
+               MU_MSG_FIELD_TYPE_BYTESIZE,
+               "size", 'z', 'Z', /* siZe */
+               FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE |
+               FLAG_RANGE_FIELD
+       },
+
+       {
+               MU_MSG_FIELD_ID_SUBJECT,
+               MU_MSG_FIELD_TYPE_STRING,
+               "subject", 's', 'S',
+               FLAG_GMIME | FLAG_XAPIAN_INDEX | FLAG_XAPIAN_VALUE |
+               FLAG_XAPIAN_TERM
+       },
+
+       {
+               MU_MSG_FIELD_ID_TAGS,
+               MU_MSG_FIELD_TYPE_STRING_LIST,
+               "tag", 'x', 'X',
+               FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE
+       },
+
+
+       {       /* remember which thread this message is in */
+               MU_MSG_FIELD_ID_THREAD_ID,
+               MU_MSG_FIELD_TYPE_STRING,
+               "thread", 0, 'W',
+               FLAG_XAPIAN_TERM
+       },
+
+       {
+               MU_MSG_FIELD_ID_TO,
+               MU_MSG_FIELD_TYPE_STRING,
+               "to", 't', 'T',
+               FLAG_GMIME | FLAG_XAPIAN_CONTACT | FLAG_XAPIAN_VALUE
+       },
+
+       {       /* special, internal field, to get a unique key */
+               MU_MSG_FIELD_ID_UID,
+               MU_MSG_FIELD_TYPE_STRING,
+               "uid", 0, 'U',
+               FLAG_XAPIAN_TERM
+       }
+
+       /* note, mu-store also use the 'Q' internal prefix for its uids */
+};
+
+/* the MsgField data in an array, indexed by the MsgFieldId;
+ * this allows for O(1) access
+ */
+static MuMsgField* _msg_field_data[MU_MSG_FIELD_ID_NUM];
+static const MuMsgField* mu_msg_field (MuMsgFieldId id)
+{
+       static gboolean _initialized = FALSE;
+
+       /* initialize the array, but only once... */
+       if (G_UNLIKELY(!_initialized)) {
+               int i;
+               for (i = 0; i != G_N_ELEMENTS(FIELD_DATA); ++i)
+                       _msg_field_data[FIELD_DATA[i]._id] =
+                               (MuMsgField*)&FIELD_DATA[i];
+               _initialized = TRUE;
+       }
+
+       return _msg_field_data[id];
+}
+
+
+void
+mu_msg_field_foreach (MuMsgFieldForeachFunc func, gconstpointer data)
+{
+       int i;
+       for (i = 0; i != MU_MSG_FIELD_ID_NUM; ++i)
+               func (i, data);
+}
+
+
+MuMsgFieldId
+mu_msg_field_id_from_name (const char* str, gboolean err)
+{
+       int i;
+
+       g_return_val_if_fail (str, MU_MSG_FIELD_ID_NONE);
+
+       for (i = 0; i != G_N_ELEMENTS(FIELD_DATA); ++i)
+               if (g_strcmp0(str, FIELD_DATA[i]._name) == 0)
+                       return FIELD_DATA[i]._id;
+       if (err)
+               g_return_val_if_reached (MU_MSG_FIELD_ID_NONE);
+
+       return MU_MSG_FIELD_ID_NONE;
+}
+
+
+MuMsgFieldId
+mu_msg_field_id_from_shortcut (char kar, gboolean err)
+{
+       int i;
+       for (i = 0; i != G_N_ELEMENTS(FIELD_DATA); ++i)
+               if (kar == FIELD_DATA[i]._shortcut)
+                       return FIELD_DATA[i]._id;
+
+       if (err)
+               g_return_val_if_reached (MU_MSG_FIELD_ID_NONE);
+
+       return MU_MSG_FIELD_ID_NONE;
+}
+
+
+gboolean
+mu_msg_field_gmime (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE);
+       return mu_msg_field(id)->_flags & FLAG_GMIME ? TRUE: FALSE;
+}
+
+
+gboolean
+mu_msg_field_xapian_index  (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE);
+       return mu_msg_field(id)->_flags &
+               (FLAG_XAPIAN_INDEX | FLAG_XAPIAN_CONTACT) ? TRUE: FALSE;
+}
+
+gboolean
+mu_msg_field_xapian_value (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE);
+       return mu_msg_field(id)->_flags & FLAG_XAPIAN_VALUE ? TRUE: FALSE;
+}
+
+gboolean
+mu_msg_field_xapian_term (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE);
+       return mu_msg_field(id)->_flags & FLAG_XAPIAN_TERM ? TRUE: FALSE;
+}
+
+
+gboolean
+mu_msg_field_is_range_field (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE);
+       return mu_msg_field(id)->_flags & FLAG_RANGE_FIELD ? TRUE: FALSE;
+}
+
+
+
+gboolean
+mu_msg_field_uses_boolean_prefix (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE);
+       return mu_msg_field(id)->_flags & FLAG_XAPIAN_BOOLEAN ? TRUE:FALSE;
+}
+
+
+
+gboolean
+mu_msg_field_is_cacheable (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE);
+       /* note the FALSE: TRUE */
+       return mu_msg_field(id)->_flags & FLAG_DONT_CACHE ? FALSE : TRUE;
+}
+
+gboolean
+mu_msg_field_xapian_contact (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE);
+       return mu_msg_field(id)->_flags & FLAG_XAPIAN_CONTACT ? TRUE: FALSE;
+}
+
+
+
+gboolean
+mu_msg_field_is_numeric (MuMsgFieldId mfid)
+{
+       MuMsgFieldType type;
+
+       g_return_val_if_fail (mu_msg_field_id_is_valid(mfid),FALSE);
+
+       type = mu_msg_field_type (mfid);
+
+       return  type == MU_MSG_FIELD_TYPE_BYTESIZE ||
+               type == MU_MSG_FIELD_TYPE_TIME_T ||
+               type == MU_MSG_FIELD_TYPE_INT;
+}
+
+const char*
+mu_msg_field_name (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),NULL);
+       return mu_msg_field(id)->_name;
+}
+
+
+char
+mu_msg_field_shortcut (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),0);
+       return mu_msg_field(id)->_shortcut;
+}
+
+
+char
+mu_msg_field_xapian_prefix (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),0);
+       return mu_msg_field(id)->_xprefix;
+}
+
+
+
+
+MuMsgFieldType
+mu_msg_field_type (MuMsgFieldId id)
+{
+       g_return_val_if_fail (mu_msg_field_id_is_valid(id),
+                             MU_MSG_FIELD_TYPE_NONE);
+       return mu_msg_field(id)->_type;
+}
diff --git a/lib/mu-msg-fields.h b/lib/mu-msg-fields.h
new file mode 100644 (file)
index 0000000..08bfe60
--- /dev/null
@@ -0,0 +1,292 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_MSG_FIELDS_H__
+#define __MU_MSG_FIELDS_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+/* don't change the order, add new types at the end, as these numbers
+ * are used in the database */
+enum _MuMsgFieldId {
+
+       /* first all the string-based ones */
+       MU_MSG_FIELD_ID_BCC         = 0,
+       MU_MSG_FIELD_ID_BODY_HTML,
+       MU_MSG_FIELD_ID_BODY_TEXT,
+       MU_MSG_FIELD_ID_CC,
+       MU_MSG_FIELD_ID_EMBEDDED_TEXT,
+       MU_MSG_FIELD_ID_FILE,
+       MU_MSG_FIELD_ID_FROM,
+       MU_MSG_FIELD_ID_MAILDIR,
+       MU_MSG_FIELD_ID_MIME, /* mime-type */
+       MU_MSG_FIELD_ID_MSGID,
+       MU_MSG_FIELD_ID_PATH,
+       MU_MSG_FIELD_ID_SUBJECT,
+       MU_MSG_FIELD_ID_TO,
+
+       MU_MSG_FIELD_ID_UID, /* special, generated from path */
+
+       /* string list items... */
+       MU_MSG_FIELD_ID_REFS,
+       MU_MSG_FIELD_ID_TAGS,
+
+       /* then the numerical ones */
+       MU_MSG_FIELD_ID_DATE,
+       MU_MSG_FIELD_ID_FLAGS,
+       MU_MSG_FIELD_ID_PRIO,
+       MU_MSG_FIELD_ID_SIZE,
+
+       /* add new ones here... */
+       MU_MSG_FIELD_ID_MAILING_LIST, /* mailing list */
+       MU_MSG_FIELD_ID_THREAD_ID,
+
+       MU_MSG_FIELD_ID_NUM
+};
+typedef guint8 MuMsgFieldId;
+
+/* some specials... */
+static const MuMsgFieldId MU_MSG_FIELD_ID_NONE = (MuMsgFieldId)-1;
+#define MU_MSG_STRING_FIELD_ID_NUM (MU_MSG_FIELD_ID_UID + 1)
+
+/* this is a shortcut for To/From/Cc/Bcc in queries; handled specially
+ * in mu-query.cc and mu-str.c */
+#define MU_MSG_FIELD_PSEUDO_CONTACT "contact"
+
+/* this is a shortcut for To/Cc/Bcc in queries; handled specially in
+ * mu-query.cc and mu-str.c */
+#define MU_MSG_FIELD_PSEUDO_RECIP "recip"
+
+#define mu_msg_field_id_is_valid(MFID) \
+       ((MFID) < MU_MSG_FIELD_ID_NUM)
+
+/* don't change the order, add new types at the end (before _NUM)*/
+enum _MuMsgFieldType {
+       MU_MSG_FIELD_TYPE_STRING,
+       MU_MSG_FIELD_TYPE_STRING_LIST,
+
+       MU_MSG_FIELD_TYPE_BYTESIZE,
+       MU_MSG_FIELD_TYPE_TIME_T,
+       MU_MSG_FIELD_TYPE_INT,
+
+       MU_MSG_FIELD_TYPE_NUM
+};
+typedef guint8 MuMsgFieldType;
+static const MuMsgFieldType MU_MSG_FIELD_TYPE_NONE = (MuMsgFieldType)-1;
+
+typedef void (*MuMsgFieldForeachFunc) (MuMsgFieldId id,
+                                      gconstpointer data);
+
+/**
+ * iterator over all possible message fields
+ *
+ * @param func a function called for each field
+ * @param data a user data pointer passed the callback function
+ */
+void mu_msg_field_foreach (MuMsgFieldForeachFunc func, gconstpointer data);
+
+
+/**
+ * get the name of the field -- this a name that can be use in queries,
+ * ie. 'subject:foo', with 'subject' being the name
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return the name of the field as a constant string, or
+ * NULL if the field is unknown
+ */
+const char* mu_msg_field_name (MuMsgFieldId id) G_GNUC_PURE;
+
+/**
+ * get the shortcut of the field -- this a shortcut that can be use in
+ * queries, ie. 's:foo', with 's' meaning 'subject' being the name
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return the shortcut character, or 0 if the field is unknown
+ */
+char mu_msg_field_shortcut (MuMsgFieldId id) G_GNUC_PURE;
+
+/**
+ * get the xapian prefix of the field -- that is, the prefix used in
+ * the Xapian database to identify the field
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return the xapian prefix char or 0 if the field is unknown
+ */
+char mu_msg_field_xapian_prefix (MuMsgFieldId id) G_GNUC_PURE;
+
+
+/**
+ * get the type of the field (string, size, time etc.)
+ *
+ * @param field a MuMsgField
+ *
+ * @return the type of the field (a #MuMsgFieldType), or
+ * MU_MSG_FIELD_TYPE_NONE if it is not found
+ */
+MuMsgFieldType mu_msg_field_type (MuMsgFieldId id) G_GNUC_PURE;
+
+
+
+/**
+ * is the field a string?
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return TRUE if the field a string, FALSE otherwise
+ */
+#define mu_msg_field_is_string(MFID)\
+       (mu_msg_field_type((MFID))==MU_MSG_FIELD_TYPE_STRING?TRUE:FALSE)
+
+
+
+/**
+ * is the field a string-list?
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return TRUE if the field a string-list, FALSE otherwise
+ */
+#define mu_msg_field_is_string_list(MFID)\
+       (mu_msg_field_type((MFID))==MU_MSG_FIELD_TYPE_STRING_LIST?TRUE:FALSE)
+
+/**
+ * is the field numeric (has type MU_MSG_FIELD_TYPE_(BYTESIZE|TIME_T|INT))?
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return TRUE if the field is numeric, FALSE otherwise
+ */
+gboolean mu_msg_field_is_numeric (MuMsgFieldId id) G_GNUC_PURE;
+
+
+/**
+ * whether the field value should be cached (in MuMsg) -- we cache
+ * values so we can use the MuMsg without needing to keep the
+ * underlying data source (the GMimeMessage or the database ptr) alive
+ * in practice, the fields we *don't* cache are the message body
+ * (html, txt), because they take too much memory
+ */
+gboolean mu_msg_field_is_cacheable (MuMsgFieldId id) G_GNUC_PURE;
+
+
+/**
+ * is the field Xapian-indexable? That is, should this field be
+ * indexed in the Xapian database, so we can use the all the
+ * phrasing, stemming etc. magic
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return TRUE if the field is Xapian-enabled, FALSE otherwise
+ */
+gboolean mu_msg_field_xapian_index (MuMsgFieldId id) G_GNUC_PURE;
+
+/**
+ * should this field be stored as a xapian term?
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return TRUE if the field is Xapian-enabled, FALSE otherwise
+ */
+gboolean mu_msg_field_xapian_term (MuMsgFieldId id) G_GNUC_PURE;
+
+/**
+ * should this field be stored as a xapian value?
+ *
+ * @param field a MuMsgField
+ *
+ * @return TRUE if the field is Xapian-enabled, FALSE otherwise
+ */
+gboolean mu_msg_field_xapian_value (MuMsgFieldId id) G_GNUC_PURE;
+
+
+/**
+ * whether we should use add_boolean_prefix (see Xapian documentation)
+ * for this field in queries. Used in mu-query.cc
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return TRUE if this field wants add_boolean_prefix, FALSE
+ * otherwise
+ */
+gboolean mu_msg_field_uses_boolean_prefix (MuMsgFieldId id) G_GNUC_PURE;
+
+/**
+ * is this a range-field? ie. date, or size
+ *
+ * @param id a MuMsgField
+ *
+ * @return TRUE if this field is a range field, FALSE otherwise
+ */
+gboolean mu_msg_field_is_range_field (MuMsgFieldId id) G_GNUC_PURE;
+
+
+/**
+ * should this field be stored as contact information? This means that
+ * e-mail address will be stored as terms, and names will be indexed
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return TRUE if the field should be stored as contact information,
+ * FALSE otherwise
+ */
+gboolean mu_msg_field_xapian_contact (MuMsgFieldId id) G_GNUC_PURE;
+
+/**
+ * is the field gmime-enabled? That is, can be field be retrieved
+ * using GMime?
+ *
+ * @param id a MuMsgFieldId
+ *
+ * @return TRUE if the field is Gmime-enabled, FALSE otherwise
+ */
+gboolean mu_msg_field_gmime (MuMsgFieldId id) G_GNUC_PURE;
+
+
+/**
+ * get the corresponding MuMsgField for a name (as in mu_msg_field_name)
+ *
+ * @param str a name
+ * @param err, if TRUE, when the shortcut is not found, will issue a
+ * g_critical warning
+ *
+ * @return a MuMsgField, or NULL if it could not be found
+ */
+MuMsgFieldId mu_msg_field_id_from_name (const char* str,
+                                       gboolean err)  G_GNUC_PURE;
+
+
+/**
+ * get the corresponding MuMsgField for a shortcut (as in mu_msg_field_shortcut)
+ *
+ * @param kar a shortcut character
+ * @param err, if TRUE, when the shortcut is not found, will issue a
+ * g_critical warning
+ *
+ * @return a MuMsgField, or NULL if it could not be found
+ */
+MuMsgFieldId  mu_msg_field_id_from_shortcut (char kar,
+                                            gboolean err) G_GNUC_PURE;
+G_END_DECLS
+
+#endif /*__MU_MSG_FIELDS_H__*/
diff --git a/lib/mu-msg-file.c b/lib/mu-msg-file.c
new file mode 100644 (file)
index 0000000..f0fa306
--- /dev/null
@@ -0,0 +1,815 @@
+/* -*- mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+**
+** 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.
+**
+*/
+
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <inttypes.h>
+
+#include <gmime/gmime.h>
+#include "mu-maildir.h"
+#include "mu-store.hh"
+#include "mu-msg-priv.h"
+
+#include "utils/mu-util.h"
+#include "utils/mu-str.h"
+
+static gboolean init_file_metadata (MuMsgFile *self, const char* path,
+                                   const char *mdir, GError **err);
+static gboolean init_mime_msg (MuMsgFile *msg, const char *path, GError **err);
+
+MuMsgFile*
+mu_msg_file_new (const char* filepath, const char *mdir, GError **err)
+{
+       MuMsgFile *self;
+
+       g_return_val_if_fail (filepath, NULL);
+
+       self = g_slice_new0 (MuMsgFile);
+
+       if (!init_file_metadata (self, filepath, mdir, err)) {
+               mu_msg_file_destroy (self);
+               return NULL;
+       }
+
+       if (!init_mime_msg (self, filepath, err)) {
+               mu_msg_file_destroy (self);
+               return NULL;
+       }
+
+       return self;
+}
+
+void
+mu_msg_file_destroy (MuMsgFile *self)
+{
+       if (!self)
+               return;
+
+       if (self->_mime_msg)
+               g_object_unref (self->_mime_msg);
+
+       g_slice_free (MuMsgFile, self);
+}
+
+static gboolean
+init_file_metadata (MuMsgFile *self, const char* path, const gchar* mdir,
+                   GError **err)
+{
+       struct stat statbuf;
+
+       if (access (path, R_OK) != 0) {
+               mu_util_g_set_error (err, MU_ERROR_FILE,
+                                    "cannot read file %s: %s",
+                                    path, strerror(errno));
+               return FALSE;
+       }
+
+       if (stat (path, &statbuf) < 0) {
+               mu_util_g_set_error (err, MU_ERROR_FILE,
+                                    "cannot stat %s: %s",
+                                    path, strerror(errno));
+               return FALSE;
+       }
+
+       if (!S_ISREG(statbuf.st_mode)) {
+               mu_util_g_set_error (err, MU_ERROR_FILE,
+                                    "not a regular file: %s", path);
+               return FALSE;
+       }
+
+       self->_timestamp = statbuf.st_mtime;
+       self->_size      = (size_t)statbuf.st_size;
+
+       /* remove double slashes, relative paths etc. from path & mdir */
+       if (!realpath (path, self->_path)) {
+               mu_util_g_set_error (err, MU_ERROR_FILE,
+                                    "could not get realpath for %s: %s",
+                                    path, strerror(errno));
+               return FALSE;
+       }
+
+       strncpy (self->_maildir, mdir ? mdir : "", PATH_MAX);
+       return TRUE;
+}
+
+static GMimeStream*
+get_mime_stream (MuMsgFile *self, const char *path, GError **err)
+{
+       FILE *file;
+       GMimeStream *stream;
+
+       file = fopen (path, "r");
+       if (!file) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE,
+                            "cannot open %s: %s",
+                            path, strerror (errno));
+               return NULL;
+       }
+
+       stream = g_mime_stream_file_new (file);
+       if (!stream) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "cannot create mime stream for %s",
+                            path);
+               fclose (file);
+               return NULL;
+       }
+
+       return stream;
+}
+
+static gboolean
+init_mime_msg (MuMsgFile *self, const char* path, GError **err)
+{
+       GMimeStream *stream;
+       GMimeParser *parser;
+
+       stream = get_mime_stream (self, path, err);
+       if (!stream)
+               return FALSE;
+
+       parser = g_mime_parser_new_with_stream (stream);
+       g_object_unref (stream);
+       if (!parser) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "cannot create mime parser for %s", path);
+               return FALSE;
+       }
+
+       self->_mime_msg = g_mime_parser_construct_message (parser, NULL);
+       g_object_unref (parser);
+       if (!self->_mime_msg) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "message seems invalid, ignoring (%s)", path);
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static char*
+get_recipient (MuMsgFile *self, GMimeAddressType atype)
+{
+       char                    *recip;
+       InternetAddressList     *recips;
+
+       recips = g_mime_message_get_addresses (self->_mime_msg, atype);
+
+       /* FALSE --> don't encode */
+       recip = (char*)internet_address_list_to_string (recips, NULL, FALSE);
+
+       if (recip && !g_utf8_validate (recip, -1, NULL)) {
+               g_debug ("invalid recipient in %s\n", self->_path);
+               mu_str_asciify_in_place (recip); /* ugly... */
+       }
+
+       if (mu_str_is_empty(recip)) {
+               g_free (recip);
+               return NULL;
+       }
+
+       if (recip)
+               mu_str_remove_ctrl_in_place (recip);
+
+       return recip;
+}
+
+/*
+ * let's try to guess the mailing list from some other
+ * headers in the mail
+ */
+static gchar*
+get_fake_mailing_list_maybe (MuMsgFile *self)
+{
+       const char* hdr;
+
+       hdr = g_mime_object_get_header (GMIME_OBJECT(self->_mime_msg),
+                                       "X-Feed2Imap-Version");
+       if (!hdr)
+               return NULL;
+
+       /* looks like a feed2imap header; guess the source-blog
+        * from the msgid */
+       {
+               const char *msgid, *e;
+               msgid = g_mime_message_get_message_id (self->_mime_msg);
+               if (msgid && (e = strchr (msgid, '-')))
+                       return g_strndup (msgid, e - msgid);
+       }
+
+       return NULL;
+}
+
+static gchar*
+get_mailing_list (MuMsgFile *self)
+{
+       char            *dechdr, *res;
+       const char      *hdr, *b, *e;
+
+       hdr = g_mime_object_get_header (GMIME_OBJECT(self->_mime_msg),
+                                       "List-Id");
+       if (mu_str_is_empty (hdr))
+               return get_fake_mailing_list_maybe (self);
+
+       dechdr = g_mime_utils_header_decode_phrase (NULL, hdr);
+       if (!dechdr)
+               return NULL;
+
+       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 res;
+}
+
+static gboolean
+looks_like_attachment (GMimeObject *part)
+{
+
+       GMimeContentDisposition *disp;
+       GMimeContentType        *ctype;
+       const char              *dispstr;
+       guint                    u;
+       const struct {
+               const char      *type;
+               const char      *sub_type;
+       } att_types[] = {
+               { "image", "*" },
+               { "audio", "*" },
+               { "application", "*"},
+               { "application", "x-patch"}
+       };
+
+       disp = g_mime_object_get_content_disposition (part);
+
+       if (!GMIME_IS_CONTENT_DISPOSITION(disp))
+               return FALSE;
+
+       dispstr = g_mime_content_disposition_get_disposition (disp);
+
+       if (g_ascii_strcasecmp (dispstr, "attachment") == 0)
+               return TRUE;
+
+       /* we also consider patches, images, audio, and non-pgp-signature
+        * application attachments to be attachments... */
+       ctype = g_mime_object_get_content_type (part);
+
+       if (g_mime_content_type_is_type (ctype, "*", "pgp-signature"))
+               return FALSE; /* don't consider as a signature */
+
+       if (g_mime_content_type_is_type (ctype, "text", "*")) {
+               if (g_mime_content_type_is_type (ctype, "*", "plain") ||
+                   g_mime_content_type_is_type (ctype, "*", "html"))
+                       return FALSE;
+               else
+                       return TRUE;
+       }
+
+       for (u = 0; u != G_N_ELEMENTS(att_types); ++u)
+               if (g_mime_content_type_is_type (
+                           ctype, att_types[u].type, att_types[u].sub_type))
+                       return TRUE;
+
+       return FALSE;
+}
+
+static void
+msg_cflags_cb (GMimeObject *parent, GMimeObject *part, MuFlags *flags)
+{
+       if (GMIME_IS_MULTIPART_SIGNED(part))
+               *flags |= MU_FLAG_SIGNED;
+
+       /* FIXME: An encrypted part might be signed at the same time.
+        *        In that case the signed flag is lost. */
+       if (GMIME_IS_MULTIPART_ENCRYPTED(part))
+               *flags |= MU_FLAG_ENCRYPTED;
+
+       if (*flags & MU_FLAG_HAS_ATTACH)
+               return;
+
+       if (!GMIME_IS_PART(part))
+               return;
+
+       if (*flags & MU_FLAG_HAS_ATTACH)
+               return;
+
+       if (looks_like_attachment (part))
+               *flags |= MU_FLAG_HAS_ATTACH;
+}
+
+static MuFlags
+get_content_flags (MuMsgFile *self)
+{
+       MuFlags  flags;
+       char    *ml;
+
+       flags = MU_FLAG_NONE;
+
+       if (GMIME_IS_MESSAGE(self->_mime_msg))
+               mu_mime_message_foreach (self->_mime_msg,
+                                        FALSE, /* never decrypt for this */
+                                        (GMimeObjectForeachFunc)msg_cflags_cb,
+                                        &flags);
+
+       ml = get_mailing_list (self);
+       if (ml) {
+               flags |= MU_FLAG_LIST;
+               g_free (ml);
+       }
+
+       return flags;
+}
+
+static MuFlags
+get_flags (MuMsgFile *self)
+{
+       MuFlags flags;
+
+       g_return_val_if_fail (self, MU_FLAG_INVALID);
+
+       flags = mu_maildir_get_flags_from_path (self->_path);
+       flags |= get_content_flags (self);
+
+       /* pseudo-flag --> unread means either NEW or NOT SEEN, just
+        * for searching convenience */
+       if ((flags & MU_FLAG_NEW) || !(flags & MU_FLAG_SEEN))
+               flags |= MU_FLAG_UNREAD;
+
+       return flags;
+}
+
+static size_t
+get_size (MuMsgFile *self)
+{
+       g_return_val_if_fail (self, 0);
+       return self->_size;
+}
+
+static MuMsgPrio
+parse_prio_str (const char* priostr)
+{
+       int i;
+       struct {
+               const char*     _str;
+               MuMsgPrio       _prio;
+       } str_prio[] = {
+               { "high",       MU_MSG_PRIO_HIGH },
+               { "1",          MU_MSG_PRIO_HIGH },
+               { "2",          MU_MSG_PRIO_HIGH },
+
+               { "normal",     MU_MSG_PRIO_NORMAL },
+               { "3",          MU_MSG_PRIO_NORMAL },
+
+               { "low",        MU_MSG_PRIO_LOW },
+               { "list",       MU_MSG_PRIO_LOW },
+               { "bulk",       MU_MSG_PRIO_LOW },
+               { "4",          MU_MSG_PRIO_LOW },
+               { "5",          MU_MSG_PRIO_LOW }
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(str_prio); ++i)
+               if (g_ascii_strcasecmp (priostr, str_prio[i]._str) == 0)
+                       return str_prio[i]._prio;
+
+       /* e.g., last-fm uses 'fm-user'... as precedence */
+       return MU_MSG_PRIO_NORMAL;
+}
+
+static MuMsgPrio
+get_prio (MuMsgFile *self)
+{
+       GMimeObject *obj;
+       const char* priostr;
+
+       g_return_val_if_fail (self, MU_MSG_PRIO_NONE);
+
+       obj = GMIME_OBJECT(self->_mime_msg);
+
+       priostr = g_mime_object_get_header (obj, "Precedence");
+       if (!priostr)
+               priostr = g_mime_object_get_header (obj, "X-Priority");
+       if (!priostr)
+               priostr = g_mime_object_get_header (obj, "Importance");
+
+       return priostr ? parse_prio_str (priostr) : MU_MSG_PRIO_NORMAL;
+}
+
+/* NOTE: buffer will be *freed* or returned unchanged */
+static char*
+convert_to_utf8 (GMimePart *part, char *buffer)
+{
+       GMimeContentType *ctype;
+       const char* charset;
+
+       ctype = g_mime_object_get_content_type (GMIME_OBJECT(part));
+       g_return_val_if_fail (GMIME_IS_CONTENT_TYPE(ctype), NULL);
+
+       /* of course, the charset specified may be incorrect... */
+       charset = g_mime_content_type_get_parameter (ctype, "charset");
+       if (charset) {
+               char *utf8;
+               if ((utf8 = mu_str_convert_to_utf8
+                    (buffer, g_mime_charset_iconv_name (charset)))) {
+                       g_free (buffer);
+                       buffer = utf8;
+               }
+       } else if (!g_utf8_validate (buffer, -1, NULL)) {
+               /* if it's already utf8, nothing to do otherwise: no
+                  charset at all, or conversion failed; ugly * hack:
+                  replace all non-ascii chars with '.' */
+               mu_str_asciify_in_place (buffer);
+       }
+
+       return buffer;
+}
+
+static gchar*
+stream_to_string (GMimeStream *stream, size_t buflen)
+{
+       char *buffer;
+       ssize_t bytes;
+
+       buffer = g_new(char, buflen + 1);
+       g_mime_stream_reset (stream);
+
+       /* we read everything in one go */
+       bytes = g_mime_stream_read (stream, buffer, buflen);
+       if (bytes < 0) {
+               g_warning ("%s: failed to read from stream", __func__);
+               g_free (buffer);
+               return NULL;
+       }
+
+       buffer[bytes]='\0';
+
+       return buffer;
+}
+
+gchar*
+mu_msg_mime_part_to_string (GMimePart *part, gboolean *err)
+{
+       GMimeDataWrapper *wrapper;
+       GMimeStream *stream;
+       ssize_t buflen;
+       char *buffer;
+
+       buffer = NULL;
+       stream = NULL;
+
+       g_return_val_if_fail (err, NULL);
+
+       *err = TRUE; /* guilty until proven innocent */
+       g_return_val_if_fail (GMIME_IS_PART(part), NULL);
+
+       wrapper = g_mime_part_get_content (part);
+       if (!wrapper) {
+               /* this happens with invalid mails */
+               g_debug ("failed to create data wrapper");
+               goto cleanup;
+       }
+
+       stream = g_mime_stream_mem_new ();
+       if (!stream) {
+               g_warning ("failed to create mem stream");
+               goto cleanup;
+       }
+
+       buflen = g_mime_data_wrapper_write_to_stream (wrapper, stream);
+       if (buflen <= 0)  {/* empty buffer, not an error */
+               *err = FALSE;
+               goto cleanup;
+       }
+
+       buffer = stream_to_string (stream, (size_t)buflen);
+
+       /* convert_to_utf8 will free the old 'buffer' if needed */
+       buffer = convert_to_utf8 (part, buffer);
+       *err   = FALSE;
+
+cleanup:
+       if (G_IS_OBJECT(stream))
+               g_object_unref (stream);
+
+       return buffer;
+}
+
+static gboolean
+contains (GSList *lst, const char *str)
+{
+       for (; lst; lst = g_slist_next(lst))
+               if (g_strcmp0 ((char*)lst->data, str) == 0)
+                       return TRUE;
+       return FALSE;
+}
+
+/*
+ * NOTE: this will get the list of references with the oldest parent
+ * at the beginning */
+static GSList*
+get_references  (MuMsgFile *self)
+{
+       GSList *msgids;
+       unsigned u;
+       const char *headers[] = { "References", "In-reply-to", NULL };
+
+       for (msgids = NULL, u = 0; headers[u]; ++u) {
+
+               char *str;
+               GMimeReferences *mime_refs;
+               int i, refs_len;
+
+               str = mu_msg_file_get_header (self, headers[u]);
+               if (!str)
+                       continue;
+
+               mime_refs = g_mime_references_parse (NULL, str);
+               g_free (str);
+
+               refs_len = g_mime_references_length (mime_refs);
+               for (i = 0; i < refs_len; ++i) {
+                       const char* msgid;
+                       msgid = g_mime_references_get_message_id (mime_refs, i);
+
+                       /* don't include duplicates */
+                       if (msgid && !contains (msgids, msgid))
+                               /* explicitly ensure it's utf8-safe,
+                                * as GMime does not ensure that */
+                               msgids = g_slist_prepend (msgids,
+                                                         g_strdup((msgid)));
+               }
+               g_mime_references_free (mime_refs);
+       }
+
+       /* reverse, because we used g_slist_prepend for performance
+        * reasons */
+       return g_slist_reverse (msgids);
+}
+
+/* see: http://does-not-exist.org/mail-archives/mutt-dev/msg08249.html */
+static GSList*
+get_tags (MuMsgFile *self)
+{
+       GSList *lst;
+       unsigned u;
+       struct {
+               const char *header;
+               char sepa;
+       } tagfields[] = {
+               { "X-Label",    ' ' },
+               { "X-Keywords", ',' },
+               { "Keywords",   ' ' }
+       };
+
+       for (lst = NULL, u = 0; u != G_N_ELEMENTS(tagfields); ++u) {
+               gchar *hdr;
+               hdr = mu_msg_file_get_header (self, tagfields[u].header);
+               if (hdr) {
+                       GSList *hlst;
+                       hlst = mu_str_to_list (hdr, tagfields[u].sepa, TRUE);
+
+                       if (lst)
+                               (g_slist_last (lst))->next = hlst;
+                       else
+                               lst = hlst;
+
+                       g_free (hdr);
+               }
+       }
+
+       return lst;
+}
+
+static char*
+cleanup_maybe (const char *str, gboolean *do_free)
+{
+       char *s;
+
+       if (!str)
+               return NULL;
+
+       if (!g_utf8_validate(str, -1, NULL)) {
+               if (*do_free)
+                       s = mu_str_asciify_in_place ((char*)str);
+               else {
+                       *do_free = TRUE;
+                       s = mu_str_asciify_in_place(g_strdup (str));
+               }
+       } else
+               s = (char*)str;
+
+       mu_str_remove_ctrl_in_place (s);
+
+       return s;
+}
+
+G_GNUC_CONST static GMimeAddressType
+address_type (MuMsgFieldId mfid)
+{
+       switch (mfid) {
+       case MU_MSG_FIELD_ID_BCC : return GMIME_ADDRESS_TYPE_BCC;
+       case MU_MSG_FIELD_ID_CC  : return GMIME_ADDRESS_TYPE_CC;
+       case MU_MSG_FIELD_ID_TO  : return GMIME_ADDRESS_TYPE_TO;
+       case MU_MSG_FIELD_ID_FROM: return GMIME_ADDRESS_TYPE_FROM;
+       default: g_return_val_if_reached (-1);
+       }
+}
+
+static gchar*
+get_msgid (MuMsgFile *self, gboolean *do_free)
+{
+       const char *msgid;
+
+       msgid = g_mime_message_get_message_id (self->_mime_msg);
+       if (msgid && strlen(msgid) < MU_STORE_MAX_TERM_LENGTH) {
+               return (char*)msgid;
+       } else { /* if there is none, fake it */
+               *do_free = TRUE;
+               return g_strdup_printf ("%016" PRIx64  "@fake-msgid",
+                                       mu_util_get_hash (self->_path));
+       }
+}
+
+char*
+mu_msg_file_get_str_field (MuMsgFile *self, MuMsgFieldId mfid,
+                          gboolean *do_free)
+{
+       g_return_val_if_fail (self, NULL);
+       g_return_val_if_fail (mu_msg_field_is_string(mfid), NULL);
+
+       *do_free = FALSE; /* default */
+
+       switch (mfid) {
+
+       case MU_MSG_FIELD_ID_BCC:
+       case MU_MSG_FIELD_ID_CC:
+       case MU_MSG_FIELD_ID_FROM:
+       case MU_MSG_FIELD_ID_TO:
+               *do_free = TRUE;
+               return get_recipient (self, address_type(mfid));
+
+       case MU_MSG_FIELD_ID_PATH: return self->_path;
+
+       case MU_MSG_FIELD_ID_MAILING_LIST:
+               *do_free = TRUE;
+               return (char*)get_mailing_list (self);
+
+       case MU_MSG_FIELD_ID_SUBJECT:
+               return (char*)cleanup_maybe
+                       (g_mime_message_get_subject (self->_mime_msg), do_free);
+
+       case MU_MSG_FIELD_ID_MSGID:
+               return get_msgid (self, do_free);
+
+       case MU_MSG_FIELD_ID_MAILDIR: return self->_maildir;
+
+       case MU_MSG_FIELD_ID_BODY_TEXT:  /* use mu_msg_get_body_text */
+       case MU_MSG_FIELD_ID_BODY_HTML:  /* use mu_msg_get_body_html */
+       case MU_MSG_FIELD_ID_EMBEDDED_TEXT:
+               g_warning ("%s is not retrievable through: %s",
+                          mu_msg_field_name (mfid), __func__);
+               return NULL;
+
+       default: g_return_val_if_reached (NULL);
+       }
+}
+
+GSList*
+mu_msg_file_get_str_list_field (MuMsgFile *self, MuMsgFieldId mfid)
+{
+       g_return_val_if_fail (self, NULL);
+       g_return_val_if_fail (mu_msg_field_is_string_list(mfid), NULL);
+
+       switch (mfid) {
+       case MU_MSG_FIELD_ID_REFS: return get_references (self);
+       case MU_MSG_FIELD_ID_TAGS: return get_tags (self);
+       default: g_return_val_if_reached (NULL);
+       }
+}
+
+gint64
+mu_msg_file_get_num_field (MuMsgFile *self, const MuMsgFieldId mfid)
+{
+       g_return_val_if_fail (self, -1);
+       g_return_val_if_fail (mu_msg_field_is_numeric(mfid), -1);
+
+       switch (mfid) {
+
+       case MU_MSG_FIELD_ID_DATE: {
+               GDateTime *dt;
+               dt = g_mime_message_get_date (self->_mime_msg);
+               return dt ? g_date_time_to_unix (dt) : 0;
+       }
+
+       case MU_MSG_FIELD_ID_FLAGS:
+               return (gint64)get_flags(self);
+
+       case MU_MSG_FIELD_ID_PRIO:
+               return (gint64)get_prio(self);
+
+       case MU_MSG_FIELD_ID_SIZE:
+               return (gint64)get_size(self);
+
+       default: g_return_val_if_reached (-1);
+       }
+}
+
+char*
+mu_msg_file_get_header (MuMsgFile *self, const char *header)
+{
+       const gchar *hdr;
+
+       g_return_val_if_fail (self, NULL);
+       g_return_val_if_fail (header, NULL);
+
+       /* sadly, g_mime_object_get_header may return non-ascii;
+        * so, we need to ensure that
+        */
+       hdr = g_mime_object_get_header (GMIME_OBJECT(self->_mime_msg),
+                                       header);
+
+       return hdr ? mu_str_utf8ify(hdr) : NULL;
+}
+
+struct _ForeachData {
+       GMimeObjectForeachFunc user_func;
+       gpointer user_data;
+       gboolean decrypt;
+};
+typedef struct _ForeachData ForeachData;
+
+static void
+foreach_cb (GMimeObject *parent, GMimeObject *part, ForeachData *fdata)
+{
+       /* invoke the callback function */
+       fdata->user_func (parent, part, fdata->user_data);
+
+       /* maybe iterate over decrypted parts */
+       if (fdata->decrypt &&
+           GMIME_IS_MULTIPART_ENCRYPTED (part)) {
+               GMimeObject *dec;
+               dec = mu_msg_crypto_decrypt_part
+                       (GMIME_MULTIPART_ENCRYPTED(part),
+                        MU_MSG_OPTION_NONE, NULL, NULL, NULL);
+               if (!dec)
+                       return;
+
+               if (GMIME_IS_MULTIPART (dec))
+                       g_mime_multipart_foreach (
+                               (GMIME_MULTIPART(dec)),
+                               (GMimeObjectForeachFunc)foreach_cb,
+                               fdata);
+               else
+                       foreach_cb (parent, dec, fdata);
+
+               g_object_unref (dec);
+       }
+}
+
+void
+mu_mime_message_foreach (GMimeMessage *msg, gboolean decrypt,
+                        GMimeObjectForeachFunc func, gpointer user_data)
+{
+       ForeachData fdata;
+
+       g_return_if_fail (GMIME_IS_MESSAGE (msg));
+       g_return_if_fail (func);
+
+       fdata.user_func = func;
+       fdata.user_data = user_data;
+       fdata.decrypt   = decrypt;
+
+       g_mime_message_foreach
+               (msg,
+                (GMimeObjectForeachFunc)foreach_cb,
+                &fdata);
+}
diff --git a/lib/mu-msg-file.h b/lib/mu-msg-file.h
new file mode 100644 (file)
index 0000000..e26c4a7
--- /dev/null
@@ -0,0 +1,105 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_MSG_FILE_H__
+#define __MU_MSG_FILE_H__
+
+struct _MuMsgFile;
+typedef struct _MuMsgFile MuMsgFile;
+
+/**
+ * create a new message from a file
+ *
+ * @param path full path to the message
+ * @param mdir
+ * @param err error to receive (when function returns NULL), or NULL
+ *
+ * @return a new MuMsg, or NULL in case of error
+ */
+MuMsgFile *mu_msg_file_new (const char *path,
+                           const char* mdir, GError **err)
+                            G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * destroy a MuMsgFile object
+ *
+ * @param self object to destroy, or NULL
+ */
+void mu_msg_file_destroy (MuMsgFile *self);
+
+
+
+/**
+ * get a specific header
+ *
+ * @param self a MuMsgFile instance
+ * @param header a header (e.g. 'X-Mailer' or 'List-Id')
+ *
+ * @return the value of the header or NULL if not found; free with g_free
+ */
+char* mu_msg_file_get_header (MuMsgFile *self, const char *header);
+
+
+/**
+ * get a string value for this message
+ *
+ * @param self a valid MuMsgFile
+ * @param msfid the message field id to get (must be of type string)
+ * @param do_free receives TRUE or FALSE, conveying if this string
+ * should be owned & freed (TRUE) or not by caller. In case 'FALSE',
+ * this function should be treated as if it were returning a const
+ * char*, and note that in that case the string is only valid as long
+ * as the MuMsgFile is alive, ie. before mu_msg_file_destroy
+ *
+ * @return a string, or NULL
+ */
+char* mu_msg_file_get_str_field (MuMsgFile *self,
+                                MuMsgFieldId msfid,
+                                gboolean *do_free)
+                                G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * get a string-list value for this message
+ *
+ * @param self a valid MuMsgFile
+ * @param msfid the message field id to get (must be of type string-list)
+ *
+ * @return a GSList*, or NULL; free with mu_str_free_list
+ */
+GSList* mu_msg_file_get_str_list_field (MuMsgFile *self, MuMsgFieldId msfid)
+                                       G_GNUC_WARN_UNUSED_RESULT;
+
+
+
+/**
+ * get a numeric value for this message -- the return value should be
+ * cast into the actual type, e.g., time_t, MuMsgPrio etc.
+ *
+ * @param self a valid MuMsgFile
+ * @param msfid the message field id to get (must be string-based one)
+ *
+ * @return the numeric value, or -1 in case of error
+ */
+gint64 mu_msg_file_get_num_field (MuMsgFile *self, MuMsgFieldId mfid);
+
+
+#endif /*__MU_MSG_FILE_H__*/
diff --git a/lib/mu-msg-iter.cc b/lib/mu-msg-iter.cc
new file mode 100644 (file)
index 0000000..a401976
--- /dev/null
@@ -0,0 +1,437 @@
+/* -*- 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 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 <stdlib.h>
+#include <unistd.h>
+
+#include <iostream>
+
+#include <string.h>
+#include <errno.h>
+#include <algorithm>
+#include <xapian.h>
+
+#include <string>
+#include <set>
+#include <map>
+
+#include "utils/mu-util.h"
+#include "utils/mu-utils.hh"
+
+#include "mu-msg.h"
+#include "mu-msg-iter.h"
+#include "mu-threader.h"
+
+struct ltstr {
+       bool operator () (const std::string &s1,
+                         const std::string &s2) const {
+               return g_strcmp0 (s1.c_str(), s2.c_str()) < 0;
+       }
+};
+typedef std::map <std::string, unsigned, ltstr> msgid_docid_map;
+
+class ThreadKeyMaker: public Xapian::KeyMaker {
+public:
+       ThreadKeyMaker (GHashTable *threadinfo): _threadinfo(threadinfo) {}
+       virtual std::string operator()(const Xapian::Document &doc) const  {
+               MuMsgIterThreadInfo *ti;
+               ti = (MuMsgIterThreadInfo*)g_hash_table_lookup
+                       (_threadinfo,
+                        GUINT_TO_POINTER(doc.get_docid()));
+               return std::string (ti && ti->threadpath ? ti->threadpath : "");
+       }
+private:
+       GHashTable *_threadinfo;
+};
+
+struct _MuMsgIter {
+public:
+       _MuMsgIter (Xapian::Enquire &enq, size_t maxnum,
+                   MuMsgFieldId sortfield, MuMsgIterFlags flags):
+               _enq(enq), _thread_hash (0), _msg(0), _flags(flags),
+               _skip_unreadable(flags & MU_MSG_ITER_FLAG_SKIP_UNREADABLE),
+               _skip_dups (flags & MU_MSG_ITER_FLAG_SKIP_DUPS) {
+
+               bool descending       = (flags & MU_MSG_ITER_FLAG_DESCENDING);
+               bool threads          = (flags & MU_MSG_ITER_FLAG_THREADS);
+
+               // first, we get _all_ matches (G_MAXINT), based the threads
+               // on that, then return <maxint> of those
+               _matches         = _enq.get_mset (0, G_MAXINT);
+
+               if (_matches.empty())
+                       return;
+
+               if (threads) {
+                       _matches.fetch();
+                       _cursor = _matches.begin();
+                       // NOTE: temporarily turn-off skipping duplicates, since we
+                       // need threadinfo for *all*
+                       _skip_dups = false;
+                       _thread_hash = mu_threader_calculate
+                               (this, _matches.size(), sortfield, descending);
+                       _skip_dups = (flags & MU_MSG_ITER_FLAG_SKIP_DUPS);
+                       ThreadKeyMaker  keymaker(_thread_hash);
+                       enq.set_sort_by_key (&keymaker, false);
+                       _matches   = _enq.get_mset (0, maxnum);
+
+               } else if (sortfield != MU_MSG_FIELD_ID_NONE) {
+                       enq.set_sort_by_value ((Xapian::valueno)sortfield,
+                                              descending);
+                       _matches   = _enq.get_mset (0, maxnum);
+                       _cursor    = _matches.begin();
+               }
+               _cursor = _matches.begin();
+       }
+
+       ~_MuMsgIter () {
+               if (_thread_hash)
+                       g_hash_table_destroy (_thread_hash);
+
+               set_msg (NULL);
+       }
+
+       const Xapian::Enquire& enquire() const { return _enq; }
+       Xapian::MSet& matches() { return _matches; }
+
+       Xapian::MSet::const_iterator cursor () const { return _cursor; }
+       void set_cursor (Xapian::MSetIterator cur) { _cursor = cur; }
+       void cursor_next () { ++_cursor; }
+
+       GHashTable *thread_hash () { return _thread_hash; }
+
+       MuMsg *msg() const { return _msg; }
+       MuMsg *set_msg (MuMsg *msg) {
+               if (_msg)
+                       mu_msg_unref (_msg);
+               return _msg = msg;
+       }
+
+       MuMsgIterFlags flags() const { return _flags; }
+
+       const std::string msgid () const {
+               const Xapian::Document doc (cursor().get_document());
+               return doc.get_value(MU_MSG_FIELD_ID_MSGID);
+       }
+
+       unsigned docid () const {
+               const Xapian::Document doc (cursor().get_document());
+               return doc.get_docid();
+       }
+
+       bool looks_like_dup () const {
+               try {
+                       const Xapian::Document doc (cursor().get_document());
+                       // is this message in the preferred map? if
+                       // so, it's not a duplicate, otherwise, it
+                       // isn't
+                       msgid_docid_map::const_iterator pref_iter (_preferred_map.find (msgid()));
+                       if (pref_iter != _preferred_map.end()) {
+                               //std::cerr << "in the set!" << std::endl;
+                               if ((*pref_iter).second == docid())
+                                       return false; // in the set: not a dup!
+                               else
+                                       return true;
+                       }
+
+                       // otherwise, simply check if we've already seen this message-id,
+                       // and, if so, it's considered a dup
+                       if (_msg_uid_set.find (msgid()) != _msg_uid_set.end()) {
+                               return true;
+                       } else {
+                               _msg_uid_set.insert (msgid());
+                               return false;
+                       }
+               } catch (...) {
+                       return true;
+               }
+       }
+
+       static void each_preferred (const char *msgid, gpointer docidp,
+                                   msgid_docid_map *preferred_map) {
+               (*preferred_map)[msgid] = GPOINTER_TO_SIZE(docidp);
+       }
+
+       void set_preferred_map (GHashTable *preferred_hash) {
+               if (!preferred_hash)
+                       _preferred_map.clear();
+               else
+                       g_hash_table_foreach (preferred_hash,
+                                             (GHFunc)each_preferred, &_preferred_map);
+       }
+
+       bool skip_dups ()       const { return _skip_dups; }
+       bool skip_unreadable () const { return _skip_unreadable; }
+
+private:
+       const Xapian::Enquire           _enq;
+       Xapian::MSet                    _matches;
+       Xapian::MSet::const_iterator    _cursor;
+
+       GHashTable      *_thread_hash;
+       MuMsg           *_msg;
+
+       MuMsgIterFlags _flags;
+
+       mutable std::set <std::string, ltstr> _msg_uid_set;
+       bool _skip_unreadable;
+
+       // the 'preferred map' (msgid->docid) is used when checking
+       // for duplicates; if a message is in the preferred map, it
+       // will not be excluded (but other messages with the same
+       // msgid will)
+       msgid_docid_map _preferred_map;
+       bool _skip_dups;
+};
+
+static gboolean
+is_msg_file_readable (MuMsgIter *iter)
+{
+       gboolean readable;
+       std::string path
+               (iter->cursor().get_document().get_value(MU_MSG_FIELD_ID_PATH));
+
+       if (path.empty())
+               return FALSE;
+
+       readable = (access (path.c_str(), R_OK) == 0) ? TRUE : FALSE;
+       return readable;
+}
+
+
+MuMsgIter*
+mu_msg_iter_new (XapianEnquire *enq, size_t maxnum,
+                MuMsgFieldId sortfield, MuMsgIterFlags flags,
+                GError **err)
+{
+       g_return_val_if_fail (enq, NULL);
+       /* sortfield should be set to .._NONE when we're not threading */
+       g_return_val_if_fail (mu_msg_field_id_is_valid (sortfield) ||
+                             sortfield == MU_MSG_FIELD_ID_NONE,
+                             FALSE);
+       try {
+               MuMsgIter *iter (new MuMsgIter ((Xapian::Enquire&)*enq,
+                                               maxnum,
+                                               sortfield,
+                                               flags));
+               // note: we check if it's a dup even for the first message,
+               // since we need its uid in the set for checking later messages
+               if ((iter->skip_unreadable() && !is_msg_file_readable (iter)) ||
+                   (iter->skip_dups() && iter->looks_like_dup ()))
+                       mu_msg_iter_next (iter); /* skip! */
+
+               return iter;
+
+       } catch (const Xapian::DatabaseModifiedError &dbmex) {
+               mu_util_g_set_error (err, MU_ERROR_XAPIAN_MODIFIED,
+                                    "database was modified; please reopen");
+               return 0;
+
+       } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN (err, MU_ERROR_XAPIAN, 0);
+}
+
+void
+mu_msg_iter_destroy (MuMsgIter *iter)
+{
+       try { delete iter; } MU_XAPIAN_CATCH_BLOCK;
+}
+
+void
+mu_msg_iter_set_preferred (MuMsgIter *iter, GHashTable *preferred_hash)
+{
+       g_return_if_fail (iter);
+       iter->set_preferred_map (preferred_hash);
+}
+
+MuMsg*
+mu_msg_iter_get_msg_floating (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, NULL);
+       g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL);
+
+       try {
+               MuMsg *msg;
+               GError *err;
+               Xapian::Document *docp;
+
+               docp = new Xapian::Document(iter->cursor().get_document());
+
+               err = NULL;
+               msg = iter->set_msg (mu_msg_new_from_doc((XapianDocument*)docp,
+                                                        &err));
+               if (!msg)
+                       MU_HANDLE_G_ERROR(err);
+
+               return msg;
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL);
+}
+
+gboolean
+mu_msg_iter_reset (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, FALSE);
+
+       iter->set_msg (NULL);
+
+       try {
+               iter->set_cursor(iter->matches().begin());
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (FALSE);
+
+       return TRUE;
+}
+
+gboolean
+mu_msg_iter_next (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, FALSE);
+
+       iter->set_msg (NULL);
+
+       if (mu_msg_iter_is_done(iter))
+               return FALSE;
+
+       try {
+               iter->cursor_next();
+
+               if (iter->cursor() == iter->matches().end())
+                       return FALSE;
+
+               if ((iter->skip_unreadable() && !is_msg_file_readable (iter)) ||
+                   (iter->skip_dups() && iter->looks_like_dup ()))
+                       return mu_msg_iter_next (iter); /* skip! */
+
+               return TRUE;
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE);
+}
+
+
+gboolean
+mu_msg_iter_is_done (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, TRUE);
+
+       try {
+               return iter->cursor() == iter->matches().end() ? TRUE : FALSE;
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (TRUE);
+}
+
+gboolean
+mu_msg_iter_is_first  (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, FALSE);
+
+       return iter->cursor() == iter->matches().begin();
+}
+
+gboolean
+mu_msg_iter_is_last  (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, FALSE);
+
+       if (mu_msg_iter_is_done (iter))
+               return FALSE;
+
+       return iter->cursor() + 1 == iter->matches().end();
+}
+
+/* hmmm.... is it impossible to get a 0 docid, or just very improbable? */
+unsigned
+mu_msg_iter_get_docid (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, (unsigned int)-1);
+       g_return_val_if_fail (!mu_msg_iter_is_done(iter),
+                             (unsigned int)-1);
+       try {
+               return iter->docid();
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN ((unsigned int)-1);
+}
+
+
+char*
+mu_msg_iter_get_msgid (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, NULL);
+       g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL);
+
+       try {
+               return g_strdup (iter->msgid().c_str());
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL);
+}
+
+char**
+mu_msg_iter_get_refs (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, NULL);
+       g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL);
+
+       try {
+               std::string refs (
+                       iter->cursor().get_document().get_value(MU_MSG_FIELD_ID_REFS));
+               if (refs.empty())
+                       return NULL;
+               return g_strsplit (refs.c_str(),",", -1);
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL);
+}
+
+char*
+mu_msg_iter_get_thread_id (MuMsgIter *iter)
+{
+       g_return_val_if_fail (iter, NULL);
+       g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL);
+
+       try {
+               const std::string thread_id (
+                       iter->cursor().get_document().get_value(MU_MSG_FIELD_ID_THREAD_ID).c_str());
+               return thread_id.empty() ? NULL : g_strdup (thread_id.c_str());
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL);
+}
+
+const MuMsgIterThreadInfo*
+mu_msg_iter_get_thread_info (MuMsgIter *iter)
+{
+       g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL);
+
+       /* maybe we don't have thread info */
+       if (!iter->thread_hash())
+               return NULL;
+
+       try {
+               const MuMsgIterThreadInfo *ti;
+               unsigned int docid;
+
+               docid = mu_msg_iter_get_docid (iter);
+               ti    = (const MuMsgIterThreadInfo*)g_hash_table_lookup
+                       (iter->thread_hash(), GUINT_TO_POINTER(docid));
+
+               if (!ti)
+                       g_warning ("no ti for %u\n", docid);
+
+               return ti;
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL);
+}
diff --git a/lib/mu-msg-iter.h b/lib/mu-msg-iter.h
new file mode 100644 (file)
index 0000000..bce6a50
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_MSG_ITER_H__
+#define __MU_MSG_ITER_H__
+
+#include <glib.h>
+#include <mu-msg.h>
+
+G_BEGIN_DECLS
+
+
+/**
+ * MuMsgIter is a structure to iterate over the results of a
+ * query. You can iterate only in one-direction, and you can do it
+ * only once.
+ *
+ */
+
+struct _MuMsgIter;
+typedef struct _MuMsgIter MuMsgIter;
+
+
+enum _MuMsgIterFlags {
+       MU_MSG_ITER_FLAG_NONE                 = 0,
+       /* sort Z->A (only for threads) */
+       MU_MSG_ITER_FLAG_DESCENDING           = 1 << 0,
+       /* ignore results for which there is no existing
+        * readable message-file? */
+       MU_MSG_ITER_FLAG_SKIP_UNREADABLE      = 1 << 1,
+       /* ignore duplicate messages? */
+       MU_MSG_ITER_FLAG_SKIP_DUPS            = 1 << 2,
+       /* calculate threads? */
+       MU_MSG_ITER_FLAG_THREADS              = 1 << 3
+};
+typedef unsigned MuMsgIterFlags;
+
+/**
+ * create a new MuMsgIter -- basically, an iterator over the search
+ * results
+ *
+ * @param enq a Xapian::Enquire* cast to XapianEnquire* (because this
+ * is C, not C++),providing access to search results
+ * @param maxnum the maximum number of results
+ * @param sortfield field to sort by
+ * @param flags flags for this iterator (see MsgIterFlags)
+
+ * @param err receives error information. if the error is
+ * MU_ERROR_XAPIAN_MODIFIED, the database should be reloaded.
+ *
+ * @return a new MuMsgIter, or NULL in case of error
+ */
+MuMsgIter *mu_msg_iter_new (XapianEnquire *enq,
+                           size_t maxnum,
+                           MuMsgFieldId sortfield,
+                           MuMsgIterFlags flags,
+                           GError **err) G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * get the next message (which you got from
+ * e.g. mu_query_run)
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return TRUE if it succeeded, FALSE otherwise (e.g., because there
+ * are no more messages in the query result)
+ */
+gboolean  mu_msg_iter_next  (MuMsgIter *iter);
+
+/**
+ * Does this iterator point to the first item?
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return TRUE or FALSE
+ */
+gboolean  mu_msg_iter_is_first  (MuMsgIter *iter);
+
+/**
+ * Does this iterator point to the last item?
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return TRUE or FALSE
+ */
+gboolean  mu_msg_iter_is_last  (MuMsgIter *iter);
+
+
+/**
+ * reset the iterator to the beginning
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return TRUE if it succeeded, FALSE otherwise
+ */
+gboolean mu_msg_iter_reset (MuMsgIter *iter);
+
+/**
+ * does this iterator point past the end of the list?
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return TRUE if the iter points past end of the list, FALSE
+ * otherwise
+ */
+gboolean         mu_msg_iter_is_done (MuMsgIter *iter);
+
+
+/**
+ * destroy the sequence of messages; ie. /all/ of them
+ *
+ * @param msg a valid MuMsgIter message or NULL
+ */
+void            mu_msg_iter_destroy           (MuMsgIter *iter);
+
+
+/**
+ * get the corresponding MuMsg for this iter; this instance is owned
+ * by MuMsgIter, and becomes invalid after either mu_msg_iter_destroy
+ * or mu_msg_iter_next. _do not_ unref it; it's a floating reference.
+ *
+ * @param iter a valid MuMsgIter instance*
+ *
+ * @return a MuMsg instance, or NULL in case of error
+ */
+MuMsg* mu_msg_iter_get_msg_floating (MuMsgIter *iter)
+          G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * Provide a preferred_hash, which is a hashtable msgid->docid to
+ * indicate the messages which should /not/ be seen as duplicates.
+ *
+ * @param iter a valid MuMsgIter iterator
+ * @param preferred_hash a hashtable msgid->docid of message /not/ to
+ * mark as duplicates, or NULL
+ */
+void mu_msg_iter_set_preferred (MuMsgIter *iter, GHashTable *preferred_hash);
+
+/**
+ * get the document id for the current message
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return the docid or (unsigned int)-1 in case of error
+ */
+guint    mu_msg_iter_get_docid         (MuMsgIter *iter);
+
+
+/**
+ * calculate the message threads
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return TRUE if it worked, FALSE otherwise.
+ */
+gboolean mu_msg_iter_calculate_threads (MuMsgIter *iter);
+
+
+enum _MuMsgIterThreadProp {
+       MU_MSG_ITER_THREAD_PROP_NONE           = 0 << 0,
+
+       MU_MSG_ITER_THREAD_PROP_ROOT           = 1 << 0,
+       MU_MSG_ITER_THREAD_PROP_FIRST_CHILD    = 1 << 1,
+       MU_MSG_ITER_THREAD_PROP_LAST_CHILD     = 1 << 2,
+       MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT   = 1 << 3,
+       MU_MSG_ITER_THREAD_PROP_DUP            = 1 << 4,
+       MU_MSG_ITER_THREAD_PROP_HAS_CHILD      = 1 << 5
+};
+typedef guint8 MuMsgIterThreadProp;
+
+struct _MuMsgIterThreadInfo {
+       gchar *threadpath; /* a string describing the thread-path in
+                           * such a way that we can sort by this
+                           * string to get the right order. */
+       guint level;       /* thread-depth -- [0...] */
+       MuMsgIterThreadProp prop;
+};
+typedef struct _MuMsgIterThreadInfo MuMsgIterThreadInfo;
+
+/**
+ * get a the MuMsgThreaderInfo struct for this message; this only
+ * works when you created the mu-msg-iter with threading enabled
+ * (otherwise, return NULL)
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return an info struct
+ */
+const MuMsgIterThreadInfo* mu_msg_iter_get_thread_info (MuMsgIter *iter);
+
+/**
+ * get the message-id for this message
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return the message-id; free with g_free().
+ */
+char* mu_msg_iter_get_msgid (MuMsgIter *iter)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * get the list of references for this messages as a NULL-terminated
+ * string array
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return a NULL-terminated string array. free with g_strfreev when
+ * it's no longer needed.
+ */
+char** mu_msg_iter_get_refs (MuMsgIter *iter)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * get the thread-id for this message
+ *
+ * @param iter a valid MuMsgIter iterator
+ *
+ * @return the thread-id; free with g_free().
+ */
+char* mu_msg_iter_get_thread_id (MuMsgIter *iter)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+/* FIXME */
+const char* mu_msg_iter_get_path (MuMsgIter *iter);
+
+G_END_DECLS
+
+#endif /*__MU_MSG_ITER_H__*/
diff --git a/lib/mu-msg-json.c b/lib/mu-msg-json.c
new file mode 100644 (file)
index 0000000..52b20f9
--- /dev/null
@@ -0,0 +1,524 @@
+/*
+** Copyright (C) 2018 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.h>
+#include <ctype.h>
+
+#include <json-glib/json-glib.h>
+
+#include "mu-msg.h"
+#include "mu-msg-iter.h"
+#include "mu-msg-part.h"
+#include "mu-maildir.h"
+
+static void
+add_list_member (JsonBuilder *bob, const char* elm, const GSList *lst)
+{
+       const GSList *cur;
+
+       if (!lst)
+               return; /* empty list, don't include */
+
+       bob = json_builder_set_member_name (bob, elm);
+       bob = json_builder_begin_array (bob);
+
+       for (cur = lst; cur; cur = g_slist_next(cur))
+               bob = json_builder_add_string_value (bob, (const char*)cur->data);
+
+       bob = json_builder_end_array (bob);
+}
+
+static void
+add_string_member (JsonBuilder *bob, const char* elm, const char *str)
+{
+       if (!str)
+               return; /* don't include */
+
+       bob = json_builder_set_member_name (bob, elm);
+       bob = json_builder_add_string_value (bob, str);
+}
+
+static void
+add_int_member (JsonBuilder *bob, const char* elm, gint64 num)
+{
+       bob = json_builder_set_member_name (bob, elm);
+       bob = json_builder_add_int_value (bob, num);
+}
+
+static void
+add_bool_member (JsonBuilder *bob, const char* elm, gboolean b)
+{
+       bob = json_builder_set_member_name (bob, elm);
+       bob = json_builder_add_boolean_value (bob, b);
+}
+
+
+static void
+consume_array_member (JsonBuilder *bob, const char* elm, JsonArray *arr)
+{
+       JsonNode *node;
+
+       if (!arr)
+               return; /* nothing to do */
+
+       node = json_node_new (JSON_NODE_ARRAY);
+       json_node_init_array (node, arr);
+       json_array_unref (arr);
+
+       bob = json_builder_set_member_name (bob, elm);
+       bob = json_builder_add_value (bob, node); /* consumes */
+}
+
+
+
+typedef struct {
+       JsonArray *from, *to, *cc, *bcc, *reply_to;
+} ContactData;
+
+static void
+add_contact (JsonArray **arr, MuMsgContact *c)
+{
+       JsonObject *cell;
+
+       if (!*arr)
+               *arr = json_array_new ();
+
+       cell = json_object_new ();
+       if (c->name)
+               json_object_set_string_member (cell, "name", c->name);
+       if (c->email)
+               json_object_set_string_member (cell, "email", c->email);
+
+       json_array_add_object_element (*arr, cell); /* consumes */
+}
+
+static gboolean
+each_contact (MuMsgContact *c, ContactData *cdata)
+{
+       switch (mu_msg_contact_type (c)) {
+
+       case MU_MSG_CONTACT_TYPE_FROM:
+               add_contact(&cdata->from, c);
+               break;
+       case MU_MSG_CONTACT_TYPE_TO:
+               add_contact(&cdata->to ,c);
+               break;
+       case MU_MSG_CONTACT_TYPE_CC:
+               add_contact(&cdata->cc, c);
+               break;
+       case MU_MSG_CONTACT_TYPE_BCC:
+               add_contact(&cdata->bcc, c);
+               break;
+       case MU_MSG_CONTACT_TYPE_REPLY_TO:
+               add_contact(&cdata->reply_to, c);
+               break;
+       default: g_return_val_if_reached (FALSE);
+       }
+
+       return TRUE;
+}
+
+
+static void
+maybe_append_list_post_as_reply_to (JsonBuilder *bob, MuMsg *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 char*      list_post;
+
+       list_post = mu_msg_get_header (msg, "List-Post");
+       if (!list_post)
+               return;
+
+       rx = g_regex_new ("^(<?mailto:)?([a-z0-9%+@.-]+)>?", G_REGEX_CASELESS, 0, NULL);
+       g_return_if_fail(rx);
+
+       if (g_regex_match (rx, list_post, 0, &minfo)) {
+               char    *addr;
+               addr = g_match_info_fetch (minfo, 2);
+
+               bob = json_builder_set_member_name (bob, "reply-to");
+               bob = json_builder_begin_array(bob);
+               bob = json_builder_begin_object(bob);
+               add_string_member(bob, "email", addr);
+               g_free (addr);
+
+               bob = json_builder_end_object(bob);
+               bob = json_builder_end_array(bob);
+       }
+
+       g_match_info_free (minfo);
+       g_regex_unref (rx);
+}
+
+
+static void
+add_contacts (JsonBuilder *bob, MuMsg *msg)
+{
+       ContactData cdata;
+       memset (&cdata, 0, sizeof(cdata));
+
+       mu_msg_contact_foreach (msg,
+                               (MuMsgContactForeachFunc)each_contact,
+                               &cdata);
+
+       consume_array_member (bob, "to" ,      cdata.to);
+       consume_array_member (bob, "from" ,    cdata.from);
+       consume_array_member (bob, "cc" ,      cdata.cc);
+       consume_array_member (bob, "bcc" ,     cdata.bcc);
+       consume_array_member (bob, "reply-to", cdata.reply_to);
+
+       if (!cdata.reply_to)
+               maybe_append_list_post_as_reply_to (bob, msg);
+}
+
+struct _FlagData {
+       JsonBuilder             *bob;
+       MuFlags                  msgflags;
+};
+typedef struct _FlagData        FlagData;
+
+static void
+each_flag (MuFlags flag, FlagData *fdata)
+{
+       if (!(flag & fdata->msgflags))
+               return;
+
+       json_builder_add_string_value (fdata->bob,
+                                      mu_flag_name(flag));
+}
+
+static void
+add_flags (JsonBuilder *bob, MuMsg *msg)
+{
+       FlagData fdata;
+
+       fdata.msgflags = mu_msg_get_flags (msg);
+       fdata.bob      = bob;
+
+       bob = json_builder_set_member_name (bob, "flags");
+
+       bob = json_builder_begin_array (bob);
+       mu_flags_foreach ((MuFlagsForeachFunc)each_flag, &fdata);
+       bob = json_builder_end_array (bob);
+
+}
+
+static char*
+get_temp_file (MuMsg *msg, MuMsgOptions opts, unsigned index)
+{
+       char *path;
+       GError *err;
+
+       err = NULL;
+       path = mu_msg_part_get_cache_path (msg, opts, index, &err);
+       if (!path)
+               goto errexit;
+
+       if (!mu_msg_part_save (msg, opts, path, index, &err))
+               goto errexit;
+
+       return path;
+
+errexit:
+       g_warning ("failed to save mime part: %s",
+                  err->message ? err->message : "something went wrong");
+       g_clear_error (&err);
+       g_free (path);
+       return NULL;
+}
+
+
+static gchar*
+get_temp_file_maybe (MuMsg *msg, MuMsgPart *part, MuMsgOptions opts)
+{
+       opts |= MU_MSG_OPTION_USE_EXISTING;
+
+       if  (!(opts & MU_MSG_OPTION_EXTRACT_IMAGES) ||
+            g_ascii_strcasecmp (part->type, "image") != 0)
+               return NULL;
+
+       return get_temp_file (msg, opts, part->index);
+}
+
+
+struct _PartInfo {
+       JsonBuilder             *bob;
+       MuMsgOptions             opts;
+};
+typedef struct _PartInfo        PartInfo;
+
+
+static void
+add_part_crypto (JsonBuilder *bob, MuMsgPart *mpart, PartInfo *pinfo)
+{
+       const char                      *verdict;
+       MuMsgPartSigStatusReport        *report;
+
+
+       add_string_member (bob, "decryption",
+                          pinfo->opts & MU_MSG_PART_TYPE_DECRYPTED ? "ok" :
+                          pinfo->opts & MU_MSG_PART_TYPE_ENCRYPTED ? "failed" :
+                          NULL);
+
+       report = mpart->sig_status_report;
+       if (!report)
+               return;
+
+       switch (report->verdict) {
+       case MU_MSG_PART_SIG_STATUS_GOOD:  verdict = "verified"; break;
+       case MU_MSG_PART_SIG_STATUS_BAD:   verdict = "bad"; break;
+       case MU_MSG_PART_SIG_STATUS_ERROR: verdict = "unverified"; break;
+       default: verdict = NULL;
+       }
+
+       add_string_member (bob, "signature", verdict);
+       add_string_member (bob, "signers", report->signers);
+}
+
+static void
+add_part_type (JsonBuilder *bob, MuMsgPartType ptype)
+{
+       unsigned u;
+       struct PartTypes {
+               MuMsgPartType ptype;
+               const char* name;
+       } ptypes[] = {
+               { MU_MSG_PART_TYPE_LEAF,       "leaf" },
+               { MU_MSG_PART_TYPE_MESSAGE,    "message" },
+               { MU_MSG_PART_TYPE_INLINE,     "inline" },
+               { MU_MSG_PART_TYPE_ATTACHMENT, "attachment" },
+               { MU_MSG_PART_TYPE_SIGNED,     "signed" },
+               { MU_MSG_PART_TYPE_ENCRYPTED,  "encrypted" }
+       };
+
+       bob =  json_builder_set_member_name (bob, "type");
+       bob = json_builder_begin_array(bob);
+
+       for (u = 0; u!= G_N_ELEMENTS(ptypes); ++u)
+               if (ptype & ptypes[u].ptype)
+                       json_builder_add_string_value (bob, ptypes[u].name);
+
+       bob = json_builder_end_array(bob);
+}
+
+
+static void
+each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo)
+{
+       char            *name, *tmpfile;
+
+       pinfo->bob = json_builder_begin_object(pinfo->bob);
+
+       name     = mu_msg_part_get_filename (part, TRUE);
+       tmpfile  = get_temp_file_maybe (msg, part, pinfo->opts);
+
+       add_int_member    (pinfo->bob, "index", part->index);
+       add_string_member (pinfo->bob, "name", name);
+
+       if (part->type && part->subtype) {
+               char *mime_type =
+                       g_strdup_printf ("%s/%s", part->type, part->subtype);
+               add_string_member (pinfo->bob, "mime-type", mime_type);
+               g_free(mime_type);
+       }
+
+       add_string_member (pinfo->bob, "temp", tmpfile);
+       add_part_type (pinfo->bob, part->part_type);
+
+       if (mu_msg_part_maybe_attachment (part))
+               add_bool_member (pinfo->bob, "attachment", TRUE);
+
+       add_string_member (pinfo->bob, "cid", mu_msg_part_get_content_id(part));
+       add_int_member (pinfo->bob, "size", part->size);
+
+       add_part_crypto (pinfo->bob, part, pinfo);
+
+       g_free (name);
+       g_free (tmpfile);
+
+       pinfo->bob = json_builder_end_object(pinfo->bob);
+}
+
+
+static void
+add_parts (JsonBuilder *bob, MuMsg *msg, MuMsgOptions opts)
+{
+       PartInfo pinfo;
+
+       pinfo.opts = opts;
+       bob        = json_builder_set_member_name (bob, "parts");
+       bob        = json_builder_begin_array (bob);
+
+       mu_msg_part_foreach (msg, opts, (MuMsgPartForeachFunc)each_part, &pinfo);
+
+       bob = json_builder_end_array (bob);
+}
+
+static void
+add_thread_info (JsonBuilder *bob, const MuMsgIterThreadInfo *ti)
+{
+       bob = json_builder_set_member_name (bob, "thread");
+       bob = json_builder_begin_object(bob);
+
+       add_string_member (bob, "path",  ti->threadpath);
+       add_int_member    (bob, "level", ti->level);
+
+       bob = json_builder_set_member_name (bob, "flags");
+       bob = json_builder_begin_array (bob);
+
+       if (ti->prop & MU_MSG_ITER_THREAD_PROP_FIRST_CHILD)
+               bob = json_builder_add_string_value (bob, "first-child");
+       if (ti->prop & MU_MSG_ITER_THREAD_PROP_LAST_CHILD)
+               bob = json_builder_add_string_value (bob, "last-child");
+       if (ti->prop & MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT)
+               bob = json_builder_add_string_value (bob, "empty-parent");
+       if (ti->prop & MU_MSG_ITER_THREAD_PROP_DUP)
+               bob = json_builder_add_string_value (bob, "duplicate");
+       if (ti->prop & MU_MSG_ITER_THREAD_PROP_HAS_CHILD)
+               bob = json_builder_add_string_value (bob, "has-child");
+
+       bob = json_builder_end_array (bob);
+       bob = json_builder_end_object(bob);
+}
+
+static void
+add_body_txt_params (JsonBuilder *bob, MuMsg *msg, MuMsgOptions opts)
+{
+       const GSList    *params;
+
+       params = mu_msg_get_body_text_content_type_parameters (msg, opts);
+       if (!params)
+               return;
+
+       bob = json_builder_set_member_name (bob, "body-txt-params");
+       bob = json_builder_begin_array (bob);
+
+        while (params) {
+               const char *key, *val;
+
+               key = (const char *)params->data;
+               params = g_slist_next(params);
+               if (!params)
+                       break;
+               val = (const char *)params->data;
+
+                if (key && val) {
+                       bob = json_builder_begin_object(bob);
+                       add_string_member(bob, key, val);
+                       bob = json_builder_end_object(bob);
+                }
+
+               params = g_slist_next(params);
+        }
+
+        bob = json_builder_end_array(bob);
+}
+
+static void /* ie., parts that require opening the message file */
+add_file_parts (JsonBuilder *bob, MuMsg *msg, MuMsgOptions opts)
+{
+       const char      *str;
+       GError          *err;
+
+       err = NULL;
+
+       if (!mu_msg_load_msg_file (msg, &err)) {
+               g_warning ("failed to load message file: %s",
+                          err ? err->message : "some error occurred");
+               g_clear_error (&err);
+               return;
+       }
+
+       add_parts (bob, msg, opts);
+       add_contacts (bob, msg);
+
+       /* add the user-agent / x-mailer */
+       str = mu_msg_get_header (msg, "User-Agent");
+       if (!str)
+               str = mu_msg_get_header (msg, "X-Mailer");
+       add_string_member (bob, "user-agent", str);
+       add_body_txt_params (bob, msg, opts);
+       add_string_member (bob, "body-txt", mu_msg_get_body_text(msg, opts));
+       add_string_member (bob, "body-html", mu_msg_get_body_html(msg, opts));
+}
+
+struct _JsonNode*
+mu_msg_to_json (MuMsg *msg, unsigned docid, const MuMsgIterThreadInfo *ti,
+               MuMsgOptions opts)
+{
+       JsonNode        *node;
+       JsonBuilder     *bob;
+
+       time_t  t;
+       size_t  s;
+
+       g_return_val_if_fail (msg, NULL);
+       g_return_val_if_fail (!((opts & MU_MSG_OPTION_HEADERS_ONLY) &&
+                               (opts & MU_MSG_OPTION_EXTRACT_IMAGES)),NULL);
+       bob = json_builder_new ();
+       bob = json_builder_begin_object (bob);
+
+       if (ti)
+               add_thread_info (bob, ti);
+
+       add_string_member (bob, "subject", mu_msg_get_subject (msg));
+
+       /* in the no-headers-only case (see below) we get a more complete list
+        * of contacts, so no need to get them here if that's the case */
+       if (opts & MU_MSG_OPTION_HEADERS_ONLY)
+               add_contacts (bob, msg);
+
+       t = mu_msg_get_date (msg);
+       if (t != (time_t)-1)
+               add_int_member (bob, "date", t);
+
+       s = mu_msg_get_size (msg);
+       if (s != (size_t)-1)
+               add_int_member (bob, "size", s);
+
+       add_string_member (bob, "message-id", mu_msg_get_msgid (msg));
+       add_string_member (bob, "mailing-list", mu_msg_get_mailing_list (msg));
+       add_string_member (bob, "path", mu_msg_get_path (msg));
+       add_string_member (bob, "maildir", mu_msg_get_maildir (msg));
+       add_string_member (bob, "priority", mu_msg_prio_name(mu_msg_get_prio(msg)));
+
+       add_flags (bob, msg);
+
+       add_list_member (bob, "tags", mu_msg_get_tags(msg));
+       add_list_member (bob, "references", mu_msg_get_references (msg));
+       add_string_member (bob, "in-reply-to",
+                          mu_msg_get_header (msg, "In-Reply-To"));
+
+       /* headers are retrieved from the database, views from the
+        * message file file attr things can only be gotten from the
+        * file (ie., mu view), not from the database (mu find).  */
+       if (!(opts & MU_MSG_OPTION_HEADERS_ONLY))
+               add_file_parts (bob, msg, opts);
+
+       bob  = json_builder_end_object (bob);
+       node = json_builder_get_root (bob);
+
+       g_clear_object (&bob);
+
+       return node;
+}
diff --git a/lib/mu-msg-part.c b/lib/mu-msg-part.c
new file mode 100644 (file)
index 0000000..099274d
--- /dev/null
@@ -0,0 +1,1009 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+/*
+** Copyright (C) 2008-2014 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 <string.h>
+#include <unistd.h>
+
+#include "utils/mu-util.h"
+#include "utils/mu-str.h"
+#include "mu-msg-priv.h"
+#include "mu-msg-part.h"
+
+struct _DoData {
+       GMimeObject *mime_obj;
+       unsigned    index;
+};
+typedef struct _DoData DoData;
+
+static void
+do_it_with_index (MuMsg *msg, MuMsgPart *part, DoData *ddata)
+{
+       if (ddata->mime_obj)
+               return;
+
+       if (part->index == ddata->index) {
+               /* Add a reference to this object, this way if it is
+                * encrypted it will not be garbage collected before
+                * we are done with it. */
+               g_object_ref (part->data);
+               ddata->mime_obj = (GMimeObject*)part->data;
+       }
+}
+
+static GMimeObject*
+get_mime_object_at_index (MuMsg *msg, MuMsgOptions opts, unsigned index)
+{
+       DoData ddata;
+
+       ddata.mime_obj  = NULL;
+       ddata.index     = index;
+
+       /* wipe out some irrelevant options */
+       opts &= ~MU_MSG_OPTION_VERIFY;
+       opts &= ~MU_MSG_OPTION_EXTRACT_IMAGES;
+
+       mu_msg_part_foreach (msg, opts,
+                            (MuMsgPartForeachFunc)do_it_with_index,
+                            &ddata);
+
+       return ddata.mime_obj;
+}
+
+
+typedef gboolean (*MuMsgPartMatchFunc) (MuMsgPart *, gpointer);
+struct _MatchData {
+       MuMsgPartMatchFunc match_func;
+       gpointer user_data;
+       int index;
+};
+typedef struct _MatchData MatchData;
+
+static void
+check_match (MuMsg *msg, MuMsgPart *part, MatchData *mdata)
+{
+       if (mdata->index != -1)
+               return;
+
+       if (mdata->match_func (part, mdata->user_data))
+               mdata->index = part->index;
+}
+
+static int
+get_matching_part_index (MuMsg *msg, MuMsgOptions opts,
+                        MuMsgPartMatchFunc func, gpointer user_data)
+{
+       MatchData mdata;
+
+       mdata.match_func = func;
+       mdata.user_data  = user_data;
+       mdata.index      = -1;
+
+       mu_msg_part_foreach (msg, opts,
+                            (MuMsgPartForeachFunc)check_match,
+                            &mdata);
+       return mdata.index;
+}
+
+
+static void
+accumulate_text_message (MuMsg *msg, MuMsgPart *part, GString **gstrp)
+{
+       const gchar             *str;
+       char                    *adrs;
+       GMimeMessage            *mimemsg;
+       InternetAddressList     *addresses;
+
+       /* put sender, recipients and subject in the string, so they
+        * can be indexed as well */
+       mimemsg   = GMIME_MESSAGE (part->data);
+       addresses = g_mime_message_get_addresses (mimemsg, GMIME_ADDRESS_TYPE_FROM);
+       adrs      = internet_address_list_to_string (addresses, NULL, FALSE);
+
+       g_string_append_printf
+               (*gstrp, "%s%s", adrs ? adrs : "", adrs ? "\n" : "");
+       g_free (adrs);
+
+       str = g_mime_message_get_subject (mimemsg);
+       g_string_append_printf
+               (*gstrp, "%s%s", str ? str : "", str ? "\n" : "");
+
+       addresses = g_mime_message_get_all_recipients (mimemsg);
+       adrs      = internet_address_list_to_string (addresses, NULL, FALSE);
+       g_object_unref (addresses);
+
+       g_string_append_printf
+               (*gstrp, "%s%s", adrs ? adrs : "", adrs ? "\n" : "");
+       g_free (adrs);
+}
+
+static void
+accumulate_text_part (MuMsg *msg, MuMsgPart *part, GString **gstrp)
+{
+       GMimeContentType        *ctype;
+       gboolean                 err;
+       char                    *txt;
+
+       ctype = g_mime_object_get_content_type ((GMimeObject*)part->data);
+       if (!g_mime_content_type_is_type (ctype, "text", "plain"))
+               return; /* not plain text */
+
+       txt = mu_msg_mime_part_to_string((GMimePart*)part->data, &err);
+       if (txt)
+               g_string_append (*gstrp, txt);
+
+       g_free (txt);
+}
+
+static void
+accumulate_text (MuMsg *msg, MuMsgPart *part, GString **gstrp)
+{
+       if (GMIME_IS_MESSAGE(part->data))
+               accumulate_text_message (msg, part, gstrp);
+       else if (GMIME_IS_PART (part->data))
+               accumulate_text_part (msg, part, gstrp);
+}
+
+/* declaration, so we can use it earlier */
+static gboolean
+handle_mime_object (MuMsg *msg, GMimeObject *mobj, GMimeObject *parent,
+                   MuMsgOptions opts, unsigned *index, gboolean decrypted,
+                   MuMsgPartForeachFunc func, gpointer user_data);
+
+static char*
+get_text_from_mime_msg (MuMsg *msg, GMimeMessage *mmsg, MuMsgOptions opts)
+{
+       GString *gstr;
+       unsigned index;
+
+       index = 1;
+       gstr  = g_string_sized_new (4096);
+       handle_mime_object (msg,
+                           mmsg->mime_part,
+                           (GMimeObject *) mmsg,
+                           opts,
+                           &index,
+                           FALSE,
+                           (MuMsgPartForeachFunc)accumulate_text,
+                           &gstr);
+
+       return g_string_free (gstr, FALSE);
+}
+
+
+char*
+mu_msg_part_get_text (MuMsg *msg, MuMsgPart *self, MuMsgOptions opts)
+{
+       GMimeObject  *mobj;
+       GMimeMessage *mime_msg;
+       gboolean err;
+
+       g_return_val_if_fail (msg, NULL);
+       g_return_val_if_fail (self && GMIME_IS_OBJECT(self->data),
+                             NULL);
+
+       mobj = (GMimeObject*)self->data;
+
+       err = FALSE;
+       if (GMIME_IS_PART (mobj)) {
+               if (self->part_type & MU_MSG_PART_TYPE_TEXT_PLAIN)
+                       return mu_msg_mime_part_to_string ((GMimePart*)mobj,
+                                                          &err);
+               else
+                       return NULL; /* non-text MimePart */
+       }
+
+       mime_msg = NULL;
+
+       if (GMIME_IS_MESSAGE_PART (mobj))
+               mime_msg = g_mime_message_part_get_message
+                       ((GMimeMessagePart*)mobj);
+       else if (GMIME_IS_MESSAGE (mobj))
+               mime_msg = (GMimeMessage*)mobj;
+
+       /* apparently, g_mime_message_part_get_message may still
+        * return NULL */
+       if (mime_msg)
+               return get_text_from_mime_msg (msg, mime_msg, opts);
+       return NULL;
+}
+
+
+/* note: this will return -1 in case of error or if the size is
+ * unknown */
+static ssize_t
+get_part_size (GMimePart *part)
+{
+       GMimeDataWrapper *wrapper;
+       GMimeStream *stream;
+
+       wrapper = g_mime_part_get_content (part);
+       if (!GMIME_IS_DATA_WRAPPER(wrapper))
+               return -1;
+
+       stream = g_mime_data_wrapper_get_stream (wrapper);
+       if (!stream)
+               return -1; /* no stream -> size is 0 */
+       else
+               return g_mime_stream_length (stream);
+
+       /* NOTE: stream/wrapper are owned by gmime, no unreffing */
+}
+
+
+static char*
+cleanup_filename (char *fname)
+{
+       GString *gstr;
+       gchar *cur;
+       gunichar uc;
+
+       gstr = g_string_sized_new (strlen (fname));
+
+       /* replace control characters, slashes, and colons by '-' */
+       for (cur = fname; cur && *cur; cur = g_utf8_next_char (cur)) {
+               uc = g_utf8_get_char (cur);
+               if (g_unichar_iscntrl (uc) || uc == '/' || uc == ':')
+                       g_string_append_unichar (gstr, '-');
+               else
+                       g_string_append_unichar (gstr, uc);
+       }
+
+       g_free (fname);
+       return g_string_free (gstr, FALSE);
+}
+
+
+/*
+ * when a part doesn't have a filename, it can be useful to 'guess' one based on
+ * its mime-type, which allows other tools to handle them correctly, e.g. from
+ * mu4e.
+ *
+ * For now, we only handle calendar invitations in that way, but others may
+ * follow.
+ */
+static char*
+guess_file_name (GMimeObject *mobj, unsigned index)
+{
+       GMimeContentType *ctype;
+
+       ctype = g_mime_object_get_content_type (mobj);
+
+       /* special case for calendars; map to '.vcs' */
+       if (g_mime_content_type_is_type (ctype, "text", "calendar"))
+               return g_strdup_printf ("vcal-%u.vcs", index);
+
+       /* fallback */
+       return g_strdup_printf ("%u.msgpart", index);
+}
+
+
+static char*
+mime_part_get_filename (GMimeObject *mobj, unsigned index,
+                       gboolean construct_if_needed)
+{
+       gchar *fname;
+
+       fname = NULL;
+
+       if (GMIME_IS_PART (mobj)) {
+               /* the easy case: the part has a filename */
+               fname = (gchar*)g_mime_part_get_filename (GMIME_PART(mobj));
+               if (fname) /* don't include directory components */
+                       fname = g_path_get_basename (fname);
+       }
+
+       if (!fname && !construct_if_needed)
+               return NULL;
+
+       if (GMIME_IS_MESSAGE_PART(mobj)) {
+               GMimeMessage *msg;
+               const char *subj;
+               msg  = g_mime_message_part_get_message
+                       (GMIME_MESSAGE_PART(mobj));
+               subj = g_mime_message_get_subject (msg);
+               fname = g_strdup_printf ("%s.eml", subj ? subj : "message");
+       }
+
+       if (!fname)
+               fname = guess_file_name (mobj, index);
+
+       /* replace control characters, slashes, and colons */
+       fname = cleanup_filename (fname);
+
+       return fname;
+}
+
+
+char*
+mu_msg_part_get_filename (MuMsgPart *mpart, gboolean construct_if_needed)
+{
+       g_return_val_if_fail (mpart, NULL);
+       g_return_val_if_fail (GMIME_IS_OBJECT(mpart->data), NULL);
+
+       return mime_part_get_filename ((GMimeObject*)mpart->data,
+                                      mpart->index, construct_if_needed);
+}
+
+const gchar*
+mu_msg_part_get_content_id (MuMsgPart *mpart)
+{
+       g_return_val_if_fail (mpart, NULL);
+       g_return_val_if_fail (GMIME_IS_OBJECT(mpart->data), NULL);
+       return g_mime_object_get_content_id((GMimeObject*)mpart->data);
+}
+
+
+
+static MuMsgPartType
+get_disposition (GMimeObject *mobj)
+{
+       const char *disp;
+
+       disp = g_mime_object_get_disposition (mobj);
+       if (!disp)
+               return MU_MSG_PART_TYPE_NONE;
+
+       if (strcasecmp (disp, GMIME_DISPOSITION_ATTACHMENT) == 0)
+               return MU_MSG_PART_TYPE_ATTACHMENT;
+
+       if (strcasecmp (disp, GMIME_DISPOSITION_INLINE) == 0)
+               return MU_MSG_PART_TYPE_INLINE;
+
+       return MU_MSG_PART_TYPE_NONE;
+}
+
+/* call 'func' with information about this MIME-part */
+static inline void
+check_signature (MuMsg *msg, GMimeMultipartSigned *part, MuMsgOptions opts)
+{
+       GError *err;
+
+       err = NULL;
+       mu_msg_crypto_verify_part (part, opts, &err);
+       if (err) {
+               g_warning ("error verifying signature: %s", err->message);
+               g_clear_error (&err);
+       }
+}
+
+
+/* Note: this is function will be called by GMime when it needs a
+ * password. However, GMime <= 2.6.10 does not handle
+ * getting passwords correctly, so this might fail.  see:
+ * password_requester in mu-msg-crypto.c */
+static gchar*
+get_console_pw (const char* user_id, const char *prompt_ctx,
+               gboolean reprompt, gpointer user_data)
+{
+       char *prompt, *pass;
+
+       if (!g_mime_check_version(2,6,11))
+               g_printerr (
+                       "*** the gmime library you are using has version "
+                       "%u.%u.%u (<= 2.6.10)\n"
+                       "*** this version has a bug in its password "
+                       "retrieval routine, and probably won't work.\n",
+                       gmime_major_version, gmime_minor_version,
+                       gmime_micro_version);
+
+       if (reprompt)
+               g_print ("Authentication failed. Please try again\n");
+
+       prompt = g_strdup_printf ("Password for %s: ", user_id);
+
+       pass = mu_util_read_password (prompt);
+       g_free (prompt);
+
+       return pass;
+}
+
+
+static gboolean
+handle_encrypted_part (MuMsg *msg, GMimeMultipartEncrypted *part,
+                      MuMsgOptions opts, unsigned *index,
+                      MuMsgPartForeachFunc func, gpointer user_data)
+{
+       GError *err;
+       gboolean rv;
+       GMimeObject *dec;
+       MuMsgPartPasswordFunc pw_func;
+
+       if (opts & MU_MSG_OPTION_CONSOLE_PASSWORD)
+               pw_func = (MuMsgPartPasswordFunc)get_console_pw;
+       else
+               pw_func = NULL;
+
+
+       err = NULL;
+       dec = mu_msg_crypto_decrypt_part (part, opts, pw_func, NULL, &err);
+       if (err) {
+               g_warning ("error decrypting part: %s", err->message);
+               g_clear_error (&err);
+       }
+
+       if (dec) {
+               rv = handle_mime_object (msg, dec, (GMimeObject *) part,
+                                        opts, index, TRUE, func, user_data);
+               g_object_unref (dec);
+       } else {
+               /* On failure to decrypt, list the encrypted part as
+                * an attachment
+                */
+               GMimeObject *encrypted;
+
+               encrypted = g_mime_multipart_get_part (
+                       GMIME_MULTIPART (part), 1);
+
+               g_return_val_if_fail (GMIME_IS_PART(encrypted), FALSE);
+
+               rv = handle_mime_object (msg, encrypted, (GMimeObject *) part,
+                                        opts, index, FALSE, func, user_data);
+       }
+
+       return rv;
+}
+
+static gboolean
+looks_like_text_body_part (GMimeContentType *ctype)
+{
+       unsigned u;
+       static struct {
+               const char *type;
+               const char *subtype;
+       } types[] = {
+               { "text", "plain" },
+               { "text", "x-markdown" },
+               { "text", "x-diff"  },
+               { "text", "x-patch" },
+               { "application", "x-patch"}
+               /* possible other types */
+       };
+
+       for (u = 0; u != G_N_ELEMENTS(types); ++u)
+               if (g_mime_content_type_is_type (
+                           ctype, types[u].type, types[u].subtype))
+                       return TRUE;
+
+       return FALSE;
+}
+
+
+static MuMsgPartSigStatusReport*
+copy_status_report_maybe (GObject *obj)
+{
+       MuMsgPartSigStatusReport *report, *copy;
+
+       report = g_object_get_data (obj, SIG_STATUS_REPORT);
+       if (!report)
+               return NULL; /* nothing to copy */
+
+       copy = g_slice_new0(MuMsgPartSigStatusReport);
+       copy->verdict = report->verdict;
+
+       if (report->report)
+               copy->report  = g_strdup (report->report);
+       if (report->signers)
+               copy->signers = g_strdup (report->signers);
+
+       return copy;
+}
+
+
+
+/* call 'func' with information about this MIME-part */
+static gboolean
+handle_part (MuMsg *msg, GMimePart *part, GMimeObject *parent,
+            MuMsgOptions opts, unsigned *index, gboolean decrypted,
+            MuMsgPartForeachFunc func, gpointer user_data)
+{
+       GMimeContentType        *ct;
+       MuMsgPart                msgpart;
+
+       memset (&msgpart, 0, sizeof(MuMsgPart));
+
+       msgpart.size        = get_part_size (part);
+       msgpart.part_type   = MU_MSG_PART_TYPE_LEAF;
+       msgpart.part_type  |= get_disposition ((GMimeObject*)part);
+       if (decrypted)
+               msgpart.part_type |= MU_MSG_PART_TYPE_DECRYPTED;
+       else if ((opts & MU_MSG_OPTION_DECRYPT) &&
+                GMIME_IS_MULTIPART_ENCRYPTED (parent))
+               msgpart.part_type |= MU_MSG_PART_TYPE_ENCRYPTED;
+
+
+       ct = g_mime_object_get_content_type ((GMimeObject*)part);
+       if (GMIME_IS_CONTENT_TYPE(ct)) {
+               msgpart.type    = g_mime_content_type_get_media_type (ct);
+               msgpart.subtype = g_mime_content_type_get_media_subtype (ct);
+               /* store in the part_type as well, for quick checking */
+               if (looks_like_text_body_part (ct))
+                       msgpart.part_type |= MU_MSG_PART_TYPE_TEXT_PLAIN;
+               else if (g_mime_content_type_is_type (ct, "text", "html"))
+                       msgpart.part_type |= MU_MSG_PART_TYPE_TEXT_HTML;
+       }
+
+       /* put the verification info in the pgp-signature and every
+        * descendent of a pgp-encrypted part */
+       msgpart.sig_status_report = NULL;
+       if (g_ascii_strcasecmp (msgpart.subtype, "pgp-signature") == 0 ||
+           decrypted) {
+               msgpart.sig_status_report =
+                       copy_status_report_maybe (G_OBJECT(parent));
+               if (msgpart.sig_status_report)
+                       msgpart.part_type |= MU_MSG_PART_TYPE_SIGNED;
+       }
+
+       msgpart.data    = (gpointer)part;
+       msgpart.index   = (*index)++;
+
+       func (msg, &msgpart, user_data);
+
+       mu_msg_part_sig_status_report_destroy (msgpart.sig_status_report);
+
+       return TRUE;
+}
+
+
+/* call 'func' with information about this MIME-part */
+static gboolean
+handle_message_part (MuMsg *msg, GMimeMessagePart *mimemsgpart,
+                    GMimeObject *parent, MuMsgOptions opts, unsigned *index,
+                    gboolean decrypted,
+                    MuMsgPartForeachFunc func, gpointer user_data)
+{
+       MuMsgPart msgpart;
+
+       memset (&msgpart, 0, sizeof(MuMsgPart));
+
+       msgpart.type        = "message";
+       msgpart.subtype     = "rfc822";
+       msgpart.index       = (*index)++;
+
+       /* msgpart.size        = 0; /\* maybe calculate this? *\/ */
+
+       msgpart.part_type  = MU_MSG_PART_TYPE_MESSAGE;
+       msgpart.part_type |= get_disposition ((GMimeObject*)mimemsgpart);
+
+       msgpart.data        = (gpointer)mimemsgpart;
+       func (msg, &msgpart, user_data);
+
+       if (opts & MU_MSG_OPTION_RECURSE_RFC822) {
+               GMimeMessage *mmsg; /* may return NULL for some
+                                    * messages */
+               mmsg = g_mime_message_part_get_message (mimemsgpart);
+               if (mmsg)
+                       return handle_mime_object (msg,
+                                                  mmsg->mime_part,
+                                                  parent,
+                                                  opts,
+                                                  index,
+                                                  decrypted,
+                                                  func,
+                                                  user_data);
+       }
+
+       return TRUE;
+}
+
+static gboolean
+handle_multipart (MuMsg *msg, GMimeMultipart *mpart, GMimeObject *parent,
+                 MuMsgOptions opts, unsigned *index, gboolean decrypted,
+                 MuMsgPartForeachFunc func, gpointer user_data)
+{
+       gboolean res;
+       GMimeObject *part;
+       guint i;
+
+       res = TRUE;
+       for (i = 0; i < mpart->children->len; i++) {
+               part = (GMimeObject *) mpart->children->pdata[i];
+               res &= handle_mime_object (msg, part, parent,
+                                          opts, index, decrypted,
+                                          func, user_data);
+       }
+
+       return res;
+}
+
+
+static gboolean
+handle_mime_object (MuMsg *msg, GMimeObject *mobj, GMimeObject *parent,
+                   MuMsgOptions opts, unsigned *index, gboolean decrypted,
+                   MuMsgPartForeachFunc func, gpointer user_data)
+{
+       if (GMIME_IS_PART (mobj))
+               return handle_part
+                       (msg, GMIME_PART(mobj), parent,
+                        opts, index, decrypted, func, user_data);
+       else if (GMIME_IS_MESSAGE_PART (mobj))
+               return handle_message_part
+                       (msg, GMIME_MESSAGE_PART(mobj),
+                        parent, opts, index, decrypted, func, user_data);
+       else if ((opts & MU_MSG_OPTION_VERIFY) &&
+                GMIME_IS_MULTIPART_SIGNED (mobj)) {
+               check_signature
+                       (msg, GMIME_MULTIPART_SIGNED (mobj), opts);
+               return handle_multipart
+                       (msg, GMIME_MULTIPART (mobj), mobj, opts,
+                        index, decrypted, func, user_data);
+       } else if ((opts & MU_MSG_OPTION_DECRYPT) &&
+                  GMIME_IS_MULTIPART_ENCRYPTED (mobj))
+               return handle_encrypted_part
+                       (msg, GMIME_MULTIPART_ENCRYPTED (mobj),
+                        opts, index, func, user_data);
+       else if (GMIME_IS_MULTIPART (mobj))
+               return handle_multipart
+                       (msg, GMIME_MULTIPART (mobj), parent, opts,
+                        index, decrypted, func, user_data);
+       return TRUE;
+}
+
+
+gboolean
+mu_msg_part_foreach (MuMsg *msg, MuMsgOptions opts,
+                    MuMsgPartForeachFunc func, gpointer user_data)
+{
+       unsigned index;
+
+       index = 1;
+       g_return_val_if_fail (msg, FALSE);
+
+       if (!mu_msg_load_msg_file (msg, NULL))
+               return FALSE;
+
+       return handle_mime_object (msg,
+                                  msg->_file->_mime_msg->mime_part,
+                                  (GMimeObject *) msg->_file->_mime_msg,
+                                  opts,
+                                  &index,
+                                  FALSE,
+                                  func,
+                                  user_data);
+}
+
+
+static gboolean
+write_part_to_fd (GMimePart *part, int fd, GError **err)
+{
+       GMimeStream *stream;
+       GMimeDataWrapper *wrapper;
+       gboolean rv;
+
+       stream = g_mime_stream_fs_new (fd);
+       if (!GMIME_IS_STREAM(stream)) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "failed to create stream");
+               return FALSE;
+       }
+       g_mime_stream_fs_set_owner (GMIME_STREAM_FS(stream), FALSE);
+
+       wrapper = g_mime_part_get_content (part);
+       if (!GMIME_IS_DATA_WRAPPER(wrapper)) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "failed to create wrapper");
+               g_object_unref (stream);
+               return FALSE;
+       }
+       g_object_ref (part); /* FIXME: otherwise, the unrefs below
+                             * give errors...*/
+
+       if (g_mime_data_wrapper_write_to_stream (wrapper, stream) == -1) {
+               rv = FALSE;
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "failed to write to stream");
+       } else
+               rv = TRUE;
+
+       /* g_object_unref (wrapper); we don't own it */
+       g_object_unref (stream);
+
+       return rv;
+}
+
+
+
+static gboolean
+write_object_to_fd (GMimeObject *obj, int fd, GError **err)
+{
+       gchar *str;
+       str = g_mime_object_to_string (obj, NULL);
+
+       if (!str) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "could not get string from object");
+               return FALSE;
+       }
+
+       if (write (fd, str, strlen(str)) == -1) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "failed to write object: %s",
+                            strerror(errno));
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+
+static gboolean
+save_object (GMimeObject *obj, MuMsgOptions opts, const char *fullpath,
+            GError **err)
+{
+       int fd;
+       gboolean rv;
+       gboolean use_existing, overwrite;
+
+       use_existing = opts & MU_MSG_OPTION_USE_EXISTING;
+       overwrite    = opts & MU_MSG_OPTION_OVERWRITE;
+
+       /* don't try to overwrite when we already have it; useful when
+        * you're sure it's not a different file with the same name */
+       if (use_existing && access (fullpath, F_OK) == 0)
+               return TRUE;
+
+       /* ok, try to create the file */
+       fd = mu_util_create_writeable_fd (fullpath, 0600, overwrite);
+       if (fd == -1) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE,
+                            "could not open '%s' for writing: %s",
+                            fullpath, errno ? strerror(errno) : "error");
+               return FALSE;
+       }
+
+       if (GMIME_IS_PART (obj))
+               rv = write_part_to_fd ((GMimePart*)obj, fd, err);
+       else
+               rv = write_object_to_fd (obj, fd, err);
+
+       if (close (fd) != 0 && !err) { /* don't write on top of old err */
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE,
+                            "could not close '%s': %s",
+                            fullpath, errno ? strerror(errno) : "error");
+               return FALSE;
+       }
+
+       return rv;
+}
+
+
+gchar*
+mu_msg_part_get_path (MuMsg *msg, MuMsgOptions opts,
+                     const char* targetdir, unsigned index, GError **err)
+{
+       char *fname, *filepath;
+       GMimeObject* mobj;
+
+       g_return_val_if_fail (msg, NULL);
+
+       if (!mu_msg_load_msg_file (msg, NULL))
+               return NULL;
+
+       mobj = get_mime_object_at_index (msg, opts, index);
+       if (!mobj){
+               mu_util_g_set_error (err, MU_ERROR_GMIME,
+                                    "cannot find part %u", index);
+               return NULL;
+       }
+
+       fname = mime_part_get_filename (mobj, index, TRUE);
+       filepath = g_build_path (G_DIR_SEPARATOR_S, targetdir ? targetdir : "",
+                                fname, NULL);
+
+       /* Unref it since it was referenced earlier by
+        * get_mime_object_at_index */
+       g_object_unref (mobj);
+       g_free (fname);
+
+       return filepath;
+}
+
+
+
+gchar*
+mu_msg_part_get_cache_path (MuMsg *msg, MuMsgOptions opts, guint partid,
+                           GError **err)
+{
+       char *dirname, *filepath;
+       const char* path;
+
+       g_return_val_if_fail (msg, NULL);
+
+       if (!mu_msg_load_msg_file (msg, NULL))
+               return NULL;
+
+       path = mu_msg_get_path (msg);
+
+       /* g_compute_checksum_for_string may be better, but requires
+        * rel. new glib (2.16) */
+       dirname = g_strdup_printf ("%s%c%x%c%u",
+                                  mu_util_cache_dir(), G_DIR_SEPARATOR,
+                                  g_str_hash (path), G_DIR_SEPARATOR,
+                                  partid);
+
+       if (!mu_util_create_dir_maybe (dirname, 0700, FALSE)) {
+               mu_util_g_set_error (err, MU_ERROR_FILE,
+                                    "failed to create dir %s", dirname);
+               g_free (dirname);
+               return NULL;
+       }
+
+       filepath = mu_msg_part_get_path (msg, opts, dirname, partid, err);
+       g_free (dirname);
+
+       return filepath;
+}
+
+
+gboolean
+mu_msg_part_save (MuMsg *msg, MuMsgOptions opts,
+                 const char *fullpath, guint partidx, GError **err)
+{
+       gboolean         rv;
+       GMimeObject     *part;
+
+       g_return_val_if_fail (msg, FALSE);
+       g_return_val_if_fail (fullpath, FALSE);
+       g_return_val_if_fail (!((opts & MU_MSG_OPTION_OVERWRITE) &&
+                               (opts & MU_MSG_OPTION_USE_EXISTING)), FALSE);
+
+       rv = FALSE;
+
+       if (!mu_msg_load_msg_file (msg, err))
+               return rv;
+
+       part = get_mime_object_at_index (msg, opts, partidx);
+
+       /* special case: convert a message-part into a message */
+       if (GMIME_IS_MESSAGE_PART (part))
+               part = (GMimeObject*)g_mime_message_part_get_message
+                       (GMIME_MESSAGE_PART (part));
+
+       if (!part)
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "part %u does not exist", partidx);
+       else if (!GMIME_IS_PART(part) && !GMIME_IS_MESSAGE(part))
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME,
+                            "unexpected type %s for part %u",
+                            G_OBJECT_TYPE_NAME((GObject*)part),
+                            partidx);
+       else
+               rv = save_object (part, opts, fullpath, err);
+
+       g_clear_object(&part);
+
+       return rv;
+}
+
+
+gchar*
+mu_msg_part_save_temp (MuMsg *msg, MuMsgOptions opts, guint partidx,
+                      GError **err)
+{
+       gchar *filepath;
+
+       filepath = mu_msg_part_get_cache_path (msg, opts, partidx, err);
+       if (!filepath)
+               return NULL;
+
+       if (!mu_msg_part_save (msg, opts, filepath, partidx, err)) {
+               g_free (filepath);
+               return NULL;
+       }
+
+       return filepath;
+}
+
+static gboolean
+match_cid (MuMsgPart *mpart, const char *cid)
+{
+       const char *this_cid;
+
+       this_cid = g_mime_object_get_content_id ((GMimeObject*)mpart->data);
+
+       return g_strcmp0 (this_cid, cid) ? TRUE : FALSE;
+}
+
+int
+mu_msg_find_index_for_cid (MuMsg *msg, MuMsgOptions opts,
+                          const char *sought_cid)
+{
+       const char* cid;
+
+       g_return_val_if_fail (msg, -1);
+       g_return_val_if_fail (sought_cid, -1);
+
+       if (!mu_msg_load_msg_file (msg, NULL))
+               return -1;
+
+       cid = g_str_has_prefix (sought_cid, "cid:") ?
+               sought_cid + 4 : sought_cid;
+
+       return get_matching_part_index (msg, opts,
+                                       (MuMsgPartMatchFunc)match_cid,
+                                       (gpointer)cid);
+}
+
+struct _RxMatchData {
+       GSList       *_lst;
+       const GRegex *_rx;
+       guint         _idx;
+};
+typedef struct _RxMatchData RxMatchData;
+
+
+static void
+match_filename_rx (MuMsg *msg, MuMsgPart *mpart, RxMatchData *mdata)
+{
+       char *fname;
+
+       fname = mu_msg_part_get_filename (mpart, FALSE);
+       if (!fname)
+               return;
+
+       if (g_regex_match (mdata->_rx, fname, 0, NULL))
+               mdata->_lst = g_slist_prepend (mdata->_lst,
+                                              GUINT_TO_POINTER(mpart->index));
+       g_free (fname);
+}
+
+
+GSList*
+mu_msg_find_files (MuMsg *msg, MuMsgOptions opts, const GRegex *pattern)
+{
+       RxMatchData mdata;
+
+       g_return_val_if_fail (msg, NULL);
+       g_return_val_if_fail (pattern, NULL);
+
+       if (!mu_msg_load_msg_file (msg, NULL))
+               return NULL;
+
+       mdata._lst = NULL;
+       mdata._rx  = pattern;
+       mdata._idx = 0;
+
+       mu_msg_part_foreach (msg, opts,
+                            (MuMsgPartForeachFunc)match_filename_rx,
+                            &mdata);
+       return mdata._lst;
+}
+
+
+gboolean
+mu_msg_part_maybe_attachment (MuMsgPart *part)
+{
+       g_return_val_if_fail (part, FALSE);
+
+       /* attachments must be leaf parts */
+       if (!(part->part_type & MU_MSG_PART_TYPE_LEAF))
+               return FALSE;
+
+       /* parts other than text/plain, text/html are considered
+        * attachments as well */
+       if (!(part->part_type & MU_MSG_PART_TYPE_TEXT_PLAIN) &&
+           !(part->part_type & MU_MSG_PART_TYPE_TEXT_HTML))
+               return TRUE;
+
+       return part->part_type & MU_MSG_PART_TYPE_ATTACHMENT ? TRUE : FALSE;
+}
diff --git a/lib/mu-msg-part.h b/lib/mu-msg-part.h
new file mode 100644 (file)
index 0000000..4f26ddf
--- /dev/null
@@ -0,0 +1,268 @@
+/* -*-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.
+**
+*/
+
+#ifndef __MU_MSG_PART_H__
+#define __MU_MSG_PART_H__
+
+#include <glib.h>
+#include <unistd.h> /* for ssize_t */
+
+#define SIG_STATUS_REPORT "sig-status-report"
+
+G_BEGIN_DECLS
+
+enum _MuMsgPartType {
+       MU_MSG_PART_TYPE_NONE           = 0,
+
+       /* MIME part without children */
+       MU_MSG_PART_TYPE_LEAF           = 1 << 1,
+       /* an RFC822 message part? */
+       MU_MSG_PART_TYPE_MESSAGE        = 1 << 2,
+       /* disposition inline? */
+       MU_MSG_PART_TYPE_INLINE         = 1 << 3,
+       /* disposition attachment? */
+       MU_MSG_PART_TYPE_ATTACHMENT     = 1 << 4,
+       /* a signed part? */
+       MU_MSG_PART_TYPE_SIGNED         = 1 << 5,
+       /* an encrypted part? */
+       MU_MSG_PART_TYPE_ENCRYPTED      = 1 << 6,
+       /* a decrypted part? */
+       MU_MSG_PART_TYPE_DECRYPTED      = 1 << 7,
+       /* a text/plain part? */
+       MU_MSG_PART_TYPE_TEXT_PLAIN     = 1 << 8,
+       /* a text/html part? */
+       MU_MSG_PART_TYPE_TEXT_HTML      = 1 << 9
+};
+typedef enum _MuMsgPartType MuMsgPartType;
+
+
+/* the signature status */
+enum _MuMsgPartSigStatus {
+       MU_MSG_PART_SIG_STATUS_UNSIGNED         = 0,
+
+       MU_MSG_PART_SIG_STATUS_GOOD,
+       MU_MSG_PART_SIG_STATUS_BAD,
+       MU_MSG_PART_SIG_STATUS_ERROR,
+       MU_MSG_PART_SIG_STATUS_FAIL
+};
+typedef enum _MuMsgPartSigStatus MuMsgPartSigStatus;
+
+typedef struct {
+       MuMsgPartSigStatus       verdict;
+       const char              *report;
+       const char              *signers;
+} MuMsgPartSigStatusReport;
+
+/**
+ * destroy a MuMsgPartSignatureStatusReport object
+ *
+ * @param report a MuMsgPartSignatureStatusReport object
+ */
+void mu_msg_part_sig_status_report_destroy (MuMsgPartSigStatusReport *report);
+
+
+struct _MuMsgPart {
+
+       /* index of this message part */
+       unsigned         index;
+
+       /* cid */
+       /* const char       *content_id; */
+
+       /* content-type: type/subtype, ie. text/plain */
+       const char       *type;
+       const char       *subtype;
+
+       /* size of the part; or < 0 if unknown */
+       ssize_t          size;
+
+       gpointer         data; /* opaque data */
+
+       MuMsgPartType            part_type;
+       MuMsgPartSigStatusReport *sig_status_report;
+ };
+typedef struct _MuMsgPart MuMsgPart;
+
+/**
+ * get some appropriate file name for the mime-part
+ *
+ * @param mpart a MuMsgPart
+ * @param construct_if_needed if there is no
+ * real filename, construct one.
+ *
+ * @return the file name (free with g_free)
+ */
+char *mu_msg_part_get_filename (MuMsgPart *mpart, gboolean construct_if_needed)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * get appropriate content id for the mime-part
+ *
+ * @param mpart a MuMsgPart
+ *
+ * @return const content id
+ */
+const gchar*
+mu_msg_part_get_content_id (MuMsgPart *mpart)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * get the text in the MuMsgPart (ie. in its GMimePart)
+ *
+ * @param msg a MuMsg
+ * @param part a MuMsgPart
+ * @param opts MuMsgOptions
+ *
+ * @return utf8 string for this MIME part, to be freed by caller
+ */
+char* mu_msg_part_get_text (MuMsg *msg, MuMsgPart *part, MuMsgOptions opts)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * does this msg part look like an attachment?
+ *
+ * @param part a message part
+ *
+ * @return TRUE if it looks like an attachment, FALSE otherwise
+ */
+gboolean mu_msg_part_maybe_attachment (MuMsgPart *part);
+
+
+/**
+ * save a specific attachment to some targetdir
+ *
+ * @param msg a valid MuMsg instance
+ * @param opts mu-message options (OVERWRITE/USE_EXISTING)
+ * @gchar filepath the filepath to save
+ * @param partidx index of the attachment you want to save
+ * @param err receives error information (when function returns NULL)
+ *
+ * @return full path to the message part saved or NULL in case or
+ * error; free with g_free
+ */
+gboolean mu_msg_part_save (MuMsg *msg, MuMsgOptions opts,
+                          const char *filepath, guint partidx,
+                          GError **err);
+
+
+/**
+ * save a message part to a temporary file and return the full path to
+ * this file
+ *
+ * @param msg a MuMsg message
+ * @param opts mu-message options (OVERWRITE/USE_EXISTING)
+ * @param partidx index of the part to save
+ * @param err receives error information if any
+ *
+ * @return the full path to the temp file, or NULL in case of error
+ */
+gchar* mu_msg_part_save_temp (MuMsg *msg, MuMsgOptions opts,
+                             guint partidx, GError **err)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+
+/**
+ * get a filename for the saving the message part; try the filename
+ * specified for the message part if any, otherwise determine a unique
+ * name based on the partidx and the message path
+ *
+ * @param msg a msg
+ * @param opts mu-message options
+ * @param targetdir where to store the part
+ * @param partidx the part for which to determine a filename
+ * @param err receives error information (when function returns NULL)
+ *
+ * @return a filepath (g_free when done with it) or NULL in case of error
+ */
+gchar* mu_msg_part_get_path (MuMsg *msg, MuMsgOptions opts,
+                            const char* targetdir,
+                            guint partidx, GError **err)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * get a full path name for a file for saving the message part INDEX;
+ * this path is unique (1:1) for this particular message and part for
+ * this user. Thus, it can be used as a cache.
+ *
+ * Will create the directory if needed.
+ *
+ * @param msg a msg
+ * @param opts mu-message options
+ * @param partidx the part for which to determine a filename
+ * @param err receives error information (when function returns NULL)
+ *
+ * @return a filepath (g_free when done with it) or NULL in case of error
+ */
+gchar* mu_msg_part_get_cache_path (MuMsg *msg, MuMsgOptions opts,
+                                  guint partidx, GError **err)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * get the part index for the message part with a certain content-id
+ *
+ * @param msg a message
+ * @param content_id a content-id to search
+ *
+ * @return the part index number of the found part, or -1 if it was not found
+ */
+int mu_msg_find_index_for_cid (MuMsg *msg, MuMsgOptions opts, const char* content_id);
+
+
+
+/**
+ * retrieve a list of indices for mime-parts with filenames matching a regex
+ *
+ * @param msg a message
+ * @param opts
+ * @param a regular expression to match the filename with
+ *
+ * @return a list with indices for the files matching the pattern; the
+ * indices are the GPOINTER_TO_UINT(lst->data) of the list. They must
+ * be freed with g_slist_free
+ */
+GSList* mu_msg_find_files (MuMsg *msg, MuMsgOptions opts, const GRegex *pattern);
+
+
+typedef void (*MuMsgPartForeachFunc) (MuMsg *msg, MuMsgPart*, gpointer);
+
+
+/**
+ * call a function for each of the mime part in a message
+ *
+ * @param msg a valid MuMsg* instance
+ * @param func a callback function to call for each contact; when
+ * the callback does not return TRUE, it won't be called again
+ * @param user_data a user-provide pointer that will be passed to the callback
+ * @param options, bit-wise OR'ed
+ *
+ * @return FALSE in case of error, TRUE otherwise
+ */
+gboolean mu_msg_part_foreach (MuMsg *msg, MuMsgOptions opts,
+                             MuMsgPartForeachFunc func, gpointer user_data);
+
+G_END_DECLS
+
+#endif /*__MU_MSG_PART_H__*/
diff --git a/lib/mu-msg-prio.c b/lib/mu-msg-prio.c
new file mode 100644 (file)
index 0000000..96a959a
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+** Copyright (C) 2012-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, 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-msg-prio.h"
+
+
+const char*
+mu_msg_prio_name (MuMsgPrio prio)
+{
+       switch (prio) {
+       case MU_MSG_PRIO_LOW    : return "low";
+       case MU_MSG_PRIO_NORMAL : return "normal";
+       case MU_MSG_PRIO_HIGH   : return "high";
+       default                 : g_return_val_if_reached (NULL);
+       }
+}
+
+MuMsgPrio
+mu_msg_prio_from_char (char k)
+{
+       g_return_val_if_fail (k == 'l' || k == 'n' || k == 'h',
+                             MU_MSG_PRIO_NONE);
+       return (MuMsgPrio)k;
+}
+
+char
+mu_msg_prio_char (MuMsgPrio prio)
+{
+       if (!(prio == 'l' || prio == 'n' || prio == 'h')) {
+               g_warning ("prio: %c", (char)prio);
+       }
+
+
+       g_return_val_if_fail (prio == 'l' || prio == 'n' || prio == 'h',
+                             0);
+
+       return (char)prio;
+}
+
+
+void
+mu_msg_prio_foreach (MuMsgPrioForeachFunc func, gpointer user_data)
+{
+       g_return_if_fail (func);
+
+       func (MU_MSG_PRIO_LOW, user_data);
+       func (MU_MSG_PRIO_NORMAL, user_data);
+       func (MU_MSG_PRIO_HIGH, user_data);
+}
diff --git a/lib/mu-msg-prio.h b/lib/mu-msg-prio.h
new file mode 100644 (file)
index 0000000..df3e8c3
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_MSG_PRIO_H__
+#define __MU_MSG_PRIO_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+enum _MuMsgPrio {
+       MU_MSG_PRIO_LOW    = 'l',
+       MU_MSG_PRIO_NORMAL = 'n',
+       MU_MSG_PRIO_HIGH   = 'h'
+};
+typedef enum _MuMsgPrio MuMsgPrio;
+
+static const MuMsgPrio MU_MSG_PRIO_NONE = (MuMsgPrio)0;
+
+
+/**
+ * get a printable name for the message priority
+ * (ie., MU_MSG_PRIO_LOW=>"low" etc.)
+ *
+ * @param prio a message priority
+ *
+ * @return a printable name for this priority
+ */
+const char* mu_msg_prio_name (MuMsgPrio prio) G_GNUC_CONST;
+
+
+/**
+ * get the MuMsgPriority corresponding to a one-character shortcut
+ * ('l'=>MU_MSG_PRIO_, 'n'=>MU_MSG_PRIO_NORMAL or
+ * 'h'=>MU_MSG_PRIO_HIGH)
+ *
+ * @param k a character
+ *
+ * @return a message priority
+ */
+MuMsgPrio mu_msg_prio_from_char (char k) G_GNUC_CONST;
+
+
+/**
+ * get the one-character shortcut corresponding to a message priority
+ * ('l'=>MU_MSG_PRIO_, 'n'=>MU_MSG_PRIO_NORMAL or
+ * 'h'=>MU_MSG_PRIO_HIGH)
+ *
+ * @param prio a message priority
+ *
+ * @return a shortcut character or 0 in case of error
+ */
+char mu_msg_prio_char (MuMsgPrio prio) G_GNUC_CONST;
+
+typedef void (*MuMsgPrioForeachFunc) (MuMsgPrio prio, gpointer user_data);
+/**
+ * call a function for each message priority
+ *
+ * @param func a callback function
+ * @param user_data a user pointer to pass to the callback
+ */
+void mu_msg_prio_foreach (MuMsgPrioForeachFunc func, gpointer user_data);
+
+
+
+G_END_DECLS
+
+#endif /*__MU_MSG_PRIO_H__*/
diff --git a/lib/mu-msg-priv.h b/lib/mu-msg-priv.h
new file mode 100644 (file)
index 0000000..8f61719
--- /dev/null
@@ -0,0 +1,140 @@
+/* -*-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 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_MSG_PRIV_H__
+#define __MU_MSG_PRIV_H__
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <gmime/gmime.h>
+#include <stdlib.h>
+
+#include <mu-msg.h>
+#include <mu-msg-file.h>
+#include <mu-msg-doc.h>
+#include "mu-msg-part.h"
+
+G_BEGIN_DECLS
+
+struct _MuMsgFile {
+       GMimeMessage    *_mime_msg;
+       time_t           _timestamp;
+       size_t           _size;
+       char             _path    [PATH_MAX + 1];
+       char             _maildir [PATH_MAX + 1];
+};
+
+
+/* we put the the MuMsg definition in this separate -priv file, so we
+ * can split the mu_msg implementations over separate files */
+struct _MuMsg {
+
+       guint            _refcount;
+
+       /* our two backend */
+       MuMsgFile       *_file; /* based on GMime, ie. a file on disc */
+       MuMsgDoc        *_doc;  /* based on Xapian::Document */
+
+       /* lists where we push allocated strings / GSLists of string
+        * so we can free them when the struct gets destroyed (and we
+        * can return them as 'const to callers)
+        */
+       GSList          *_free_later_str;
+       GSList          *_free_later_lst;
+};
+
+
+/**
+ * convert a GMimePart to a string
+ *
+ * @param part a GMimePart
+ * @param err will receive TRUE if there was an error, FALSE
+ * otherwise. Must NOT be NULL.
+ *
+ * @return utf8 string for this MIME part, to be freed by caller
+ */
+gchar* mu_msg_mime_part_to_string (GMimePart *part, gboolean *err)
+      G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * Like g_mime_message_foreach, but will recurse into encrypted parts
+ * if @param decrypt is TRUE and mu was built with crypto support
+ *
+ * @param msg a GMimeMessage
+ * @param decrypt whether to try to automatically decrypt
+ * @param func user callback function for each part
+ * @param user_data user point passed to callback function
+ * @param err receives error information
+ *
+ */
+void mu_mime_message_foreach (GMimeMessage *msg, gboolean decrypt,
+                             GMimeObjectForeachFunc func,
+                             gpointer user_data);
+
+/**
+ * callback function to retrieve a password from the user
+ *
+ * @param user_id the user name / id to get the password for
+ * @param prompt_ctx a string containing some helpful context for the prompt
+ * @param reprompt whether this is a reprompt after an earlier, incorrect password
+ * @param user_data the user_data pointer passed to mu_msg_part_decrypt_foreach
+ *
+ * @return a newly allocated (g_free'able) string
+ */
+typedef char* (*MuMsgPartPasswordFunc)   (const char *user_id, const char *prompt_ctx,
+                                         gboolean reprompt, gpointer user_data);
+
+
+/**
+ * verify the signature of a signed message part
+ *
+ * @param sig a signed message part
+ * @param opts message options
+ * @param err receive error information
+ *
+ * @return a status report object, free with mu_msg_part_sig_status_report_destroy
+ */
+void mu_msg_crypto_verify_part (GMimeMultipartSigned *sig,
+                                MuMsgOptions opts,
+                                GError **err);
+
+/**
+ * decrypt the given encrypted mime multipart
+ *
+ * @param enc encrypted part
+ * @param opts options
+ * @param password_func callback function to retrieve as password (or NULL)
+ * @param user_data pointer passed to the password func
+ * @param err receives error data
+ *
+ * @return the decrypted part, or NULL in case of error
+ */
+GMimeObject* mu_msg_crypto_decrypt_part (GMimeMultipartEncrypted *enc, MuMsgOptions opts,
+                                        MuMsgPartPasswordFunc func, gpointer user_data,
+                                        GError **err)
+                                       G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+G_END_DECLS
+
+#endif /*__MU_MSG_PRIV_H__*/
diff --git a/lib/mu-msg-sexp.c b/lib/mu-msg-sexp.c
new file mode 100644 (file)
index 0000000..359ae22
--- /dev/null
@@ -0,0 +1,635 @@
+/*
+** Copyright (C) 2011-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, 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.h>
+#include <ctype.h>
+
+#include "utils/mu-str.h"
+#include "mu-msg.h"
+#include "mu-msg-iter.h"
+#include "mu-msg-part.h"
+#include "mu-maildir.h"
+
+static void
+append_sexp_attr_list (GString *gstr, const char* elm, const GSList *lst)
+{
+       const GSList *cur;
+
+       if (!lst)
+               return; /* empty list, don't include */
+
+       g_string_append_printf (gstr, "\t:%s ( ", elm);
+
+       for (cur = lst; cur; cur = g_slist_next(cur)) {
+               char *str;
+               str = mu_str_escape_c_literal
+                       ((const gchar*)cur->data, TRUE);
+               g_string_append_printf (gstr, "%s ", str);
+               g_free (str);
+       }
+
+       g_string_append (gstr, ")\n");
+}
+
+static void
+append_sexp_attr (GString *gstr, const char* elm, const char *str)
+{
+       gchar *esc, *utf8, *cur;
+
+       if (!str || strlen(str) == 0)
+               return; /* empty: don't include */
+
+
+       utf8 = mu_str_utf8ify (str);
+
+       for (cur = utf8; *cur; ++cur)
+               if (iscntrl(*cur))
+                       *cur = ' ';
+
+       esc = mu_str_escape_c_literal (utf8, TRUE);
+       g_free (utf8);
+
+       g_string_append_printf (gstr, "\t:%s %s\n", elm, esc);
+       g_free (esc);
+}
+
+static void
+append_sexp_body_attr (GString *gstr, const char* elm, const char *str)
+{
+       gchar *esc;
+
+       if (!str || strlen(str) == 0)
+               return; /* empty: don't include */
+
+       esc = mu_str_escape_c_literal (str, TRUE);
+
+       g_string_append_printf (gstr, "\t:%s %s\n", elm, esc);
+       g_free (esc);
+}
+
+struct _ContactData {
+       gboolean from, to, cc, bcc, reply_to;
+       GString *gstr;
+       MuMsgContactType prev_ctype;
+};
+typedef struct _ContactData ContactData;
+
+static gchar*
+get_name_email_pair (MuMsgContact *c)
+{
+       gchar *name, *email, *pair;
+
+       name  = (char*)mu_msg_contact_name(c);
+       email = (char*)mu_msg_contact_email(c);
+
+       name  = name ? mu_str_escape_c_literal (name, TRUE) : NULL;
+       email = email ? mu_str_escape_c_literal (email, TRUE) : NULL;
+
+       pair = g_strdup_printf ("(%s . %s)",
+                               name ? name : "nil",
+                               email ? email : "nil");
+       g_free (name);
+       g_free (email);
+
+       return pair;
+}
+
+
+static void
+add_prefix_maybe (GString *gstr, gboolean *field, const char *prefix)
+{
+       /* if there's nothing in the field yet, add the prefix */
+       if (!*field)
+               g_string_append (gstr, prefix);
+
+       *field = TRUE;
+}
+
+static gboolean
+each_contact (MuMsgContact *c, ContactData *cdata)
+{
+       char *pair;
+       MuMsgContactType ctype;
+
+       ctype = mu_msg_contact_type (c);
+
+       /* if the current type is not the previous type, close the
+        * previous first */
+       if (cdata->prev_ctype != ctype && cdata->prev_ctype != (unsigned)-1)
+               g_string_append (cdata->gstr, ")\n");
+
+       switch (ctype) {
+
+       case MU_MSG_CONTACT_TYPE_FROM:
+               add_prefix_maybe (cdata->gstr, &cdata->from, "\t:from (");
+               break;
+       case MU_MSG_CONTACT_TYPE_TO:
+               add_prefix_maybe (cdata->gstr, &cdata->to, "\t:to (");
+               break;
+       case MU_MSG_CONTACT_TYPE_CC:
+               add_prefix_maybe (cdata->gstr, &cdata->cc, "\t:cc (");
+               break;
+       case MU_MSG_CONTACT_TYPE_BCC:
+               add_prefix_maybe (cdata->gstr, &cdata->bcc, "\t:bcc (");
+               break;
+       case MU_MSG_CONTACT_TYPE_REPLY_TO:
+               add_prefix_maybe (cdata->gstr, &cdata->reply_to,
+                                 "\t:reply-to (");
+               break;
+       default: g_return_val_if_reached (FALSE);
+       }
+
+       cdata->prev_ctype = ctype;
+
+       pair = get_name_email_pair (c);
+       g_string_append (cdata->gstr, pair);
+       g_free (pair);
+
+       return TRUE;
+}
+
+static void
+maybe_append_list_post (GString *gstr, MuMsg *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 char*      list_post;
+
+       list_post = mu_msg_get_header (msg, "List-Post");
+       if (!list_post)
+               return;
+
+       rx = g_regex_new ("<?mailto:([a-z0-9!@#$%&'*+-/=?^_`{|}~]+)>?",
+                          G_REGEX_CASELESS, (GRegexMatchFlags)0, NULL);
+       g_return_if_fail(rx);
+
+       if (g_regex_match (rx, list_post, 0, &minfo)) {
+               char    *addr;
+               addr = g_match_info_fetch (minfo, 1);
+               g_string_append_printf (gstr,"\t:list-post ((nil . \"%s\"))\n", addr);
+               g_free(addr);
+       }
+
+       g_match_info_free (minfo);
+       g_regex_unref (rx);
+}
+
+
+
+static void
+append_sexp_contacts (GString *gstr, MuMsg *msg)
+{
+       ContactData cdata;
+
+       cdata.from       = cdata.to = cdata.cc = cdata.bcc
+                        = cdata.reply_to = FALSE;
+       cdata.gstr       = gstr;
+       cdata.prev_ctype = (unsigned)-1;
+
+       mu_msg_contact_foreach (msg, (MuMsgContactForeachFunc)each_contact,
+                               &cdata);
+       if (cdata.from || cdata.to || cdata.cc || cdata.bcc || cdata.reply_to)
+               gstr = g_string_append (gstr, ")\n");
+
+        maybe_append_list_post (gstr, msg);
+}
+
+struct _FlagData {
+       char *flagstr;
+       MuFlags msgflags;
+};
+typedef struct _FlagData FlagData;
+
+static void
+each_flag (MuFlags flag, FlagData *fdata)
+{
+       if (!(flag & fdata->msgflags))
+               return;
+
+       if (!fdata->flagstr)
+               fdata->flagstr = g_strdup (mu_flag_name(flag));
+       else {
+               gchar *tmp;
+               tmp = g_strconcat (fdata->flagstr, " ",
+                                  mu_flag_name(flag), NULL);
+               g_free (fdata->flagstr);
+               fdata->flagstr = tmp;
+       }
+}
+
+static void
+append_sexp_flags (GString *gstr, MuMsg *msg)
+{
+       FlagData fdata;
+
+       fdata.msgflags = mu_msg_get_flags (msg);
+       fdata.flagstr  = NULL;
+
+       mu_flags_foreach ((MuFlagsForeachFunc)each_flag, &fdata);
+       if (fdata.flagstr)
+               g_string_append_printf (gstr, "\t:flags (%s)\n",
+                                       fdata.flagstr);
+       g_free (fdata.flagstr);
+}
+
+static char*
+get_temp_file (MuMsg *msg, MuMsgOptions opts, unsigned index)
+{
+       char *path;
+       GError *err;
+
+       err = NULL;
+       path = mu_msg_part_get_cache_path (msg, opts, index, &err);
+       if (!path)
+               goto errexit;
+
+       if (!mu_msg_part_save (msg, opts, path, index, &err))
+               goto errexit;
+
+       return path;
+
+errexit:
+       g_warning ("failed to save mime part: %s",
+                  err->message ? err->message : "something went wrong");
+       g_clear_error (&err);
+       g_free (path);
+       return NULL;
+}
+
+
+static gchar*
+get_temp_file_maybe (MuMsg *msg, MuMsgPart *part, MuMsgOptions opts)
+{
+       char *tmp, *tmpfile;
+
+       opts |= MU_MSG_OPTION_USE_EXISTING;
+
+       if  (!(opts & MU_MSG_OPTION_EXTRACT_IMAGES) ||
+            g_ascii_strcasecmp (part->type, "image") != 0)
+               return NULL;
+
+       tmp = get_temp_file (msg, opts, part->index);
+       if (!tmp)
+               return NULL;
+
+       tmpfile = mu_str_escape_c_literal (tmp, TRUE);
+       g_free (tmp);
+       return tmpfile;
+}
+
+
+struct _PartInfo {
+       char        *parts;
+       MuMsgOptions opts;
+};
+typedef struct _PartInfo PartInfo;
+
+static char*
+sig_verdict (MuMsgPart *mpart)
+{
+       char                            *signers, *s;
+       const char                      *verdict;
+       MuMsgPartSigStatusReport        *report;
+
+       report = mpart->sig_status_report;
+       if (!report)
+               return g_strdup ("");
+
+       switch (report->verdict) {
+       case MU_MSG_PART_SIG_STATUS_GOOD:
+               verdict = ":signature verified";
+               break;
+       case MU_MSG_PART_SIG_STATUS_BAD:
+               verdict = ":signature bad";
+               break;
+       case MU_MSG_PART_SIG_STATUS_ERROR:
+               verdict = ":signature unverified";
+               break;
+       default:
+               verdict = "";
+               break;
+       }
+
+       if (!report->signers)
+               return g_strdup (verdict);
+
+       signers = mu_str_escape_c_literal (report->signers, TRUE);
+       s       = g_strdup_printf ("%s :signers %s", verdict, signers);
+       g_free (signers);
+
+       return s;
+}
+
+static const char*
+dec_verdict (MuMsgPart *mpart)
+{
+       MuMsgPartType ptype;
+
+       ptype = mpart->part_type;
+
+       if (ptype & MU_MSG_PART_TYPE_DECRYPTED)
+               return ":decryption succeeded";
+       else if (ptype & MU_MSG_PART_TYPE_ENCRYPTED)
+               return ":decryption failed";
+       else
+               return "";
+}
+
+
+static gchar *
+get_part_type_string (MuMsgPartType ptype)
+{
+       GString *gstr;
+       unsigned u;
+       struct PartTypes {
+               MuMsgPartType ptype;
+               const char* name;
+       } ptypes[] = {
+               { MU_MSG_PART_TYPE_LEAF,       "leaf" },
+               { MU_MSG_PART_TYPE_MESSAGE,    "message" },
+               { MU_MSG_PART_TYPE_INLINE,     "inline" },
+               { MU_MSG_PART_TYPE_ATTACHMENT, "attachment" },
+               { MU_MSG_PART_TYPE_SIGNED,     "signed" },
+               { MU_MSG_PART_TYPE_ENCRYPTED,  "encrypted" }
+       };
+
+       gstr = g_string_sized_new (100); /* more than enough */
+       gstr = g_string_append_c (gstr, '(');
+
+       for (u = 0; u!= G_N_ELEMENTS(ptypes); ++u) {
+               if (ptype & ptypes[u].ptype) {
+                       if (gstr->len > 1)
+                               gstr = g_string_append_c (gstr, ' ');
+                       gstr = g_string_append (gstr, ptypes[u].name);
+               }
+       }
+
+       gstr = g_string_append_c (gstr, ')');
+
+       return g_string_free (gstr, FALSE);
+}
+
+
+static void
+each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo)
+{
+       char            *name, *encname, *tmp, *parttype;
+       char            *tmpfile, *cidesc, *verdict;
+       const char      *cid;
+
+       name     = mu_msg_part_get_filename (part, TRUE);
+       encname  = name ?
+               mu_str_escape_c_literal(name, TRUE) :
+               g_strdup("\"noname\"");
+       g_free (name);
+
+       tmpfile  = get_temp_file_maybe (msg, part, pinfo->opts);
+       parttype = get_part_type_string (part->part_type);
+       verdict  = sig_verdict (part);
+
+       cid    = mu_msg_part_get_content_id(part);
+       cidesc = cid ? mu_str_escape_c_literal(cid, TRUE) : NULL;
+
+       tmp = g_strdup_printf
+               ("%s(:index %d :name %s :mime-type \"%s/%s\"%s%s "
+                ":type %s "
+                ":attachment %s %s%s :size %i %s %s)",
+                pinfo->parts ? pinfo->parts: "",
+                part->index,
+                encname,
+                part->type ? part->type : "application",
+                part->subtype ? part->subtype : "octet-stream",
+                tmpfile ? " :temp" : "", tmpfile ? tmpfile : "",
+                parttype,
+                mu_msg_part_maybe_attachment (part) ? "t" : "nil",
+                cidesc ? " :cid" : "", cidesc ? cidesc : "",
+                (int)part->size,
+                verdict,
+                dec_verdict (part));
+
+       g_free (encname);
+       g_free (tmpfile);
+       g_free (parttype);
+       g_free (verdict);
+       g_free (cidesc);
+
+       g_free (pinfo->parts);
+       pinfo->parts = tmp;
+}
+
+
+static void
+append_sexp_parts (GString *gstr, MuMsg *msg, MuMsgOptions opts)
+{
+       PartInfo pinfo;
+
+       pinfo.parts = NULL;
+       pinfo.opts  = opts;
+
+       if (!mu_msg_part_foreach (msg, opts, (MuMsgPartForeachFunc)each_part,
+                                 &pinfo)) {
+               /* do nothing */
+       } else if (pinfo.parts) {
+               g_string_append_printf (gstr, "\t:parts (%s)\n", pinfo.parts);
+               g_free (pinfo.parts);
+       }
+}
+
+static void
+append_sexp_thread_info (GString *gstr, const MuMsgIterThreadInfo *ti)
+{
+       g_string_append_printf
+               (gstr, "\t:thread (:path \"%s\" :level %u%s%s%s%s%s)\n",
+                ti->threadpath,
+                ti->level,
+                ti->prop & MU_MSG_ITER_THREAD_PROP_FIRST_CHILD  ?
+                " :first-child t" : "",
+                ti->prop & MU_MSG_ITER_THREAD_PROP_LAST_CHILD   ?
+                " :last-child t" : "",
+                ti->prop & MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT ?
+                " :empty-parent t" : "",
+                ti->prop & MU_MSG_ITER_THREAD_PROP_DUP          ?
+                " :duplicate t" : "",
+                ti->prop & MU_MSG_ITER_THREAD_PROP_HAS_CHILD    ?
+                " :has-child t" : "");
+}
+
+static void
+append_sexp_param (GString *gstr, const GSList *param)
+{
+       for (;param; param = g_slist_next (param)) {
+               const char *str;
+               char *key, *value;
+
+               str   = param->data;
+               key   = str ? mu_str_escape_c_literal (str, FALSE) : g_strdup ("");
+
+               param = g_slist_next (param);
+               str   = param->data;
+               value = str ? mu_str_escape_c_literal (str, FALSE) : g_strdup ("");
+
+               g_string_append_printf (gstr, "(\"%s\" . \"%s\")", key, value);
+               g_free (key);
+               g_free (value);
+
+               if (param->next)
+                       g_string_append_c (gstr, ' ');
+       }
+}
+
+static void
+append_message_file_parts (GString *gstr, MuMsg *msg, MuMsgOptions opts)
+{
+       const char      *str;
+       GError          *err;
+       const GSList    *params;
+
+       err = NULL;
+
+       if (!mu_msg_load_msg_file (msg, &err)) {
+               g_warning ("failed to load message file: %s",
+                          err ? err->message : "some error occurred");
+               g_clear_error (&err);
+               return;
+       }
+
+       append_sexp_parts (gstr, msg, opts);
+       append_sexp_contacts (gstr, msg);
+
+       /* add the user-agent / x-mailer */
+       str = mu_msg_get_header (msg, "User-Agent");
+       if (str || (str = mu_msg_get_header (msg, "X-Mailer")))
+               append_sexp_attr (gstr, "user-agent", str);
+
+       params = mu_msg_get_body_text_content_type_parameters (msg, opts);
+       if (params) {
+               g_string_append_printf (gstr, "\t:body-txt-params (");
+               append_sexp_param (gstr, params);
+               g_string_append_printf (gstr, ")\n");
+       }
+
+       append_sexp_body_attr (gstr, "body-txt",
+                         mu_msg_get_body_text(msg, opts));
+       append_sexp_body_attr (gstr, "body-html",
+                         mu_msg_get_body_html(msg, opts));
+}
+
+static void
+append_sexp_date_and_size (GString *gstr, MuMsg *msg)
+{
+       time_t t;
+       size_t s;
+
+       t = mu_msg_get_date (msg);
+       if (t == (time_t)-1)  /* invalid date? */
+               t = 0;
+
+       s = mu_msg_get_size (msg);
+       if (s == (size_t)-1)   /* invalid size? */
+               s = 0;
+
+       g_string_append_printf
+               (gstr,
+                "\t:date (%d %u 0)\n\t:size %u\n",
+                (unsigned)(t >> 16),
+                (unsigned)(t & 0xffff),
+                (unsigned)s);
+}
+
+
+static void
+append_sexp_tags (GString *gstr, MuMsg *msg)
+{
+       const GSList *tags, *t;
+       gchar *tagesc;
+       GString *tagstr = g_string_new("");
+
+       tags = mu_msg_get_tags (msg);
+
+       for(t = tags; t; t = t->next) {
+               if (t != tags)
+                       g_string_append(tagstr, " ");
+
+               tagesc = mu_str_escape_c_literal((const gchar *)t->data, TRUE);
+               g_string_append(tagstr, tagesc);
+
+               g_free(tagesc);
+       }
+
+       if (tagstr->len > 0)
+               g_string_append_printf (gstr, "\t:tags (%s)\n",
+                                       tagstr->str);
+       g_string_free (tagstr, TRUE);
+}
+
+char*
+mu_msg_to_sexp (MuMsg *msg, unsigned docid, const MuMsgIterThreadInfo *ti,
+               MuMsgOptions opts)
+{
+       GString *gstr;
+
+       g_return_val_if_fail (msg, NULL);
+       g_return_val_if_fail (!((opts & MU_MSG_OPTION_HEADERS_ONLY) &&
+                               (opts & MU_MSG_OPTION_EXTRACT_IMAGES)),NULL);
+       gstr = g_string_sized_new
+               ((opts & MU_MSG_OPTION_HEADERS_ONLY) ?  1024 : 8192);
+
+       if (docid == 0)
+               g_string_append (gstr, "(\n");
+       else
+               g_string_append_printf (gstr, "(\n\t:docid %u\n", docid);
+
+       if (ti)
+               append_sexp_thread_info (gstr, ti);
+
+       append_sexp_attr (gstr, "subject", mu_msg_get_subject (msg));
+
+       /* in the no-headers-only case (see below) we get a more complete list
+        * of contacts, so no need to get them here if that's the case */
+       if (opts & MU_MSG_OPTION_HEADERS_ONLY)
+               append_sexp_contacts (gstr, msg);
+
+       append_sexp_date_and_size (gstr, msg);
+
+       append_sexp_attr (gstr, "message-id", mu_msg_get_msgid (msg));
+       append_sexp_attr (gstr, "mailing-list",
+                         mu_msg_get_mailing_list (msg));
+       append_sexp_attr (gstr, "path",  mu_msg_get_path (msg));
+       append_sexp_attr (gstr, "maildir", mu_msg_get_maildir (msg));
+       g_string_append_printf (gstr, "\t:priority %s\n",
+                               mu_msg_prio_name(mu_msg_get_prio(msg)));
+       append_sexp_flags (gstr, msg);
+       append_sexp_tags  (gstr, msg);
+
+       append_sexp_attr_list (gstr, "references",
+                              mu_msg_get_references (msg));
+       append_sexp_attr (gstr, "in-reply-to",
+                         mu_msg_get_header (msg, "In-Reply-To"));
+
+       /* headers are retrieved from the database, views from the
+        * message file file attr things can only be gotten from the
+        * file (ie., mu view), not from the database (mu find).  */
+       if (!(opts & MU_MSG_OPTION_HEADERS_ONLY))
+               append_message_file_parts (gstr, msg, opts);
+
+       g_string_append (gstr, ")\n");
+       return g_string_free (gstr, FALSE);
+}
diff --git a/lib/mu-msg.c b/lib/mu-msg.c
new file mode 100644 (file)
index 0000000..976f3c9
--- /dev/null
@@ -0,0 +1,1013 @@
+/* -*- 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 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 <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <ctype.h>
+
+#include <gmime/gmime.h>
+
+#include "mu-msg-priv.h" /* include before mu-msg.h */
+#include "mu-msg.h"
+#include "utils/mu-str.h"
+
+#include "mu-maildir.h"
+
+/* 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. */
+static gboolean _gmime_initialized = FALSE;
+
+static void
+gmime_init (void)
+{
+       g_return_if_fail (!_gmime_initialized);
+
+       g_mime_init();
+       _gmime_initialized = TRUE;
+}
+
+static void
+gmime_uninit (void)
+{
+       g_return_if_fail (_gmime_initialized);
+
+       g_mime_shutdown();
+       _gmime_initialized = FALSE;
+}
+
+static MuMsg*
+msg_new (void)
+{
+       MuMsg *self;
+
+       self = g_slice_new0 (MuMsg);
+       self->_refcount = 1;
+
+       return self;
+}
+
+MuMsg*
+mu_msg_new_from_file (const char *path, const char *mdir,
+                     GError **err)
+{
+       MuMsg *self;
+       MuMsgFile *msgfile;
+
+       g_return_val_if_fail (path, NULL);
+
+       if (G_UNLIKELY(!_gmime_initialized)) {
+               gmime_init ();
+               atexit (gmime_uninit);
+       }
+
+       msgfile = mu_msg_file_new (path, mdir, err);
+       if (!msgfile)
+               return NULL;
+
+       self = msg_new ();
+       self->_file = msgfile;
+
+       return self;
+}
+
+MuMsg*
+mu_msg_new_from_doc (XapianDocument *doc, GError **err)
+{
+       MuMsg *self;
+       MuMsgDoc *msgdoc;
+
+       g_return_val_if_fail (doc, NULL);
+
+       if (G_UNLIKELY(!_gmime_initialized)) {
+               gmime_init ();
+               atexit (gmime_uninit);
+       }
+
+       msgdoc = mu_msg_doc_new (doc, err);
+       if (!msgdoc)
+               return NULL;
+
+       self = msg_new ();
+       self->_doc = msgdoc;
+
+       return self;
+}
+
+static void
+mu_msg_destroy (MuMsg *self)
+{
+       if (!self)
+               return;
+
+       mu_msg_file_destroy (self->_file);
+       mu_msg_doc_destroy  (self->_doc);
+
+       { /* cleanup the strings / lists we stored */
+               mu_str_free_list (self->_free_later_str);
+               g_slist_foreach (self->_free_later_lst,
+                                (GFunc)mu_str_free_list, NULL);
+               g_slist_free (self->_free_later_lst);
+       }
+
+       g_slice_free (MuMsg, self);
+}
+
+MuMsg*
+mu_msg_ref (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+
+       ++self->_refcount;
+
+       return self;
+}
+
+void
+mu_msg_unref (MuMsg *self)
+{
+       g_return_if_fail (self);
+       g_return_if_fail (self->_refcount >= 1);
+
+       if (--self->_refcount == 0)
+               mu_msg_destroy (self);
+}
+
+static const gchar*
+free_later_str (MuMsg *self, gchar *str)
+{
+       if (str)
+               self->_free_later_str =
+                       g_slist_prepend (self->_free_later_str, str);
+       return str;
+}
+
+static const GSList*
+free_later_lst (MuMsg *self, GSList *lst)
+{
+       if (lst)
+               self->_free_later_lst =
+                       g_slist_prepend (self->_free_later_lst, lst);
+       return lst;
+}
+
+/* use this instead of mu_msg_get_path so we don't get into infinite
+ * regress...*/
+static const char*
+get_path (MuMsg *self)
+{
+       char *val;
+       gboolean do_free;
+
+       do_free = TRUE;
+       val     = NULL;
+
+       if (self->_doc)
+               val = mu_msg_doc_get_str_field
+                       (self->_doc, MU_MSG_FIELD_ID_PATH);
+
+       /* not in the cache yet? try to get it from the file backend,
+        * in case we are using that */
+       if (!val && self->_file)
+               val = mu_msg_file_get_str_field
+                       (self->_file, MU_MSG_FIELD_ID_PATH, &do_free);
+
+       /* shouldn't happen */
+       if (!val)
+               g_warning ("%s: cannot find path", __func__);
+
+       return free_later_str (self, val);
+}
+
+/* for some data, we need to read the message file from disk */
+gboolean
+mu_msg_load_msg_file (MuMsg *self, GError **err)
+{
+       const char *path;
+
+       g_return_val_if_fail (self, FALSE);
+
+       if (self->_file)
+               return TRUE; /* nothing to do */
+
+       if (!(path = get_path (self))) {
+               mu_util_g_set_error (err, MU_ERROR_INTERNAL,
+                                    "cannot get path for message");
+               return FALSE;
+       }
+
+       self->_file = mu_msg_file_new (path, NULL, err);
+
+       return  (self->_file != NULL);
+}
+
+void
+mu_msg_unload_msg_file (MuMsg *msg)
+{
+       g_return_if_fail (msg);
+
+       mu_msg_file_destroy (msg->_file);
+       msg->_file = NULL;
+}
+
+static const GSList*
+get_str_list_field (MuMsg *self, MuMsgFieldId mfid)
+{
+       GSList *val;
+
+       val = NULL;
+
+       if (self->_doc && mu_msg_field_xapian_value (mfid))
+               val = mu_msg_doc_get_str_list_field (self->_doc, mfid);
+       else if (mu_msg_field_gmime (mfid)) {
+               /* if we don't have a file object yet, we need to
+                * create it from the file on disk */
+               if (!mu_msg_load_msg_file (self, NULL))
+                       return NULL;
+               val = mu_msg_file_get_str_list_field (self->_file, mfid);
+       }
+
+       return free_later_lst (self, val);
+}
+
+static const char*
+get_str_field (MuMsg *self, MuMsgFieldId mfid)
+{
+       char *val;
+       gboolean do_free;
+
+       do_free = TRUE;
+       val     = NULL;
+
+       if (self->_doc && mu_msg_field_xapian_value (mfid))
+               val = mu_msg_doc_get_str_field (self->_doc, mfid);
+
+       else if (mu_msg_field_gmime (mfid)) {
+               /* if we don't have a file object yet, we need to
+                * create it from the file on disk */
+               if (!mu_msg_load_msg_file (self, NULL))
+                       return NULL;
+               val = mu_msg_file_get_str_field (self->_file, mfid, &do_free);
+       } else
+               val = NULL;
+
+       return do_free ? free_later_str (self, val) : val;
+}
+
+static gint64
+get_num_field (MuMsg *self, MuMsgFieldId mfid)
+{
+       guint64 val;
+
+       val = -1;
+       if (self->_doc && mu_msg_field_xapian_value (mfid))
+               val = mu_msg_doc_get_num_field (self->_doc, mfid);
+       else {
+               /* if we don't have a file object yet, we need to
+                * create it from the file on disk */
+               if (!mu_msg_load_msg_file (self, NULL))
+                       return -1;
+               val = mu_msg_file_get_num_field (self->_file, mfid);
+       }
+
+       return val;
+}
+
+const char*
+mu_msg_get_header (MuMsg *self, const char *header)
+{
+       g_return_val_if_fail (self, NULL);
+       g_return_val_if_fail (header, NULL);
+
+       /* if we don't have a file object yet, we need to
+        * create it from the file on disk */
+       if (!mu_msg_load_msg_file (self, NULL))
+               return NULL;
+
+       return free_later_str
+               (self, mu_msg_file_get_header (self->_file, header));
+}
+
+time_t
+mu_msg_get_timestamp (MuMsg *self)
+{
+       const char *path;
+       struct stat statbuf;
+
+       g_return_val_if_fail (self, 0);
+
+       if (self->_file)
+               return self->_file->_timestamp;
+
+       path = mu_msg_get_path (self);
+       if (!path || stat (path, &statbuf) < 0)
+               return 0;
+
+       return statbuf.st_mtime;
+}
+
+const char*
+mu_msg_get_path (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_field (self, MU_MSG_FIELD_ID_PATH);
+}
+
+const char*
+mu_msg_get_subject  (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_field (self, MU_MSG_FIELD_ID_SUBJECT);
+}
+
+const char*
+mu_msg_get_msgid  (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_field (self, MU_MSG_FIELD_ID_MSGID);
+}
+
+const char*
+mu_msg_get_mailing_list (MuMsg *self)
+{
+       const char      *ml;
+       char            *decml;
+
+       g_return_val_if_fail (self, NULL);
+
+       ml = get_str_field (self, MU_MSG_FIELD_ID_MAILING_LIST);
+       if (!ml)
+               return NULL;
+
+       decml = g_mime_utils_header_decode_text (NULL, ml);
+       if (!decml)
+               return NULL;
+
+       return free_later_str (self, decml);
+}
+
+const char*
+mu_msg_get_maildir (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_field (self, MU_MSG_FIELD_ID_MAILDIR);
+}
+
+const char*
+mu_msg_get_from (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_field (self, MU_MSG_FIELD_ID_FROM);
+}
+
+const char*
+mu_msg_get_to (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_field (self, MU_MSG_FIELD_ID_TO);
+}
+
+const char*
+mu_msg_get_cc (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_field (self, MU_MSG_FIELD_ID_CC);
+}
+
+const char*
+mu_msg_get_bcc (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_field (self, MU_MSG_FIELD_ID_BCC);
+}
+
+time_t
+mu_msg_get_date (MuMsg *self)
+{
+       g_return_val_if_fail (self, (time_t)-1);
+       return (time_t)get_num_field (self, MU_MSG_FIELD_ID_DATE);
+}
+
+MuFlags
+mu_msg_get_flags (MuMsg *self)
+{
+       g_return_val_if_fail (self, MU_FLAG_NONE);
+       return (MuFlags)get_num_field (self, MU_MSG_FIELD_ID_FLAGS);
+}
+
+size_t
+mu_msg_get_size (MuMsg *self)
+{
+       g_return_val_if_fail (self, (size_t)-1);
+       return (size_t)get_num_field (self, MU_MSG_FIELD_ID_SIZE);
+}
+
+MuMsgPrio
+mu_msg_get_prio (MuMsg *self)
+{
+       g_return_val_if_fail (self, MU_MSG_PRIO_NORMAL);
+       return (MuMsgPrio)get_num_field (self, MU_MSG_FIELD_ID_PRIO);
+}
+
+struct _BodyData {
+       GString *gstr;
+       gboolean want_html;
+};
+typedef struct _BodyData BodyData;
+
+static void
+accumulate_body (MuMsg *msg, MuMsgPart *mpart, BodyData *bdata)
+{
+       char            *txt;
+       GMimePart       *mimepart;
+       gboolean         has_err, is_plain, is_html;
+
+       if (!GMIME_IS_PART(mpart->data))
+               return;
+       if (mpart->part_type &  MU_MSG_PART_TYPE_ATTACHMENT)
+               return;
+
+       mimepart = (GMimePart*)mpart->data;
+       is_html  = mpart->part_type &   MU_MSG_PART_TYPE_TEXT_HTML;
+       is_plain = mpart->part_type &   MU_MSG_PART_TYPE_TEXT_PLAIN;
+
+       txt         = NULL;
+       has_err     = TRUE;
+       if ((bdata->want_html && is_html) || (!bdata->want_html && is_plain))
+               txt = mu_msg_mime_part_to_string (mimepart, &has_err);
+
+       if (!has_err && txt)
+               bdata->gstr = g_string_append (bdata->gstr, txt);
+
+       g_free (txt);
+}
+
+static char*
+get_body (MuMsg *self, MuMsgOptions opts, gboolean want_html)
+{
+       BodyData bdata;
+
+       bdata.want_html = want_html;
+       bdata.gstr      = g_string_sized_new (4096);
+
+       /* wipe out some irrelevant options */
+       opts &= ~MU_MSG_OPTION_VERIFY;
+       opts &= ~MU_MSG_OPTION_EXTRACT_IMAGES;
+
+       mu_msg_part_foreach (self, opts,
+                            (MuMsgPartForeachFunc)accumulate_body,
+                            &bdata);
+
+       if (bdata.gstr->len == 0) {
+               g_string_free (bdata.gstr, TRUE);
+               return NULL;
+       } else
+               return g_string_free (bdata.gstr, FALSE);
+}
+
+typedef struct {
+       GMimeContentType        *ctype;
+       gboolean                 want_html;
+} ContentTypeData;
+
+static void
+find_content_type (MuMsg *msg, MuMsgPart *mpart, ContentTypeData *cdata)
+{
+       GMimePart *wanted;
+
+       if (!GMIME_IS_PART(mpart->data))
+               return;
+
+       /* text-like attachments are included when in text-mode */
+
+       if (!cdata->want_html &&
+           (mpart->part_type & MU_MSG_PART_TYPE_TEXT_PLAIN))
+               wanted = mpart->data;
+       else if (!(mpart->part_type & MU_MSG_PART_TYPE_ATTACHMENT) &&
+                cdata->want_html &&
+                (mpart->part_type & MU_MSG_PART_TYPE_TEXT_HTML))
+               wanted = mpart->data;
+       else
+               wanted = NULL;
+
+       if (wanted)
+               cdata->ctype = g_mime_object_get_content_type (
+                       GMIME_OBJECT(wanted));
+}
+
+static const GSList*
+get_content_type_parameters (MuMsg *self, MuMsgOptions opts, gboolean want_html)
+{
+       ContentTypeData cdata;
+
+       cdata.want_html = want_html;
+       cdata.ctype     = NULL;
+
+       /* wipe out some irrelevant options */
+       opts &= ~MU_MSG_OPTION_VERIFY;
+       opts &= ~MU_MSG_OPTION_EXTRACT_IMAGES;
+
+       mu_msg_part_foreach (self, opts,
+                            (MuMsgPartForeachFunc)find_content_type,
+                            &cdata);
+
+       if (cdata.ctype) {
+
+               GSList                  *gslist;
+               GMimeParamList          *paramlist;
+               const GMimeParam        *param;
+               int i, len;
+
+               gslist    = NULL;
+               paramlist = g_mime_content_type_get_parameters (cdata.ctype);
+               len       = g_mime_param_list_length (paramlist);
+
+               for (i = 0; i < len; ++i) {
+                       param = g_mime_param_list_get_parameter_at (paramlist, i);
+                       gslist = g_slist_prepend (gslist, g_strdup (param->name));
+                       gslist = g_slist_prepend (gslist, g_strdup (param->value));
+               }
+
+               return free_later_lst (self, g_slist_reverse (gslist));
+       }
+       return NULL;
+}
+
+const GSList*
+mu_msg_get_body_text_content_type_parameters (MuMsg *self, MuMsgOptions opts)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_content_type_parameters(self, opts, FALSE);
+}
+
+const char*
+mu_msg_get_body_html (MuMsg *self, MuMsgOptions opts)
+{
+       g_return_val_if_fail (self, NULL);
+       return free_later_str (self, get_body (self, opts, TRUE));
+}
+
+const char*
+mu_msg_get_body_text (MuMsg *self, MuMsgOptions opts)
+{
+       g_return_val_if_fail (self, NULL);
+       return free_later_str (self, get_body (self, opts, FALSE));
+}
+
+const GSList*
+mu_msg_get_references (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_list_field (self, MU_MSG_FIELD_ID_REFS);
+}
+
+const GSList*
+mu_msg_get_tags (MuMsg *self)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_list_field (self, MU_MSG_FIELD_ID_TAGS);
+}
+
+const char*
+mu_msg_get_field_string (MuMsg *self, MuMsgFieldId mfid)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_field (self, mfid);
+}
+
+const GSList*
+mu_msg_get_field_string_list (MuMsg *self, MuMsgFieldId mfid)
+{
+       g_return_val_if_fail (self, NULL);
+       return get_str_list_field (self, mfid);
+}
+
+gint64
+mu_msg_get_field_numeric (MuMsg *self, MuMsgFieldId mfid)
+{
+       g_return_val_if_fail (self, -1);
+       return get_num_field (self, mfid);
+}
+
+static gboolean
+fill_contact (MuMsgContact *self, InternetAddress *addr,
+             MuMsgContactType ctype)
+{
+       if (!addr)
+               return FALSE;
+
+       self->full_address = internet_address_to_string (
+               addr, NULL, FALSE);
+
+       self->name = internet_address_get_name (addr);
+       if (mu_str_is_empty (self->name)) {
+               self->name = NULL;
+       }
+
+       self->type = ctype;
+
+       /* we only support internet mailbox addresses; if we don't
+        * check, g_mime hits an assert
+        */
+       if (INTERNET_ADDRESS_IS_MAILBOX(addr))
+               self->email= internet_address_mailbox_get_addr
+                       (INTERNET_ADDRESS_MAILBOX(addr));
+       else
+               self->email  = NULL;
+
+       /* if there's no address, just a name, it's probably a local
+        * address (without @) */
+       if (self->name && !self->email)
+               self->email = self->name;
+
+       /* note, the address could be NULL e.g. when the recipient is something
+        * like 'Undisclosed recipients'
+        */
+       return self->email != NULL;
+}
+
+static void
+address_list_foreach (InternetAddressList *addrlist, MuMsgContactType ctype,
+                     MuMsgContactForeachFunc func, gpointer user_data)
+{
+       int i, len;
+
+       if (!addrlist)
+               return;
+
+       len = internet_address_list_length(addrlist);
+
+       for (i = 0; i != len; ++i) {
+               MuMsgContact    contact;
+               gboolean        keep_going;
+
+               if (!fill_contact(&contact,
+                                 internet_address_list_get_address (addrlist, i),
+                                 ctype))
+                       continue;
+
+               keep_going = func(&contact, user_data);
+               g_free ((char*)contact.full_address);
+
+               if (!keep_going)
+                       break;
+       }
+}
+
+static void
+addresses_foreach (const char* addrs, MuMsgContactType ctype,
+                  MuMsgContactForeachFunc func, gpointer user_data)
+{
+       InternetAddressList *addrlist;
+
+       if (!addrs)
+               return;
+
+       addrlist = internet_address_list_parse (NULL, addrs);
+       if (addrlist) {
+               address_list_foreach (addrlist, ctype, func, user_data);
+               g_object_unref (addrlist);
+       }
+}
+
+static void
+msg_contact_foreach_file (MuMsg *msg, MuMsgContactForeachFunc func,
+                         gpointer user_data)
+{
+       int i;
+       struct {
+               GMimeAddressType _gmime_type;
+               MuMsgContactType _type;
+       } ctypes[] = {
+               {GMIME_ADDRESS_TYPE_FROM,     MU_MSG_CONTACT_TYPE_FROM},
+               {GMIME_ADDRESS_TYPE_REPLY_TO, MU_MSG_CONTACT_TYPE_REPLY_TO},
+               {GMIME_ADDRESS_TYPE_TO,       MU_MSG_CONTACT_TYPE_TO},
+               {GMIME_ADDRESS_TYPE_CC,       MU_MSG_CONTACT_TYPE_CC},
+               {GMIME_ADDRESS_TYPE_BCC,      MU_MSG_CONTACT_TYPE_BCC},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(ctypes); ++i) {
+               InternetAddressList *addrlist;
+               addrlist = g_mime_message_get_addresses (msg->_file->_mime_msg,
+                                                        ctypes[i]._gmime_type);
+               address_list_foreach (addrlist, ctypes[i]._type, func, user_data);
+       }
+}
+
+static void
+msg_contact_foreach_doc (MuMsg *msg, MuMsgContactForeachFunc func,
+                        gpointer user_data)
+{
+       addresses_foreach (mu_msg_get_from (msg),
+                          MU_MSG_CONTACT_TYPE_FROM, func, user_data);
+       addresses_foreach (mu_msg_get_to (msg),
+                          MU_MSG_CONTACT_TYPE_TO, func, user_data);
+       addresses_foreach (mu_msg_get_cc (msg),
+                          MU_MSG_CONTACT_TYPE_CC, func, user_data);
+       addresses_foreach (mu_msg_get_bcc (msg),
+                          MU_MSG_CONTACT_TYPE_BCC, func, user_data);
+}
+
+void
+mu_msg_contact_foreach (MuMsg *msg, MuMsgContactForeachFunc func,
+                       gpointer user_data)
+{
+       g_return_if_fail (msg);
+       g_return_if_fail (func);
+
+       if (msg->_file)
+               msg_contact_foreach_file (msg, func, user_data);
+       else if (msg->_doc)
+               msg_contact_foreach_doc (msg, func, user_data);
+       else
+               g_return_if_reached ();
+}
+
+static int
+cmp_str (const char *s1, const char *s2)
+{
+       if (s1 == s2)
+               return 0;
+       else if (!s1)
+               return -1;
+       else if (!s2)
+               return 1;
+
+       /* optimization 1: ascii */
+       if (isascii(s1[0]) && isascii(s2[0])) {
+               int diff;
+               diff = tolower(s1[0]) - tolower(s2[0]);
+               if (diff != 0)
+                       return diff;
+       }
+
+       /* utf 8 */
+       {
+               char *u1, *u2;
+               int diff;
+
+               u1 = g_utf8_strdown (s1, -1);
+               u2 = g_utf8_strdown (s2, -1);
+
+               diff = g_utf8_collate (u1, u2);
+
+               g_free (u1);
+               g_free (u2);
+
+               return diff;
+       }
+}
+
+static int
+cmp_subject (const char* s1, const char *s2)
+{
+       if (s1 == s2)
+               return 0;
+       else if (!s1)
+               return -1;
+       else if (!s2)
+               return 1;
+
+       return cmp_str (
+               mu_str_subject_normalize (s1),
+               mu_str_subject_normalize (s2));
+}
+
+int
+mu_msg_cmp (MuMsg *m1, MuMsg *m2, MuMsgFieldId mfid)
+{
+       g_return_val_if_fail (m1, 0);
+       g_return_val_if_fail (m2, 0);
+       g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), 0);
+
+       /* even though date is a numeric field, we can sort it by its
+        * string repr. in the database, which is much faster */
+       if (mfid == MU_MSG_FIELD_ID_DATE ||
+           mu_msg_field_is_string (mfid))
+               return cmp_str (get_str_field (m1, mfid),
+                               get_str_field (m2, mfid));
+
+       if (mfid == MU_MSG_FIELD_ID_SUBJECT)
+               return cmp_subject (get_str_field (m1, mfid),
+                                   get_str_field (m2, mfid));
+
+       /* TODO: note, we cast (potentially > MAXINT to int) */
+       if (mu_msg_field_is_numeric (mfid))
+               return get_num_field(m1, mfid) - get_num_field(m2, mfid);
+
+       return 0; /* TODO: handle lists */
+}
+
+gboolean
+mu_msg_is_readable (MuMsg *self)
+{
+       g_return_val_if_fail (self, FALSE);
+
+       return access (mu_msg_get_path (self), R_OK) == 0 ? TRUE : FALSE;
+}
+
+/* we need do to determine the
+ *   /home/foo/Maildir/bar
+ * from the /bar
+ * that we got
+ */
+static char*
+get_target_mdir (MuMsg *msg, const char *target_maildir, GError **err)
+{
+       char *rootmaildir, *rv;
+       const char *maildir;
+       gboolean not_top_level;
+
+       /* maildir is the maildir stored in the message, e.g. '/foo' */
+       maildir = mu_msg_get_maildir(msg);
+       if (!maildir) {
+               mu_util_g_set_error (err, MU_ERROR_GMIME,
+                                    "message without maildir");
+               return NULL;
+       }
+
+       /* the 'rootmaildir' is the filesystem path from root to
+        * maildir, ie.  /home/user/Maildir/foo */
+       rootmaildir = mu_maildir_get_maildir_from_path (mu_msg_get_path(msg));
+       if (!rootmaildir) {
+               mu_util_g_set_error (err, MU_ERROR_GMIME,
+                                    "cannot determine maildir");
+               return NULL;
+       }
+
+       /* we do a sanity check: verify that that maildir is a suffix of
+        * rootmaildir;*/
+       not_top_level = TRUE;
+       if (!g_str_has_suffix (rootmaildir, maildir) &&
+           /* special case for the top-level '/' maildir, and
+            * remember not_top_level */
+           (not_top_level = (g_strcmp0 (maildir, "/") != 0))) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE,
+                            "path is '%s', but maildir is '%s' ('%s')",
+                            rootmaildir, mu_msg_get_maildir(msg),
+                            mu_msg_get_path (msg));
+               g_free (rootmaildir);
+               return NULL;
+       }
+
+       /* if we're not at the top-level, remove the final '/' from
+        * the rootmaildir */
+       if (not_top_level)
+               rootmaildir[strlen(rootmaildir) -
+                           strlen (mu_msg_get_maildir(msg))] = '\0';
+
+       rv = g_strconcat (rootmaildir, target_maildir, NULL);
+       g_free (rootmaildir);
+
+       return rv;
+}
+
+/*
+ * move a msg to another maildir, trying to maintain 'integrity',
+ * ie. msg in 'new/' will go to new/, one in cur/ goes to cur/. be
+ * super-paranoid here...
+ */
+gboolean
+mu_msg_move_to_maildir (MuMsg *self, const char *maildir,
+                       MuFlags flags, gboolean ignore_dups,
+                       gboolean new_name, GError **err)
+{
+       char *newfullpath;
+       char *targetmdir;
+
+       g_return_val_if_fail (self, FALSE);
+       g_return_val_if_fail (maildir, FALSE);     /* i.e. "/inbox" */
+
+       /* targetmdir is the full path to maildir, i.e.,
+        * /home/foo/Maildir/inbox */
+       targetmdir = get_target_mdir (self, maildir, err);
+       if (!targetmdir)
+               return FALSE;
+
+       newfullpath = mu_maildir_move_message (mu_msg_get_path (self),
+                                              targetmdir, flags,
+                                              ignore_dups, new_name,
+                                              err);
+       if (!newfullpath) {
+               g_free (targetmdir);
+               return FALSE;
+       }
+
+       /* clear the old backends */
+       mu_msg_doc_destroy  (self->_doc);
+       self->_doc = NULL;
+
+       mu_msg_file_destroy (self->_file);
+
+       /* and create a new one */
+       self->_file = mu_msg_file_new (newfullpath, maildir, err);
+       g_free (targetmdir);
+       g_free (newfullpath);
+
+       return self->_file ? TRUE : FALSE;
+}
+
+/*
+ * Rename a message-file, keeping the same flags. This is useful for tricking
+ * some 3rd party progs such as mbsync
+ */
+gboolean
+mu_msg_tickle (MuMsg *self, GError **err)
+{
+       g_return_val_if_fail (self, FALSE);
+
+       return mu_msg_move_to_maildir (self,
+                                      mu_msg_get_maildir (self),
+                                      mu_msg_get_flags (self),
+                                      FALSE, TRUE, err);
+}
+
+const char*
+mu_str_flags_s  (MuFlags flags)
+{
+       return mu_flags_to_str_s (flags, MU_FLAG_TYPE_ANY);
+}
+
+char*
+mu_str_flags  (MuFlags flags)
+{
+       return g_strdup (mu_str_flags_s(flags));
+}
+
+static void
+cleanup_contact (char *contact)
+{
+       char *c, *c2;
+
+       /* replace "'<> with space */
+       for (c2 = contact; *c2; ++c2)
+               if (*c2 == '"' || *c2 == '\'' || *c2 == '<' || *c2 == '>')
+                       *c2 = ' ';
+
+       /* remove everything between '()' if it's after the 5th pos;
+        * good to cleanup corporate contact address spam... */
+       c = g_strstr_len (contact, -1, "(");
+       if (c && c - contact > 5)
+               *c = '\0';
+
+       g_strstrip (contact);
+}
+
+
+/* this is still somewhat simplistic... */
+const char*
+mu_str_display_contact_s (const char *str)
+{
+       static gchar contact[255];
+       gchar *c, *c2;
+
+       str = str ? str : "";
+       g_strlcpy (contact, str, sizeof(contact));
+
+       /* we check for '<', so we can strip out the address stuff in
+        * e.g. 'Hello World <hello@world.xx>, but only if there is
+        * something alphanumeric before the <
+        */
+       c = g_strstr_len (contact, -1, "<");
+       if (c != NULL) {
+               for (c2 = contact; c2 < c && !(isalnum(*c2)); ++c2);
+               if (c2 != c) /* apparently, there was something,
+                             * so we can remove the <... part*/
+                       *c = '\0';
+       }
+
+       cleanup_contact (contact);
+
+       return contact;
+}
+
+char*
+mu_str_display_contact (const char *str)
+{
+       g_return_val_if_fail (str, NULL);
+
+       return g_strdup (mu_str_display_contact_s (str));
+}
diff --git a/lib/mu-msg.h b/lib/mu-msg.h
new file mode 100644 (file)
index 0000000..e4842aa
--- /dev/null
@@ -0,0 +1,654 @@
+/* -*- mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+**
+** Copyright (C) 2010-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.
+**
+*/
+
+#ifndef __MU_MSG_H__
+#define __MU_MSG_H__
+
+#include <mu-flags.h>
+#include <mu-msg-fields.h>
+#include <mu-msg-prio.h>
+#include <utils/mu-util.h>
+
+G_BEGIN_DECLS
+
+struct _MuMsg;
+typedef struct _MuMsg MuMsg;
+
+/* options for various functions */
+enum _MuMsgOptions {
+       MU_MSG_OPTION_NONE              = 0,
+/* 1 << 0 is still free! */
+
+       /* for -> sexp conversion */
+       MU_MSG_OPTION_HEADERS_ONLY      = 1 << 1,
+       MU_MSG_OPTION_EXTRACT_IMAGES    = 1 << 2,
+
+       /* below options are for checking signatures; only effective
+        * if mu was built with crypto support */
+       MU_MSG_OPTION_VERIFY            = 1 << 4,
+       MU_MSG_OPTION_AUTO_RETRIEVE     = 1 << 5,
+       MU_MSG_OPTION_USE_AGENT         = 1 << 6,
+       /* MU_MSG_OPTION_USE_PKCS7         = 1 << 7,   /\* gpg is the default *\/ */
+
+       /* get password from console if needed */
+       MU_MSG_OPTION_CONSOLE_PASSWORD  = 1 << 7,
+
+       MU_MSG_OPTION_DECRYPT           = 1 << 8,
+
+       /* misc */
+       MU_MSG_OPTION_OVERWRITE         = 1 << 9,
+       MU_MSG_OPTION_USE_EXISTING      = 1 << 10,
+
+       /* recurse into submessages */
+       MU_MSG_OPTION_RECURSE_RFC822    = 1 << 11
+
+};
+typedef enum _MuMsgOptions MuMsgOptions;
+
+
+
+/**
+ * create a new MuMsg* instance which parses a message and provides
+ * read access to its properties; call mu_msg_unref when done with it.
+ *
+ * @param path full path to an email message file
+ * @param mdir the maildir for this message; ie, if the path is
+ * ~/Maildir/foo/bar/cur/msg, the maildir would be foo/bar; you can
+ * pass NULL for this parameter, in which case some maildir-specific
+ * information is not available.
+ * @param err receive error information (MU_ERROR_FILE or
+ * MU_ERROR_GMIME), or NULL. There will only be err info if the
+ * function returns NULL
+ *
+ * @return a new MuMsg instance or NULL in case of error; call
+ * mu_msg_unref when done with this message
+ */
+MuMsg *mu_msg_new_from_file (const char* filepath, const char *maildir,
+                            GError **err)
+                            G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * create a new MuMsg* instance based on a Xapian::Document
+ *
+ * @param store a MuStore ptr
+ * @param doc a ptr to a Xapian::Document (but cast to XapianDocument,
+ * because this is C not C++). MuMsg takes _ownership_ of this pointer;
+ * don't touch it afterwards
+ * @param err receive error information, or NULL. There
+ * will only be err info if the function returns NULL
+ *
+ * @return a new MuMsg instance or NULL in case of error; call
+ * mu_msg_unref when done with this message
+ */
+MuMsg *mu_msg_new_from_doc (XapianDocument* doc, GError **err)
+       G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ *  if we don't have a message file yet (because this message is
+ *  database-backed), load it.
+ *
+ * @param msg a MuMsg
+ * @param err receives error information
+ *
+ * @return TRUE if this succeeded, FALSE in case of error
+ */
+gboolean mu_msg_load_msg_file (MuMsg *msg, GError **err);
+
+
+/**
+ * close the file-backend, if any; this function is for the use case
+ * where you have a large amount of messages where you need some
+ * file-backed field (body or attachments). If you don't close the
+ * file-backend after retrieving the desired field, you'd quickly run
+ * out of file descriptors. If this message does not have a
+ * file-backend, do nothing.
+ *
+ * @param msg a message object
+ */
+void mu_msg_unload_msg_file (MuMsg *msg);
+
+/**
+ * increase the reference count for this message
+ *
+ * @param msg a message
+ *
+ * @return the message with its reference count increased, or NULL in
+ * case of error.
+ */
+MuMsg *mu_msg_ref (MuMsg *msg);
+
+/**
+ * decrease the reference count for this message. if the reference
+ * count reaches 0, the message will be destroyed.
+ *
+ * @param msg a message
+ */
+void mu_msg_unref (MuMsg *msg);
+
+/**
+ * cache the values from the backend (file or db), so we don't the
+ * backend anymore
+ *
+ * @param self a message
+ */
+void mu_msg_cache_values (MuMsg *self);
+
+
+/**
+ * get the plain text body of this message
+ *
+ * @param msg a valid MuMsg* instance
+ * @param opts options for getting the body
+ *
+ * @return the plain text body or NULL in case of error or if there is no
+ * such body. the returned string should *not* be modified or freed.
+ * The returned data is in UTF8 or NULL.
+ */
+const char*     mu_msg_get_body_text       (MuMsg *msg, MuMsgOptions opts);
+
+
+/**
+ * get the content type parameters for the text body part
+ *
+ * @param msg a valid MuMsg* instance
+ * @param opts options for getting the body
+ *
+ * @return the value of the requested body part content type parameter, or
+ * NULL in case of error or if there is no such body. the returned string
+ * should *not* be modified or freed. The returned data is in UTF8 or NULL.
+ */
+const GSList*  mu_msg_get_body_text_content_type_parameters    (MuMsg *self, MuMsgOptions opts);
+
+
+/**
+ * get the html body of this message
+ *
+ * @param msg a valid MuMsg* instance
+ * @param opts options for getting the body
+ *
+ * @return the html body or NULL in case of error or if there is no
+ * such body. the returned string should *not* be modified or freed.
+ */
+const char*     mu_msg_get_body_html       (MuMsg *msgMu, MuMsgOptions opts);
+
+/**
+ * get the sender (From:) of this message
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the sender of this Message or NULL in case of error or if there
+ * is no sender. the returned string should *not* be modified or freed.
+ */
+const char*     mu_msg_get_from           (MuMsg *msg);
+
+
+/**
+ * get the recipients (To:) of this message
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the sender of this Message or NULL in case of error or if there
+ * are no recipients. the returned string should *not* be modified or freed.
+ */
+const char*     mu_msg_get_to     (MuMsg *msg);
+
+
+/**
+ * get the carbon-copy recipients (Cc:) of this message
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the Cc: recipients of this Message or NULL in case of error or if
+ * there are no such recipients. the returned string should *not* be modified
+ * or freed.
+ */
+const char*     mu_msg_get_cc       (MuMsg *msg);
+
+/**
+ * get the blind carbon-copy recipients (Bcc:) of this message; this
+ * field usually only appears in outgoing messages
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the Bcc: recipients of this Message or NULL in case of
+ * error or if there are no such recipients. the returned string
+ * should *not* be modified or freed.
+ */
+const char*     mu_msg_get_bcc      (MuMsg *msg);
+
+/**
+ * get the file system path of this message
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the path of this Message or NULL in case of error.
+ * the returned string should *not* be modified or freed.
+ */
+const char*     mu_msg_get_path            (MuMsg *msg);
+
+
+/**
+ * get the maildir this message lives in; ie, if the path is
+ * ~/Maildir/foo/bar/cur/msg, the maildir would be foo/bar
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the maildir requested or NULL in case of error. The returned
+ * string should *not* be modified or freed.
+ */
+const char*    mu_msg_get_maildir        (MuMsg *msg);
+
+
+/**
+ * get the subject of this message
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the subject of this Message or NULL in case of error or if there
+ * is no subject. the returned string should *not* be modified or freed.
+ */
+const char*     mu_msg_get_subject         (MuMsg *msg);
+
+/**
+ * get the Message-Id of this message
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the Message-Id of this message (without the enclosing <>),
+ * or a fake message-id for messages that don't have them, or NULL in
+ * case of error.
+ */
+const char*     mu_msg_get_msgid           (MuMsg *msg);
+
+
+/**
+ * get the mailing list for a message, i.e. the mailing-list
+ * identifier in the List-Id header.
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the mailing list id for this message (without the enclosing <>)
+ * or NULL in case of error or if there is none. the returned string
+ * should *not* be modified or freed.
+ */
+const char*     mu_msg_get_mailing_list            (MuMsg *msg);
+
+
+/**
+ * get the message date/time (the Date: field) as time_t, using UTC
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return message date/time or 0 in case of error or if there
+ * is no such header.
+ */
+time_t          mu_msg_get_date            (MuMsg *msg);
+
+/**
+ * get the flags for this message
+ *
+ * @param msg valid MuMsg* instance
+ *
+ * @return the file/content flags as logically OR'd #MuMsgFlags or 0
+ * if there are none. Non-standard flags are ignored.
+ */
+MuFlags     mu_msg_get_flags      (MuMsg *msg);
+
+
+/**
+ * get the file size in bytes of this message
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the filesize
+ */
+size_t mu_msg_get_size (MuMsg *msg);
+
+
+/**
+ * get some field value as string
+ *
+ * @param msg a valid MuMsg instance
+ * @param field the field to retrieve; it must be a string-typed field
+ *
+ * @return a string that should not be freed
+ */
+const char*  mu_msg_get_field_string  (MuMsg *msg, MuMsgFieldId mfid);
+
+
+/**
+ * get some field value as string-list
+ *
+ * @param msg a valid MuMsg instance
+ * @param field the field to retrieve; it must be a string-list-typed field
+ *
+ * @return a list that should not be freed
+ */
+const GSList* mu_msg_get_field_string_list (MuMsg *self, MuMsgFieldId mfid);
+
+/**
+ * get some field value as string
+ *
+ * @param msg a valid MuMsg instance
+ * @param field the field to retrieve; it must be a numeric field
+ *
+ * @return a string that should not be freed
+ */
+gint64      mu_msg_get_field_numeric (MuMsg *msg, MuMsgFieldId mfid);
+
+/**
+ * get the message priority for this message (MU_MSG_PRIO_LOW,
+ * MU_MSG_PRIO_NORMAL or MU_MSG_PRIO_HIGH) the X-Priority,
+ * X-MSMailPriority, Importance and Precedence header are checked, in
+ * that order.  if no known or explicit priority is set,
+ * MU_MSG_PRIO_NORMAL is assumed
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the message priority (!= 0) or 0 in case of error
+ */
+MuMsgPrio   mu_msg_get_prio        (MuMsg *msg);
+
+/**
+ * get the timestamp (mtime) for the file containing this message
+ *
+ * @param msg a valid MuMsg* instance
+ *
+ * @return the timestamp or 0 in case of error
+ */
+time_t     mu_msg_get_timestamp       (MuMsg *msg);
+
+
+/**
+ * get a specific header from the message. This value will _not_ be
+ * cached
+ *
+ * @param self a MuMsg instance
+ * @param header a specific header (like 'X-Mailer' or 'Organization')
+ *
+ * @return a header string which is valid as long as this MuMsg is
+ */
+const char* mu_msg_get_header (MuMsg *self, const char *header);
+
+
+/**
+ * get the 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 are filtered out.
+ *
+ * @param msg a valid MuMsg
+ *
+ * @return a list with the references for this msg. Don't modify/free
+ */
+const GSList* mu_msg_get_references (MuMsg *msg);
+
+/**
+ * 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
+ */
+const GSList* mu_msg_get_tags (MuMsg *self);
+
+
+/**
+ * compare two messages for sorting
+ *
+ * @param m1 a message
+ * @param m2 another message
+ * @param mfid the message to use for the comparison
+ *
+ * @return negative if m1 is smaller, positive if m1 is smaller, 0 if
+ * they are equal
+ */
+int mu_msg_cmp (MuMsg *m1, MuMsg *m2, MuMsgFieldId mfid);
+
+
+/**
+ * check whether there there's a readable file behind this message
+ *
+ * @param self a MuMsg*
+ *
+ * @return TRUE if the message file is readable, FALSE otherwise
+ */
+gboolean mu_msg_is_readable (MuMsg *self);
+
+
+struct _MuMsgIterThreadInfo;
+
+
+/**
+ * convert the msg to a Lisp symbolic expression (for further processing in
+ * e.g. emacs)
+ *
+ * @param msg a valid message
+ * @param docid the docid for this message, or 0
+ * @param ti thread info for the current message, or NULL
+ * @param opts, bitwise OR'ed;
+ *    - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be
+ *      obtained from the database (this is much faster if the MuMsg is
+ *      database-backed, so no file needs to be opened)
+ *    - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary
+ *      files and include links to those in the sexp
+ *  and for message parts:
+ *     MU_MSG_OPTION_CHECK_SIGNATURES: check signatures
+ *     MU_MSG_OPTION_AUTO_RETRIEVE_KEY: attempt to retrieve keys online
+ *     MU_MSG_OPTION_USE_AGENT: attempt to use GPG-agent
+ *     MU_MSG_OPTION_USE_PKCS7: attempt to use PKCS (instead of gpg)
+ *
+ * @return a string with the sexp (free with g_free) or NULL in case of error
+ */
+char* mu_msg_to_sexp (MuMsg *msg, unsigned docid,
+                     const struct _MuMsgIterThreadInfo *ti,
+                     MuMsgOptions ops)
+       G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+#ifdef HAVE_JSON_GLIB
+
+struct _JsonNode; /* forward declaration */
+
+/**
+ * convert the msg to json
+ *
+ * @param msg a valid message
+ * @param docid the docid for this message, or 0
+ * @param ti thread info for the current message, or NULL
+ * @param opts, bitwise OR'ed;
+ *    - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be
+ *      obtained from the database (this is much faster if the MuMsg is
+ *      database-backed, so no file needs to be opened)
+ *    - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary
+ *      files and include links to those in the sexp
+ *
+ * @return a string with the sexp (free with g_free) or NULL in case of error
+ */
+struct _JsonNode* mu_msg_to_json (MuMsg *msg, unsigned docid,
+                                 const struct _MuMsgIterThreadInfo *ti,
+                                 MuMsgOptions ops) G_GNUC_WARN_UNUSED_RESULT;
+#endif /*HAVE_JSON_GLIB*/
+
+/**
+ * move a message to another maildir; note that this does _not_ update
+ * the database
+ *
+ * @param msg a message with an existing file system path in an actual
+ * maildir
+ * @param maildir the subdir where the message should go, relative to
+ * rootmaildir. e.g. "/archive"
+ * @param flags to set for the target (influences the filename, path)
+ * @param silently ignore the src=target case (return TRUE)
+ * @param new_name whether to create a new unique name, or keep the
+ * old one
+ * @param err (may be NULL) may contain error information; note if the
+ * function return FALSE, err is not set for all error condition
+ * (ie. not for parameter error
+ *
+ * @return TRUE if it worked, FALSE otherwise
+ */
+gboolean mu_msg_move_to_maildir (MuMsg *msg, const char *maildir,
+                                MuFlags flags, gboolean ignore_dups,
+                                gboolean new_name,
+                                GError **err);
+
+/**
+ * Tickle a message -- ie., rename a message to some new semi-random name,while
+ * maintaining the maildir and flags. This can be useful when dealing with
+ * third-party tools such as mbsync that depend on changed filenames.
+ *
+ * @param msg a message with an existing file system path in an actual
+ * maildir
+ * @param err (may be NULL) may contain error information; note if the
+ * function return FALSE, err is not set for all error condition
+ * (ie. not for parameter error
+ *
+ * @return TRUE if it worked, FALSE otherwise
+ */
+gboolean mu_msg_tickle (MuMsg *msg, GError **err);
+
+
+enum _MuMsgContactType {  /* Reply-To:? */
+       MU_MSG_CONTACT_TYPE_TO    = 0,
+       MU_MSG_CONTACT_TYPE_FROM,
+       MU_MSG_CONTACT_TYPE_CC,
+       MU_MSG_CONTACT_TYPE_BCC,
+       MU_MSG_CONTACT_TYPE_REPLY_TO,
+
+       MU_MSG_CONTACT_TYPE_NUM
+};
+typedef guint MuMsgContactType;
+
+/* not a 'real' contact type */
+#define MU_MSG_CONTACT_TYPE_ALL (MU_MSG_CONTACT_TYPE_NUM + 1)
+
+#define mu_msg_contact_type_is_valid(MCT)\
+       ((MCT) < MU_MSG_CONTACT_TYPE_NUM)
+
+struct _MuMsgContact {
+       const char              *name;         /**< Foo Bar */
+       const char              *email;        /**< foo@bar.cuux */
+       const char              *full_address; /**< Foo Bar <foo@bar.cuux> */
+       MuMsgContactType         type;         /**< MU_MSG_CONTACT_TYPE_{ TO,
+                                                 CC, BCC, FROM, REPLY_TO} */
+};
+typedef struct _MuMsgContact    MuMsgContact;
+
+
+/**
+ * macro to get the name of a contact
+ *
+ * @param ct a MuMsgContact
+ *
+ * @return the name
+ */
+#define mu_msg_contact_name(ct)    ((ct)->name)
+
+/**
+ * macro to get the email address of a contact
+ *
+ * @param ct a MuMsgContact
+ *
+ * @return the address
+ */
+#define mu_msg_contact_email(ct) ((ct)->email)
+
+/**
+ * macro to get the contact type
+ *
+ * @param ct a MuMsgContact
+ *
+ * @return the contact type
+ */
+#define mu_msg_contact_type(ct)    ((ct)->type)
+
+
+/**
+ * callback function
+ *
+ * @param contact
+ * @param user_data a user provided data pointer
+ *
+ * @return TRUE if we should continue the foreach, FALSE otherwise
+ */
+typedef gboolean  (*MuMsgContactForeachFunc) (MuMsgContact* contact,
+                                             gpointer user_data);
+
+/**
+ * call a function for each of the contacts in a message; the order is:
+ * from to cc bcc (of each there are zero or more)
+ *
+ * @param msg a valid MuMsgGMime* instance
+ * @param func a callback function to call for each contact; when
+ * the callback does not return TRUE, it won't be called again
+ * @param user_data a user-provide pointer that will be passed to the callback
+ *
+ */
+void mu_msg_contact_foreach (MuMsg *msg, MuMsgContactForeachFunc func,
+                            gpointer user_data);
+
+
+
+/**
+ * create a 'display contact' from an email header To/Cc/Bcc/From-type address
+ * ie., turn
+ *     "Foo Bar" <foo@bar.com>
+ * into
+ *      Foo Bar
+ * Note that this is based on some simple heuristics. Max length is 255 bytes.
+ *
+ *   mu_str_display_contact_s returns a statically allocated
+ *   buffer (ie, non-reentrant), while mu_str_display_contact
+ *   returns a newly allocated string that you must free with g_free
+ *   when done with it.
+ *
+ * @param str a 'contact str' (ie., what is in the To/Cc/Bcc/From
+ * fields), or NULL
+ *
+ * @return a newly allocated string with a display contact
+ */
+const char* mu_str_display_contact_s (const char *str) G_GNUC_CONST;
+char *mu_str_display_contact (const char *str) G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * get a display string for a given set of flags, OR'ed in
+ * @param flags; one character per flag:
+ * D=draft,F=flagged,N=new,P=passed,R=replied,S=seen,T=trashed
+ * a=has-attachment,s=signed, x=encrypted
+ *
+ * mu_str_file_flags_s  returns a ptr to a static buffer,
+ * while mu_str_file_flags returns dynamically allocated
+ * memory that must be freed after use.
+ *
+ * @param flags file flags
+ *
+ * @return a string representation of the flags; see above
+ * for what to do with it
+ */
+const char* mu_str_flags_s  (MuFlags flags) G_GNUC_CONST;
+char*       mu_str_flags    (MuFlags flags)
+    G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+
+G_END_DECLS
+
+#endif /*__MU_MSG_H__*/
diff --git a/lib/mu-query.cc b/lib/mu-query.cc
new file mode 100644 (file)
index 0000000..cafb3eb
--- /dev/null
@@ -0,0 +1,524 @@
+/*
+** 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 <stdexcept>
+#include <string>
+#include <cctype>
+#include <cstring>
+#include <sstream>
+
+#include <stdlib.h>
+#include <xapian.h>
+#include <glib/gstdio.h>
+
+#include "mu-query.h"
+#include "mu-msg-fields.h"
+
+#include "mu-msg-iter.h"
+
+#include "utils/mu-str.h"
+#include "utils/mu-date.h"
+#include <utils/mu-utils.hh>
+
+#include <query/mu-proc-iface.hh>
+#include <query/mu-xapian.hh>
+
+using namespace Mu;
+
+struct MuProc: public Mu::ProcIface {
+
+       MuProc (const Xapian::Database& db): db_{db} {}
+
+       static MuMsgFieldId field_id (const std::string& field) {
+
+               if (field.empty())
+                       return MU_MSG_FIELD_ID_NONE;
+
+               MuMsgFieldId id = mu_msg_field_id_from_name (field.c_str(), FALSE);
+               if (id != MU_MSG_FIELD_ID_NONE)
+                       return id;
+               else if (field.length() == 1)
+                       return mu_msg_field_id_from_shortcut (field[0], FALSE);
+               else
+                       return MU_MSG_FIELD_ID_NONE;
+       }
+
+       std::string
+       process_value (const std::string& field,
+                      const std::string& value) const override {
+               const auto id = field_id (field);
+               if (id == MU_MSG_FIELD_ID_NONE)
+                       return value;
+               switch(id) {
+               case MU_MSG_FIELD_ID_PRIO: {
+                       if (!value.empty())
+                               return std::string(1, value[0]);
+               } break;
+
+               case MU_MSG_FIELD_ID_FLAGS: {
+                       const auto flag = mu_flag_char_from_name (value.c_str());
+                       if (flag)
+                               return std::string(1, tolower(flag));
+               } break;
+
+               default:
+                       break;
+               }
+
+               return value; // XXX prio/flags, etc. alias
+       }
+
+       void add_field (std::vector<FieldInfo>& fields, MuMsgFieldId id) const {
+
+               const auto shortcut = mu_msg_field_shortcut(id);
+               if (!shortcut)
+                       return; // can't be searched
+
+               const auto name = mu_msg_field_name (id);
+               const auto pfx  = mu_msg_field_xapian_prefix (id);
+
+               if (!name || !pfx)
+                       return;
+
+               fields.push_back ({{name}, {pfx},
+                                  (bool)mu_msg_field_xapian_index(id),
+                                  id});
+       }
+
+       std::vector<FieldInfo>
+       process_field (const std::string& field) const override {
+
+               std::vector<FieldInfo> fields;
+
+               if (field == "contact" || field == "recip") { // multi fields
+                       add_field (fields, MU_MSG_FIELD_ID_TO);
+                       add_field (fields, MU_MSG_FIELD_ID_CC);
+                       add_field (fields, MU_MSG_FIELD_ID_BCC);
+                       if (field == "contact")
+                               add_field (fields, MU_MSG_FIELD_ID_FROM);
+               } else if (field == "") {
+                       add_field (fields, MU_MSG_FIELD_ID_TO);
+                       add_field (fields, MU_MSG_FIELD_ID_CC);
+                       add_field (fields, MU_MSG_FIELD_ID_BCC);
+                       add_field (fields, MU_MSG_FIELD_ID_FROM);
+                       add_field (fields, MU_MSG_FIELD_ID_SUBJECT);
+                       add_field (fields, MU_MSG_FIELD_ID_BODY_TEXT);
+               } else {
+                       const auto id = field_id (field.c_str());
+                       if (id != MU_MSG_FIELD_ID_NONE)
+                               add_field (fields, id);
+               }
+
+               return fields;
+       }
+
+       bool is_range_field (const std::string& field) const override {
+               const auto id = field_id (field.c_str());
+               if (id == MU_MSG_FIELD_ID_NONE)
+                       return false;
+               else
+                       return mu_msg_field_is_range_field (id);
+       }
+
+       Range process_range (const std::string& field, const std::string& lower,
+                            const std::string& upper) const override {
+
+               const auto id = field_id (field.c_str());
+               if (id == MU_MSG_FIELD_ID_NONE)
+                       return { lower, upper };
+
+               std::string     l2 = lower;
+               std::string     u2 = upper;
+
+               if (id == MU_MSG_FIELD_ID_DATE) {
+                       l2 = Mu::date_to_time_t_string (lower, true);
+                       u2 = Mu::date_to_time_t_string (upper, false);
+               } else if (id == MU_MSG_FIELD_ID_SIZE) {
+                       l2 = Mu::size_to_string (lower, true);
+                       u2 = Mu::size_to_string (upper, false);
+               }
+
+               return { l2, u2 };
+       }
+
+       std::vector<std::string>
+       process_regex (const std::string& field, const std::regex& rx) const override {
+
+               const auto id = field_id (field.c_str());
+               if (id == MU_MSG_FIELD_ID_NONE)
+                       return {};
+
+               char pfx[] = {  mu_msg_field_xapian_prefix(id), '\0' };
+
+               std::vector<std::string> terms;
+               for (auto it = db_.allterms_begin(pfx); it != db_.allterms_end(pfx); ++it) {
+                       if (std::regex_search((*it).c_str() + 1, rx)) // avoid copy
+                               terms.push_back(*it);
+               }
+
+               return terms;
+       }
+
+       const Xapian::Database& db_;
+};
+
+struct _MuQuery {
+public:
+       _MuQuery (MuStore *store): _store(mu_store_ref(store)) {}
+       ~_MuQuery () { mu_store_unref (_store); }
+
+       Xapian::Database& db() const {
+               const auto db = reinterpret_cast<Xapian::Database*>
+                       (mu_store_get_read_only_database (_store));
+               if (!db)
+                       throw Mu::Error(Error::Code::NotFound, "no database");
+               return *db;
+       }
+private:
+       MuStore *_store;
+};
+
+static const Xapian::Query
+get_query (MuQuery *mqx, const char* searchexpr, bool raw, GError **err) try {
+
+       Mu::WarningVec warns;
+       const auto tree = Mu::parse (searchexpr, warns,
+                                     std::make_unique<MuProc>(mqx->db()));
+       for (const auto w: warns)
+               std::cerr << w << std::endl;
+
+       return Mu::xapian_query (tree);
+
+} catch (...) {
+       mu_util_g_set_error (err,MU_ERROR_XAPIAN_QUERY,
+                            "parse error in query");
+       throw;
+}
+
+MuQuery*
+mu_query_new (MuStore *store, GError **err)
+{
+       g_return_val_if_fail (store, NULL);
+
+       try {
+               return new MuQuery (store);
+       } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN (err, MU_ERROR_XAPIAN, 0);
+
+       return 0;
+}
+
+void
+mu_query_destroy (MuQuery *self)
+{
+       try { delete self; } MU_XAPIAN_CATCH_BLOCK;
+}
+
+
+/* this function is for handling the case where a DatabaseModified
+ * exception is raised. We try to reopen the database, and run the
+ * query again. */
+static MuMsgIter *
+try_requery (MuQuery *self, const char* searchexpr, MuMsgFieldId sortfieldid,
+            int maxnum, MuQueryFlags flags, GError **err)
+{
+       try {
+               /* let's assume that infinite regression is
+                * impossible */
+               self->db().reopen();
+               MU_WRITE_LOG ("reopening db after modification");
+               return mu_query_run (self, searchexpr, sortfieldid,
+                                    maxnum, flags, err);
+
+       } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN (err, MU_ERROR_XAPIAN, 0);
+}
+
+
+static MuMsgIterFlags
+msg_iter_flags (MuQueryFlags flags)
+{
+       MuMsgIterFlags iflags;
+
+       iflags = MU_MSG_ITER_FLAG_NONE;
+
+       if (flags & MU_QUERY_FLAG_DESCENDING)
+               iflags |= MU_MSG_ITER_FLAG_DESCENDING;
+       if (flags & MU_QUERY_FLAG_SKIP_UNREADABLE)
+               iflags |= MU_MSG_ITER_FLAG_SKIP_UNREADABLE;
+       if (flags & MU_QUERY_FLAG_SKIP_DUPS)
+               iflags |= MU_MSG_ITER_FLAG_SKIP_DUPS;
+       if (flags & MU_QUERY_FLAG_THREADS)
+               iflags |= MU_MSG_ITER_FLAG_THREADS;
+
+       return iflags;
+}
+
+
+
+static Xapian::Enquire
+get_enquire (MuQuery *self, const char *searchexpr, MuMsgFieldId sortfieldid,
+            bool descending, bool raw, GError **err)
+{
+       Xapian::Enquire enq (self->db());
+
+       try {
+               if (raw)
+                       enq.set_query(Xapian::Query(Xapian::Query(searchexpr)));
+               else if (!mu_str_is_empty(searchexpr) &&
+                   g_strcmp0 (searchexpr, "\"\"") != 0) /* NULL or "" or """" */
+                       enq.set_query(get_query (self, searchexpr, raw, err));
+               else/* empty or "" means "matchall" */
+                       enq.set_query(Xapian::Query::MatchAll);
+       } catch (...) {
+               mu_util_g_set_error (err, MU_ERROR_XAPIAN_QUERY,
+                                    "parse error in query");
+               throw;
+       }
+
+       enq.set_cutoff(0,0);
+       return enq;
+}
+
+/*
+ * record all threadids for the messages; also 'orig_set' receives all
+ * original matches (a map msgid-->docid), so we can make sure the
+ * originals are not seen as 'duplicates' later (when skipping
+ * duplicates).  We want to favor the originals over the related
+ * messages, when skipping duplicates.
+ */
+static GHashTable*
+get_thread_ids (MuMsgIter *iter, GHashTable **orig_set)
+{
+       GHashTable *ids;
+
+       ids       = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                          (GDestroyNotify)g_free, NULL);
+       *orig_set = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                          (GDestroyNotify)g_free, NULL);
+
+       while (!mu_msg_iter_is_done (iter)) {
+               char            *thread_id, *msgid;
+               unsigned         docid;
+               /* record the thread id for the message */
+               if ((thread_id = mu_msg_iter_get_thread_id (iter)))
+                       g_hash_table_insert (ids, thread_id,
+                                            GSIZE_TO_POINTER(TRUE));
+               /* record the original set */
+               docid = mu_msg_iter_get_docid(iter);
+               if (docid != 0 && (msgid = mu_msg_iter_get_msgid (iter)))
+                       g_hash_table_insert (*orig_set, msgid,
+                                            GSIZE_TO_POINTER(docid));
+
+               if (!mu_msg_iter_next (iter))
+                       break;
+       }
+
+       return ids;
+}
+
+
+static Xapian::Query
+get_related_query (MuMsgIter *iter, GHashTable **orig_set)
+{
+       GHashTable *hash;
+       GList *id_list, *cur;
+       std::vector<Xapian::Query> qvec;
+       static std::string pfx (1, mu_msg_field_xapian_prefix
+                               (MU_MSG_FIELD_ID_THREAD_ID));
+
+       /* orig_set receives the hash msgid->docid of the set of
+        * original matches */
+       hash = get_thread_ids (iter, orig_set);
+       /* id_list now gets a list of all thread-ids seen in the query
+        * results; either in the Message-Id field or in
+        * References. */
+       id_list = g_hash_table_get_keys (hash);
+
+       // now, we create a vector with queries for each of the
+       // thread-ids, which we combine below. This is /much/ faster
+       // than creating the query as 'query = Query (OR, query)'...
+       for (cur = id_list; cur; cur = g_list_next(cur))
+               qvec.push_back (Xapian::Query((std::string
+                                              (pfx + (char*)cur->data))));
+
+       g_hash_table_destroy (hash);
+       g_list_free (id_list);
+
+       return Xapian::Query (Xapian::Query::OP_OR, qvec.begin(), qvec.end());
+}
+
+
+static void
+get_related_messages (MuQuery *self, MuMsgIter **iter, int maxnum,
+                      MuMsgFieldId sortfieldid, MuQueryFlags flags,
+                      Xapian::Query orig_query)
+{
+       GHashTable *orig_set;
+       Xapian::Enquire enq (self->db());
+       MuMsgIter *rel_iter;
+       const bool inc_related = flags & MU_QUERY_FLAG_INCLUDE_RELATED;
+
+       orig_set = NULL;
+       Xapian::Query new_query = get_related_query (*iter, &orig_set);
+       /* If related message are not desired, filter out messages which would not
+          have matched the original query.
+        */
+       if (!inc_related)
+           new_query = Xapian::Query (Xapian::Query::OP_AND, orig_query, new_query);
+       enq.set_query(new_query);
+       enq.set_cutoff(0,0);
+
+       rel_iter= mu_msg_iter_new (
+               reinterpret_cast<XapianEnquire*>(&enq),
+               maxnum,
+               sortfieldid,
+               msg_iter_flags (flags),
+               NULL);
+
+       mu_msg_iter_destroy (*iter);
+
+       // set the preferred set for the iterator (ie., the set of
+       // messages not considered to be duplicates) to be the
+       // original matches -- the matches without considering
+       // 'related'
+       mu_msg_iter_set_preferred (rel_iter, orig_set);
+       g_hash_table_destroy (orig_set);
+
+       *iter = rel_iter;
+}
+
+
+MuMsgIter*
+mu_query_run (MuQuery *self, const char *searchexpr, MuMsgFieldId sortfieldid,
+             int maxnum, MuQueryFlags flags, GError **err)
+{
+       g_return_val_if_fail (self, NULL);
+       g_return_val_if_fail (searchexpr, NULL);
+       g_return_val_if_fail (mu_msg_field_id_is_valid (sortfieldid)    ||
+                             sortfieldid    == MU_MSG_FIELD_ID_NONE,
+                             NULL);
+       try {
+               MuMsgIter *iter;
+               MuQueryFlags first_flags;
+               const bool threads     = flags & MU_QUERY_FLAG_THREADS;
+               const bool inc_related = flags & MU_QUERY_FLAG_INCLUDE_RELATED;
+               const bool descending  = flags & MU_QUERY_FLAG_DESCENDING;
+               const bool raw         = flags & MU_QUERY_FLAG_RAW;
+               Xapian::Enquire enq (get_enquire(self, searchexpr, sortfieldid,
+                                                descending, raw, err));
+
+               /* when we're doing a 'include-related query', wea're actually
+                * doing /two/ queries; one to get the initial matches, and
+                * based on that one to get all messages in threads in those
+                * matches.
+                */
+
+               /* get the 'real' maxnum if it was specified as < 0 */
+               maxnum = maxnum < 0 ? self->db().get_doccount() : maxnum;
+               /* Calculating threads involves two queries, so do the calculation only in
+                * the second query instead of in both.
+                */
+               if (threads)
+                       first_flags = (MuQueryFlags)(flags & ~MU_QUERY_FLAG_THREADS);
+               else
+                       first_flags = flags;
+               /* Perform the initial query, returning up to max num results.
+                */
+               iter  = mu_msg_iter_new (
+                       reinterpret_cast<XapianEnquire*>(&enq),
+                       maxnum,
+                       sortfieldid,
+                       msg_iter_flags (first_flags),
+                       err);
+               /* If we want threads or related messages, find related messages using a
+                * second query based on the message ids / refs of the first query's result.
+                * Do this even if we don't want to include related messages in the final
+                * result so we can apply the threading algorithm to the related message set
+                * of a maxnum-sized result instead of the unbounded result of the first
+                * query. If threads are desired but related message are not, we will remove
+                * the undesired related messages later.
+                */
+               if(threads||inc_related)
+                       get_related_messages (self, &iter, maxnum, sortfieldid, flags,
+                                             enq.get_query());
+
+               if (err && *err && (*err)->code == MU_ERROR_XAPIAN_MODIFIED) {
+                       g_clear_error (err);
+                       return try_requery (self, searchexpr, sortfieldid,
+                                           maxnum, flags, err);
+               } else
+                       return iter;
+
+       } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN (err, MU_ERROR_XAPIAN, 0);
+}
+
+
+size_t
+mu_query_count_run (MuQuery *self, const char *searchexpr) try
+{
+       g_return_val_if_fail (self, 0);
+       g_return_val_if_fail (searchexpr, 0);
+
+        const auto enq{get_enquire(self, searchexpr,MU_MSG_FIELD_ID_NONE, false, false, NULL)};
+        auto mset(enq.get_mset(0, self->db().get_doccount()));
+        mset.fetch();
+
+        return mset.size();
+
+} MU_XAPIAN_CATCH_BLOCK_RETURN (0);
+
+
+
+
+char*
+mu_query_internal_xapian (MuQuery *self, const char *searchexpr, GError **err)
+{
+       g_return_val_if_fail (self, NULL);
+       g_return_val_if_fail (searchexpr, NULL);
+
+       try {
+               Xapian::Query query (get_query(self, searchexpr, false, err));
+               return g_strdup(query.get_description().c_str());
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN(NULL);
+}
+
+
+char*
+mu_query_internal (MuQuery *self, const char *searchexpr,
+                  gboolean warn, GError **err)
+{
+       g_return_val_if_fail (self, NULL);
+       g_return_val_if_fail (searchexpr, NULL);
+
+       try {
+               Mu::WarningVec warns;
+               const auto tree = Mu::parse (searchexpr, warns,
+                                             std::make_unique<MuProc>(self->db()));
+               std::stringstream ss;
+               ss << tree;
+
+               if (warn) {
+                       for (const auto w: warns)
+                               std::cerr << w << std::endl;
+               }
+
+               return g_strdup(ss.str().c_str());
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN(NULL);
+}
diff --git a/lib/mu-query.h b/lib/mu-query.h
new file mode 100644 (file)
index 0000000..26cdb0f
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_QUERY_H__
+#define __MU_QUERY_H__
+
+#include <glib.h>
+#include <mu-store.hh>
+#include <mu-msg-iter.h>
+#include <utils/mu-util.h>
+
+G_BEGIN_DECLS
+
+struct _MuQuery;
+typedef struct _MuQuery MuQuery;
+
+/**
+ * create a new MuQuery instance.
+ *
+ * @param store a MuStore object
+ * @param err receives error information (if there is any); if
+ * function returns non-NULL, err will _not_be set. err can be NULL
+ * possible errors (err->code) are MU_ERROR_XAPIAN_DIR and
+ * MU_ERROR_XAPIAN_NOT_UPTODATE
+ *
+ * @return a new MuQuery instance, or NULL in case of error.
+ * when the instance is no longer needed, use mu_query_destroy
+ * to free it
+ */
+MuQuery* mu_query_new  (MuStore *store, GError **err)
+      G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * destroy the MuQuery instance
+ *
+ * @param self a MuQuery instance, or NULL
+ */
+void mu_query_destroy  (MuQuery *self);
+
+
+typedef enum {
+       MU_QUERY_FLAG_NONE            = 0 << 0, /**< no flags */
+       MU_QUERY_FLAG_DESCENDING      = 1 << 0, /**< sort z->a */
+       MU_QUERY_FLAG_SKIP_UNREADABLE = 1 << 1, /**< skip unreadable msgs */
+       MU_QUERY_FLAG_SKIP_DUPS       = 1 << 2, /**< skip duplicate msgs */
+       MU_QUERY_FLAG_INCLUDE_RELATED = 1 << 3, /**< include related msgs */
+       MU_QUERY_FLAG_THREADS         = 1 << 4, /**< calculate threading info */
+       MU_QUERY_FLAG_RAW             = 1 << 5  /**< don't parse the query */
+} MuQueryFlags;
+
+/**
+ * run a Xapian query; for the syntax, please refer to the mu-query
+ * manpage
+ *
+ * @param self a valid MuQuery instance
+ * @param expr the search expression; use "" to match all messages
+ * @param sortfield the field id to sort by or MU_MSG_FIELD_ID_NONE if
+ * sorting is not desired
+ * @param maxnum maximum number of search results to return, or <= 0 for
+ * unlimited
+ * @param flags bitwise OR'd flags to influence the query (see MuQueryFlags)
+ * @param err receives error information (if there is any); if
+ * function returns non-NULL, err will _not_be set. err can be NULL
+ * possible error (err->code) is MU_ERROR_QUERY,
+ *
+ * @return a MuMsgIter instance you can iterate over, or NULL in
+ * case of error
+ */
+MuMsgIter* mu_query_run (MuQuery *self, const char* expr,
+                        MuMsgFieldId sortfieldid, int maxnum,
+                        MuQueryFlags flags, GError **err)
+    G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * run a Xapian query to count the number of matches; for the syntax, please
+ * refer to the mu-query manpage
+ *
+ * @param self a valid MuQuery instance
+ * @param expr the search expression; use "" to match all messages
+ *
+ * @return the number of matches
+ */
+size_t mu_query_count_run (MuQuery *self, const char *searchexpr);
+
+/**
+ * get Xapian's internal string representation of the query
+ *
+ * @param self a MuQuery instance
+ * @param searchexpr a xapian search expression
+ * @param warn print warnings to stderr
+ * @param err receives error information (if there is any); if
+ * function returns non-NULL, err will _not_be set. err can be NULL
+ *
+ * @return the string representation of the xapian query, or NULL in case of
+ * error; free the returned value with g_free
+ */
+char* mu_query_internal (MuQuery *self, const char *searchexpr,
+                        gboolean warn, GError **err)
+    G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * get Xapian's internal string representation of the query
+ *
+ * @param self a MuQuery instance
+ * @param searchexpr a xapian search expression
+ * @param err receives error information (if there is any); if
+ * function returns non-NULL, err will _not_be set. err can be NULL
+ *
+ * @return the string representation of the xapian query, or NULL in case of
+ * error; free the returned value with g_free
+ */
+char* mu_query_internal_xapian (MuQuery *self, const char* searchexpr,
+                               GError **err)
+    G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+
+G_END_DECLS
+
+#endif /*__MU_QUERY_H__*/
diff --git a/lib/mu-runtime.cc b/lib/mu-runtime.cc
new file mode 100644 (file)
index 0000000..b8fb606
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+** Copyright (C) 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.
+**
+*/
+
+#include "mu-runtime.h"
+#include "utils/mu-util.h"
+
+#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 Mu            = "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 + Mu + Sepa + XapianDir);
+        RuntimePaths.emplace(MU_RUNTIME_PATH_CACHE, g_get_user_cache_dir() +
+                             Sepa + Mu);
+        RuntimePaths.emplace(MU_RUNTIME_PATH_MIMECACHE, g_get_user_cache_dir() +
+                             Sepa + Mu + Sepa + PartsDir);
+        RuntimePaths.emplace(MU_RUNTIME_PATH_LOGDIR, g_get_user_cache_dir() +
+                             Sepa + Mu);
+        RuntimePaths.emplace(MU_RUNTIME_PATH_BOOKMARKS, g_get_user_config_dir() +
+                             Sepa + Mu);
+}
+
+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)
+{
+        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";
+
+       if (!mu_log_init (log_path.c_str(), MU_LOG_OPTIONS_BACKUP)) {
+                mu_runtime_uninit();
+                return FALSE;
+        }
+
+        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.h b/lib/mu-runtime.h
new file mode 100644 (file)
index 0000000..f300815
--- /dev/null
@@ -0,0 +1,67 @@
+/* -*- 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>
+#include <utils/mu-log.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
+ *
+ * @return TRUE if succeeded, FALSE in case of error
+ */
+gboolean mu_runtime_init (const char *muhome, const char *name);
+
+/**
+ * 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 for attachments etc. */
+       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.c b/lib/mu-script.c
new file mode 100644 (file)
index 0000000..c5b167f
--- /dev/null
@@ -0,0 +1,360 @@
+/*
+** 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*/
+
+#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 "utils/mu-str.h"
+#include "mu-script.h"
+#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, G_REGEX_CASELESS|G_REGEX_OPTIMIZE, 0, err);
+       if (!rx)
+               return FALSE;
+
+       match = FALSE;
+       if (msi->_name)
+               match = g_regex_match (rx, msi->_name, 0, NULL);
+       if (!match && msi->_oneline)
+               match = g_regex_match (rx, msi->_oneline, 0, NULL);
+
+       return match;
+}
+
+void
+mu_script_info_list_destroy (GSList *lst)
+{
+       g_slist_foreach (lst, (GFunc)script_info_destroy, NULL);
+       g_slist_free    (lst);
+}
+
+
+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, 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 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);
+
+       argv = g_new0 (char*, 6);
+       argv[0] = g_strdup("guile2.2");
+       argv[1] = g_strdup("-l");
+
+       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",
+                                    strerror(errno));
+                return FALSE;
+       }
+
+       s       = mu_script_info_path (msi);
+       argv[2] = g_strdup (s ? s : "");
+
+       mainargs = mu_str_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.h b/lib/mu-script.h
new file mode 100644 (file)
index 0000000..586b6f0
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_SCRIPT_H__
+#define __MU_SCRIPT_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+/* Opaque structure with information about a script */
+struct _MuScriptInfo;
+typedef struct _MuScriptInfo 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);
+
+G_END_DECLS
+
+#endif /*__MU_SCRIPT_H__*/
diff --git a/lib/mu-store.cc b/lib/mu-store.cc
new file mode 100644 (file)
index 0000000..d9141e9
--- /dev/null
@@ -0,0 +1,1433 @@
+/*
+** 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 <mutex>
+#include <array>
+#include <cstdlib>
+#include <xapian.h>
+#include <unordered_map>
+#include <atomic>
+#include <iostream>
+#include <cstring>
+#include "mu-store.hh"
+#include "utils/mu-str.h"
+#include "utils/mu-error.hh"
+
+#include "mu-msg-part.h"
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+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 BatchSize            = 150'000;
+
+constexpr auto ExpectedSchemaVersion = MU_STORE_SCHEMA_VERSION;
+
+
+
+extern "C" {
+static unsigned add_or_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err);
+}
+
+/* we cache these prefix strings, so we don't have to allocate them all
+ * the time; this should save 10-20 string allocs per message */
+G_GNUC_CONST static const std::string&
+prefix (MuMsgFieldId mfid)
+{
+       static std::string fields[MU_MSG_FIELD_ID_NUM];
+       static bool initialized = false;
+
+       if (G_UNLIKELY(!initialized)) {
+               for (int i = 0; i != MU_MSG_FIELD_ID_NUM; ++i)
+                       fields[i] = std::string (1, mu_msg_field_xapian_prefix
+                                                ((MuMsgFieldId)i));
+               initialized = true;
+       }
+
+       return fields[mfid];
+}
+
+static void
+add_synonym_for_flag (MuFlags flag, Xapian::WritableDatabase *db)
+{
+       static const std::string pfx(prefix(MU_MSG_FIELD_ID_FLAGS));
+
+       db->clear_synonyms (pfx + mu_flag_name (flag));
+       db->add_synonym (pfx + mu_flag_name (flag), pfx +
+                        (std::string(1, (char)(tolower(mu_flag_char(flag))))));
+}
+
+
+static void
+add_synonym_for_prio (MuMsgPrio prio, Xapian::WritableDatabase *db)
+{
+       static const std::string pfx (prefix(MU_MSG_FIELD_ID_PRIO));
+
+       std::string s1 (pfx + mu_msg_prio_name (prio));
+       std::string s2 (pfx + (std::string(1, mu_msg_prio_char (prio))));
+
+       db->clear_synonyms (s1);
+       db->clear_synonyms (s2);
+
+       db->add_synonym (s1, s2);
+}
+
+struct Store::Private {
+
+#define LOCKED std::lock_guard<std::mutex> l(lock_);
+
+        Private (const std::string& path, bool readonly):
+                db_path_{path},
+                db_{readonly?
+                        std::make_shared<Xapian::Database>(db_path_) :
+                        std::make_shared<Xapian::WritableDatabase>(db_path_, Xapian::DB_OPEN)},
+                root_maildir_{db()->get_metadata(RootMaildirKey)},
+                created_{atoll(db()->get_metadata(CreatedKey).c_str())},
+                schema_version_{db()->get_metadata(SchemaVersionKey)},
+                personal_addresses_{Mu::split(db()->get_metadata(PersonalAddressesKey),",")},
+                contacts_{db()->get_metadata(ContactsKey)} {
+        }
+
+        Private (const std::string& path, const std::string& root_maildir,
+                 const StringVec& personal_addresses):
+                db_path_{path},
+                db_{std::make_shared<Xapian::WritableDatabase>(
+                                db_path_, Xapian::DB_CREATE_OR_OVERWRITE)},
+                root_maildir_{root_maildir},
+                created_{time({})},
+                schema_version_{MU_STORE_SCHEMA_VERSION},
+                personal_addresses_{personal_addresses} {
+
+                writable_db()->set_metadata(SchemaVersionKey, schema_version_);
+                writable_db()->set_metadata(RootMaildirKey, root_maildir_);
+                writable_db()->set_metadata(CreatedKey, Mu::format("%" PRId64, (int64_t)created_));
+
+                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);
+
+        }
+
+        ~Private() try {
+                LOCKED;
+                if (wdb()) {
+                        wdb()->set_metadata (ContactsKey, contacts_.serialize());
+                        if (in_transaction_)  // auto-commit.
+                                wdb()->commit_transaction();
+                }
+        } MU_XAPIAN_CATCH_BLOCK;
+
+        std::shared_ptr<Xapian::Database> db() const {
+                if (!db_)
+                        throw Mu::Error(Error::Code::NotFound, "no database found");
+                return db_;
+        }
+
+        std::shared_ptr<Xapian::WritableDatabase> wdb() const {
+                return std::dynamic_pointer_cast<Xapian::WritableDatabase>(db_);
+        }
+
+        std::shared_ptr<Xapian::WritableDatabase> writable_db() const {
+                auto w_db{wdb()};
+                if (!w_db)
+                        throw Mu::Error(Error::Code::AccessDenied, "database is read-only");
+                else
+                        return w_db;
+        }
+
+        void begin_transaction () try {
+                wdb()->begin_transaction();
+                in_transaction_ = true;
+                dirtiness_      = 0;
+        } MU_XAPIAN_CATCH_BLOCK;
+
+        void commit_transaction () try {
+                in_transaction_ = false;
+                dirtiness_      = 0;
+                wdb()->commit_transaction();
+        } MU_XAPIAN_CATCH_BLOCK;
+
+        void add_synonyms () {
+                mu_flags_foreach ((MuFlagsForeachFunc)add_synonym_for_flag,
+                                  writable_db().get());
+                mu_msg_prio_foreach ((MuMsgPrioForeachFunc)add_synonym_for_prio,
+                                     writable_db().get());
+        }
+
+        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());
+        }
+
+
+
+        const std::string                 db_path_;
+        std::shared_ptr<Xapian::Database> db_;
+        const std::string                 root_maildir_;
+        const time_t                      created_{};
+        const std::string                 schema_version_;
+        const StringVec                   personal_addresses_;
+        Contacts                          contacts_;
+
+        std::atomic<bool>                 in_transaction_{};
+        std::mutex                        lock_;
+
+        size_t                            dirtiness_{};
+
+        mutable std::atomic<std::size_t> ref_count_{1};
+};
+
+
+static void
+hash_str (char *buf, size_t buf_size, const char *data)
+{
+        g_snprintf(buf, buf_size, "016%" PRIx64, mu_util_get_hash(data));
+}
+
+
+static std::string
+get_uid_term (const char* path)
+{
+        char uid_term[1 + 16 + 1] = {'\0'};
+        uid_term[0] = mu_msg_field_xapian_prefix(MU_MSG_FIELD_ID_UID);
+        hash_str(uid_term + 1, sizeof(uid_term)-1, path);
+
+        return std::string{uid_term, sizeof(uid_term)};
+}
+
+
+#undef  LOCKED
+#define LOCKED std::lock_guard<std::mutex> l(priv_->lock_);
+
+Store::Store (const std::string& path, bool readonly):
+        priv_{std::make_unique<Private>(path, readonly)}
+{
+        if (ExpectedSchemaVersion != schema_version())
+                throw Mu::Error(Error::Code::SchemaMismatch,
+                                "expected schema-version %s, but got %s",
+                                ExpectedSchemaVersion, schema_version().c_str());
+}
+
+Store::Store (const std::string& path, const std::string& maildir,
+              const StringVec& personal_addresses):
+        priv_{std::make_unique<Private>(path, maildir, personal_addresses)}
+{}
+
+Store::~Store() = default;
+
+bool
+Store::read_only() const
+{
+        return !priv_->wdb();
+}
+
+const std::string&
+Store::root_maildir () const
+{
+        return priv_->root_maildir_;
+}
+
+const StringVec&
+Store::personal_addresses(void) const
+{
+       return priv_->personal_addresses_;
+}
+
+const std::string&
+Store::database_path() const
+{
+        return priv_->db_path_;
+}
+
+const Contacts&
+Store::contacts() const
+{
+        LOCKED;
+        return priv_->contacts_;
+}
+
+std::size_t
+Store::size() const
+{
+        return priv_->db()->get_doccount();
+}
+
+bool
+Store::empty() const
+{
+        return size() == 0;
+}
+
+
+const std::string&
+Store::schema_version() const
+{
+        return priv_->schema_version_;
+}
+
+time_t
+Store::created() const
+{
+        return priv_->created_;
+}
+
+static std::string
+maildir_from_path (const std::string& root, const std::string& path)
+{
+        if (G_UNLIKELY(root.empty()) || root.length() >= path.length() ||
+            path.find(root) != 0)
+                throw Mu::Error{Error::Code::InvalidArgument,
+                                "root '%s' is not a proper suffix of 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)
+                throw Mu::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)))
+                throw Mu::Error{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 mdir;
+}
+
+
+unsigned
+Store::add_message (const std::string& path)
+{
+        LOCKED;
+
+        GError *gerr{};
+        const auto maildir{maildir_from_path(root_maildir(), path)};
+        auto msg{mu_msg_new_from_file (path.c_str(), maildir.c_str(), &gerr)};
+        if (G_UNLIKELY(!msg))
+                throw Error{Error::Code::Message, "failed to create message: %s",
+                                gerr ? gerr->message : "something went wrong"};
+
+        auto store{reinterpret_cast<MuStore*>(this)}; // yuk.
+       const auto docid{add_or_update_msg (store, 0, msg, &gerr)};
+       mu_msg_unref (msg);
+        if (G_UNLIKELY(docid == MU_STORE_INVALID_DOCID))
+                throw Error{Error::Code::Message, "failed to store message: %s",
+                                gerr ? gerr->message : "something went wrong"};
+
+       return docid;
+}
+
+bool
+Store::remove_message (const std::string& path)
+{
+        LOCKED;
+
+       try {
+               const std::string term{(get_uid_term(path.c_str()))};
+                auto wdb{priv()->wdb()};
+
+               wdb->delete_document (term);
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (false);
+
+        return true;
+}
+
+
+
+time_t
+Store::dirstamp (const std::string& path) const
+{
+        LOCKED;
+
+        const auto ts = priv_->db()->get_metadata(path);
+        if (ts.empty())
+                return 0;
+        else
+                return (time_t)strtoll(ts.c_str(), NULL, 16);
+}
+
+void
+Store::set_dirstamp (const std::string& path, time_t tstamp)
+{
+        LOCKED;
+
+        std::array<char, 2*sizeof(tstamp)+1> data{};
+        const std::size_t len = g_snprintf (data.data(), data.size(), "%zx", tstamp);
+
+        priv_->writable_db()->set_metadata(path, std::string{data.data(), len});
+}
+
+
+MuMsg*
+Store::find_message (unsigned docid) const
+{
+        LOCKED;
+
+       try {
+               Xapian::Document *doc{new Xapian::Document{priv_->db()->get_document (docid)}};
+                GError *gerr{};
+                auto msg{mu_msg_new_from_doc (reinterpret_cast<XapianDocument*>(doc), &gerr)};
+                if (!msg) {
+                        g_warning ("could not create message: %s", gerr ? gerr->message :
+                                   "something went wrong");
+                        g_clear_error(&gerr);
+                }
+
+                return msg;
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (nullptr);
+}
+
+
+bool
+Store::contains_message (const std::string& path) const
+{
+        LOCKED;
+
+       try {
+               const std::string term (get_uid_term(path.c_str()));
+               return priv_->db()->term_exists (term);
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN(false);
+}
+
+void
+Store::begin_transaction () try
+{
+        LOCKED;
+        priv_->begin_transaction();
+
+} MU_XAPIAN_CATCH_BLOCK;
+
+void
+Store::commit_transaction () try
+{
+        LOCKED;
+        priv_->commit_transaction();
+
+} MU_XAPIAN_CATCH_BLOCK;
+
+bool
+Store::in_transaction () const
+{
+        return priv_->in_transaction_;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+// C compat
+extern "C" {
+
+
+struct MuStore_ { Mu::Store* self; };
+
+
+static const Mu::Store*
+self (const MuStore *store)
+{
+        if (!store) {
+                g_error ("invalid store"); // terminates
+                return {};
+        }
+
+        return reinterpret_cast<const Mu::Store*>(store);
+}
+
+static Mu::Store*
+mutable_self (MuStore *store)
+{
+        if (!store) {
+                g_error ("invalid store"); // terminates
+                return {};
+        }
+
+        auto s = reinterpret_cast<Mu::Store*>(store);
+        if (s->read_only()) {
+                g_error ("store is read-only"); // terminates
+                return {};
+        }
+
+        return s;
+}
+
+
+MuStore*
+mu_store_new_readable (const char* xpath, GError **err)
+{
+       g_return_val_if_fail (xpath, NULL);
+
+       g_debug ("opening database at %s (read-only)", xpath);
+
+       try {
+               return reinterpret_cast<MuStore*>(new Store (xpath));
+
+        } catch (const Mu::Error& me) {
+                g_warning ("failed to open database: %s", me.what());
+        } catch (const Xapian::Error& dbe) {
+                g_warning ("failed to open database @ %s: %s", xpath,
+                           dbe.get_error_string() ? dbe.get_error_string() : "something went wrong");
+        }
+
+        g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_OPEN,
+                     "failed to open database @ %s", xpath);
+
+        return NULL;
+}
+
+MuStore*
+mu_store_new_writable (const char* xpath, GError **err)
+{
+       g_return_val_if_fail (xpath, NULL);
+
+       g_debug ("opening database at %s (writable)", xpath);
+
+        try {
+                return reinterpret_cast<MuStore*>(new Store (xpath, false/*!readonly*/));
+
+        } catch (const Mu::Error& me) {
+                if (me.code() == Mu::Error::Code::SchemaMismatch) {
+                        g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_SCHEMA_MISMATCH,
+                                     "%s", me.what());
+                        return NULL;
+                }
+        } catch (const Xapian::DatabaseLockError& dle) {
+                g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK,
+                             "database @ %s is write-locked", xpath);
+                return NULL;
+        } catch (const Xapian::Error& dbe) {
+                g_warning ("failed to open database @ %s: %s", xpath,
+                           dbe.get_error_string() ? dbe.get_error_string() : "something went wrong");
+        }
+
+        g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_OPEN,
+                     "failed to open database @ %s", xpath);
+
+        return NULL;
+}
+
+
+MuStore*
+mu_store_new_create (const char* xpath, const char *root_maildir,
+                     const char **personal_addresses, GError **err)
+{
+       g_return_val_if_fail (xpath, NULL);
+        g_return_val_if_fail (root_maildir, NULL);
+
+       g_debug ("create database at %s (root-maildir=%s)", xpath, root_maildir);
+
+       try {
+                StringVec addrs;
+                for (auto i = 0; personal_addresses && personal_addresses[i]; ++i)
+                        addrs.emplace_back(personal_addresses[i]);
+
+                return reinterpret_cast<MuStore*>(
+                       new Store (xpath, std::string{root_maildir}, addrs));
+
+        } catch (const Xapian::DatabaseLockError& dle) {
+                g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK,
+                             "database @ %s is write-locked already", xpath);
+        } catch (...) {
+                g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN,
+                             "error opening database @ %s", xpath);
+        }
+
+        return NULL;
+}
+
+
+MuStore*
+mu_store_ref (MuStore* store)
+{
+        g_return_val_if_fail (store, NULL);
+        g_return_val_if_fail (self(store)->priv()->ref_count_ > 0, NULL);
+
+        ++self(store)->priv()->ref_count_;
+        return store;
+}
+
+
+MuStore*
+mu_store_unref (MuStore* store)
+{
+        g_return_val_if_fail (store, NULL);
+        g_return_val_if_fail (self(store)->priv()->ref_count_ > 0, NULL);
+
+       auto me = reinterpret_cast<Mu::Store*>(store);
+
+        if (--me->priv()->ref_count_ == 0)
+                delete me;
+
+        return NULL;
+}
+
+gboolean
+mu_store_is_read_only (const MuStore *store)
+{
+       g_return_val_if_fail (store, FALSE);
+
+       try {
+               return self(store)->read_only() ? TRUE : FALSE;
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE);
+
+}
+
+
+const MuContacts*
+mu_store_contacts (MuStore *store)
+{
+       g_return_val_if_fail (store, FALSE);
+
+       try {
+               return self(store)->contacts().mu_contacts();
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE);
+}
+
+unsigned
+mu_store_count (const MuStore *store, GError **err)
+{
+       g_return_val_if_fail (store, (unsigned)-1);
+
+       try {
+               return self(store)->size();
+
+       } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN,
+                                              (unsigned)-1);
+}
+
+const char*
+mu_store_schema_version (const MuStore *store)
+{
+       g_return_val_if_fail (store, NULL);
+
+       return self(store)->schema_version().c_str();
+}
+
+XapianDatabase*
+mu_store_get_read_only_database (MuStore *store)
+{
+       g_return_val_if_fail (store, NULL);
+       return (XapianWritableDatabase*)self(store)->priv()->db().get();
+}
+
+
+
+
+gboolean
+mu_store_contains_message (const MuStore *store, const char* path)
+{
+       g_return_val_if_fail (store, FALSE);
+       g_return_val_if_fail (path, FALSE);
+
+       try {
+               return self(store)->contains_message(path) ? TRUE : FALSE;
+
+        } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE);
+}
+
+unsigned
+mu_store_get_docid_for_path (const MuStore *store, const char* path, GError **err)
+{
+       g_return_val_if_fail (store, FALSE);
+       g_return_val_if_fail (path, FALSE);
+
+       try {
+               const std::string term (get_uid_term(path));
+               Xapian::Query query (term);
+               Xapian::Enquire enq (*self(store)->priv()->db().get());
+
+               enq.set_query (query);
+
+               Xapian::MSet mset (enq.get_mset (0,1));
+               if (mset.empty())
+                       throw Mu::Error(Error::Code::NotFound,
+                                        "message @ %s not found in store", path);
+
+               return *mset.begin();
+
+       } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN,
+                                              MU_STORE_INVALID_DOCID);
+}
+
+
+MuError
+mu_store_foreach (MuStore *store,
+                 MuStoreForeachFunc func, void *user_data, GError **err)
+{
+       g_return_val_if_fail (store, MU_ERROR);
+       g_return_val_if_fail (func, MU_ERROR);
+
+       try {
+               Xapian::Enquire enq (*self(store)->priv()->db().get());
+
+               enq.set_query  (Xapian::Query::MatchAll);
+               enq.set_cutoff (0,0);
+
+               Xapian::MSet matches(enq.get_mset (0, self(store)->size()));
+               if (matches.empty())
+                       return MU_OK; /* database is empty */
+
+               for (Xapian::MSet::iterator iter = matches.begin();
+                    iter != matches.end(); ++iter) {
+                       Xapian::Document doc (iter.get_document());
+                       const std::string path(doc.get_value(MU_MSG_FIELD_ID_PATH));
+                       MuError res = func (path.c_str(), user_data);
+                       if (res != MU_OK)
+                               return res;
+               }
+
+       } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN,
+                                              MU_ERROR_XAPIAN);
+
+       return MU_OK;
+}
+
+
+MuMsg*
+mu_store_get_msg (const MuStore *store, unsigned docid, GError **err)
+{
+       g_return_val_if_fail (store, NULL);
+       g_return_val_if_fail (docid != 0, NULL);
+
+        return self(store)->find_message(docid);
+}
+
+
+const char*
+mu_store_database_path (const MuStore *store)
+{
+        g_return_val_if_fail (store, NULL);
+
+        return self(store)->database_path().c_str();
+}
+
+
+const char*
+mu_store_root_maildir (const MuStore *store)
+{
+        g_return_val_if_fail (store, NULL);
+
+        return self(store)->root_maildir().c_str();
+}
+
+
+time_t
+mu_store_created (const MuStore *store)
+{
+        g_return_val_if_fail (store, (time_t)0);
+
+        return self(store)->created();
+}
+
+char**
+mu_store_personal_addresses (const MuStore *store)
+{
+       g_return_val_if_fail (store, NULL);
+
+        const auto size = self(store)->personal_addresses().size();
+        auto addrs = g_new0 (char*, 1 + size);
+        for (size_t i = 0; i != size; ++i)
+                addrs[i] = g_strdup(self(store)->personal_addresses()[i].c_str());
+
+        return addrs;
+}
+
+void
+mu_store_flush (MuStore *store) try {
+
+        g_return_if_fail (store);
+
+        if (self(store)->priv()->in_transaction_)
+                mutable_self(store)->commit_transaction ();
+
+        mutable_self(store)->priv()->wdb()->set_metadata(
+                ContactsKey, self(store)->priv()->contacts_.serialize());
+
+} MU_XAPIAN_CATCH_BLOCK;
+
+static void
+add_terms_values_date (Xapian::Document& doc, MuMsg *msg, MuMsgFieldId mfid)
+{
+       const auto dstr = Mu::date_to_time_t_string (
+               (time_t)mu_msg_get_field_numeric (msg, mfid));
+
+       doc.add_value ((Xapian::valueno)mfid, dstr);
+}
+
+static void
+add_terms_values_size (Xapian::Document& doc, MuMsg *msg, MuMsgFieldId mfid)
+{
+       const auto szstr =
+               Mu::size_to_string (mu_msg_get_field_numeric (msg, mfid));
+       doc.add_value ((Xapian::valueno)mfid, szstr);
+}
+
+G_GNUC_CONST
+static const std::string&
+flag_val (char flagchar)
+{
+       static const std::string
+               pfx (prefix(MU_MSG_FIELD_ID_FLAGS)),
+               draftstr   (pfx + (char)tolower(mu_flag_char(MU_FLAG_DRAFT))),
+               flaggedstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_FLAGGED))),
+               passedstr  (pfx + (char)tolower(mu_flag_char(MU_FLAG_PASSED))),
+               repliedstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_REPLIED))),
+               seenstr    (pfx + (char)tolower(mu_flag_char(MU_FLAG_SEEN))),
+               trashedstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_TRASHED))),
+               newstr     (pfx + (char)tolower(mu_flag_char(MU_FLAG_NEW))),
+               signedstr  (pfx + (char)tolower(mu_flag_char(MU_FLAG_SIGNED))),
+               cryptstr   (pfx + (char)tolower(mu_flag_char(MU_FLAG_ENCRYPTED))),
+               attachstr  (pfx + (char)tolower(mu_flag_char(MU_FLAG_HAS_ATTACH))),
+               unreadstr  (pfx + (char)tolower(mu_flag_char(MU_FLAG_UNREAD))),
+               liststr    (pfx + (char)tolower(mu_flag_char(MU_FLAG_LIST)));
+
+       switch (flagchar) {
+
+       case 'D': return draftstr;
+       case 'F': return flaggedstr;
+       case 'P': return passedstr;
+       case 'R': return repliedstr;
+       case 'S': return seenstr;
+       case 'T': return trashedstr;
+
+       case 'N': return newstr;
+
+       case 'z': return signedstr;
+       case 'x': return cryptstr;
+       case 'a': return attachstr;
+       case 'l': return liststr;
+
+       case 'u': return unreadstr;
+
+       default:
+               g_return_val_if_reached (flaggedstr);
+               return flaggedstr;
+       }
+}
+
+/* pre-calculate; optimization */
+G_GNUC_CONST static const std::string&
+prio_val (MuMsgPrio prio)
+{
+       static const std::string pfx (prefix(MU_MSG_FIELD_ID_PRIO));
+
+       static const std::string
+               low (pfx + std::string(1, mu_msg_prio_char(MU_MSG_PRIO_LOW))),
+               norm (pfx + std::string(1, mu_msg_prio_char(MU_MSG_PRIO_NORMAL))),
+               high (pfx + std::string(1, mu_msg_prio_char(MU_MSG_PRIO_HIGH)));
+
+       switch (prio) {
+       case MU_MSG_PRIO_LOW:    return low;
+       case MU_MSG_PRIO_NORMAL: return norm;
+       case MU_MSG_PRIO_HIGH:   return high;
+       default:
+               g_return_val_if_reached (norm);
+               return norm;
+       }
+}
+
+
+static void // add term, truncate if needed.
+add_term (Xapian::Document& doc, const std::string& term)
+{
+       if (term.length() < MU_STORE_MAX_TERM_LENGTH)
+               doc.add_term(term);
+       else
+               doc.add_term(term.substr(0, MU_STORE_MAX_TERM_LENGTH));
+}
+
+
+
+static void
+add_terms_values_number (Xapian::Document& doc, MuMsg *msg, MuMsgFieldId mfid)
+{
+       gint64 num = mu_msg_get_field_numeric (msg, mfid);
+
+       const std::string numstr (Xapian::sortable_serialise((double)num));
+       doc.add_value ((Xapian::valueno)mfid, numstr);
+
+       if (mfid == MU_MSG_FIELD_ID_FLAGS) {
+               const char *cur = mu_flags_to_str_s
+                       ((MuFlags)num,(MuFlagType)MU_FLAG_TYPE_ANY);
+               g_return_if_fail (cur);
+               while (*cur) {
+                       add_term (doc, flag_val(*cur));
+                       ++cur;
+               }
+
+       } else if (mfid == MU_MSG_FIELD_ID_PRIO)
+               add_term (doc, prio_val((MuMsgPrio)num));
+}
+
+
+/* for string and string-list */
+static void
+add_terms_values_str (Xapian::Document& doc, const char *val, MuMsgFieldId mfid)
+{
+       const auto flat = Mu::utf8_flatten (val);
+
+       if (mu_msg_field_xapian_index (mfid)) {
+               Xapian::TermGenerator termgen;
+               termgen.set_document (doc);
+               termgen.index_text (flat, 1, prefix(mfid));
+       }
+
+       if (mu_msg_field_xapian_term(mfid))
+               add_term(doc, prefix(mfid) + flat);
+
+}
+
+static void
+add_terms_values_string (Xapian::Document& doc, MuMsg *msg, MuMsgFieldId mfid)
+{
+       const char *orig;
+
+       if (!(orig = mu_msg_get_field_string (msg, mfid)))
+               return; /* nothing to do */
+
+       /* the value is what we display in search results; the
+        * unchanged original */
+       if (mu_msg_field_xapian_value(mfid))
+               doc.add_value ((Xapian::valueno)mfid, orig);
+
+       add_terms_values_str (doc, orig, mfid);
+}
+
+static void
+add_terms_values_string_list (Xapian::Document& doc, MuMsg *msg,
+                             MuMsgFieldId mfid)
+{
+       const GSList *lst;
+
+       lst = mu_msg_get_field_string_list (msg, mfid);
+       if (!lst)
+               return;
+
+       if (mu_msg_field_xapian_value (mfid)) {
+               gchar *str;
+               str = mu_str_from_list (lst, ',');
+               if (str)
+                       doc.add_value ((Xapian::valueno)mfid, str);
+               g_free (str);
+       }
+
+       if (mu_msg_field_xapian_term (mfid)) {
+               for (; lst; lst = g_slist_next ((GSList*)lst))
+                       add_terms_values_str (doc, (const gchar*)lst->data,
+                                             mfid);
+       }
+}
+
+
+struct PartData {
+       PartData (Xapian::Document& doc, MuMsgFieldId mfid):
+               _doc (doc), _mfid(mfid) {}
+       Xapian::Document _doc;
+       MuMsgFieldId _mfid;
+};
+
+/* index non-body text parts */
+static void
+maybe_index_text_part (MuMsg *msg, MuMsgPart *part, PartData *pdata)
+{
+       char *txt;
+       Xapian::TermGenerator termgen;
+
+       /* only deal with attachments/messages; inlines are indexed as
+        * body parts */
+       if (!(part->part_type & MU_MSG_PART_TYPE_ATTACHMENT) &&
+           !(part->part_type & MU_MSG_PART_TYPE_MESSAGE))
+               return;
+
+       txt = mu_msg_part_get_text (msg, part, MU_MSG_OPTION_NONE);
+       if (!txt)
+               return;
+
+       termgen.set_document(pdata->_doc);
+       const auto str = Mu::utf8_flatten (txt);
+       g_free (txt);
+
+       termgen.index_text (str, 1, prefix(MU_MSG_FIELD_ID_EMBEDDED_TEXT));
+}
+
+
+static void
+each_part (MuMsg *msg, MuMsgPart *part, PartData *pdata)
+{
+       char *fname;
+       static const std::string
+               file (prefix(MU_MSG_FIELD_ID_FILE)),
+               mime (prefix(MU_MSG_FIELD_ID_MIME));
+
+       /* save the mime type of any part */
+       if (part->type) {
+               char ctype[MU_STORE_MAX_TERM_LENGTH + 1];
+               g_snprintf(ctype, sizeof(ctype), "%s/%s", part->type, part->subtype);
+               add_term(pdata->_doc, mime + ctype);
+       }
+
+       if ((fname = mu_msg_part_get_filename (part, FALSE))) {
+               const auto flat = Mu::utf8_flatten (fname);
+               g_free (fname);
+               add_term(pdata->_doc, file + flat);
+       }
+
+       maybe_index_text_part (msg, part, pdata);
+}
+
+
+static void
+add_terms_values_attach (Xapian::Document& doc, MuMsg *msg,
+                        MuMsgFieldId mfid)
+{
+       PartData pdata (doc, mfid);
+       mu_msg_part_foreach (msg, MU_MSG_OPTION_RECURSE_RFC822,
+                            (MuMsgPartForeachFunc)each_part, &pdata);
+}
+
+
+static void
+add_terms_values_body (Xapian::Document& doc, MuMsg *msg,
+                      MuMsgFieldId mfid)
+{
+       if (mu_msg_get_flags(msg) & MU_FLAG_ENCRYPTED)
+               return; /* ignore encrypted bodies */
+
+       auto str = mu_msg_get_body_text (msg, MU_MSG_OPTION_NONE);
+       if (!str) /* FIXME: html->txt fallback needed */
+               str = mu_msg_get_body_html (msg, MU_MSG_OPTION_NONE);
+       if (!str)
+               return; /* no body... */
+
+       Xapian::TermGenerator termgen;
+       termgen.set_document(doc);
+
+       const auto flat = Mu::utf8_flatten(str);
+       termgen.index_text (flat, 1, prefix(mfid));
+}
+
+struct MsgDoc {
+       Xapian::Document *_doc;
+       MuMsg            *_msg;
+       Store            *_store;
+       /* callback data, to determine whether this message is 'personal' */
+       gboolean          _personal;
+       const StringVec  *_my_addresses;
+};
+
+
+static void
+add_terms_values_default (MuMsgFieldId mfid, MsgDoc *msgdoc)
+{
+       if (mu_msg_field_is_numeric (mfid))
+               add_terms_values_number
+                       (*msgdoc->_doc, msgdoc->_msg, mfid);
+       else if (mu_msg_field_is_string (mfid))
+               add_terms_values_string
+                       (*msgdoc->_doc, msgdoc->_msg, mfid);
+       else if (mu_msg_field_is_string_list(mfid))
+               add_terms_values_string_list
+                       (*msgdoc->_doc, msgdoc->_msg, mfid);
+       else
+               g_return_if_reached ();
+}
+
+static void
+add_terms_values (MuMsgFieldId mfid, MsgDoc* msgdoc)
+{
+       /* note: contact-stuff (To/Cc/From) will handled in
+        * each_contact_info, not here */
+       if (!mu_msg_field_xapian_index(mfid) &&
+           !mu_msg_field_xapian_term(mfid) &&
+           !mu_msg_field_xapian_value(mfid))
+               return;
+
+       switch (mfid) {
+       case MU_MSG_FIELD_ID_DATE:
+               add_terms_values_date (*msgdoc->_doc, msgdoc->_msg, mfid);
+               break;
+       case MU_MSG_FIELD_ID_SIZE:
+               add_terms_values_size (*msgdoc->_doc, msgdoc->_msg, mfid);
+               break;
+       case MU_MSG_FIELD_ID_BODY_TEXT:
+               add_terms_values_body (*msgdoc->_doc, msgdoc->_msg, mfid);
+               break;
+       /* note: add_terms_values_attach handles _FILE, _MIME and
+        * _ATTACH_TEXT msgfields */
+       case MU_MSG_FIELD_ID_FILE:
+               add_terms_values_attach (*msgdoc->_doc, msgdoc->_msg, mfid);
+               break;
+       case MU_MSG_FIELD_ID_MIME:
+       case MU_MSG_FIELD_ID_EMBEDDED_TEXT:
+               break;
+       case MU_MSG_FIELD_ID_THREAD_ID:
+       case MU_MSG_FIELD_ID_UID:
+               break; /* already taken care of elsewhere */
+       default:
+               return add_terms_values_default (mfid, msgdoc);
+       }
+}
+
+
+static const std::string&
+xapian_pfx (MuMsgContact *contact)
+{
+       static const std::string empty;
+
+       /* use ptr to string to prevent copy... */
+       switch (contact->type) {
+       case MU_MSG_CONTACT_TYPE_TO:
+               return prefix(MU_MSG_FIELD_ID_TO);
+       case MU_MSG_CONTACT_TYPE_FROM:
+               return prefix(MU_MSG_FIELD_ID_FROM);
+       case MU_MSG_CONTACT_TYPE_CC:
+               return prefix(MU_MSG_FIELD_ID_CC);
+       case MU_MSG_CONTACT_TYPE_BCC:
+               return prefix(MU_MSG_FIELD_ID_BCC);
+       default:
+               g_warning ("unsupported contact type %u",
+                          (unsigned)contact->type);
+               return empty;
+       }
+}
+
+
+static void
+add_address_subfields (Xapian::Document& doc, const char *addr,
+                      const std::string& pfx)
+{
+       const char *at, *domain_part;
+       char *name_part;
+
+       /* add "foo" and "bar.com" as terms as well for
+        * "foo@bar.com" */
+       if (G_UNLIKELY(!(at = (g_strstr_len (addr, -1, "@")))))
+               return;
+
+       name_part   = g_strndup(addr, at - addr); // foo
+       domain_part = at + 1;
+
+       add_term(doc, pfx + name_part);
+       add_term(doc, pfx + domain_part);
+
+       g_free (name_part);
+}
+
+static gboolean
+each_contact_info (MuMsgContact *contact, MsgDoc *msgdoc)
+{
+       /* for now, don't store reply-to addresses */
+       if (mu_msg_contact_type (contact) == MU_MSG_CONTACT_TYPE_REPLY_TO)
+               return TRUE;
+
+       const std::string pfx (xapian_pfx(contact));
+       if (pfx.empty())
+               return TRUE; /* unsupported contact type */
+
+       if (!mu_str_is_empty(contact->name)) {
+               Xapian::TermGenerator termgen;
+               termgen.set_document (*msgdoc->_doc);
+               const auto flat = Mu::utf8_flatten(contact->name);
+               termgen.index_text (flat, 1, pfx);
+       }
+
+       if (!mu_str_is_empty(contact->email)) {
+               const auto flat = Mu::utf8_flatten(contact->email);
+               add_term(*msgdoc->_doc, pfx + flat);
+               add_address_subfields (*msgdoc->_doc, contact->email, pfx);
+               /* store it also in our contacts cache */
+               auto& contacts = msgdoc->_store->priv()->contacts_;
+                contacts.add(Mu::ContactInfo(contact->full_address,
+                                             contact->email,
+                                             contact->name ? contact->name : "",
+                                             msgdoc->_personal,
+                                             mu_msg_get_date(msgdoc->_msg)));
+       }
+
+       return TRUE;
+}
+
+
+static gboolean
+each_contact_check_if_personal (MuMsgContact *contact, MsgDoc *msgdoc)
+{
+       if (msgdoc->_personal || !contact->email)
+               return TRUE;
+
+       for (const auto& cur : *msgdoc->_my_addresses) {
+               if (g_ascii_strcasecmp
+                    (contact->email,
+                     (const char*)cur.c_str()) == 0) {
+                       msgdoc->_personal = TRUE;
+                       break;
+               }
+       }
+
+       return TRUE;
+}
+
+static Xapian::Document
+new_doc_from_message (MuStore *store, MuMsg *msg)
+{
+       Xapian::Document doc;
+       MsgDoc docinfo = {&doc, msg, mutable_self(store), 0, NULL};
+
+       mu_msg_field_foreach ((MuMsgFieldForeachFunc)add_terms_values, &docinfo);
+
+       /* determine whether this is 'personal' email, ie. one of my
+        * e-mail addresses is explicitly mentioned -- it's not a
+        * mailing list message. Callback will update docinfo->_personal */
+        const auto& personal_addresses = self(store)->personal_addresses();
+        if (personal_addresses.size()) {
+               docinfo._my_addresses = &personal_addresses;
+               mu_msg_contact_foreach
+                       (msg,
+                        (MuMsgContactForeachFunc)each_contact_check_if_personal,
+                        &docinfo);
+       }
+
+       /* also store the contact-info as separate terms, and add it
+        * to the cache */
+       mu_msg_contact_foreach (msg, (MuMsgContactForeachFunc)each_contact_info,
+                               &docinfo);
+
+       // g_printerr ("\n--%s\n--\n", doc.serialise().c_str());
+
+       return doc;
+}
+
+static void
+update_threading_info (Xapian::WritableDatabase* db,
+                      MuMsg *msg, Xapian::Document& doc)
+{
+       const GSList *refs;
+
+       // refs contains a list of parent messages, with the oldest
+       // one first until the last one, which is the direct parent of
+       // the current message. of course, it may be empty.
+       //
+       // NOTE: there may be cases where the list is truncated; we happily
+       // ignore that case.
+       refs  = mu_msg_get_references (msg);
+
+        char thread_id[16+1];
+        hash_str(thread_id, sizeof(thread_id),
+                 refs ? (const char*)refs->data : mu_msg_get_msgid (msg));
+
+       add_term (doc, prefix(MU_MSG_FIELD_ID_THREAD_ID) + thread_id);
+       doc.add_value((Xapian::valueno)MU_MSG_FIELD_ID_THREAD_ID, thread_id);
+}
+
+
+static unsigned
+add_or_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err)
+{
+       g_return_val_if_fail (store, MU_STORE_INVALID_DOCID);
+       g_return_val_if_fail (msg, MU_STORE_INVALID_DOCID);
+
+       try {
+               Xapian::docid id;
+               Xapian::Document doc (new_doc_from_message(store, msg));
+               const std::string term (get_uid_term (mu_msg_get_path(msg)));
+
+                auto self = mutable_self(store);
+                auto wdb  = self->priv()->wdb();
+
+               if (!self->in_transaction())
+                       self->priv()->begin_transaction();
+
+               add_term (doc, term);
+
+               // update the threading info if this message has a message id
+               if (mu_msg_get_msgid (msg))
+                       update_threading_info (wdb.get(), msg, doc);
+
+               if (docid == 0)
+                       id = wdb->replace_document (term, doc);
+               else {
+                       wdb->replace_document (docid, doc);
+                       id = docid;
+               }
+
+                if (++self->priv()->dirtiness_ >= BatchSize)
+                        self->priv()->commit_transaction();
+
+               return id;
+
+       } MU_XAPIAN_CATCH_BLOCK_G_ERROR (err, MU_ERROR_XAPIAN_STORE_FAILED);
+
+       return MU_STORE_INVALID_DOCID;
+}
+
+unsigned
+mu_store_add_msg (MuStore *store, MuMsg *msg, GError **err)
+{
+       g_return_val_if_fail (store, MU_STORE_INVALID_DOCID);
+       g_return_val_if_fail (msg, MU_STORE_INVALID_DOCID);
+
+       return add_or_update_msg (store, 0, msg, err);
+}
+
+unsigned
+mu_store_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err)
+{
+       g_return_val_if_fail (store, MU_STORE_INVALID_DOCID);
+       g_return_val_if_fail (msg, MU_STORE_INVALID_DOCID);
+       g_return_val_if_fail (docid != 0, MU_STORE_INVALID_DOCID);
+
+       return add_or_update_msg (store, docid, msg, err);
+}
+
+unsigned
+mu_store_add_path (MuStore *store, const char *path, GError **err)  try {
+
+        MuMsg *msg;
+        unsigned docid;
+
+        g_return_val_if_fail (store, FALSE);
+        g_return_val_if_fail (path, FALSE);
+
+        const auto maildir{maildir_from_path(self(store)->root_maildir(), path)};
+        msg = mu_msg_new_from_file (path, maildir.c_str(), err);
+        if (!msg)
+                return MU_STORE_INVALID_DOCID;
+
+        docid = add_or_update_msg (store, 0, msg, err);
+        mu_msg_unref (msg);
+
+        return docid;
+
+} catch (const Mu::Error& me) {
+        g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN,
+                     "%s", me.what());
+        return MU_STORE_INVALID_DOCID;
+} catch (...) {
+        g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_INTERNAL,
+                     "caught exception");
+        return MU_STORE_INVALID_DOCID;
+}
+
+XapianWritableDatabase*
+mu_store_get_writable_database (MuStore *store)
+{
+       g_return_val_if_fail (store, NULL);
+
+       return (XapianWritableDatabase*)mutable_self(store)->priv()->wdb().get();
+}
+
+
+gboolean
+mu_store_remove_path (MuStore *store, const char *msgpath)
+{
+       g_return_val_if_fail (store, FALSE);
+       g_return_val_if_fail (msgpath, FALSE);
+
+       try {
+               const std::string term{(get_uid_term(msgpath))};
+                auto wdb = mutable_self(store)->priv()->wdb();
+
+               wdb->delete_document (term);
+               //store->inc_processed();
+
+               return TRUE;
+
+       } MU_XAPIAN_CATCH_BLOCK_RETURN (FALSE);
+}
+
+
+gboolean
+mu_store_set_dirstamp (MuStore *store, const char* dirpath,
+                            time_t stamp, GError **err)
+{
+        g_return_val_if_fail (store, FALSE);
+       g_return_val_if_fail (dirpath, FALSE);
+
+       mutable_self(store)->set_dirstamp(dirpath, stamp);
+
+        return TRUE;
+}
+
+time_t
+mu_store_get_dirstamp (const MuStore *store, const char *dirpath, GError **err)
+{
+       g_return_val_if_fail (store, 0);
+       g_return_val_if_fail (dirpath, 0);
+
+        return self(store)->dirstamp(dirpath);
+}
+
+void
+mu_store_print_info  (const MuStore *store, gboolean nocolor)
+{
+       const auto green{nocolor ? "" : MU_COLOR_GREEN};
+       const auto def{nocolor ? "" : MU_COLOR_DEFAULT};
+
+        std::cout << "database-path      : "
+                  << green << self(store)->database_path() << def << "\n"
+                  << "messages in store  : "
+                  << green << self(store)->size() << def << "\n"
+                  << "schema-version     : "
+                  << green << self(store)->schema_version() << def << "\n";
+
+       const auto created{mu_store_created (store)};
+       const auto tstamp{localtime (&created)};
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-y2k"
+        char tbuf[64];
+       strftime (tbuf, sizeof(tbuf), "%c", tstamp);
+#pragma GCC diagnostic pop
+
+        std::cout << "created            : " << green << tbuf << def << "\n"
+                  << "maildir            : "
+                  << green << self(store)->root_maildir() << def << "\n";
+
+       std::cout << ("personal-addresses : ");
+
+       auto addrs{mu_store_personal_addresses (store)};
+        if (!addrs || g_strv_length(addrs) == 0)
+                std::cout << green << "<none>" << def << "\n";
+        else {
+                for (auto i = 0U; addrs[i]; ++i) {
+                        std::cout << (i != 0 ?  "                     " : "")
+                                  << green << addrs[i] << def << "\n";
+                }
+        }
+
+       g_strfreev(addrs);
+}
+}
diff --git a/lib/mu-store.hh b/lib/mu-store.hh
new file mode 100644 (file)
index 0000000..f34c70b
--- /dev/null
@@ -0,0 +1,568 @@
+/*
+** 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_STORE_HH__
+#define __MU_STORE_HH__
+
+#include <mu-msg.h>
+
+#ifdef __cplusplus
+
+#include "mu-contacts.hh"
+
+#include <xapian.h>
+
+#include <string>
+#include <vector>
+#include <ctime>
+
+#include <utils/mu-utils.hh>
+
+namespace Mu {
+
+class Store {
+public:
+        /**
+         * Construct a store for an existing document database
+         *
+         * @param path path to the database
+         * @param readonly whether to open the database in read-only mode
+         */
+        Store (const std::string& path, bool readonly=true);
+
+        /**
+         * 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_addressesaddresses that should be recognized as
+         * 'personal' for identifying personal messages.
+         */
+        Store (const std::string& path, const std::string& maildir,
+               const StringVec& personal_addresses);
+
+        /**
+         * DTOR
+         */
+        ~Store();
+
+        /**
+         * Is the store read-only?
+         *
+         * @return true or false
+         */
+        bool read_only() const;
+
+        /**
+         * Path to the database; this is some subdirectory of the path
+         * passed to the constructor.
+         *
+         * @return the database path
+         */
+        const std::string& database_path() const;
+
+        /**
+         * Path to the top-level Maildir
+         *
+         * @return the maildir
+         */
+        const std::string& root_maildir() const;
+
+        /**
+         * Version of the database-schema
+         *
+         * @return the maildir
+         */
+        const std::string& schema_version() const;
+
+
+        /**
+         * Time of creation of the store
+         *
+         * @return creation time
+         */
+        std::time_t created() const;
+
+        /**
+         * Get a vec with the personal addresses
+         *
+         * @return personal addresses
+         */
+        const StringVec& personal_addresses() const;
+
+        /**
+         * Get the Contacts object for this store
+         *
+         * @return the Contacts object
+         */
+        const Contacts& contacts() const;
+
+        /**
+         * Add a message to the store.
+         *
+         * @param path the message path.
+         *
+         * @return the doc id of the added message
+         */
+        unsigned add_message (const std::string& path);
+
+        /**
+         * Add a message to the store.
+         *
+         * @param path the message path.
+         *
+         * @return true if removing happened; false otherwise.
+         */
+        bool remove_message (const std::string& path);
+
+        /**
+         * Fina  message in the store.
+         *
+         * @param docid doc id for the message to find
+         *
+         * @return a message (owned by caller), or nullptr
+         */
+        MuMsg* find_message (unsigned docid) 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;
+
+        /**
+         * 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;
+
+        /**
+         * Begin a database transaction
+         */
+        void begin_transaction();
+
+        /**
+         * Commit a database transaction
+         *
+         */
+        void commit_transaction();
+
+        /**
+         * Are we in a transaction?
+         *
+         * @return true or false
+         */
+        bool in_transaction() const;
+
+
+        /**
+         * 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:
+        std::unique_ptr<Private> priv_;
+};
+
+} // namespace Mu
+
+
+#endif /*__cplusplus*/
+
+#include <glib.h>
+#include <inttypes.h>
+#include <utils/mu-util.h>
+#include <mu-contacts.hh>
+
+G_BEGIN_DECLS
+
+struct MuStore_;
+typedef struct MuStore_ MuStore;
+
+/* http://article.gmane.org/gmane.comp.search.xapian.general/3656 */
+#define MU_STORE_MAX_TERM_LENGTH (240)
+
+
+/**
+ * create a new read-only Xapian store, for querying documents
+ *
+ * @param path the path to the database
+ * @param err to receive error info or NULL. err->code is MuError value
+ *
+ * @return a new MuStore object with ref count == 1, or NULL in case of error;
+ * free with mu_store_unref
+ */
+MuStore* mu_store_new_readable (const char* xpath, GError **err)
+                  G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+/**
+ * create a new writable Xapian store, a place to store documents
+ *
+ * @param path the path to the database
+ * @param err to receive error info or NULL. err->code is MuError value
+ *
+ * @return a new MuStore object with ref count == 1, or NULL in case
+ * of error; free with mu_store_unref
+ */
+MuStore*  mu_store_new_writable (const char *xpath, GError **err)
+        G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * create a new writable Xapian store, a place to store documents, and
+ * create/overwrite the existing database.
+ *
+ * @param path the path to the database
+ * @param path to the maildir
+ * @param personal_addressesaddresses that should be recognized as
+ * 'personal' for identifying personal messages.
+ * @param err to receive error info or NULL. err->code is MuError value
+ *
+ * @return a new MuStore object with ref count == 1, or NULL in case
+ * of error; free with mu_store_unref
+ */
+MuStore*  mu_store_new_create (const char *xpath, const char *maildir,
+                               const char **personal_addresses, GError **err)
+        G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * increase the reference count for this store with 1
+ *
+ * @param store a valid store object
+ *
+ * @return the same store with increased ref count, or NULL in case of
+ * error
+ */
+MuStore* mu_store_ref (MuStore *store);
+
+/**
+ * decrease the reference count for this store with 1
+ *
+ * @param store a valid store object
+ *
+ * @return NULL
+ */
+MuStore* mu_store_unref (MuStore *store);
+
+
+/**
+ * we need this when using Xapian::(Writable)Database* from C
+ */
+typedef gpointer XapianWritableDatabase;
+typedef gpointer XapianDatabase;
+
+
+/**
+ * get the underlying writable database object for this store; not
+ * that this pointer becomes in valid after mu_store_destroy
+ *
+ * @param store a valid store
+ *
+ * @return a Xapian::WritableDatabase (you'll need to cast in C++), or
+ * NULL in case of error.
+ */
+XapianWritableDatabase* mu_store_get_writable_database (MuStore *store);
+
+
+/**
+ * get the underlying read-only database object for this store; not that this
+ * pointer becomes in valid after mu_store_destroy
+ *
+ * @param store a valid store
+ *
+ * @return a Xapian::Database (you'll need to cast in C++), or
+ * NULL in case of error.
+ */
+XapianDatabase* mu_store_get_read_only_database (MuStore *store);
+
+/**
+ * get the version of the xapian database (ie., the version of the
+ * 'schema' we are using). If this version != MU_STORE_SCHEMA_VERSION,
+ * it's means we need to a full reindex.
+ *
+ * @param store the store to inspect
+ *
+ * @return the version of the database as a newly allocated string
+ * (free with g_free); if there is no version yet, it will return NULL
+ */
+const char* mu_store_schema_version (const MuStore* store);
+
+
+/**
+ * Get the database-path for this message store
+ *
+ * @param store the store to inspetc
+ *
+ * @return the database-path
+ */
+const char *mu_store_database_path (const MuStore *store);
+
+
+/**
+ * Get the root-maildir for this message store.
+ *
+ * @param store the store
+ *
+ * @return the maildir.
+ */
+const char *mu_store_root_maildir(const MuStore *store);
+
+
+/**
+ * Get the time this database was created
+ *
+ * @param store the store
+ *
+ * @return the maildir.
+ */
+time_t mu_store_created(const MuStore *store);
+
+/**
+ * Get the list of personal addresses from the store
+ *
+ * @param store the message store
+ *
+ * @return the list of personal addresses, or NULL in case of error.
+ *
+ * Free with g_strfreev().
+ */
+char** mu_store_personal_addresses (const MuStore *store);
+
+/**
+ * Get the a MuContacts* ptr for this store.
+ *
+ * @param store a store
+ *
+ * @return the contacts ptr
+ */
+const MuContacts* mu_store_contacts (MuStore *store);
+
+
+/**
+ * get the numbers of documents in the database
+ *
+ * @param index a valid MuStore instance
+ * @param err to receive error info or NULL. err->code is MuError value
+ *
+ * @return the number of documents in the database; (unsigned)-1 in
+ * case of error
+ */
+unsigned mu_store_count (const MuStore *store, GError **err);
+
+
+/**
+ * try to flush/commit all outstanding work to the database and the contacts
+ * cache.
+ *
+ * @param store a valid xapian store
+ */
+void mu_store_flush (MuStore *store);
+
+#define MU_STORE_INVALID_DOCID 0
+
+/**
+ * store an email message in the XapianStore
+ *
+ * @param store a valid store
+ * @param msg a valid message
+ * @param err receives error information, if any, or NULL
+ *
+ * @return the docid of the stored message, or 0
+ * (MU_STORE_INVALID_DOCID) in case of error
+ */
+unsigned mu_store_add_msg   (MuStore *store, MuMsg *msg, GError **err);
+
+
+/**
+ * update an email message in the XapianStore
+ *
+ * @param store a valid store
+ * @param the docid for the message
+ * @param msg a valid message
+ * @param err receives error information, if any, or NULL
+ *
+ * @return the docid of the stored message, or 0
+ * (MU_STORE_INVALID_DOCID) in case of error
+ */
+unsigned mu_store_update_msg (MuStore *store, unsigned docid, MuMsg *msg,
+                             GError **err);
+
+/**
+ * store an email message in the XapianStore; similar to
+ * mu_store_store, but instead takes a path as parameter instead of a
+ * MuMsg*
+ *
+ * @param store a valid store
+ * @param path full filesystem path to a valid message
+ * @param err receives error information, if any, or NULL
+ *
+ * @return the docid of the stored message, or 0
+ * (MU_STORE_INVALID_DOCID) in case of error
+ */
+unsigned mu_store_add_path (MuStore *store, const char *path, GError **err);
+
+/**
+ * remove a message from the database based on its path
+ *
+ * @param store a valid store
+ * @param msgpath path of the message (note, this is only used to
+ * *identify* the message; a common use of this function is to remove
+ * a message from the database, for which there is no message anymore
+ * in the filesystem.
+ *
+ * @return TRUE if it succeeded, FALSE otherwise
+ */
+gboolean mu_store_remove_path (MuStore *store, const char* msgpath);
+
+/**
+ * does a certain message exist in the database already?
+ *
+ * @param store a store
+ * @param path the message path
+ *
+ * @return TRUE if the message exists, FALSE otherwise
+ */
+gboolean mu_store_contains_message (const MuStore *store,  const char* path);
+
+/**
+ * get the docid for message at path
+ *
+ * @param store a store
+ * @param path the message path
+ * @param err to receive error info or NULL. err->code is MuError value
+ *
+ * @return the docid if the message was found, MU_STORE_INVALID_DOCID (0) otherwise
+ * */
+unsigned mu_store_get_docid_for_path (const MuStore *store, const char* path,
+                                      GError **err);
+
+/**
+ * store a timestamp for a directory
+ *
+ * @param store a valid store
+ * @param dirpath path to some directory
+ * @param stamp a timestamp
+ * @param err to receive error info or NULL. err->code is MuError value
+ *
+ * @return TRUE if setting the timestamp succeeded, FALSE otherwise
+ */
+gboolean mu_store_set_dirstamp (MuStore *store, const char* dirpath,
+                                time_t stamp, GError **err);
+
+/**
+ * get the timestamp for a directory
+ *
+ * @param store a valid store
+ * @param msgpath path to some directory
+ * @param err to receive error info or NULL. err->code is MuError value
+ *
+ * @return the timestamp, or 0 in case of error
+ */
+time_t mu_store_get_dirstamp (const MuStore *store, const char* dirpath,
+                              GError **err);
+
+/**
+ * check whether this store is read-only
+ *
+ * @param store a store
+ *
+ * @return TRUE if the store is read-only, FALSE otherwise (and in
+ * case of error)
+ */
+gboolean mu_store_is_read_only (const MuStore *store);
+
+/**
+ * call a function for each document in the database
+ *
+ * @param self a valid store
+ * @param func a callback function to to call for each document
+ * @param user_data a user pointer passed to the callback function
+ * @param err to receive error info or NULL. err->code is MuError value
+ *
+ * @return MU_OK if all went well, MU_STOP if the foreach was interrupted,
+ * MU_ERROR in case of error
+ */
+typedef MuError (*MuStoreForeachFunc) (const char* path, gpointer user_data);
+MuError  mu_store_foreach (MuStore *self, MuStoreForeachFunc func,
+                          void *user_data, GError **err);
+
+/**
+ * check if the database is locked for writing
+ *
+ * @param xpath path to a xapian database
+ *
+ * @return TRUE if it is locked, FALSE otherwise (or in case of error)
+ */
+gboolean mu_store_database_is_locked (const gchar *xpath);
+
+/**
+ * get a specific message, based on its Xapian docid
+ *
+ * @param self a valid MuQuery instance
+ * @param docid the Xapian docid for the wanted message
+ * @param err receives error information, or NULL
+ *
+ * @return a MuMsg instance (use mu_msg_unref when done with it), or
+ * NULL in case of error
+ */
+MuMsg* mu_store_get_msg (const MuStore *self, unsigned docid, GError **err)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * Print some information about the store
+ *
+ * @param store a store
+ * @param nocolor whether to _not_ show color
+ */
+void mu_store_print_info  (const MuStore *store, gboolean nocolor);
+
+
+G_END_DECLS
+
+#endif /* __MU_STORE_HH__ */
diff --git a/lib/mu-threader.c b/lib/mu-threader.c
new file mode 100644 (file)
index 0000000..80857ef
--- /dev/null
@@ -0,0 +1,455 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+/*
+** 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.
+**
+*/
+#include <math.h>   /* for log, ceil */
+#include <string.h> /* for memset */
+
+#include "mu-threader.h"
+#include "mu-container.h"
+#include "utils/mu-str.h"
+
+/* msg threading implementation based on JWZ's algorithm, as described in:
+ *    http://www.jwz.org/doc/threading.html
+ *
+ * the implementation follows the terminology from that doc, so should
+ * be understandable from that... I did change things a bit though
+ *
+ * the end result of the threading operation is a hashtable which maps
+ * docids (ie., Xapian documents == messages) to 'thread paths'; a
+ * thread path is a string denoting the 2-dimensional place of a
+ * message in a list of messages,
+ *
+ * Msg1                        => 00000
+ * Msg2                        => 00001
+ *   Msg3 (child of Msg2)      => 00001:00000
+ *   Msg4 (child of Msg2)      => 00001:00001
+ *     Msg5 (child of Msg4)    => 00001:00001:00000
+ * Msg6                        => 00002
+ *
+ * the padding-0's are added to make them easy to sort using strcmp;
+ * the number hexadecimal numbers, and the length of the 'segments'
+ * (the parts separated by the ':') is equal to ceil(log_16(matchnum))
+ *
+ */
+
+/* step 1 */ static GHashTable* create_containers (MuMsgIter *iter);
+/* step 2 */ static MuContainer *find_root_set (GHashTable *ids);
+static MuContainer* prune_empty_containers (MuContainer *root);
+/* static void group_root_set_by_subject (GSList *root_set); */
+GHashTable* create_doc_id_thread_path_hash (MuContainer *root,
+                                           size_t match_num);
+
+/* msg threading algorithm, based on JWZ's algorithm,
+ * http://www.jwz.org/doc/threading.html */
+GHashTable*
+mu_threader_calculate (MuMsgIter *iter, size_t matchnum,
+                      MuMsgFieldId sortfield, gboolean descending)
+{
+       GHashTable *id_table, *thread_ids;
+       MuContainer *root_set;
+
+       g_return_val_if_fail (iter, FALSE);
+       g_return_val_if_fail (mu_msg_field_id_is_valid (sortfield) ||
+                             sortfield == MU_MSG_FIELD_ID_NONE,
+                             FALSE);
+
+       /* step 1 */
+       id_table = create_containers (iter);
+       if (matchnum == 0)
+               return id_table; /* just return an empty table */
+
+       /* step 2 -- the root_set is the list of children without parent */
+       root_set = find_root_set (id_table);
+
+       /* step 3: skip until the end; we still need to containers */
+
+       /* step 4: prune empty containers */
+       root_set = prune_empty_containers (root_set);
+
+       /* sort root set */
+       if (sortfield != MU_MSG_FIELD_ID_NONE)
+               root_set = mu_container_sort (root_set, sortfield, descending,
+                                             NULL);
+
+       /* step 5: group root set by subject */
+       /* group_root_set_by_subject (root_set); */
+
+       /* sort */
+       mu_msg_iter_reset (iter); /* go all the way back */
+
+       /* finally, deliver the docid => thread-path hash */
+       thread_ids = mu_container_thread_info_hash_new (root_set,
+                                                       matchnum);
+
+       g_hash_table_destroy (id_table); /* step 3*/
+
+       return thread_ids;
+}
+
+G_GNUC_UNUSED static void
+check_dup (const char *msgid, MuContainer *c, GHashTable *hash)
+{
+       if (g_hash_table_lookup (hash, c)) {
+               g_warning ("ALREADY!!");
+               mu_container_dump (c, FALSE);
+               g_assert (0);
+       } else
+               g_hash_table_insert (hash, c, GUINT_TO_POINTER(TRUE));
+}
+
+
+G_GNUC_UNUSED static void
+assert_no_duplicates (GHashTable *ids)
+{
+       GHashTable *hash;
+
+       hash = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+       g_hash_table_foreach (ids, (GHFunc)check_dup, hash);
+
+       g_hash_table_destroy (hash);
+}
+
+
+/* a referred message is a message that is referred by some other
+ * message */
+static MuContainer*
+find_or_create_referred (GHashTable *id_table, const char *msgid,
+                        gboolean *created)
+{
+       MuContainer *c;
+
+       g_return_val_if_fail (msgid, NULL);
+
+       c = g_hash_table_lookup (id_table, msgid);
+       *created = !c;
+       if (!c) {
+               c = mu_container_new (NULL, 0, msgid);
+               g_hash_table_insert (id_table, (gpointer)msgid, c);
+               /* assert_no_duplicates (id_table); */
+       }
+
+
+       return c;
+}
+
+/* find a container for the given msgid; if it does not exist yet,
+ * create a new one, and register it */
+static MuContainer*
+find_or_create (GHashTable *id_table, MuMsg *msg, guint docid)
+{
+       MuContainer     *c;
+       const char*      msgid;
+       char             fake[32];
+
+       g_return_val_if_fail (msg, NULL);
+       g_return_val_if_fail (docid != 0, NULL);
+
+       msgid = mu_msg_get_msgid (msg);
+       if (!msgid)
+               msgid = mu_msg_get_path (msg); /* fake it */
+       if (!msgid) { /* no path either? seems to happen... */
+               g_warning ("message without path");
+               g_snprintf (fake, sizeof(fake), "fake:%p", (gpointer)msg);
+               msgid = fake;
+       }
+
+       /* XXX the '<none>' works around a crash; find a better
+        * solution */
+       c = g_hash_table_lookup (id_table, msgid);
+
+       /* If id_table contains an empty MuContainer for this ID: * *
+        * Store this message in the MuContainer's message slot. */
+       if (c) {
+               if (!c->msg) {
+                       c->msg    = mu_msg_ref (msg);
+                       c->docid  = docid;
+                       return c;
+               } else {
+                       /* special case, not in the JWZ algorithm: the
+                        * container exists already and has a message; this
+                        * means that we are seeing *another message* with a
+                        * message-id we already saw... create this message,
+                        * and mark it as a duplicate, and a child of the one
+                        * we saw before; use its path as a fake message-id
+                        * */
+                       MuContainer *c2;
+                       const char* fake_msgid;
+
+                       fake_msgid = mu_msg_get_path (msg);
+
+                       c2        = mu_container_new (msg, docid, fake_msgid);
+                       c2->flags = MU_CONTAINER_FLAG_DUP;
+                       /*c       = */ mu_container_append_children (c, c2);
+
+                       g_hash_table_insert (id_table, (gpointer)fake_msgid, c2);
+
+                       return NULL; /* don't process this message further */
+               }
+       } else { /* Else: Create a new MuContainer object holding
+                   this message; Index the MuContainer by
+                   Message-ID in id_table. */
+               c = mu_container_new (msg, docid, msgid);
+               g_hash_table_insert (id_table, (gpointer)msgid, c);
+               /* assert_no_duplicates (id_table); */
+
+               return c;
+       }
+}
+
+static gboolean
+child_elligible (MuContainer *parent, MuContainer *child, gboolean created)
+{
+       if (!parent || !child)
+               return FALSE;
+       if (child->parent)
+               return FALSE;
+       /* if (created) */
+       /*      return TRUE; */
+       if (mu_container_reachable (parent, child))
+               return FALSE;
+       if (mu_container_reachable (child, parent))
+               return FALSE;
+
+       return TRUE;
+}
+
+
+
+static void /* 1B */
+handle_references (GHashTable *id_table, MuContainer *c)
+{
+       const GSList *refs, *cur;
+       MuContainer *parent;
+       gboolean created;
+
+       refs = mu_msg_get_references (c->msg);
+       if (!refs)
+               return; /* nothing to do */
+
+       /* For each element in the message's References field:
+
+          Find a MuContainer object for the given Message-ID: If
+          there's one in id_table use that; Otherwise, make (and
+          index) one with a null Message. */
+
+       /* go over over our list of refs, until 1 before the last... */
+       created = FALSE;
+       for (parent = NULL, cur = refs; cur; cur = g_slist_next (cur)) {
+
+               MuContainer *child;
+               child = find_or_create_referred (id_table, (gchar*)cur->data,
+                                                &created);
+
+               /* if we find the current message in their own refs, break now
+                  so that parent != c in next step */
+               if (child == c)
+                       break;
+
+               /*Link the References field's MuContainers 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 (child_elligible (parent, child, created))
+                       /*parent =*/
+                       mu_container_append_children (parent, child);
+
+               parent = child;
+       }
+
+       /* 'parent' points to the last ref: our direct parent;
+
+          Set the parent of this message to be the last element in
+          References. Note that this message may have a parent
+          already: this can happen because we saw this ID in a
+          References field, and presumed a parent based on the other
+          entries in that field. Now that we have the actual message,
+          we can be more definitive, so throw away the old parent and
+          use this new one. Find this MuContainer in the parent's
+          children list, and unlink it.
+
+          Note that this could cause this message to now have no
+          parent, if it has no references field, but some message
+          referred to it as the non-first element of its
+          references. (Which would have been some kind of lie...)
+
+          Note that at all times, the various ``parent'' and ``child'' fields
+          must be kept inter-consistent. */
+
+        /* optimization: if the the message was newly added, it's by
+          definition not reachable yet */
+
+       /* So, we move c and its descendants to become a child of parent if:
+          * both are not NULL
+          * parent is not a descendant of c.
+          * both are different from each other (guaranteed in last loop) */
+
+       if (parent && c && !(c->child && mu_container_reachable (c->child, parent))) {
+
+               /* if c already has a parent, remove c from its parent children
+                  and reparent it, as now we know who is c's parent reliably */
+               if (c->parent) {
+                       mu_container_remove_child(c->parent, c);
+                       c->next = c->last = c->parent = NULL;
+               }
+
+               /*parent = */mu_container_append_children (parent, c);
+       }
+}
+
+
+
+/* step 1: create the containers, connect them, and fill the id_table */
+static GHashTable*
+create_containers (MuMsgIter *iter)
+{
+       GHashTable *id_table;
+       id_table = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                         NULL,
+                                         (GDestroyNotify)mu_container_destroy);
+
+       for (mu_msg_iter_reset (iter); !mu_msg_iter_is_done (iter);
+            mu_msg_iter_next (iter)) {
+
+               MuContainer *c;
+               MuMsg *msg;
+               unsigned docid;
+
+               /* 1.A */
+               msg   = mu_msg_iter_get_msg_floating (iter); /* don't unref */
+               docid = mu_msg_iter_get_docid (iter);
+
+               c = find_or_create (id_table, msg, docid);
+
+               /* 1.B and C */
+               if (c)
+                       handle_references (id_table, c);
+       }
+
+       return id_table;
+}
+
+
+
+static void
+filter_root_set (const gchar *msgid, MuContainer *c, MuContainer **root_set)
+{
+       /* ignore children */
+       if (c->parent)
+               return;
+
+       /* ignore duplicates */
+       if (c->flags & MU_CONTAINER_FLAG_DUP)
+               return;
+
+       if (*root_set == NULL) {
+               *root_set = c;
+               return;
+       } else
+               *root_set = mu_container_append_siblings (*root_set, c);
+}
+
+
+/* 2.  Walk over the elements of id_table, and gather a list of the
+   MuContainer objects that have no parents, but do have children */
+static MuContainer*
+find_root_set (GHashTable *ids)
+{
+       MuContainer *root_set;
+
+       root_set = NULL;
+       g_hash_table_foreach (ids, (GHFunc)filter_root_set, &root_set);
+
+       return root_set;
+}
+
+
+static gboolean
+prune_maybe (MuContainer *c)
+{
+       MuContainer *cur;
+
+       for (cur = c->child; cur; cur = cur->next) {
+               if (cur->flags & MU_CONTAINER_FLAG_DELETE) {
+                       c = mu_container_remove_child (c, cur);
+               } else if (cur->flags & MU_CONTAINER_FLAG_SPLICE) {
+                       c = mu_container_splice_grandchildren (c, cur);
+                       c = mu_container_remove_child (c, cur);
+               }
+       }
+
+       g_return_val_if_fail (c, FALSE);
+
+       /* don't touch containers with messages */
+       if (c->msg)
+               return TRUE;
+
+       /* A. If it is an msg-less container with no children, mark it for
+        * deletion. */
+       if (!c->child) {
+               c->flags |= MU_CONTAINER_FLAG_DELETE;
+               return TRUE;
+       }
+
+       /* B. If the MuContainer has no Message, 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.
+        */
+       if (c->child->next) /* ie., > 1 child */
+               return TRUE;
+
+       c->flags |= MU_CONTAINER_FLAG_SPLICE;
+
+       return TRUE;
+}
+
+
+static MuContainer*
+prune_empty_containers (MuContainer *root_set)
+{
+       MuContainer *cur;
+
+       mu_container_foreach (root_set,
+                             (MuContainerForeachFunc)prune_maybe,
+                             NULL);
+
+       /* and prune the root_set itself... */
+       for (cur = root_set; cur; cur = cur->next) {
+               if (cur->flags & MU_CONTAINER_FLAG_DELETE) {
+                       root_set = mu_container_remove_sibling (root_set, cur);
+               } else if (cur->flags & MU_CONTAINER_FLAG_SPLICE) {
+                       root_set = mu_container_splice_children (root_set, cur);
+                       root_set = mu_container_remove_sibling (root_set, cur);
+               }
+       }
+
+       return root_set;
+}
diff --git a/lib/mu-threader.h b/lib/mu-threader.h
new file mode 100644 (file)
index 0000000..dc8d414
--- /dev/null
@@ -0,0 +1,56 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_THREADER_H__
+#define __MU_THREADER_H__
+
+#include <glib.h>
+#include <mu-msg-iter.h>
+
+G_BEGIN_DECLS
+
+/**
+ * takes an iter and the total number of matches, and from this
+ * generates a hash-table with information about the thread structure
+ * of these matches.
+ *
+ * the algorithm to find this structure is based on JWZ's
+ * message-threading algorithm, as descrbed in:
+ *     http://www.jwz.org/doc/threading.html
+ *
+ * the returned hashtable maps the Xapian docid of iter (msg) to a ptr
+ * to a MuMsgIterThreadInfo structure (see mu-msg-iter.h)
+ *
+ * @param iter an iter; note this function will mu_msgi_iter_reset this iterator
+ * @param matches the number of matches in the set *
+ * @param sortfield the field to sort results by, or
+ * MU_MSG_FIELD_ID_NONE if no sorting should be performed
+ * @param revert if TRUE, if revert the sorting order
+ *
+ * @return a hashtable; free with g_hash_table_destroy when done with it
+ */
+GHashTable *mu_threader_calculate (MuMsgIter *iter, size_t matches,
+                                  MuMsgFieldId sortfield, gboolean revert);
+
+
+G_END_DECLS
+
+#endif /*__MU_THREADER_H__*/
diff --git a/lib/query/Makefile.am b/lib/query/Makefile.am
new file mode 100644 (file)
index 0000000..6923d2b
--- /dev/null
@@ -0,0 +1,99 @@
+## Copyright (C) 2017-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
+
+@VALGRIND_CHECK_RULES@
+
+AM_CXXFLAGS=                   \
+       -I$(srcdir)/..          \
+       -I$(top_srcdir)/lib     \
+       $(GLIB_CFLAGS)          \
+       $(XAPIAN_CXXFLAGS)      \
+       $(WARN_CXXFLAGS)        \
+       $(ASAN_CXXFLAGS)        \
+       $(CODE_COVERAGE_CFLAGS) \
+       -Wno-inline             \
+       -Wno-switch-enum
+
+AM_CPPFLAGS=                   \
+       $(CODE_COVERAGE_CPPFLAGS)
+
+AM_LDFLAGS=                    \
+       $(ASAN_LDFLAGS)         \
+       $(WARN_LDFLAGS)
+
+noinst_PROGRAMS=               \
+       tokenize                \
+       parse
+
+noinst_LTLIBRARIES=            \
+       libmu-query.la
+
+libmu_query_la_SOURCES=        \
+       mu-data.hh              \
+       mu-parser.cc            \
+       mu-parser.hh            \
+       mu-proc-iface.hh        \
+       mu-tokenizer.cc         \
+       mu-tokenizer.hh         \
+       mu-tree.hh              \
+       mu-xapian.cc            \
+       mu-xapian.hh
+
+libmu_query_la_LIBADD=         \
+       $(WARN_LDFLAGS)         \
+       $(GLIB_LIBS)            \
+       $(XAPIAN_LIBS)          \
+       ../utils/libmu-utils.la \
+       $(CODE_COVERAGE_LIBS)
+
+VALGRIND_SUPPRESSIONS_FILES=   \
+       ${top_srcdir}/mu.supp
+
+tokenize_SOURCES=              \
+       tokenize.cc
+
+tokenize_LDADD=                        \
+       $(WARN_LDFLAGS)         \
+       libmu-query.la
+
+parse_SOURCES=                 \
+       parse.cc
+
+parse_LDADD=                   \
+       $(WARN_LDFLAGS)         \
+       libmu-query.la
+
+noinst_PROGRAMS+=$(TEST_PROGS)
+
+TEST_PROGS+=                   \
+       test-tokenizer
+test_tokenizer_SOURCES=                \
+       test-tokenizer.cc
+test_tokenizer_LDADD=          \
+       libmu-query.la
+
+TEST_PROGS+=                   \
+       test-parser
+test_parser_SOURCES=           \
+       test-parser.cc
+test_parser_LDADD=             \
+       libmu-query.la
+
+TESTS=$(TEST_PROGS)
+
+include $(top_srcdir)/aminclude_static.am
diff --git a/lib/query/mu-data.hh b/lib/query/mu-data.hh
new file mode 100644 (file)
index 0000000..25d3634
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+**  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 __DATA_HH__
+#define __DATA_HH__
+
+#include <string>
+#include <iostream>
+#include <regex>
+
+#include <utils//mu-utils.hh>
+
+namespace Mu {
+
+// class representing some data item; either a Value or a Range a Value can still be a Regex (but
+// that's not a separate type here)
+struct Data {
+       enum class Type { Value, Range };
+       virtual ~Data() = default;
+
+       Type            type;   /**< type of data */
+       std::string     field;  /**< full name of the field */
+       std::string     prefix; /**< Xapian prefix for thef field */
+       unsigned        id;     /**< Xapian value no for the field  */
+
+protected:
+       Data (Type _type, const std::string& _field, const std::string& _prefix,
+             unsigned _id): type(_type), field(_field), prefix(_prefix), id(_id) {}
+};
+
+
+/**
+ * operator<<
+ *
+ * @param os an output stream
+ * @param t a data type
+ *
+ * @return the updated output stream
+ */
+inline std::ostream&
+operator<< (std::ostream& os, Data::Type t)
+{
+       switch (t) {
+       case Data::Type::Value: os << "value"; break;
+       case Data::Type::Range: os << "range"; break;
+       default: os << "bug"; break;
+       }
+       return os;
+}
+
+
+/**
+ *  Range type -- [a..b]
+ */
+struct Range: public Data {
+       /**
+        * Construct a range
+        *
+        * @param _field the field
+        * @param _prefix the xapian prefix
+        * @param _id xapian value number
+        * @param _lower lower bound
+        * @param _upper upper bound
+        */
+       Range (const std::string& _field, const std::string& _prefix,
+              unsigned _id,
+              const std::string& _lower,const std::string& _upper):
+
+               Data(Data::Type::Range, _field, _prefix, _id),
+               lower(_lower), upper(_upper) {}
+
+       std::string lower;      /**< lower bound */
+       std::string upper;      /**< upper bound */
+};
+
+
+/**
+ * Basic value
+ *
+ */
+struct Value: public Data {
+       /**
+        * Construct a Value
+        *
+        * @param _field the field
+        * @param _prefix the xapian prefix
+        * @param _id xapian value number
+        * @param _value the value
+        */
+       Value (const std::string& _field, const std::string& _prefix,
+              unsigned _id, const std::string& _value, bool _phrase = false):
+               Data(Value::Type::Value, _field, _prefix, _id),
+               value(_value), phrase(_phrase) {}
+
+       std::string     value;  /**< the value */
+       bool            phrase;
+};
+
+
+/**
+ * operator<<
+ *
+ * @param os an output stream
+ * @param v a data ptr
+ *
+ * @return the updated output stream
+ */
+inline std::ostream&
+operator<< (std::ostream& os, const std::unique_ptr<Data>& v)
+{
+       switch (v->type) {
+       case Data::Type::Value: {
+               const auto bval = dynamic_cast<Value*> (v.get());
+               os << ' ' << quote(v->field) << ' '
+                  << quote(utf8_flatten(bval->value));
+               if (bval->phrase)
+                       os << " (ph)";
+
+               break;
+       }
+       case Data::Type::Range: {
+               const auto rval = dynamic_cast<Range*> (v.get());
+               os << ' ' << quote(v->field) << ' '
+                  << quote(rval->lower) << ' '
+                  << quote(rval->upper);
+               break;
+       }
+       default:
+               os << "unexpected type";
+               break;
+       }
+
+       return os;
+}
+
+} // namespace Mu
+
+
+#endif /* __DATA_HH__ */
diff --git a/lib/query/mu-parser.cc b/lib/query/mu-parser.cc
new file mode 100644 (file)
index 0000000..8becb1d
--- /dev/null
@@ -0,0 +1,344 @@
+/*
+**  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 "mu-tokenizer.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-error.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__))
+
+static Token
+look_ahead (const Mu::Tokens& tokens)
+{
+       return tokens.front();
+}
+
+static Mu::Tree
+empty()
+{
+       return {{Node::Type::Empty}};
+}
+
+static Mu::Tree term_1 (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings);
+
+
+static Mu::Tree
+value (const ProcIface::FieldInfoVec& fields, const std::string& v,
+       size_t pos, ProcPtr proc, WarningVec& warnings)
+{
+       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,
+                            std::make_unique<Value>(
+                                    item.field, item.prefix, item.id,
+                                    proc->process_value(item.field, val),
+                                    item.supports_phrase)});
+       }
+
+       // a 'multi-field' such as "recip:"
+       Tree tree(Node{Node::Type::OpOr});
+       for (const auto& item: fields)
+               tree.add_child (Tree({Node::Type::Value,
+                                     std::make_unique<Value>(
+                                             item.field, item.prefix, item.id,
+                                             proc->process_value(item.field, val),
+                                             item.supports_phrase)}));
+       return tree;
+}
+
+static Mu::Tree
+regex (const ProcIface::FieldInfoVec& fields, const std::string& v,
+       size_t pos, ProcPtr proc, WarningVec& warnings)
+{
+       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 = proc->process_regex (field.field, rx);
+                       for (const auto& term: terms) {
+                               tree.add_child (Tree(
+                                       {Node::Type::Value,
+                                        std::make_unique<Value>(field.field, "",
+                                                                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, proc, warnings);
+       }
+}
+
+
+
+static Mu::Tree
+range (const ProcIface::FieldInfoVec& fields, const std::string& lower,
+       const std::string& upper, size_t pos, ProcPtr proc,
+       WarningVec& warnings)
+{
+       if (fields.empty())
+               throw BUG("expected field");
+
+       const auto& field = fields.front();
+       if (!proc->is_range_field(field.field))
+               return value (fields, lower + ".." + upper, pos, proc, warnings);
+
+       auto prange = proc->process_range (field.field, lower, upper);
+       if (prange.lower > prange.upper)
+               prange = proc->process_range (field.field, upper, lower);
+
+       return Tree({Node::Type::Range,
+                            std::make_unique<Range>(field.field, field.prefix, field.id,
+                                                    prange.lower, prange.upper)});
+}
+
+
+static Mu::Tree
+data (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings)
+{
+       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 = proc->process_field (field);
+       if (fields.empty()) {// not valid field...
+               warnings.push_back ({token.pos, format ("invalid field '%s'", field.c_str())});
+               fields = proc->process_field ("");
+               // fallback, treat the whole of foo:bar as a value
+               return value (fields, field + ":" + val, token.pos, proc, warnings);
+       }
+
+       // does it look like a regexp?
+       if (val.length() >=2 )
+               if (val[0] == '/' && val[val.length()-1] == '/')
+                       return regex (fields, val, token.pos, proc, 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, proc, warnings);
+       else if (proc->is_range_field(fields.front().field)) {
+               // range field without a range - treat as field:val..val
+               return range (fields, val, val, token.pos, proc, warnings);
+       }
+
+       // if nothing else, it's a value.
+       return value (fields, val, token.pos, proc, warnings);
+}
+
+static Mu::Tree
+unit (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings)
+{
+       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, proc, warnings));
+               return tree;
+       }
+
+       if (token.type == Token::Type::Open) {
+               tokens.pop_front();
+               auto tree = term_1 (tokens, proc, 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, proc, warnings);
+}
+
+static Mu::Tree factor_1 (Mu::Tokens& tokens, ProcPtr proc,
+                          WarningVec& warnings);
+
+static Mu::Tree
+factor_2 (Mu::Tokens& tokens, Node::Type& op, ProcPtr proc,
+         WarningVec& warnings)
+{
+       if (tokens.empty())
+               return empty();
+
+       const auto token = look_ahead(tokens);
+
+       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();
+       }
+
+       return factor_1 (tokens, proc, warnings);
+}
+
+static Mu::Tree
+factor_1 (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings)
+{
+       Node::Type op { Node::Type::Invalid };
+
+       auto t  = unit (tokens, proc, warnings);
+       auto a2 = factor_2 (tokens, op, proc, warnings);
+
+       if (a2.empty())
+               return t;
+
+       Tree tree {{op}};
+       tree.add_child(std::move(t));
+       tree.add_child(std::move(a2));
+
+       return tree;
+}
+
+
+static Mu::Tree
+term_2 (Mu::Tokens& tokens, Node::Type& op, ProcPtr proc,
+       WarningVec& warnings)
+{
+       if (tokens.empty())
+               return empty();
+
+       const auto token = look_ahead (tokens);
+
+       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();
+       }
+
+       tokens.pop_front();
+
+       return term_1 (tokens, proc, warnings);
+}
+
+static Mu::Tree
+term_1 (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings)
+{
+       Node::Type op { Node::Type::Invalid };
+
+       auto t  = factor_1 (tokens, proc, warnings);
+       auto o2 = term_2 (tokens, op, proc, warnings);
+
+       if (o2.empty())
+               return t;
+       else {
+               Tree tree {{op}};
+               tree.add_child(std::move(t));
+               tree.add_child(std::move(o2));
+               return tree;
+       }
+}
+
+static Mu::Tree
+query (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings)
+{
+       if (tokens.empty())
+               return empty ();
+       else
+               return term_1 (tokens, proc, warnings);
+}
+
+Mu::Tree
+Mu::parse (const std::string& expr, WarningVec& warnings, ProcPtr proc)
+{
+       try {
+               auto tokens = tokenize (expr);
+               return query (tokens, proc, warnings);
+
+       } catch (const std::runtime_error& ex) {
+               std::cerr << ex.what() << std::endl;
+               return empty();
+       }
+}
diff --git a/lib/query/mu-parser.hh b/lib/query/mu-parser.hh
new file mode 100644 (file)
index 0000000..0c2ffe9
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+**  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 <string>
+#include <vector>
+#include <memory>
+
+#include <query/mu-data.hh>
+#include <query/mu-tree.hh>
+#include <query/mu-proc-iface.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;
+       }
+};
+
+
+/**
+ * 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;
+}
+
+/**
+ * Parse a query string
+ *
+ * @param query a query string
+ * @param warnings vec to receive warnings
+ * @param proc a Processor object
+ *
+ * @return a parse-tree
+ */
+using WarningVec=std::vector<Warning>;
+using ProcPtr = const std::unique_ptr<ProcIface>&;
+Tree parse (const std::string& query, WarningVec& warnings,
+           ProcPtr proc = std::make_unique<DummyProc>());
+
+} // namespace Mu
+
+#endif /* __PARSER_HH__ */
diff --git a/lib/query/mu-proc-iface.hh b/lib/query/mu-proc-iface.hh
new file mode 100644 (file)
index 0000000..b43ef41
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+** 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 __PROC_IFACE_HH__
+#define __PROC_IFACE_HH__
+
+#include <string>
+#include <vector>
+#include <tuple>
+#include <regex>
+
+namespace Mu {
+
+struct ProcIface {
+
+       virtual ~ProcIface() = default;
+
+       /**
+        * 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;
+               unsigned                id;
+       };
+       using FieldInfoVec = std::vector<FieldInfo>;
+
+       virtual FieldInfoVec process_field (const std::string& field) const = 0;
+
+       /**
+        * Process a value
+        *
+        * @param field a field name
+        * @param value a value
+        *
+        * @return the processed value
+        */
+       virtual std::string process_value (
+               const std::string& field, const std::string& value) const = 0;
+
+       /**
+        * Is this a range field?
+        *
+        * @param field some field
+        *
+        * @return true if it is a range-field; false otherwise.
+        */
+       virtual bool is_range_field (const std::string& field) const = 0;
+
+
+       /**
+        * Process a range field
+        *
+        * @param fieldstr a fieldstr, e.g "date" or "d" for the date field
+        * @param lower lower bound or empty
+        * @param upper upper bound or empty
+        *
+        * @return the processed range
+        */
+       struct Range {
+               std::string lower;
+               std::string upper;
+       };
+       virtual Range process_range (const std::string& field, const std::string& lower,
+                                    const std::string& upper) const = 0;
+
+       /**
+        *
+        *
+        * @param field
+        * @param rx
+        *
+        * @return
+        */
+       virtual std::vector<std::string>
+       process_regex (const std::string& field, const std::regex& rx) const = 0;
+
+}; // ProcIface
+
+
+struct DummyProc: public ProcIface { // For testing
+
+       std::vector<FieldInfo>
+       process_field (const std::string& field) const override {
+               return {{ field, "x", false, 0 }};
+       }
+
+       std::string
+       process_value (const std::string& field, const std::string& value) const override {
+               return value;
+       }
+
+       bool is_range_field (const std::string& field) const override {
+               return field == "range";
+       }
+
+       Range process_range (const std::string& field, const std::string& lower,
+                            const std::string& upper) const override {
+               return { lower, upper };
+       }
+
+       std::vector<std::string>
+       process_regex (const std::string& field, const std::regex& rx) const override {
+               return {};
+       }
+}; //Dummy
+
+
+} // Mu
+
+#endif /* __PROC_IFACE_HH__ */
diff --git a/lib/query/mu-tokenizer.cc b/lib/query/mu-tokenizer.cc
new file mode 100644 (file)
index 0000000..f314d04
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+**  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/query/mu-tokenizer.hh b/lib/query/mu-tokenizer.hh
new file mode 100644 (file)
index 0000000..ac083c8
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+**  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;
+
+       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/query/mu-tree.hh b/lib/query/mu-tree.hh
new file mode 100644 (file)
index 0000000..eacda98
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+**  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 TREE_HH__
+#define TREE_HH__
+
+#include <vector>
+#include <string>
+#include <iostream>
+
+#include <query/mu-data.hh>
+#include <utils/mu-error.hh>
+
+namespace Mu {
+
+// 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, std::unique_ptr<Data>&& _data):
+               type{_type}, data{std::move(_data)} {}
+       Node(Type _type): type{_type} {}
+       Node(Node&& rhs) = default;
+
+       Type                    type;
+       std::unique_ptr<Data>   data;
+
+       static const char* type_name (Type t) {
+               switch (t) {
+               case Type::Empty:    return ""; break;
+               case Type::OpAnd:    return "and"; break;
+               case Type::OpOr:     return "or"; break;
+               case Type::OpXor:    return "xor"; break;
+               case Type::OpAndNot: return "andnot"; break;
+               case Type::OpNot:    return "not"; break;
+               case Type::Value:    return "value"; break;
+               case Type::Range:    return "range"; break;
+               case Type::Invalid:  return "<invalid>"; break;
+               default:
+                       throw Mu::Error(Error::Code::Internal, "unexpected type");
+               }
+       }
+
+       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.data)
+               os << t.data;
+
+       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/query/mu-xapian.cc b/lib/query/mu-xapian.cc
new file mode 100644 (file)
index 0000000..5112594
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+** 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.
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_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)
+{
+       Xapian::Query::op op;
+
+       switch (tree.node.type) {
+       case 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()));
+       case Node::Type::OpAnd: op    = Xapian::Query::OP_AND; break;
+       case Node::Type::OpOr:  op    = Xapian::Query::OP_OR; break;
+       case Node::Type::OpXor: op    = Xapian::Query::OP_XOR; break;
+       case Node::Type::OpAndNot: op = Xapian::Query::OP_AND_NOT; break;
+       default: throw Mu::Error (Error::Code::Internal, "invalid op"); // bug
+       }
+
+       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 Value* val, const std::string& str, bool maybe_wildcard)
+{
+       const auto vlen{str.length()};
+       if (!maybe_wildcard || vlen <= 1 || str[vlen - 1] != '*')
+               return Xapian::Query(val->prefix + str);
+       else
+               return Xapian::Query(Xapian::Query::OP_WILDCARD,
+                                    val->prefix + str.substr(0, vlen - 1));
+}
+
+static Xapian::Query
+xapian_query_value (const Mu::Tree& tree)
+{
+       const auto v = dynamic_cast<Value*> (tree.node.data.get());
+       if (!v->phrase)
+               return make_query(v, v->value, true/*maybe-wildcard*/);
+
+       const auto parts = split (v->value, " ");
+       if (parts.empty())
+               return Xapian::Query::MatchNothing; // shouldn't happen
+
+       if (parts.size() == 1)
+               return make_query(v, parts.front(), true/*maybe-wildcard*/);
+
+       std::vector<Xapian::Query> phvec;
+       for (const auto p: parts)
+               phvec.emplace_back(make_query(v, p, 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 r { dynamic_cast<Range *>(tree.node.data.get()) };
+
+       return Xapian::Query(Xapian::Query::OP_VALUE_RANGE, (Xapian::valueno)r->id,
+                            r->lower, r->upper);
+}
+
+Xapian::Query
+Mu::xapian_query (const Mu::Tree& tree)
+{
+       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
+       }
+}
diff --git a/lib/query/mu-xapian.hh b/lib/query/mu-xapian.hh
new file mode 100644 (file)
index 0000000..503d9eb
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+** 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.
+*/
+
+
+#ifndef __XAPIAN_HH__
+#define __XAPIAN_HH__
+
+#include <xapian.h>
+#include <query/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 /* __XAPIAN_H__ */
diff --git a/lib/query/parse.cc b/lib/query/parse.cc
new file mode 100644 (file)
index 0000000..d988d6e
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+**  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 <string>
+#include <iostream>
+#include "mu-parser.hh"
+
+int
+main (int argc, char *argv[])
+{
+       std::string s;
+
+       for (auto i = 1; i < argc; ++i)
+               s += " " + std::string(argv[i]);
+
+       Mu::WarningVec warnings;
+
+       const auto tree = Mu::parse (s, warnings);
+       for (const auto& w: warnings)
+               std::cerr << "1:" << w.pos << ": " << w.msg << std::endl;
+
+       std::cout << tree << std::endl;
+
+       return 0;
+}
diff --git a/lib/query/test-parser.cc b/lib/query/test-parser.cc
new file mode 100644 (file)
index 0000000..2eadda9
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+** 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-parser.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)
+{
+       for (const auto& casus : cases ) {
+
+               WarningVec warnings;
+               const auto tree = 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;
+               }
+               g_assert_true (casus.expected == ss.str());
+
+               // g_assert_cmpuint (casus.warnings.size(), ==, warnings.size());
+               // for (auto i = 0; i != (int)casus.warnings.size(); ++i) {
+               //      std::cout << "exp:" << casus.warnings[i] << std::endl;
+               //      std::cout << "got:" << warnings[i] << std::endl;
+               //      g_assert_true (casus.warnings[i] == warnings[i]);
+               // }
+       }
+}
+
+static void
+test_basic ()
+{
+       CaseVec cases = {
+               //{ "", R"#((atom :value ""))#"},
+               { "foo",  R"#((value "" "foo"))#", },
+               { "foo       or         bar",
+                 R"#((or(value "" "foo")(value "" "bar")))#" },
+               { "foo and bar",
+                 R"#((and(value "" "foo")(value "" "bar")))#"},
+       };
+
+       test_cases (cases);
+}
+
+static void
+test_complex ()
+{
+       CaseVec cases = {
+               { "foo and bar or cuux",
+                 R"#((or(and(value "" "foo")(value "" "bar")))#" +
+                 std::string(R"#((value "" "cuux")))#") },
+
+               { "a and not b",
+                 R"#((and(value "" "a")(not(value "" "b"))))#"
+               },
+               { "a and b and c",
+                 R"#((and(value "" "a")(and(value "" "b")(value "" "c"))))#"
+               },
+               { "(a or b) and c",
+                 R"#((and(or(value "" "a")(value "" "b"))(value "" "c")))#"
+               },
+               { "a b", // implicit and
+                 R"#((and(value "" "a")(value "" "b")))#"
+               },
+               { "a not b", // implicit and not
+                 R"#((and(value "" "a")(not(value "" "b"))))#"
+               },
+               { "not b", // implicit and not
+                 R"#((not(value "" "b")))#"
+               }
+       };
+
+       test_cases (cases);
+}
+
+
+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 "" "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/query/test-tokenizer.cc b/lib/query/test-tokenizer.cc
new file mode 100644 (file)
index 0000000..62fa7ef
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+** 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 (const 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/query/tokenize.cc b/lib/query/tokenize.cc
new file mode 100644 (file)
index 0000000..81f6ef0
--- /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/test-mu-common.c b/lib/test-mu-common.c
new file mode 100644 (file)
index 0000000..5cabb76
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+** 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 <string.h>
+
+#include <langinfo.h>
+#include <locale.h>
+
+#include "test-mu-common.h"
+
+char*
+test_mu_common_get_random_tmpdir (void)
+{
+       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*
+set_tz (const char* tz)
+{
+       static const char* oldtz;
+
+       oldtz = getenv ("TZ");
+       if (tz)
+               setenv ("TZ", tz, 1);
+       else
+               unsetenv ("TZ");
+
+       tzset ();
+       return oldtz;
+}
+
+
+gboolean
+set_en_us_utf8_locale (void)
+{
+       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;
+}
+
+
+void
+black_hole (void)
+{
+       return; /* do nothing */
+}
diff --git a/lib/test-mu-common.h b/lib/test-mu-common.h
new file mode 100644 (file)
index 0000000..31f4d3a
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __TEST_MU_COMMON_H__
+#define __TEST_MU_COMMON_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+/**
+ * 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 (void);
+
+
+
+/**
+ * set the output to /dev/null
+ *
+ */
+void black_hole (void);
+
+/**
+ * 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
+ */
+gboolean  set_en_us_utf8_locale (void);
+
+G_END_DECLS
+
+#endif /*__TEST_MU_COMMON_H__*/
diff --git a/lib/test-mu-contacts.cc b/lib/test-mu-contacts.cc
new file mode 100644 (file)
index 0000000..141f8b9
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+** Copyright (C) 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.
+**
+*/
+
+
+#include "config.h"
+
+#include <glib.h>
+#include "test-mu-common.h"
+#include "mu-contacts.hh"
+
+static void
+test_mu_contacts_01()
+{
+        Mu::Contacts contacts ("");
+
+        g_assert_true (contacts.empty());
+        g_assert_cmpuint (contacts.size(), ==, 0);
+
+        contacts.add(std::move(Mu::ContactInfo ("Foo <foo.bar@example.com>",
+                                                "foo.bar@example.com", "Foo",
+                                                false, 12345)));
+        g_assert_false (contacts.empty());
+        g_assert_cmpuint (contacts.size(), ==, 1);
+
+        contacts.add(std::move(Mu::ContactInfo ("Cuux <cuux.fnorb@example.com>",
+                                                "cuux@example.com", "Cuux", true,
+                                                54321)));
+
+        g_assert_cmpuint (contacts.size(), ==, 2);
+
+        contacts.add(std::move(Mu::ContactInfo ("foo.bar@example.com",
+                                                "foo.bar@example.com", "Foo",
+                                                false, 77777)));
+        g_assert_cmpuint (contacts.size(), ==, 2);
+
+        contacts.add(std::move(Mu::ContactInfo ("Foo.Bar@Example.Com",
+                                                "Foo.Bar@Example.Com", "Foo",
+                                                false, 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);
+}
+
+
+
+int
+main (int argc, char *argv[])
+{
+        g_test_init (&argc, &argv, NULL);
+
+        g_test_add_func ("/mu-contacts/01", test_mu_contacts_01);
+
+        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/test-mu-container.c b/lib/test-mu-container.c
new file mode 100644 (file)
index 0000000..475e302
--- /dev/null
@@ -0,0 +1,83 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** 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.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+
+#include "test-mu-common.h"
+#include "mu-container.h"
+
+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,
+                          G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       return g_test_run ();
+}
diff --git a/lib/test-mu-date.c b/lib/test-mu-date.c
new file mode 100644 (file)
index 0000000..5cb6d5f
--- /dev/null
@@ -0,0 +1,93 @@
+/* -*-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 "test-mu-common.h"
+#include "mu-date.h"
+
+
+
+
+
+static void
+test_mu_date_interpret_begin (void)
+{
+       time_t now;
+       now = time (NULL);
+
+       g_assert_cmpstr (mu_date_interpret_s ("now", TRUE) , ==,
+                        mu_date_str_s("%Y%m%d%H%M%S", now));
+
+       g_assert_cmpstr (mu_date_interpret_s ("today", TRUE) , ==,
+                        mu_date_str_s("%Y%m%d000000", now));
+}
+
+static void
+test_mu_date_interpret_end (void)
+{
+       time_t now;
+       now = time (NULL);
+
+       g_assert_cmpstr (mu_date_interpret_s ("now", FALSE) , ==,
+                        mu_date_str_s("%Y%m%d%H%M%S", now));
+
+       g_assert_cmpstr (mu_date_interpret_s ("today", FALSE) , ==,
+                        mu_date_str_s("%Y%m%d235959", now));
+}
+
+
+
+
+
+int
+main (int argc, char *argv[])
+{
+       g_test_init (&argc, &argv, NULL);
+
+       g_test_add_func ("/mu-str/mu_date_parse_hdwmy",
+                        test_mu_date_parse_hdwmy);
+       g_test_add_func ("/mu-str/mu_date_complete_begin",
+                        test_mu_date_complete_begin);
+       g_test_add_func ("/mu-str/mu_date_complete_end",
+                        test_mu_date_complete_end);
+
+       g_test_add_func ("/mu-str/mu_date_interpret_begin",
+                        test_mu_date_interpret_begin);
+       g_test_add_func ("/mu-str/mu_date_interpret_end",
+                        test_mu_date_interpret_end);
+
+
+       g_log_set_handler (NULL,
+                          G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       return g_test_run ();
+}
diff --git a/lib/test-mu-flags.c b/lib/test-mu-flags.c
new file mode 100644 (file)
index 0000000..36db9c8
--- /dev/null
@@ -0,0 +1,193 @@
+/* -*-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 "mu-flags.h"
+#include "test-mu-common.h"
+
+
+static void
+test_mu_flag_char (void)
+{
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_DRAFT),         ==, 'D');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_FLAGGED),       ==, 'F');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_PASSED),        ==, 'P');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_REPLIED),       ==, 'R');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_SEEN),          ==, 'S');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_TRASHED),       ==, 'T');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_NEW),           ==, 'N');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_SIGNED),        ==, 'z');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_ENCRYPTED),     ==, 'x');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_HAS_ATTACH),    ==, 'a');
+       g_assert_cmpuint (mu_flag_char (MU_FLAG_UNREAD),        ==, 'u');
+       g_assert_cmpuint (mu_flag_char (12345),                 ==,  0);
+}
+
+
+
+static void
+test_mu_flag_name (void)
+{
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_DRAFT),          ==, "draft");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_FLAGGED),        ==, "flagged");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_PASSED),         ==, "passed");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_REPLIED),        ==, "replied");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_SEEN),           ==, "seen");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_TRASHED),        ==, "trashed");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_NEW),            ==, "new");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_SIGNED),         ==, "signed");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_ENCRYPTED),      ==, "encrypted");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_HAS_ATTACH),     ==, "attach");
+       g_assert_cmpstr (mu_flag_name (MU_FLAG_UNREAD),         ==, "unread");
+       g_assert_cmpstr (mu_flag_name (12345),                  ==,  NULL);
+}
+
+static void
+test_mu_flags_to_str_s (void)
+{
+       g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_PASSED|MU_FLAG_SIGNED,
+                                          MU_FLAG_TYPE_ANY),
+                        ==, "Pz");
+       g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_NEW, MU_FLAG_TYPE_ANY),
+                        ==, "N");
+       g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_HAS_ATTACH|MU_FLAG_TRASHED,
+                                          MU_FLAG_TYPE_ANY),
+                        ==, "Ta");
+       g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_NONE, MU_FLAG_TYPE_ANY),
+                        ==, "");
+
+       g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_PASSED|MU_FLAG_SIGNED,
+                                          MU_FLAG_TYPE_CONTENT),
+                        ==, "z");
+
+       g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_NEW, MU_FLAG_TYPE_MAILDIR),
+                        ==, "N");
+       g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_HAS_ATTACH|MU_FLAG_TRASHED,
+                                          MU_FLAG_TYPE_MAILFILE),
+                        ==, "T");
+
+       g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_NONE, MU_FLAG_TYPE_PSEUDO),
+                        ==, "");
+}
+
+
+static void
+test_mu_flags_from_str (void)
+{
+       /* note, the 3rd arg to mu_flags_from_str determines whether
+        * invalid flags will be ignored (if TRUE) or MU_FLAG_INVALID (if FALSE)
+        */
+
+       g_assert_cmpuint (mu_flags_from_str ("RP", MU_FLAG_TYPE_ANY, TRUE), ==,
+                         MU_FLAG_REPLIED | MU_FLAG_PASSED);
+       g_assert_cmpuint (mu_flags_from_str ("Nz", MU_FLAG_TYPE_ANY, TRUE), ==,
+                         MU_FLAG_NEW | MU_FLAG_SIGNED);
+       g_assert_cmpuint (mu_flags_from_str ("axD", MU_FLAG_TYPE_ANY, TRUE), ==,
+                         MU_FLAG_HAS_ATTACH | MU_FLAG_ENCRYPTED | MU_FLAG_DRAFT);
+
+       g_assert_cmpuint (mu_flags_from_str ("RP", MU_FLAG_TYPE_MAILFILE, TRUE), ==,
+                         MU_FLAG_REPLIED | MU_FLAG_PASSED);
+       g_assert_cmpuint (mu_flags_from_str ("Nz", MU_FLAG_TYPE_MAILFILE, TRUE), ==,
+                         MU_FLAG_NONE);
+
+       /* ignore errors or not */
+       g_assert_cmpuint (mu_flags_from_str ("qwi", MU_FLAG_TYPE_MAILFILE, FALSE), ==,
+                         MU_FLAG_INVALID);
+       g_assert_cmpuint (mu_flags_from_str ("qwi", MU_FLAG_TYPE_MAILFILE, TRUE), ==,
+                         0);
+
+
+}
+
+static void
+test_mu_flags_from_str_delta (void)
+{
+       g_assert_cmpuint (mu_flags_from_str_delta ("+S-R",
+                                                  MU_FLAG_REPLIED | MU_FLAG_DRAFT,
+                                                  MU_FLAG_TYPE_ANY),==,
+                         MU_FLAG_SEEN | MU_FLAG_DRAFT);
+
+       g_assert_cmpuint (mu_flags_from_str_delta ("",
+                                                  MU_FLAG_REPLIED | MU_FLAG_DRAFT,
+                                                  MU_FLAG_TYPE_ANY),==,
+                         MU_FLAG_REPLIED | MU_FLAG_DRAFT);
+
+       g_assert_cmpuint (mu_flags_from_str_delta ("-N+P+S-D",
+                                                  MU_FLAG_SIGNED | MU_FLAG_DRAFT,
+                                                  MU_FLAG_TYPE_ANY),==,
+                         MU_FLAG_PASSED | MU_FLAG_SEEN | MU_FLAG_SIGNED);
+}
+
+
+static void
+test_mu_flags_custom_from_str (void)
+{
+       unsigned u;
+
+       struct {
+               const char *str;
+               const char *expected;
+       } cases[] = {
+               { "ABC", "ABC" },
+               { "PAF", "A" },
+               { "ShelloPwoFrDldR123", "helloworld123" },
+               { "SPD", NULL }
+       };
+
+       for (u = 0; u != G_N_ELEMENTS(cases); ++u) {
+               char *cust;
+               cust = mu_flags_custom_from_str (cases[u].str);
+               if (g_test_verbose())
+                       g_print ("%s: str:%s; got:%s; expected:%s\n",
+                                __func__, cases[u].str, cust, cases[u].expected);
+               g_assert_cmpstr (cust, ==, cases[u].expected);
+               g_free (cust);
+       }
+}
+
+
+
+int
+main (int argc, char *argv[])
+{
+       int rv;
+       g_test_init (&argc, &argv, NULL);
+
+       /* mu_msg_str_date */
+       g_test_add_func ("/mu-flags/test-mu-flag-char", test_mu_flag_char);
+       g_test_add_func ("/mu-flags/test-mu-flag-name",test_mu_flag_name);
+       g_test_add_func ("/mu-flags/test-mu-flags-to-str-s",test_mu_flags_to_str_s);
+       g_test_add_func ("/mu-flags/test-mu-flags-from-str",test_mu_flags_from_str);
+       g_test_add_func ("/mu-flags/test-mu-flags-from-str-delta",test_mu_flags_from_str_delta );
+       g_test_add_func ("/mu-flags/test-mu-flags-custom-from-str",
+                        test_mu_flags_custom_from_str);
+
+       g_log_set_handler (NULL,
+                          G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       rv = g_test_run ();
+
+       return rv;
+}
diff --git a/lib/test-mu-maildir.c b/lib/test-mu-maildir.c
new file mode 100644 (file)
index 0000000..3952127
--- /dev/null
@@ -0,0 +1,693 @@
+/*
+** Copyright (C) 2008-2016 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.
+**
+*/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "test-mu-common.h"
+#include "mu-maildir.h"
+#include "utils/mu-util.h"
+
+static void
+test_mu_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_cmpuint (mu_maildir_mkdir (mdir, 0755, FALSE, NULL),
+                         ==, 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_mu_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_cmpuint (mu_maildir_mkdir (mdir, 0755, TRUE, NULL),
+                         ==, 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_mu_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_cmpuint (mu_maildir_mkdir (mdir, 0755, FALSE, NULL),
+                         ==, 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_mu_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  */
+       g_assert_cmpuint (mu_maildir_mkdir (mdir, 0755, FALSE, NULL),
+                         ==, (geteuid()==0 ? TRUE : 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_mu_maildir_mkdir_05 (void)
+{
+       /* this must fail */
+       g_test_log_set_fatal_handler ((GTestLogFatalFunc)ignore_error, NULL);
+
+       g_assert_cmpuint (mu_maildir_mkdir (NULL, 0755, TRUE, NULL),
+                                           ==, FALSE);
+}
+
+
+static gchar*
+copy_test_data (void)
+{
+       gchar *dir, *cmd;
+
+       dir = test_mu_common_get_random_tmpdir();
+       cmd = g_strdup_printf ("mkdir -m 0700 %s", dir);
+       if (g_test_verbose())
+               g_print ("cmd: %s\n", cmd);
+       g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL));
+       g_free (cmd);
+
+       cmd = g_strdup_printf ("cp -R %s %s", MU_TESTMAILDIR, dir);
+       if (g_test_verbose())
+               g_print ("cmd: %s\n", cmd);
+       g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL));
+       g_free (cmd);
+
+       return dir;
+}
+
+
+typedef struct {
+       int _file_count;
+       int _dir_entered;
+       int _dir_left;
+} WalkData;
+
+static MuError
+dir_cb (const char *fullpath, gboolean enter, WalkData *data)
+{
+       if (enter)
+               ++data->_dir_entered;
+       else
+               ++data->_dir_left;
+
+       if (g_test_verbose())
+               g_print ("%s: %s: %s (%u)\n", __func__, enter ? "entering" : "leaving",
+                        fullpath, enter ? data->_dir_entered : data->_dir_left);
+
+       return MU_OK;
+}
+
+
+static MuError
+msg_cb (const char *fullpath, const char* mdir, struct stat *statinfo,
+       WalkData *data)
+{
+       ++data->_file_count;
+       return MU_OK;
+}
+
+
+static void
+test_mu_maildir_walk_01 (void)
+{
+       char *tmpdir;
+       WalkData data;
+       MuError rv;
+
+       tmpdir = copy_test_data ();
+       memset (&data, 0, sizeof(WalkData));
+
+       rv = mu_maildir_walk (tmpdir,
+                             (MuMaildirWalkMsgCallback)msg_cb,
+                             (MuMaildirWalkDirCallback)dir_cb,
+                             TRUE,
+                             &data);
+
+       g_assert_cmpuint (MU_OK, ==, rv);
+       g_assert_cmpuint (data._file_count, ==, 19);
+
+       g_assert_cmpuint (data._dir_entered,==, 5);
+       g_assert_cmpuint (data._dir_left,==, 5);
+
+       g_free (tmpdir);
+}
+
+
+static void
+test_mu_maildir_walk (void)
+{
+       char *tmpdir, *cmd, *dir;
+       WalkData data;
+       MuError rv;
+
+       tmpdir = copy_test_data ();
+       memset (&data, 0, sizeof(WalkData));
+
+       /* mark the 'new' dir with '.noindex', to ignore it */
+       dir =  g_strdup_printf ("%s%ctestdir%cnew", tmpdir,
+                               G_DIR_SEPARATOR, G_DIR_SEPARATOR);
+       cmd = g_strdup_printf ("chmod 700 %s", dir);
+       g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL));
+       g_free (cmd);
+
+       cmd = g_strdup_printf ("touch %s%c.noindex", dir, G_DIR_SEPARATOR);
+       g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL));
+       g_free (cmd);
+       g_free (dir);
+
+       rv = mu_maildir_walk (tmpdir,
+                             (MuMaildirWalkMsgCallback)msg_cb,
+                             (MuMaildirWalkDirCallback)dir_cb,
+                             TRUE,
+                             &data);
+
+       g_assert_cmpuint (MU_OK, ==, rv);
+       g_assert_cmpuint (data._file_count, ==, 15);
+
+       g_assert_cmpuint (data._dir_entered,==, 4);
+       g_assert_cmpuint (data._dir_left,==, 4);
+
+       g_free (tmpdir);
+}
+
+static void
+test_mu_maildir_walk_with_noupdate (void)
+{
+       char *tmpdir, *cmd, *dir;
+       WalkData data;
+       MuError rv;
+
+       tmpdir = copy_test_data ();
+
+       /* mark the 'new' dir with '.noindex', to ignore it */
+       dir =  g_strdup_printf ("%s%ctestdir%cnew", tmpdir,
+                               G_DIR_SEPARATOR, G_DIR_SEPARATOR);
+       cmd = g_strdup_printf ("chmod 700 %s", dir);
+       g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL));
+       g_free (cmd);
+
+       memset (&data, 0, sizeof(WalkData));
+       rv = mu_maildir_walk (tmpdir,
+                             (MuMaildirWalkMsgCallback)msg_cb,
+                             (MuMaildirWalkDirCallback)dir_cb,
+                             FALSE, /* ie., non-full update */
+                             &data);
+
+       g_assert_cmpuint (MU_OK, ==, rv);
+       g_assert_cmpuint (data._file_count, ==, 19);
+       g_assert_cmpuint (data._dir_entered,==, 5);
+       g_assert_cmpuint (data._dir_left,==, 5);
+
+       /* again, full update. results should be the same, since there
+        * is no noupdate yet */
+       memset (&data, 0, sizeof(WalkData));
+       rv = mu_maildir_walk (tmpdir,
+                             (MuMaildirWalkMsgCallback)msg_cb,
+                             (MuMaildirWalkDirCallback)dir_cb,
+                             TRUE, /* ie., full update */
+                             &data);
+
+       g_assert_cmpuint (MU_OK, ==, rv);
+       g_assert_cmpuint (data._file_count, ==, 19);
+       g_assert_cmpuint (data._dir_entered,==, 5);
+       g_assert_cmpuint (data._dir_left,==, 5);
+
+       /* add a '.noupdate' file; this affects the outcome when the
+        * 4th arg to mu_maildir_walk is FALSE */
+       cmd = g_strdup_printf ("touch %s%c.noupdate", dir, G_DIR_SEPARATOR);
+       g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL));
+       g_free (cmd);
+
+       memset (&data, 0, sizeof(WalkData));
+       rv = mu_maildir_walk (tmpdir,
+                             (MuMaildirWalkMsgCallback)msg_cb,
+                             (MuMaildirWalkDirCallback)dir_cb,
+                             FALSE, /* non-full update */
+                             &data);
+
+       g_assert_cmpuint (MU_OK, ==, rv);
+       g_assert_cmpuint (data._file_count, ==, 15);
+
+       g_assert_cmpuint (data._dir_entered,==, 4);
+       g_assert_cmpuint (data._dir_left,==, 4);
+
+       /* now run again, but do a full update */
+       memset (&data, 0, sizeof(WalkData));
+       rv = mu_maildir_walk (tmpdir,
+                             (MuMaildirWalkMsgCallback)msg_cb,
+                             (MuMaildirWalkDirCallback)dir_cb,
+                             TRUE, /* full update */
+                             &data);
+
+       g_assert_cmpuint (MU_OK, ==, rv);
+       g_assert_cmpuint (data._file_count, ==, 19);
+
+       g_assert_cmpuint (data._dir_entered,==, 5);
+       g_assert_cmpuint (data._dir_left,==, 5);
+
+       g_free (dir);
+       g_free (tmpdir);
+}
+
+
+
+
+
+
+static void
+test_mu_maildir_get_flags_from_path (void)
+{
+       int i;
+       struct {
+               const char *path;
+               MuFlags flags;
+       } paths[] = {
+               {
+                       "/home/foo/Maildir/test/cur/123456:2,FSR",
+                       MU_FLAG_REPLIED | MU_FLAG_SEEN | MU_FLAG_FLAGGED
+               },
+               {
+                       "/home/foo/Maildir/test/new/123456",
+                       MU_FLAG_NEW
+               },
+               {
+                       /* NOTE: when in new/, the :2,.. stuff is ignored */
+                       "/home/foo/Maildir/test/new/123456:2,FR",
+                       MU_FLAG_NEW
+               },
+               {
+                       "/home/foo/Maildir/test/cur/123456:2,DTP",
+                       MU_FLAG_DRAFT | MU_FLAG_TRASHED |
+                       MU_FLAG_PASSED
+               },
+               {
+                       "/home/foo/Maildir/test/cur/123456:2,S",
+                       MU_FLAG_SEEN
+               }
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(paths); ++i) {
+               MuFlags flags;
+               flags = mu_maildir_get_flags_from_path(paths[i].path);
+               g_assert_cmpuint(flags, ==, paths[i].flags);
+       }
+}
+
+
+
+static void
+assert_matches_regexp (const char *str, const char *rx)
+{
+       if (!g_regex_match_simple (rx, str, 0, 0)) {
+               if (g_test_verbose ())
+                       g_print ("%s does not match %s", str, rx);
+               g_assert (0);
+       }
+}
+
+
+
+static void
+test_mu_maildir_get_new_path_new (void)
+{
+       int i;
+
+       struct {
+               const char *oldpath;
+               MuFlags flags;
+               const char *newpath;
+       } paths[] = {
+               {
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       MU_FLAG_REPLIED,
+                       "/home/foo/Maildir/test/cur/123456:2,R"
+               }, {
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       MU_FLAG_NEW,
+                       "/home/foo/Maildir/test/new/123456"
+               }, {
+                       "/home/foo/Maildir/test/new/123456:2,FR",
+                       MU_FLAG_SEEN | MU_FLAG_REPLIED,
+                       "/home/foo/Maildir/test/cur/123456:2,RS"
+               }, {
+                       "/home/foo/Maildir/test/new/1313038887_0.697:2,",
+                       MU_FLAG_SEEN | MU_FLAG_FLAGGED | MU_FLAG_PASSED,
+                       "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"
+               }, {
+                       "/home/djcb/Maildir/trash/new/1312920597.2206_16.cthulhu",
+                       MU_FLAG_SEEN,
+                       "/home/djcb/Maildir/trash/cur/1312920597.2206_16.cthulhu:2,S"
+               }
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(paths); ++i) {
+               char    *str, *newbase;
+               str     = mu_maildir_get_new_path (paths[i].oldpath, NULL,
+                                                  paths[i].flags, TRUE);
+               newbase = g_path_get_basename (str);
+               assert_matches_regexp (newbase,
+                                      "\\d+\\."
+                                      "[[:xdigit:]]{16}\\."
+                                      "[[:alnum:]][[:alnum:]-]+(:2,.*)?");
+               g_free (newbase);
+               g_free(str);
+       }
+}
+
+
+
+
+static void
+test_mu_maildir_get_new_path_01 (void)
+{
+       int i;
+
+       struct {
+               const char *oldpath;
+               MuFlags flags;
+               const char *newpath;
+       } paths[] = {
+               {
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       MU_FLAG_REPLIED,
+                       "/home/foo/Maildir/test/cur/123456:2,R"
+               }, {
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       MU_FLAG_NEW,
+                       "/home/foo/Maildir/test/new/123456"
+               }, {
+                       "/home/foo/Maildir/test/new/123456:2,FR",
+                       MU_FLAG_SEEN | MU_FLAG_REPLIED,
+                       "/home/foo/Maildir/test/cur/123456:2,RS"
+               }, {
+                       "/home/foo/Maildir/test/new/1313038887_0.697:2,",
+                       MU_FLAG_SEEN | MU_FLAG_FLAGGED | MU_FLAG_PASSED,
+                       "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"
+               }, {
+                       "/home/djcb/Maildir/trash/new/1312920597.2206_16.cthulhu",
+                       MU_FLAG_SEEN,
+                       "/home/djcb/Maildir/trash/cur/1312920597.2206_16.cthulhu:2,S"
+               }
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(paths); ++i) {
+               gchar *str;
+               str = mu_maildir_get_new_path(paths[i].oldpath, NULL,
+                                             paths[i].flags, FALSE);
+               g_assert_cmpstr(str, ==, paths[i].newpath);
+               g_free(str);
+       }
+}
+
+
+static void
+test_mu_maildir_get_new_path_02 (void)
+{
+       int i;
+
+       struct {
+               const char *oldpath;
+               MuFlags flags;
+               const char *targetdir;
+               const char *newpath;
+       } paths[] = {
+               {
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       MU_FLAG_REPLIED, "/home/foo/Maildir/blabla",
+                       "/home/foo/Maildir/blabla/cur/123456:2,R"
+               }, {
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       MU_FLAG_NEW, "/home/bar/Maildir/coffee",
+                       "/home/bar/Maildir/coffee/new/123456"
+               }, {
+                       "/home/foo/Maildir/test/new/123456",
+                       MU_FLAG_SEEN | MU_FLAG_REPLIED,
+                       "/home/cuux/Maildir/tea",
+                       "/home/cuux/Maildir/tea/cur/123456:2,RS"
+               }, {
+                       "/home/foo/Maildir/test/new/1313038887_0.697:2,",
+                       MU_FLAG_SEEN | MU_FLAG_FLAGGED | MU_FLAG_PASSED,
+                       "/home/boy/Maildir/stuff",
+                       "/home/boy/Maildir/stuff/cur/1313038887_0.697:2,FPS"
+               }
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(paths); ++i) {
+               gchar *str;
+               str = mu_maildir_get_new_path(paths[i].oldpath,
+                                             paths[i].targetdir,
+                                             paths[i].flags, FALSE);
+               g_assert_cmpstr(str, ==, paths[i].newpath);
+               g_free(str);
+       }
+}
+
+
+static void
+test_mu_maildir_get_new_path_custom (void)
+{
+       int i;
+
+       struct {
+               const char *oldpath;
+               MuFlags flags;
+               const char *targetdir;
+               const char *newpath;
+       } paths[] = {
+               {
+                       "/home/foo/Maildir/test/cur/123456:2,FR",
+                       MU_FLAG_REPLIED, "/home/foo/Maildir/blabla",
+                       "/home/foo/Maildir/blabla/cur/123456:2,R"
+               }, {
+                       "/home/foo/Maildir/test/cur/123456:2,hFeRllo123",
+                       MU_FLAG_FLAGGED, "/home/foo/Maildir/blabla",
+                       "/home/foo/Maildir/blabla/cur/123456:2,Fhello123"
+               }, {
+                       "/home/foo/Maildir/test/cur/123456:2,abc",
+                       MU_FLAG_PASSED, "/home/foo/Maildir/blabla",
+                       "/home/foo/Maildir/blabla/cur/123456:2,Pabc"
+               }
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(paths); ++i) {
+               gchar *str;
+               str = mu_maildir_get_new_path(paths[i].oldpath,
+                                             paths[i].targetdir,
+                                             paths[i].flags, FALSE);
+               g_assert_cmpstr(str, ==, paths[i].newpath);
+               g_free(str);
+       }
+}
+
+
+
+static void
+test_mu_maildir_get_maildir_from_path (void)
+{
+       unsigned u;
+
+       struct {
+               const char *path, *exp;
+       } cases[] = {
+               {"/home/foo/Maildir/test/cur/123456:2,FR",
+                "/home/foo/Maildir/test"},
+               {"/home/foo/Maildir/lala/new/1313038887_0.697:2,",
+                "/home/foo/Maildir/lala"}
+       };
+
+
+       for (u = 0; u != G_N_ELEMENTS(cases); ++u) {
+               gchar *mdir;
+               mdir = mu_maildir_get_maildir_from_path (cases[u].path);
+               g_assert_cmpstr(mdir,==,cases[u].exp);
+               g_free (mdir);
+       }
+}
+
+
+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_mu_maildir_mkdir_01);
+       g_test_add_func ("/mu-maildir/mu-maildir-mkdir-02",
+                        test_mu_maildir_mkdir_02);
+       g_test_add_func ("/mu-maildir/mu-maildir-mkdir-03",
+                        test_mu_maildir_mkdir_03);
+       g_test_add_func ("/mu-maildir/mu-maildir-mkdir-04",
+                        test_mu_maildir_mkdir_04);
+       g_test_add_func ("/mu-maildir/mu-maildir-mkdir-05",
+                        test_mu_maildir_mkdir_05);
+
+
+       /* mu_util_maildir_walk */
+       g_test_add_func ("/mu-maildir/mu-maildir-walk-01",
+                        test_mu_maildir_walk_01);
+       g_test_add_func ("/mu-maildir/mu-maildir-walk",
+                        test_mu_maildir_walk);
+       g_test_add_func ("/mu-maildir/mu-maildir-walk-with-noupdate",
+                        test_mu_maildir_walk_with_noupdate);
+
+       /* get/set flags */
+       g_test_add_func("/mu-maildir/mu-maildir-get-new-path-new",
+                       test_mu_maildir_get_new_path_new);
+
+       g_test_add_func("/mu-maildir/mu-maildir-get-new-path-01",
+                       test_mu_maildir_get_new_path_01);
+       g_test_add_func("/mu-maildir/mu-maildir-get-new-path-02",
+                       test_mu_maildir_get_new_path_02);
+       g_test_add_func("/mu-maildir/mu-maildir-get-new-path-custom",
+                       test_mu_maildir_get_new_path_custom);
+       g_test_add_func("/mu-maildir/mu-maildir-get-flags-from-path",
+                       test_mu_maildir_get_flags_from_path);
+
+
+       g_test_add_func("/mu-maildir/mu-maildir-get-maildir-from-path",
+                       test_mu_maildir_get_maildir_from_path);
+
+       g_log_set_handler (NULL,
+                          G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL|
+                          G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       return g_test_run ();
+}
diff --git a/lib/test-mu-msg-fields.c b/lib/test-mu-msg-fields.c
new file mode 100644 (file)
index 0000000..27864d8
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+** 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 "test-mu-common.h"
+#include "mu-msg-fields.h"
+
+static void
+test_mu_msg_field_body (void)
+{
+       MuMsgFieldId field;
+
+       field = MU_MSG_FIELD_ID_BODY_TEXT;
+
+       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)
+{
+       MuMsgFieldId field;
+
+       field = MU_MSG_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)
+{
+       MuMsgFieldId field;
+
+       field = MU_MSG_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)
+{
+       MuMsgFieldId field;
+
+       field = MU_MSG_FIELD_ID_PRIO;
+
+       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)
+{
+       MuMsgFieldId field;
+
+       field = MU_MSG_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,
+                          G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL |
+                          G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       return g_test_run ();
+}
diff --git a/lib/test-mu-msg.c b/lib/test-mu-msg.c
new file mode 100644 (file)
index 0000000..fef368b
--- /dev/null
@@ -0,0 +1,598 @@
+/* -*-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 "test-mu-common.h"
+#include "mu-msg.h"
+#include "utils/mu-str.h"
+
+
+static MuMsg*
+get_msg (const char *path)
+{
+       GError *err;
+       MuMsg *msg;
+
+       if (g_test_verbose ())
+               g_print (">> %s\n", path);
+
+       err = NULL;
+       msg = mu_msg_new_from_file (path, NULL, &err);
+
+       if (!msg) {
+               g_printerr ("failed to load %s: %s\n",
+                           path, err ? err->message : "something went wrong");
+               g_clear_error (&err);
+               g_assert (0);
+       }
+
+       return msg;
+}
+
+
+
+static gboolean
+check_contact_01 (MuMsgContact *contact, int *idx)
+{
+       switch (*idx) {
+       case 0:
+               g_assert_cmpstr (mu_msg_contact_name (contact),
+                                ==, "Mickey Mouse");
+               g_assert_cmpstr (mu_msg_contact_email (contact),
+                                ==, "anon@example.com");
+               break;
+       case 1:
+               g_assert_cmpstr (mu_msg_contact_name (contact),
+                                ==, "Donald Duck");
+               g_assert_cmpstr (mu_msg_contact_email (contact),
+                                ==, "gcc-help@gcc.gnu.org");
+               break;
+       default:
+               g_assert_not_reached ();
+       }
+       ++(*idx);
+
+       return TRUE;
+}
+
+
+static void
+test_mu_msg_01 (void)
+{
+       MuMsg *msg;
+       gint i;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/1220863042.12663_1.mindcrime!2,S");
+
+       g_assert_cmpstr (mu_msg_get_to(msg),
+                        ==, "Donald Duck <gcc-help@gcc.gnu.org>");
+       g_assert_cmpstr (mu_msg_get_subject(msg),
+                        ==, "gcc include search order");
+       g_assert_cmpstr (mu_msg_get_from(msg),
+                        ==, "Mickey Mouse <anon@example.com>");
+       g_assert_cmpstr (mu_msg_get_msgid(msg),
+                        ==, "3BE9E6535E3029448670913581E7A1A20D852173@"
+                        "emss35m06.us.lmco.com");
+       g_assert_cmpstr (mu_msg_get_header(msg, "Mailing-List"),
+                                        ==,
+                        "contact gcc-help-help@gcc.gnu.org; run by ezmlm");
+       g_assert_cmpuint (mu_msg_get_prio(msg), /* 'klub' */
+                         ==, MU_MSG_PRIO_NORMAL);
+       g_assert_cmpuint (mu_msg_get_date(msg),
+                         ==, 1217530645);
+
+       i = 0;
+       mu_msg_contact_foreach (msg, (MuMsgContactForeachFunc)check_contact_01,
+                               &i);
+       g_assert_cmpint (i,==,2);
+
+       mu_msg_unref (msg);
+}
+
+
+
+
+
+
+static gboolean
+check_contact_02 (MuMsgContact *contact, int *idx)
+{
+       switch (*idx) {
+       case 0:
+               g_assert_cmpstr (mu_msg_contact_name (contact),
+                                ==, NULL);
+               g_assert_cmpstr (mu_msg_contact_email (contact),
+                                ==, "anon@example.com");
+               break;
+       case 1:
+               g_assert_cmpstr (mu_msg_contact_name (contact),
+                                ==, NULL);
+               g_assert_cmpstr (mu_msg_contact_email (contact),
+                                ==, "help-gnu-emacs@gnu.org");
+               break;
+       default:
+               g_assert_not_reached ();
+       }
+       ++(*idx);
+
+       return TRUE;
+}
+
+
+
+static void
+test_mu_msg_02 (void)
+{
+       MuMsg *msg;
+       int i;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/1220863087.12663_19.mindcrime!2,S");
+
+       g_assert_cmpstr (mu_msg_get_to(msg),
+                        ==, "help-gnu-emacs@gnu.org");
+       g_assert_cmpstr (mu_msg_get_subject(msg),
+                        ==, "Re: Learning LISP; Scheme vs elisp.");
+       g_assert_cmpstr (mu_msg_get_from(msg),
+                        ==, "anon@example.com");
+       g_assert_cmpstr (mu_msg_get_msgid(msg),
+                        ==, "r6bpm5-6n6.ln1@news.ducksburg.com");
+       g_assert_cmpstr (mu_msg_get_header(msg, "Errors-To"),
+                        ==, "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org");
+       g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */
+                         ==, MU_MSG_PRIO_LOW);
+       g_assert_cmpuint (mu_msg_get_date(msg),
+                         ==, 1218051515);
+
+       i = 0;
+       mu_msg_contact_foreach (msg,
+                               (MuMsgContactForeachFunc)check_contact_02,
+                               &i);
+       g_assert_cmpint (i,==,2);
+       g_assert_cmpuint (mu_msg_get_flags(msg), ==, MU_FLAG_SEEN|MU_FLAG_LIST);
+
+       mu_msg_unref (msg);
+}
+
+static void
+test_mu_msg_03 (void)
+{
+       MuMsg           *msg;
+       const GSList    *params;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/1283599333.1840_11.cthulhu!2,");
+       g_assert_cmpstr (mu_msg_get_to(msg),
+                        ==, "Bilbo Baggins <bilbo@anotherexample.com>");
+       g_assert_cmpstr (mu_msg_get_subject(msg),
+                        ==, "Greetings from Lothlórien");
+       g_assert_cmpstr (mu_msg_get_from(msg),
+                        ==, "Frodo Baggins <frodo@example.com>");
+       g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */
+                         ==, MU_MSG_PRIO_NORMAL);
+       g_assert_cmpuint (mu_msg_get_date(msg),
+                         ==, 0);
+       g_assert_cmpstr (mu_msg_get_body_text(msg, MU_MSG_OPTION_NONE),
+                        ==,
+                        "\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);
+
+       g_assert_cmpstr ((char*)params->data,==, "charset");
+       params = g_slist_next(params);
+       g_assert_cmpstr ((char*)params->data,==,"UTF-8");
+
+       g_assert_cmpuint (mu_msg_get_flags(msg),
+                         ==, MU_FLAG_UNREAD); /* not seen => unread */
+
+       mu_msg_unref (msg);
+}
+
+
+static void
+test_mu_msg_04 (void)
+{
+       MuMsg *msg;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/mail5");
+       g_assert_cmpstr (mu_msg_get_to(msg),
+                        ==, "George Custer <gac@example.com>");
+       g_assert_cmpstr (mu_msg_get_subject(msg),
+                        ==, "pics for you");
+       g_assert_cmpstr (mu_msg_get_from(msg),
+                        ==, "Sitting Bull <sb@example.com>");
+       g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */
+                         ==, MU_MSG_PRIO_NORMAL);
+       g_assert_cmpuint (mu_msg_get_date(msg),
+                         ==, 0);
+       g_assert_cmpuint (mu_msg_get_flags(msg),
+                         ==, MU_FLAG_HAS_ATTACH|MU_FLAG_UNREAD);
+       mu_msg_unref (msg);
+}
+
+
+static void
+test_mu_msg_multimime (void)
+{
+       MuMsg *msg;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/multimime!2,FS");
+       /* ie., are text parts properly concatenated? */
+       g_assert_cmpstr (mu_msg_get_subject(msg),
+                        ==, "multimime");
+       g_assert_cmpstr (mu_msg_get_body_text(msg, MU_MSG_OPTION_NONE),
+                        ==, "abcdef");
+       g_assert_cmpuint (mu_msg_get_flags(msg),
+                        ==, MU_FLAG_FLAGGED | MU_FLAG_SEEN |
+                        MU_FLAG_HAS_ATTACH);
+       mu_msg_unref (msg);
+}
+
+
+
+static void
+test_mu_msg_flags (void)
+{
+       unsigned u;
+
+       struct {
+               const char *path;
+               MuFlags flags;
+       } msgflags [] = {
+               { MU_TESTMAILDIR4 "/multimime!2,FS",
+                 MU_FLAG_FLAGGED | MU_FLAG_SEEN |
+                 MU_FLAG_HAS_ATTACH },
+               { MU_TESTMAILDIR4 "/special!2,Sabc",
+                 MU_FLAG_SEEN }
+
+       };
+
+       for (u = 0; u != G_N_ELEMENTS(msgflags); ++u) {
+               MuMsg *msg;
+               MuFlags flags;
+
+               g_assert ((msg = get_msg (msgflags[u].path)));
+               flags = mu_msg_get_flags (msg);
+
+               if (g_test_verbose())
+                       g_print ("=> %s [ %s, %u] <=> [ %s, %u]\n",
+                                msgflags[u].path,
+                                mu_flags_to_str_s(msgflags[u].flags,
+                                                  MU_FLAG_TYPE_ANY),
+                                (unsigned)msgflags[u].flags,
+                                mu_flags_to_str_s(flags, MU_FLAG_TYPE_ANY),
+                                (unsigned)flags);
+               g_assert_cmpuint (flags ,==, msgflags[u].flags);
+               mu_msg_unref (msg);
+       }
+}
+
+
+static void
+test_mu_msg_umlaut (void)
+{
+       MuMsg *msg;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,");
+       g_assert_cmpstr (mu_msg_get_to(msg),
+                        ==, "Helmut Kröger <hk@testmu.xxx>");
+       g_assert_cmpstr (mu_msg_get_subject(msg),
+                        ==, "Motörhead");
+       g_assert_cmpstr (mu_msg_get_from(msg),
+                        ==, "Mü <testmu@testmu.xx>");
+       g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */
+                         ==, MU_MSG_PRIO_NORMAL);
+       g_assert_cmpuint (mu_msg_get_date(msg),
+                         ==, 0);
+
+       mu_msg_unref (msg);
+}
+
+
+static void
+test_mu_msg_references (void)
+{
+       MuMsg *msg;
+       const GSList *refs;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,");
+       refs = mu_msg_get_references(msg);
+
+       g_assert_cmpuint (g_slist_length ((GSList*)refs), ==, 4);
+
+       g_assert_cmpstr ((char*)refs->data,==, "non-exist-01@msg.id");
+       refs = g_slist_next (refs);
+       g_assert_cmpstr ((char*)refs->data,==, "non-exist-02@msg.id");
+       refs = g_slist_next (refs);
+       g_assert_cmpstr ((char*)refs->data,==, "non-exist-03@msg.id");
+       refs = g_slist_next (refs);
+       g_assert_cmpstr ((char*)refs->data,==, "non-exist-04@msg.id");
+       refs = g_slist_next (refs);
+
+       mu_msg_unref (msg);
+}
+
+
+
+static void
+test_mu_msg_references_dups (void)
+{
+       MuMsg           *msg;
+       const GSList    *refs;
+       const char      *mlist;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/1252168370_3.14675.cthulhu!2,S");
+       refs = mu_msg_get_references(msg);
+
+       /* make sure duplicate msg-ids are filtered out */
+
+       g_assert_cmpuint (g_slist_length ((GSList*)refs), ==, 6);
+
+       g_assert_cmpstr ((char*)refs->data,==,
+                        "439C1136.90504@euler.org");
+       refs = g_slist_next (refs);
+       g_assert_cmpstr ((char*)refs->data,==,
+                        "4399DD94.5070309@euler.org");
+       refs = g_slist_next (refs);
+       g_assert_cmpstr ((char*)refs->data,==,
+                        "20051209233303.GA13812@gauss.org");
+       refs = g_slist_next (refs);
+       g_assert_cmpstr ((char*)refs->data,==,
+                        "439B41ED.2080402@euler.org");
+       refs = g_slist_next (refs);
+       g_assert_cmpstr ((char*)refs->data,==,
+                        "439A1E03.3090604@euler.org");
+       refs = g_slist_next (refs);
+       g_assert_cmpstr ((char*)refs->data,==,
+                        "20051211184308.GB13513@gauss.org");
+       refs = g_slist_next (refs);
+
+       mlist = mu_msg_get_mailing_list (msg);
+       g_assert_cmpstr (mlist ,==, "Example of List Id");
+
+       mu_msg_unref (msg);
+}
+
+
+static void
+test_mu_msg_references_many (void)
+{
+       MuMsg *msg;
+       unsigned u;
+       const GSList *refs, *cur;
+       const char* expt_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"
+       };
+
+       msg = get_msg (MU_TESTMAILDIR2 "/bar/cur/181736.eml");
+       refs = mu_msg_get_references(msg);
+
+       g_assert_cmpuint (G_N_ELEMENTS(expt_refs), ==,
+                         g_slist_length((GSList*)refs));
+
+       for (cur = refs, u = 0; cur; cur = g_slist_next(cur), ++u) {
+               if (g_test_verbose())
+                       g_print ("%u. '%s' =? '%s'\n",
+                                u, (char*)cur->data,
+                                expt_refs[u]);
+
+               g_assert_cmpstr ((char*)cur->data, ==, expt_refs[u]);
+       }
+
+       mu_msg_unref (msg);
+}
+
+static void
+test_mu_msg_tags (void)
+{
+       MuMsg *msg;
+       const GSList *tags;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/mail1");
+
+       g_assert_cmpstr (mu_msg_get_to(msg),
+                        ==, "Julius Caesar <jc@example.com>");
+       g_assert_cmpstr (mu_msg_get_subject(msg),
+                        ==, "Fere libenter homines id quod volunt credunt");
+       g_assert_cmpstr (mu_msg_get_from(msg),
+                        ==, "John Milton <jm@example.com>");
+       g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */
+                         ==, MU_MSG_PRIO_HIGH);
+       g_assert_cmpuint (mu_msg_get_date(msg),
+                         ==, 1217530645);
+
+       tags = mu_msg_get_tags (msg);
+       g_assert_cmpstr ((char*)tags->data,==,"Paradise");
+       g_assert_cmpstr ((char*)tags->next->data,==,"losT");
+       g_assert_cmpstr ((char*)tags->next->next->data,==,"john");
+       g_assert_cmpstr ((char*)tags->next->next->next->data,==,"milton");
+
+       g_assert (!tags->next->next->next->next);
+
+       mu_msg_unref (msg);
+}
+
+
+static void
+test_mu_msg_comp_unix_programmer (void)
+{
+       MuMsg *msg;
+       char *refs;
+
+       msg = get_msg (MU_TESTMAILDIR4 "/181736.eml");
+       g_assert_cmpstr (mu_msg_get_to(msg),
+                        ==, NULL);
+       g_assert_cmpstr (mu_msg_get_subject(msg),
+                        ==, "Re: Are writes \"atomic\" to readers of the file?");
+       g_assert_cmpstr (mu_msg_get_from(msg),
+                        ==, "Jimbo Foobarcuux <jimbo@slp53.sl.home>");
+       g_assert_cmpstr (mu_msg_get_msgid(msg),
+                        ==, "oktdp.42997$Te.22361@news.usenetserver.com");
+
+       refs = mu_str_from_list (mu_msg_get_references(msg), ',');
+       g_assert_cmpstr (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");
+       g_free (refs);
+
+       //"jimbo@slp53.sl.home (Jimbo Foobarcuux)";
+       g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */
+                         ==, MU_MSG_PRIO_NORMAL);
+       g_assert_cmpuint (mu_msg_get_date(msg),
+                         ==, 1299603860);
+
+       mu_msg_unref (msg);
+}
+
+
+
+static void
+test_mu_str_prio_01 (void)
+{
+       g_assert_cmpstr(mu_msg_prio_name(MU_MSG_PRIO_LOW), ==, "low");
+       g_assert_cmpstr(mu_msg_prio_name(MU_MSG_PRIO_NORMAL), ==, "normal");
+       g_assert_cmpstr(mu_msg_prio_name(MU_MSG_PRIO_HIGH), ==, "high");
+}
+
+
+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_mu_str_prio_02 (void)
+{
+       /* this must fail */
+       g_test_log_set_fatal_handler ((GTestLogFatalFunc)ignore_error, NULL);
+       g_assert_cmpstr (mu_msg_prio_name(666), ==, NULL);
+}
+
+
+
+static void
+test_mu_str_display_contact (void)
+{
+       int                     i;
+       struct {
+               const char*     word;
+               const char*     disp;
+       } words [] = {
+               { "\"Foo Bar\" <aap@noot.mies>", "Foo Bar"},
+               { "Foo Bar <aap@noot.mies>", "Foo Bar" },
+               { "<aap@noot.mies>", "aap@noot.mies" },
+               { "foo@bar.nl", "foo@bar.nl" }
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(words); ++i)
+               g_assert_cmpstr (mu_str_display_contact_s (words[i].word), ==,
+                                words[i].disp);
+}
+
+
+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);
+
+       /* mu_str_prio */
+       g_test_add_func ("/mu-str/mu-str-prio-01",
+                        test_mu_str_prio_01);
+       g_test_add_func ("/mu-str/mu-str-prio-02",
+                        test_mu_str_prio_02);
+
+
+       g_test_add_func ("/mu-str/mu-str-display_contact",
+                        test_mu_str_display_contact);
+
+
+       g_log_set_handler (NULL,
+                          G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL|
+                          G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       rv = g_test_run ();
+
+       return rv;
+}
diff --git a/lib/test-mu-store.c b/lib/test-mu-store.c
new file mode 100644 (file)
index 0000000..3276d57
--- /dev/null
@@ -0,0 +1,206 @@
+/* -*-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 "test-mu-common.h"
+#include "mu-store.hh"
+
+static void
+test_mu_store_new_destroy (void)
+{
+       MuStore *store;
+       gchar* tmpdir;
+       GError *err;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert (tmpdir);
+
+       err = NULL;
+       store = mu_store_new_create (tmpdir, "/tmp", NULL, &err);
+       g_assert_no_error (err);
+       g_assert (store);
+
+       g_assert_cmpuint (0,==,mu_store_count (store, NULL));
+
+       mu_store_flush (store);
+       mu_store_unref (store);
+
+       g_free (tmpdir);
+}
+
+
+static void
+test_mu_store_version (void)
+{
+       MuStore *store;
+       gchar* tmpdir;
+       GError *err;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert (tmpdir);
+
+       err = NULL;
+       store = mu_store_new_create (tmpdir, "/tmp", NULL, &err);
+       g_assert (store);
+       mu_store_unref (store);
+       store = mu_store_new_readable (tmpdir, &err);
+       g_assert (store);
+
+       g_assert (err == NULL);
+
+       g_assert_cmpuint (0,==,mu_store_count (store, NULL));
+       g_assert_cmpstr (MU_STORE_SCHEMA_VERSION,==,
+                        mu_store_schema_version(store));
+
+       mu_store_unref (store);
+       g_free (tmpdir);
+}
+
+
+G_GNUC_UNUSED static void
+test_mu_store_store_msg_and_count (void)
+{
+       MuMsg *msg;
+       MuStore *store;
+       gchar* tmpdir;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert (tmpdir);
+
+       store = mu_store_new_create (tmpdir, MU_TESTMAILDIR, NULL, NULL);
+       g_assert (store);
+       g_free (tmpdir);
+
+       g_assert_cmpuint (0,==,mu_store_count (store, NULL));
+
+       /* add one */
+       /* XXX this passes, but not make-dist; investigate */
+       msg = mu_msg_new_from_file (
+               MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,",
+               NULL, NULL);
+       g_assert (msg);
+       g_assert_cmpuint (mu_store_add_msg (store, msg, NULL),
+                         !=, MU_STORE_INVALID_DOCID);
+       g_assert_cmpuint (1,==,mu_store_count (store, NULL));
+       g_assert_cmpuint (TRUE,==,mu_store_contains_message
+                         (store,
+                          MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,"));
+       mu_msg_unref (msg);
+
+       /* add another one */
+       msg = mu_msg_new_from_file (MU_TESTMAILDIR2
+                                   "/bar/cur/mail3", NULL, NULL);
+       g_assert (msg);
+       g_assert_cmpuint (mu_store_add_msg (store, msg, NULL),
+                         !=, MU_STORE_INVALID_DOCID);
+       g_assert_cmpuint (2,==,mu_store_count (store, NULL));
+       g_assert_cmpuint (TRUE,==,
+                         mu_store_contains_message (store, MU_TESTMAILDIR2
+                                                    "/bar/cur/mail3"));
+       mu_msg_unref (msg);
+
+       /* try to add the first one again. count should be 2 still */
+       msg = mu_msg_new_from_file
+               (MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,",
+                NULL, NULL);
+       g_assert (msg);
+       g_assert_cmpuint (mu_store_add_msg (store, msg, NULL),
+                         !=, MU_STORE_INVALID_DOCID);
+       g_assert_cmpuint (2,==,mu_store_count (store, NULL));
+
+       mu_msg_unref (msg);
+       mu_store_unref (store);
+}
+
+
+G_GNUC_UNUSED static void
+test_mu_store_store_msg_remove_and_count (void)
+{
+       MuMsg *msg;
+       MuStore *store;
+       gchar* tmpdir;
+       GError *err;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert (tmpdir);
+
+       store = mu_store_new_create (tmpdir, MU_TESTMAILDIR, NULL, NULL);
+       g_assert (store);
+
+       g_assert_cmpuint (0,==,mu_store_count (store, NULL));
+
+       /* add one */
+       err = NULL;
+       msg = mu_msg_new_from_file (
+               MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,",
+               NULL, &err);
+       g_assert (msg);
+       g_assert_cmpuint (mu_store_add_msg (store, msg, NULL),
+                         !=, MU_STORE_INVALID_DOCID);
+       g_assert_cmpuint (1,==,mu_store_count (store, NULL));
+       mu_msg_unref (msg);
+
+       /* remove one */
+       mu_store_remove_path (store,
+                             MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,");
+       g_assert_cmpuint (0,==,mu_store_count (store, NULL));
+       g_assert_cmpuint (FALSE,==,mu_store_contains_message
+                         (store,
+                          MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,"));
+       g_free (tmpdir);
+       mu_store_unref (store);
+}
+
+
+int
+main (int argc, char *argv[])
+{
+       g_test_init (&argc, &argv, NULL);
+
+       /* mu_runtime_init/uninit */
+       g_test_add_func ("/mu-store/mu-store-new-destroy",
+                        test_mu_store_new_destroy);
+       g_test_add_func ("/mu-store/mu-store-version",
+                        test_mu_store_version);
+#if 0
+       /* XXX this passes, but not make-dist; investigate */
+       g_test_add_func ("/mu-store/mu-store-store-and-count",
+                        test_mu_store_store_msg_and_count);
+       g_test_add_func ("/mu-store/mu-store-store-remove-and-count",
+                        test_mu_store_store_msg_remove_and_count);
+#endif
+       if (!g_test_verbose())
+               g_log_set_handler (NULL,
+               G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION,
+               (GLogFunc)black_hole, NULL);
+
+       return g_test_run ();
+}
diff --git a/lib/testdir/cur/1220863042.12663_1.mindcrime!2,S b/lib/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/testdir/cur/1220863060.12663_3.mindcrime!2,S b/lib/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/testdir/cur/1220863087.12663_15.mindcrime!2,PS b/lib/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/testdir/cur/1220863087.12663_19.mindcrime!2,S b/lib/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/testdir/cur/1220863087.12663_5.mindcrime!2,S b/lib/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/testdir/cur/1220863087.12663_7.mindcrime!2,RS b/lib/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/testdir/cur/1252168370_3.14675.cthulhu!2,S b/lib/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/testdir/cur/1283599333.1840_11.cthulhu!2, b/lib/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/testdir/cur/1305664394.2171_402.cthulhu!2, b/lib/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/testdir/cur/encrypted!2,S b/lib/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/testdir/cur/multimime!2,FS b/lib/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/testdir/cur/multirecip!2,S b/lib/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/testdir/cur/signed!2,S b/lib/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/testdir/cur/signed-encrypted!2,S b/lib/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/testdir/cur/special!2,Sabc b/lib/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/testdir/new/1220863087.12663_21.mindcrime b/lib/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/testdir/new/1220863087.12663_23.mindcrime b/lib/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/testdir/new/1220863087.12663_25.mindcrime b/lib/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/testdir/new/1220863087.12663_9.mindcrime b/lib/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/testdir/tmp/1220863087.12663.ignore b/lib/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/testdir2/Foo/cur/arto.eml b/lib/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/testdir2/Foo/cur/fraiche.eml b/lib/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/testdir2/Foo/cur/mail5 b/lib/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/testdir2/Foo/new/.noindex b/lib/testdir2/Foo/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir2/Foo/tmp/.noindex b/lib/testdir2/Foo/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir2/bar/cur/181736.eml b/lib/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/testdir2/bar/cur/mail1 b/lib/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/testdir2/bar/cur/mail2 b/lib/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/testdir2/bar/cur/mail3 b/lib/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/testdir2/bar/cur/mail4 b/lib/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/testdir2/bar/cur/mail5 b/lib/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/testdir2/bar/cur/mail6 b/lib/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/testdir2/bar/new/.noindex b/lib/testdir2/bar/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir2/bar/tmp/.noindex b/lib/testdir2/bar/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir2/wom_bat/cur/atomic b/lib/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/testdir2/wom_bat/cur/rfc822.1 b/lib/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/testdir2/wom_bat/cur/rfc822.2 b/lib/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/testdir3/cycle/cur/cycle0 b/lib/testdir3/cycle/cur/cycle0
new file mode 100644 (file)
index 0000000..67c08ee
--- /dev/null
@@ -0,0 +1,7 @@
+From: foo@example.com 
+To: bar@example.com
+Subject: cycle0 
+Message-Id: <cycle0@msg.id>
+Date: Tue, 21 Jun 2011 11:00 +0000
+
+def
diff --git a/lib/testdir3/cycle/cur/cycle0.0 b/lib/testdir3/cycle/cur/cycle0.0
new file mode 100644 (file)
index 0000000..3222a2c
--- /dev/null
@@ -0,0 +1,8 @@
+From: foo@example.com 
+To: bar@example.com
+Subject: cycle0.0 
+Message-Id: <cycle0.0@msg.id>
+References: <cycle0@msg.id>
+Date: Tue, 21 Jun 2011 12:00 +0000
+
+def
diff --git a/lib/testdir3/cycle/cur/cycle0.0.0 b/lib/testdir3/cycle/cur/cycle0.0.0
new file mode 100644 (file)
index 0000000..907f342
--- /dev/null
@@ -0,0 +1,8 @@
+From: foo@example.com 
+To: bar@example.com
+Subject: cycle0.0.0 
+Message-Id: <cycle0.0.0@msg.id>
+References: <cycle0@msg.id> <cycle0.0@msg.id>
+Date: Tue, 21 Jun 2011 13:00 +0000
+
+def
diff --git a/lib/testdir3/cycle/cur/rogue0 b/lib/testdir3/cycle/cur/rogue0
new file mode 100644 (file)
index 0000000..2691070
--- /dev/null
@@ -0,0 +1,8 @@
+From: foo@example.com 
+To: bar@example.com
+Subject: rogue0 
+Message-Id: <rogue0@msg.id>
+References: <cycle0.0@msg.id> <cycle0@msg.id> 
+Date: Tue, 21 Jun 2011 15:00 +0000
+
+def
diff --git a/lib/testdir3/cycle/new/.noindex b/lib/testdir3/cycle/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/cycle/tmp/.noindex b/lib/testdir3/cycle/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/1st-child-promotes-thread/cur/A b/lib/testdir3/sort/1st-child-promotes-thread/cur/A
new file mode 100644 (file)
index 0000000..85130e8
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: A
+Message-Id: <A@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+A
diff --git a/lib/testdir3/sort/1st-child-promotes-thread/cur/B b/lib/testdir3/sort/1st-child-promotes-thread/cur/B
new file mode 100644 (file)
index 0000000..254aeb7
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: B
+Message-Id: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+B
diff --git a/lib/testdir3/sort/1st-child-promotes-thread/cur/C b/lib/testdir3/sort/1st-child-promotes-thread/cur/C
new file mode 100644 (file)
index 0000000..60b7db5
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: C
+Message-Id: <C@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+C
diff --git a/lib/testdir3/sort/1st-child-promotes-thread/cur/D b/lib/testdir3/sort/1st-child-promotes-thread/cur/D
new file mode 100644 (file)
index 0000000..1e4861d
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: D
+Message-Id: <D@msg.id>
+References: <B@msg.id>
+In-reply-to: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+D
diff --git a/lib/testdir3/sort/1st-child-promotes-thread/new/.noindex b/lib/testdir3/sort/1st-child-promotes-thread/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/1st-child-promotes-thread/tmp/.noindex b/lib/testdir3/sort/1st-child-promotes-thread/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/A b/lib/testdir3/sort/2nd-child-promotes-thread/cur/A
new file mode 100644 (file)
index 0000000..85130e8
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: A
+Message-Id: <A@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+A
diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/B b/lib/testdir3/sort/2nd-child-promotes-thread/cur/B
new file mode 100644 (file)
index 0000000..254aeb7
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: B
+Message-Id: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+B
diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/C b/lib/testdir3/sort/2nd-child-promotes-thread/cur/C
new file mode 100644 (file)
index 0000000..6d1e19a
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: C
+Message-Id: <C@msg.id>
+References: <B@msg.id>
+In-reply-to: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+C
diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/D b/lib/testdir3/sort/2nd-child-promotes-thread/cur/D
new file mode 100644 (file)
index 0000000..de61bc1
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: D
+Message-Id: <D@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+D
diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/E b/lib/testdir3/sort/2nd-child-promotes-thread/cur/E
new file mode 100644 (file)
index 0000000..bb7f5f3
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: E
+Message-Id: <E@msg.id>
+References: <B@msg.id>
+In-reply-to: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+E
diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/new/.noindex b/lib/testdir3/sort/2nd-child-promotes-thread/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/tmp/.noindex b/lib/testdir3/sort/2nd-child-promotes-thread/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/child-does-not-promote-thread/cur/A b/lib/testdir3/sort/child-does-not-promote-thread/cur/A
new file mode 100644 (file)
index 0000000..c59c42f
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: A
+Message-Id: <A@msg.id>
+References: <Y@msg.id>
+In-reply-to: <Y@msg.id>
+Aate: Sat, 17 May 2014 10:00:00 +0000
+
+A
diff --git a/lib/testdir3/sort/child-does-not-promote-thread/cur/X b/lib/testdir3/sort/child-does-not-promote-thread/cur/X
new file mode 100644 (file)
index 0000000..c8ac3aa
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: X
+Message-Id: <X@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+X
diff --git a/lib/testdir3/sort/child-does-not-promote-thread/cur/Y b/lib/testdir3/sort/child-does-not-promote-thread/cur/Y
new file mode 100644 (file)
index 0000000..ceadc20
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: Y
+Message-Id: <Y@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+Y
diff --git a/lib/testdir3/sort/child-does-not-promote-thread/cur/Z b/lib/testdir3/sort/child-does-not-promote-thread/cur/Z
new file mode 100644 (file)
index 0000000..365775b
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: Z
+Message-Id: <Z@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+Z
diff --git a/lib/testdir3/sort/child-does-not-promote-thread/new/.noindex b/lib/testdir3/sort/child-does-not-promote-thread/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/child-does-not-promote-thread/tmp/.noindex b/lib/testdir3/sort/child-does-not-promote-thread/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/A b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/A
new file mode 100644 (file)
index 0000000..85130e8
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: A
+Message-Id: <A@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+A
diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/B b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/B
new file mode 100644 (file)
index 0000000..254aeb7
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: B
+Message-Id: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+B
diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/C b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/C
new file mode 100644 (file)
index 0000000..6d1e19a
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: C
+Message-Id: <C@msg.id>
+References: <B@msg.id>
+In-reply-to: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+C
diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/D b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/D
new file mode 100644 (file)
index 0000000..1e4861d
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: D
+Message-Id: <D@msg.id>
+References: <B@msg.id>
+In-reply-to: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+D
diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/E b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/E
new file mode 100644 (file)
index 0000000..bb7f5f3
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: E
+Message-Id: <E@msg.id>
+References: <B@msg.id>
+In-reply-to: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+E
diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/F b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/F
new file mode 100644 (file)
index 0000000..7c4275d
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: F
+Message-Id: <F@msg.id>
+References: <B@msg.id> <D@msg.id>
+In-reply-to: <D@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+F
diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/G b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/G
new file mode 100644 (file)
index 0000000..4849455
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: G
+Message-Id: <G@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+G
diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/new/.noindex b/lib/testdir3/sort/grandchild-promotes-only-subthread/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/tmp/.noindex b/lib/testdir3/sort/grandchild-promotes-only-subthread/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/A b/lib/testdir3/sort/grandchild-promotes-thread/cur/A
new file mode 100644 (file)
index 0000000..85130e8
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: A
+Message-Id: <A@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+A
diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/B b/lib/testdir3/sort/grandchild-promotes-thread/cur/B
new file mode 100644 (file)
index 0000000..254aeb7
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: B
+Message-Id: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+B
diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/C b/lib/testdir3/sort/grandchild-promotes-thread/cur/C
new file mode 100644 (file)
index 0000000..6d1e19a
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: C
+Message-Id: <C@msg.id>
+References: <B@msg.id>
+In-reply-to: <B@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+C
diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/D b/lib/testdir3/sort/grandchild-promotes-thread/cur/D
new file mode 100644 (file)
index 0000000..de61bc1
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: D
+Message-Id: <D@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+D
diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/E b/lib/testdir3/sort/grandchild-promotes-thread/cur/E
new file mode 100644 (file)
index 0000000..d605b4b
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com
+To: testto@example.com
+Subject: E
+Message-Id: <E@msg.id>
+References: <B@msg.id> <C@msg.id>
+In-reply-to: <C@msg.id>
+Date: Sat, 17 May 2014 10:00:00 +0000
+
+E
diff --git a/lib/testdir3/sort/grandchild-promotes-thread/new/.noindex b/lib/testdir3/sort/grandchild-promotes-thread/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/sort/grandchild-promotes-thread/tmp/.noindex b/lib/testdir3/sort/grandchild-promotes-thread/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/tree/cur/child0.0 b/lib/testdir3/tree/cur/child0.0
new file mode 100644 (file)
index 0000000..58da7d6
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: Re: child 0.0 
+Message-Id: <child0.0@msg.id>
+References: <root0@msg.id>
+In-reply-to: <root0@msg.id>
+Date: Tue, 21 Jun 2011 11:10 +0000
+
+abc
diff --git a/lib/testdir3/tree/cur/child0.1 b/lib/testdir3/tree/cur/child0.1
new file mode 100644 (file)
index 0000000..163fadb
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: Re: child 0.1 
+Message-Id: <child0.1@msg.id>
+References: <root0@msg.id>
+In-reply-to: <root0@msg.id>
+Date: Tue, 21 Jun 2011 11:20 +0000
+
+abc
diff --git a/lib/testdir3/tree/cur/child0.1.0 b/lib/testdir3/tree/cur/child0.1.0
new file mode 100644 (file)
index 0000000..40a9eb2
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: Re: child 0.1.0
+Message-Id: <child0.1.0@msg.id>
+References: <root0@msg.id> <child0.1@msg.id>
+In-Reply-To: <child0.1@msg.id>
+Date: Tue, 21 Jun 2011 11:22 +0000
+
+abc
diff --git a/lib/testdir3/tree/cur/child2.0.0 b/lib/testdir3/tree/cur/child2.0.0
new file mode 100644 (file)
index 0000000..4e4446e
--- /dev/null
@@ -0,0 +1,12 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: Re: child 2.0.0
+Message-Id: <child2.0.0@msg.id>
+References: <root2@msg.id>  <nonexistant@msg.id>
+In-Reply-To: <nonexistant@msg.id>
+Date: Tue, 21 Jun 2011 15:02 +0000
+
+abc
+
+note, there's no message for 'nonexistant@msg.id', so this msg should
+be promoted to level 2.0
diff --git a/lib/testdir3/tree/cur/child3.0.0.0.0 b/lib/testdir3/tree/cur/child3.0.0.0.0
new file mode 100644 (file)
index 0000000..1e2f4ab
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: Re: child 3.0.0.0 
+Message-Id: <child3.0.0.0.0@msg.id>
+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>
+Date: Wed, 22 Jun 2011 16:33 +0000
+
+abc
diff --git a/lib/testdir3/tree/cur/child4.0 b/lib/testdir3/tree/cur/child4.0
new file mode 100644 (file)
index 0000000..7d0ed79
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: Re: child 4.0 
+Message-Id: <child4.0@msg.id>
+References: <non-exist-root4@msg.id>
+In-reply-to: <non-exist-root4@msg.id>
+Date: Tue, 24 Jun 2011 11:10 +0000
+
+abc
diff --git a/lib/testdir3/tree/cur/child4.1 b/lib/testdir3/tree/cur/child4.1
new file mode 100644 (file)
index 0000000..295d1b1
--- /dev/null
@@ -0,0 +1,9 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: Re: child 4.1
+Message-Id: <child4.1@msg.id>
+References: <non-exist-root4@msg.id>
+In-reply-to: <non-exist-root4@msg.id>
+Date: Tue, 24 Jun 2011 11:20 +0000
+
+abc
diff --git a/lib/testdir3/tree/cur/root0 b/lib/testdir3/tree/cur/root0
new file mode 100644 (file)
index 0000000..deb64bb
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: root0
+Message-Id: <root0@msg.id>
+Date: Tue, 21 Jun 2011 11:00 +0000
+
+abc
diff --git a/lib/testdir3/tree/cur/root1 b/lib/testdir3/tree/cur/root1
new file mode 100644 (file)
index 0000000..fc3efd8
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: root1
+Message-Id: <root1@msg.id>
+Date: Tue, 21 Jun 2011 12:00 +0000
+
+abc
diff --git a/lib/testdir3/tree/cur/root2 b/lib/testdir3/tree/cur/root2
new file mode 100644 (file)
index 0000000..6ba2451
--- /dev/null
@@ -0,0 +1,7 @@
+From: testfrom@example.com 
+To: testto@example.com
+Subject: root2
+Message-Id: <root2@msg.id>
+Date: Tue, 21 Jun 2011 13:00 +0000
+
+abc
diff --git a/lib/testdir3/tree/new/.noindex b/lib/testdir3/tree/new/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir3/tree/tmp/.noindex b/lib/testdir3/tree/tmp/.noindex
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/testdir4/1220863042.12663_1.mindcrime!2,S b/lib/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/testdir4/1220863087.12663_19.mindcrime!2,S b/lib/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/testdir4/1252168370_3.14675.cthulhu!2,S b/lib/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/testdir4/1283599333.1840_11.cthulhu!2, b/lib/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/testdir4/1305664394.2171_402.cthulhu!2, b/lib/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/testdir4/181736.eml b/lib/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/testdir4/encrypted!2,S b/lib/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/testdir4/mail1 b/lib/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/testdir4/mail5 b/lib/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/testdir4/multimime!2,FS b/lib/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/testdir4/signed!2,S b/lib/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/testdir4/signed-bad!2,S b/lib/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/testdir4/signed-encrypted!2,S b/lib/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/testdir4/special!2,Sabc b/lib/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/utils/Makefile.am b/lib/utils/Makefile.am
new file mode 100644 (file)
index 0000000..5a638ac
--- /dev/null
@@ -0,0 +1,109 @@
+## 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_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-date.c                                               \
+       mu-date.h                                               \
+       mu-error.hh                                             \
+       mu-log.c                                                \
+       mu-log.h                                                \
+       mu-command-parser.cc                                    \
+       mu-command-parser.hh                                    \
+       mu-sexp-parser.cc                                       \
+       mu-sexp-parser.hh                                       \
+       mu-str.c                                                \
+       mu-str.h                                                \
+       mu-util.c                                               \
+       mu-util.h                                               \
+       mu-utils.cc                                             \
+       mu-utils.hh
+
+libmu_utils_la_LIBADD=                                         \
+       $(GLIB_LIBS)                                            \
+       $(CODE_COVERAGE_LIBS)
+
+noinst_PROGRAMS=                                               \
+       $(TEST_PROGS)
+
+TEST_PROGS+=                                                   \
+       test-mu-util
+test_mu_util_SOURCES=                                          \
+       test-mu-util.c
+test_mu_util_LDADD=                                            \
+       libmu-utils.la
+
+TEST_PROGS+=                                                   \
+       test-mu-utils
+test_mu_utils_SOURCES=                                         \
+       test-utils.cc
+test_mu_utils_LDADD=                                           \
+       libmu-utils.la
+
+TEST_PROGS+=                                                   \
+       test-mu-str
+test_mu_str_SOURCES=                                           \
+       test-mu-str.c
+test_mu_str_LDADD=                                             \
+       libmu-utils.la
+
+TEST_PROGS+=                                                   \
+       test-sexp-parser
+test_sexp_parser_SOURCES=                                      \
+       test-sexp-parser.cc
+test_sexp_parser_LDADD=                                                \
+       libmu-utils.la
+
+TEST_PROGS+=                                                   \
+       test-command-parser
+test_command_parser_SOURCES=                                   \
+       test-command-parser.cc
+test_command_parser_LDADD=                                     \
+       libmu-utils.la
+
+TESTS=$(TEST_PROGS)
+
+include $(top_srcdir)/aminclude_static.am
diff --git a/lib/utils/mu-command-parser.cc b/lib/utils/mu-command-parser.cc
new file mode 100644 (file)
index 0000000..7c4aa4a
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+** 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-utils.hh"
+
+#include <iostream>
+#include <algorithm>
+
+using namespace Mu;
+using namespace Command;
+using namespace Sexp;
+
+static Mu::Error
+command_error(const std::string& msg)
+{
+        return Mu::Error(Error::Code::Command,  msg);
+}
+
+
+void
+Command::invoke(const Command::CommandMap& cmap, const Node& call)
+{
+        if (call.type != Type::List || call.children.empty() ||
+            call.children[0].type != Type::Symbol)
+                throw command_error("call must be a list starting with a symbol");
+
+        const auto& params{call.children};
+        const auto cmd_it = cmap.find(params[0].value);
+        if (cmd_it == cmap.end())
+                throw command_error("unknown command '" + params[0].value + "'");
+
+        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 = [&]() {
+                        for (size_t i = 1; i < params.size(); i += 2)
+                                if (params[i].type == Type::Symbol &&
+                                    params[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 command_error("missing required parameter '" + argname + "'");
+                        continue; // not required
+                }
+
+                // the types must match, but the 'nil' symbol is acceptable as
+                // "no value"
+                if (param_it->type != arginfo.type &&
+                    !(param_it->type == Type::Symbol && param_it->value == "nil"))
+                        throw command_error("parameter '" + argname + "' expects type " +
+                                            to_string(arginfo.type) +
+                                            " but got " + to_string(param_it->type));
+        }
+
+        // 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[i].value == ":" + arg.first;}))
+                        throw command_error("unknown parameter '" + params[i].value + "'");
+        }
+
+        if (cinfo.handler)
+                cinfo.handler(params);
+}
+
+static Parameters::const_iterator
+find_param_node (const Parameters& params, const std::string& argname)
+{
+        for (size_t i = 1; i < params.size(); i += 2) {
+                if (i + 1 != params.size() &&
+                    params[i].type == Type::Symbol &&
+                    params[i].value == ':' + argname)
+                        return params.begin() + i + 1;
+        }
+
+        return params.end();
+}
+
+constexpr auto Nil = "nil";
+
+static bool
+is_nil(const Node& node)
+{
+        return node.type == Type::Symbol && node.value == Nil;
+}
+
+const std::string&
+Command::get_string_or (const Parameters& params, const std::string& argname,
+                        const std::string& alt)
+{
+        const auto it = find_param_node (params, argname);
+        if (it == params.end() || is_nil(*it))
+                return alt;
+        else if (it->type != Type::String)
+                throw Error(Error::Code::InvalidArgument, "expected <string> but got %s (value: '%s')",
+                            to_string(it->type).c_str(),
+                            it->value.c_str());
+
+        return it->value;
+}
+
+const std::string&
+Command::get_symbol_or (const Parameters& params, const std::string& argname,
+                        const std::string& alt)
+{
+        const auto it = find_param_node (params, argname);
+        if (it == params.end() || is_nil(*it))
+                return alt;
+        else if (it->type != Type::Symbol)
+                throw Error(Error::Code::InvalidArgument, "expected <symbol> but got %s (value: '%s')",
+                            to_string(it->type).c_str(),
+                            it->value.c_str());
+
+        return it->value;
+}
+
+
+int
+Command::get_int_or (const Parameters& params, const std::string& argname,
+                     int alt)
+{
+        const auto it = find_param_node (params, argname);
+        if (it == params.end() || is_nil(*it))
+                return alt;
+        else if (it->type != Type::Integer)
+                throw Error(Error::Code::InvalidArgument, "expected <integer> but got %s",
+                            to_string(it->type).c_str());
+        else
+                return ::atoi(it->value.c_str());
+}
+
+bool
+Command::get_bool_or (const Parameters& params, const std::string& argname,
+                      bool alt)
+{
+        const auto it = find_param_node (params, argname);
+        if (it == params.end())
+                return alt;
+        else if (it->type != Type::Symbol)
+                throw Error(Error::Code::InvalidArgument, "expected <symbol> but got %s",
+                            to_string(it->type).c_str());
+        else
+                return it->value != Nil;
+}
+
+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() || is_nil(*it))
+                return {};
+        else if (it->type != Type::List)
+                throw Error(Error::Code::InvalidArgument, "expected <list> but got %s",
+                            to_string(it->type).c_str());
+
+        std::vector<std::string> vec;
+        for (const auto& n: it->children) {
+                if (n.type != Type::String)
+                        throw Error(Error::Code::InvalidArgument,
+                                    "expected string element but got %s",
+                                    to_string(n.type).c_str());
+                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..58c2f11
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+** 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-parser.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 = std::vector<Sexp::Node>;
+
+int                get_int_or    (const Parameters& parms, const std::string& argname, int alt=0);
+bool               get_bool_or   (const Parameters& parms, const std::string& argname, bool alt=false);
+const std::string& get_string_or (const Parameters& parms, const std::string& argname, const std::string& alt="");
+const std::string& get_symbol_or (const Parameters& parms, const std::string& argname, const std::string& alt="nil");
+
+
+std::vector<std::string> get_string_vec  (const Parameters& params, const std::string& argname);
+
+
+// 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::Node) 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::Node& 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-date.c b/lib/utils/mu-date.c
new file mode 100644 (file)
index 0000000..4d46397
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+** Copyright (C) 2012  <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.h>
+#include <stdlib.h>
+#include <ctype.h>
+
+#include "mu-util.h"
+#include "mu-date.h"
+#include "mu-str.h"
+
+const char*
+mu_date_str_s (const char* frm, time_t t)
+{
+       struct tm       *tmbuf;
+       static char      buf[128];
+       static int       is_utf8 = -1;
+       size_t           len;
+
+       if (G_UNLIKELY(is_utf8 == -1))
+               is_utf8 = mu_util_locale_is_utf8 () ? 1 : 0;
+
+       g_return_val_if_fail (frm, NULL);
+
+       tmbuf = localtime(&t);
+       len = strftime (buf, sizeof(buf) - 1, frm, tmbuf);
+       if (len == 0)
+               return ""; /* not necessarily an error... */
+
+       if (!is_utf8) {
+               /* charset is _not_ utf8, so we need to convert it, so
+                * the date could contain locale-specific characters*/
+               gchar *conv;
+               GError *err;
+               err = NULL;
+               conv = g_locale_to_utf8 (buf, -1, NULL, NULL, &err);
+               if (err) {
+                       g_warning ("conversion failed: %s", err->message);
+                       g_error_free (err);
+                       strcpy (buf, "<error>");
+               } else {
+                       strncpy (buf, conv, sizeof(buf)-1);
+                       buf[sizeof(buf)-1] = '\0';
+               }
+
+               g_free (conv);
+       }
+
+       return buf;
+}
+
+char*
+mu_date_str (const char *frm, time_t t)
+{
+       return g_strdup (mu_date_str_s(frm, t));
+}
+
+
+const char*
+mu_date_display_s (time_t t)
+{
+       time_t now;
+       static const time_t SECS_IN_DAY = 24 * 60 * 60;
+
+       now = time (NULL);
+
+       if (ABS(now - t) > SECS_IN_DAY)
+               return mu_date_str_s ("%x", t);
+       else
+               return mu_date_str_s ("%X", t);
+}
diff --git a/lib/utils/mu-date.h b/lib/utils/mu-date.h
new file mode 100644 (file)
index 0000000..c7cf6c7
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+** 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.
+**
+*/
+
+#include <glib.h>
+
+#ifndef __MU_DATE_H__
+#define __MU_DATE_H__
+
+G_BEGIN_DECLS
+
+
+/**
+ * @addtogroup MuDate
+ * Date-related functions
+ * @{
+ */
+
+/**
+ * get a string for a given time_t
+ *
+ * mu_date_str_s returns a ptr to a static buffer,
+ * while mu_date_str returns dynamically allocated
+ * 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
+ *
+ * @return a string representation of the time; see above for what to
+ * do with it. Length is max. 128 bytes, inc. the ending \0.  if the
+ * format is too long, the value will be truncated. in practice this
+ * should not happen.
+ */
+const char* mu_date_str_s (const char* frm, time_t t) G_GNUC_CONST;
+char*       mu_date_str   (const char* frm, time_t t) G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * get a display string for a given time_t; if the given is less than
+ * 24h from the current time, we display the time, otherwise the date,
+ * using the preferred date/time for the current locale
+ *
+ * mu_str_display_date_s returns a ptr to a static buffer,
+ *
+ * @param t the time as time_t
+ *
+ * @return a string representation of the time/date
+ */
+const char* mu_date_display_s (time_t t);
+
+G_END_DECLS
+
+#endif /*__MU_DATE_H__*/
diff --git a/lib/utils/mu-error.hh b/lib/utils/mu-error.hh
new file mode 100644 (file)
index 0000000..b2d6ed2
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+** Copyright (C) 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_ERROR_HH__
+#define MU_ERROR_HH__
+
+#include <stdexcept>
+#include "mu-utils.hh"
+#include <glib.h>
+
+namespace Mu {
+
+struct Error final: public std::exception {
+
+        enum struct Code {
+                AccessDenied,
+                Command,
+                File,
+                Index,
+                Internal,
+                InvalidArgument,
+                Message,
+                NotFound,
+                Parsing,
+                Query,
+                SchemaMismatch,
+                Store,
+        };
+
+        /**
+         * Construct an error
+         *
+         * @param codearg error-code
+         * #param msgarg the error diecription
+         */
+        Error(Code codearg, const std::string& msgarg):
+                code_{codearg}, what_{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_ = format(frm, args);
+                va_end(args);
+        }
+
+        Error(Error&& rhs)      = default;
+        Error(const Error& rhs) = delete;
+
+        /**
+         * 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_ = format(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() = default;
+
+        /**
+         * Get the descriptiove 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 { return code_; }
+
+
+
+private:
+        const Code   code_;
+        std::string  what_;
+
+};
+
+
+} // namespace Mu
+
+
+#endif /* MU_ERROR_HH__ */
diff --git a/lib/utils/mu-log.c b/lib/utils/mu-log.c
new file mode 100644 (file)
index 0000000..92ea188
--- /dev/null
@@ -0,0 +1,319 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** Copyright (C) 2008-2016 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.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_CONFIG_H*/
+
+#include "mu-log.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <time.h>
+#include <errno.h>
+#include <string.h>
+
+#include "mu-util.h"
+
+#define MU_MAX_LOG_FILE_SIZE 1000 * 1000 /* 1 MB (SI units) */
+#define MU_LOG_FILE "mu.log"
+
+struct _MuLog {
+       int _fd;           /* log file descriptor */
+
+       MuLogOptions _opts;
+
+       gboolean _color_stdout;   /* whether to use color */
+       gboolean _color_stderr;
+
+       GLogFunc _old_log_func;
+};
+typedef struct _MuLog MuLog;
+
+/* we use globals, because logging is a global operation as it
+ * globally modifies the behaviour of g_warning and friends
+ */
+static MuLog* MU_LOG = NULL;
+static void log_write (const char* domain, GLogLevelFlags level,
+                      const gchar *msg);
+
+static void
+try_close (int fd)
+{
+       if (fd < 0)
+               return;
+
+       if (close (fd) < 0)
+               g_printerr ("%s: close() of fd %d failed: %s\n",
+                                   __func__, fd, strerror(errno));
+}
+
+static void
+silence (void)
+{
+       return;
+}
+
+gboolean
+mu_log_init_silence (void)
+{
+       g_return_val_if_fail (!MU_LOG, FALSE);
+
+       MU_LOG        = g_new0 (MuLog, 1);
+       MU_LOG->_fd   = -1;
+
+       mu_log_options_set (MU_LOG_OPTIONS_NONE);
+
+       MU_LOG->_old_log_func =
+               g_log_set_default_handler ((GLogFunc)silence, NULL);
+
+       return TRUE;
+}
+
+static void
+log_handler (const gchar* log_domain, GLogLevelFlags log_level,
+            const gchar* msg)
+{
+       if ((log_level & G_LOG_LEVEL_DEBUG) &&
+           !(MU_LOG->_opts & MU_LOG_OPTIONS_DEBUG))
+               return;
+
+       log_write (log_domain ? log_domain : "mu", log_level, msg);
+}
+
+
+void
+mu_log_options_set (MuLogOptions opts)
+{
+       g_return_if_fail (MU_LOG);
+
+       MU_LOG->_opts = opts;
+
+       /* when color is, only enable it when output is to a tty */
+       if (MU_LOG->_opts & MU_LOG_OPTIONS_COLOR) {
+               MU_LOG->_color_stdout = isatty(fileno(stdout));
+               MU_LOG->_color_stderr = isatty(fileno(stderr));
+
+       }
+}
+
+
+MuLogOptions
+mu_log_options_get (void)
+{
+       g_return_val_if_fail (MU_LOG, MU_LOG_OPTIONS_NONE);
+
+       return MU_LOG->_opts;
+}
+
+
+static gboolean
+move_log_file (const char *logfile)
+{
+       gchar *logfile_old;
+       int rv;
+
+       logfile_old = g_strdup_printf ("%s.old", logfile);
+       rv = rename (logfile, logfile_old);
+       g_free (logfile_old);
+
+       if (rv != 0) {
+               g_warning ("failed to move %s to %s.old: %s",
+                          logfile, logfile, strerror(rv));
+               return FALSE;
+       } else
+               return TRUE;
+
+}
+
+
+static gboolean
+log_file_backup_maybe (const char *logfile)
+{
+       struct stat statbuf;
+
+       if (stat (logfile, &statbuf) != 0) {
+               if (errno == ENOENT)
+                       return TRUE; /* it did not exist yet, no problem */
+               else {
+                       g_warning ("failed to stat(2) %s: %s",
+                                          logfile, strerror(errno));
+                       return FALSE;
+               }
+       }
+
+       /* log file is still below the max size? */
+       if (statbuf.st_size <= MU_MAX_LOG_FILE_SIZE)
+               return TRUE;
+
+       /* log file is too big!; we move it to <logfile>.old, overwriting */
+       return move_log_file (logfile);
+}
+
+
+gboolean
+mu_log_init (const char* logfile, MuLogOptions opts)
+{
+       int fd;
+
+       /* only init once... */
+       g_return_val_if_fail (!MU_LOG, FALSE);
+       g_return_val_if_fail (logfile, FALSE);
+
+       if (opts & MU_LOG_OPTIONS_BACKUP)
+               if (!log_file_backup_maybe(logfile)) {
+                       g_warning ("failed to backup log file");
+                       return FALSE;
+               }
+
+       fd = open (logfile, O_WRONLY|O_CREAT|O_APPEND, 00600);
+       if (fd < 0) {
+               g_warning ("%s: open() of '%s' failed: %s",  __func__,
+                          logfile, strerror(errno));
+               return FALSE;
+       }
+
+       MU_LOG      = g_new0 (MuLog, 1);
+       MU_LOG->_fd = fd;
+
+       mu_log_options_set (opts);
+
+       MU_LOG->_old_log_func =
+               g_log_set_default_handler ((GLogFunc)log_handler, NULL);
+
+       MU_WRITE_LOG ("logging started");
+
+       return TRUE;
+}
+
+void
+mu_log_uninit (void)
+{
+       if (!MU_LOG)
+               return;
+
+       MU_WRITE_LOG ("logging stopped");
+
+       try_close (MU_LOG->_fd);
+       g_free (MU_LOG);
+
+       MU_LOG = NULL;
+}
+
+
+static const char*
+levelstr (GLogLevelFlags level)
+{
+       switch (level) {
+       case G_LOG_LEVEL_WARNING:  return  " [WARN] ";
+       case G_LOG_LEVEL_ERROR :   return  " [ERR ] ";
+       case G_LOG_LEVEL_DEBUG:    return  " [DBG ] ";
+       case G_LOG_LEVEL_CRITICAL: return  " [CRIT] ";
+       case G_LOG_LEVEL_MESSAGE:  return  " [MSG ] ";
+       case G_LOG_LEVEL_INFO :    return  " [INFO] ";
+       default:                   return  " [LOG ] ";
+       }
+}
+
+
+
+#define color_stdout_maybe(C)                                      \
+       do{if (MU_LOG->_color_stdout) fputs ((C),stdout);} while (0)
+#define color_stderr_maybe(C)                                      \
+       do{if (MU_LOG->_color_stderr) fputs ((C),stderr);} while (0)
+
+
+
+static void
+log_write_fd (GLogLevelFlags level, const gchar *msg)
+{
+       time_t now;
+       char timebuf [22];
+       const char *mylevel;
+
+       /* get the time/date string */
+       now = time(NULL);
+       strftime (timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
+                 localtime(&now));
+
+       if (write (MU_LOG->_fd, timebuf, strlen (timebuf)) < 0)
+               goto err;
+
+       mylevel = levelstr (level);
+       if (write (MU_LOG->_fd, mylevel, strlen (mylevel)) < 0)
+               goto err;
+
+       if (write (MU_LOG->_fd, msg, strlen (msg)) < 0)
+               goto err;
+
+       if (write (MU_LOG->_fd, "\n", strlen ("\n")) < 0)
+               goto err;
+
+       return; /* all went well */
+
+err:
+       fprintf (stderr, "%s: failed to write to log: %s\n",
+                __func__,  strerror(errno));
+}
+
+
+static void
+log_write_stdout_stderr (GLogLevelFlags level, const gchar *msg)
+{
+       const char *mu;
+
+       mu = MU_LOG->_opts & MU_LOG_OPTIONS_NEWLINE ?
+               "\nmu: " : "mu: ";
+
+       if (!(MU_LOG->_opts & MU_LOG_OPTIONS_QUIET) &&
+           (level & G_LOG_LEVEL_MESSAGE)) {
+               color_stdout_maybe (MU_COLOR_GREEN);
+               fputs (mu, stdout);
+               fputs (msg,    stdout);
+               fputs ("\n",   stdout);
+               color_stdout_maybe (MU_COLOR_DEFAULT);
+       }
+
+       /* for errors, log them to stderr as well */
+       if (level & G_LOG_LEVEL_ERROR ||
+           level & G_LOG_LEVEL_CRITICAL ||
+           level & G_LOG_LEVEL_WARNING) {
+               color_stderr_maybe (MU_COLOR_RED);
+               fputs (mu,     stderr);
+               fputs (msg,    stderr);
+               fputs ("\n",   stderr);
+               color_stderr_maybe (MU_COLOR_DEFAULT);
+       }
+}
+
+
+static void
+log_write (const char* domain, GLogLevelFlags level, const gchar *msg)
+{
+       g_return_if_fail (MU_LOG);
+
+       log_write_fd (level, msg);
+       log_write_stdout_stderr (level, msg);
+}
diff --git a/lib/utils/mu-log.h b/lib/utils/mu-log.h
new file mode 100644 (file)
index 0000000..fc05d0b
--- /dev/null
@@ -0,0 +1,99 @@
+/* -*-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 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_LOG_H__
+#define __MU_LOG_H__
+
+#include <glib.h>
+
+/* mu log is the global logging system */
+
+G_BEGIN_DECLS
+
+enum _MuLogOptions {
+       MU_LOG_OPTIONS_NONE     = 0,
+
+       /* when size of log file > MU_MAX_LOG_FILE_SIZE, move the log
+        * file to <log file>.old and start a new one. The .old file will
+        * overwrite existing files of that name */
+       MU_LOG_OPTIONS_BACKUP   = 1 << 1,
+
+       /* quiet: don't log non-errors to stdout/stderr */
+       MU_LOG_OPTIONS_QUIET    = 1 << 2,
+
+       /* should lines be preceded by \n? useful when errors come
+        * during indexing */
+       MU_LOG_OPTIONS_NEWLINE  = 1 << 3,
+
+       /* color in output (iff output is to a tty) */
+       MU_LOG_OPTIONS_COLOR    = 1 << 4,
+
+       /* log everything to stderr */
+       MU_LOG_OPTIONS_STDERR   = 1 << 5,
+
+       /* debug: debug include debug-level information */
+       MU_LOG_OPTIONS_DEBUG    = 1 << 6
+};
+typedef enum _MuLogOptions MuLogOptions;
+
+
+/**
+ * write logging information to a log file
+ *
+ * @param full path to the log file (does not have to exist yet, but
+ * it's directory must)
+ * @param opts logging options
+ *
+ * @return TRUE if initialization succeeds, FALSE otherwise
+ */
+gboolean mu_log_init (const char *logfile, MuLogOptions opts)
+          G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * be silent except for runtime errors, which will be written to
+ * stderr.
+ *
+ * @return TRUE if initialization succeeds, FALSE otherwise
+ */
+gboolean mu_log_init_silence    (void) G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * uninitialize the logging system, and free all resources
+ */
+void mu_log_uninit             (void);
+
+/**
+ * set logging options, a logical-OR'd value of MuLogOptions
+ *
+ * @param opts the options (logically OR'd)
+ */
+void mu_log_options_set (MuLogOptions opts);
+
+/**
+ * get logging options, a logical-OR'd value of MuLogOptions
+ *
+ * @param opts the options (logically OR'd)
+ */
+MuLogOptions mu_log_options_get (void);
+
+G_END_DECLS
+
+#endif /*__MU_LOG_H__*/
diff --git a/lib/utils/mu-sexp-parser.cc b/lib/utils/mu-sexp-parser.cc
new file mode 100644 (file)
index 0000000..52f418a
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+** Copyright (C) 2020 djcb <djcb@evergrey>
+**
+** 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-parser.hh"
+#include "mu-utils.hh"
+
+using namespace Mu;
+using namespace Sexp;
+
+__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 = format(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 Node parse (const std::string& expr, size_t& pos);
+
+static Node
+parse_list (const std::string& expr, size_t& pos)
+{
+        if (expr[pos] != '(') // sanity check.
+                throw parsing_error(pos, "expected: '(' but got '%c", expr[pos]);
+
+        std::vector<Node> children;
+
+        ++pos;
+        while (expr[pos] != ')' && pos != expr.size())
+                children.emplace_back(parse(expr, pos));
+
+        if (expr[pos] != ')')
+                throw parsing_error(pos, "expected: ')' but got '%c'", expr[pos]);
+        ++pos;
+        return Node{std::move(children)};
+}
+
+// parse string
+static Node
+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 Node{Type::String, std::move(str)};
+}
+
+static Node
+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 Node {Type::Integer, std::move(num)};
+}
+
+static Node
+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 Node { Type::Symbol, std::move(symbol)};
+}
+
+
+static Node
+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 =[&]() -> Node {
+                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;
+}
+
+Node
+Sexp::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;
+}
diff --git a/lib/utils/mu-sexp-parser.hh b/lib/utils/mu-sexp-parser.hh
new file mode 100644 (file)
index 0000000..ecb97f2
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+** Copyright (C) 2020 djcb <djcb@evergrey>
+**
+** 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_PARSER_HH__
+#define MU_SEXP_PARSER_HH__
+
+#include <string>
+#include <vector>
+
+#include "utils/mu-error.hh"
+
+namespace Mu {
+namespace Sexp {
+
+/// Simple s-expression parser 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))
+
+/// Node type
+enum struct Type { List, String, Integer, Symbol };
+
+/// Parse node
+struct Node {
+        /**
+         * Construct a new non-list node
+         *
+         * @param typearg the type of node
+         * @param valuearg the value
+         */
+        Node(Type typearg, std::string&& valuearg):
+                type{typearg}, value{std::move(valuearg)} {
+                if (typearg == Type::List)
+                        throw Error(Error::Code::Parsing,
+                                    "atomic type cannot be a <list>");
+        }
+
+        /**
+         * Construct a list node
+
+         * @param childrenarg  the list children
+         *
+         * @return
+         */
+        explicit Node(std::vector<Node>&& childrenarg):
+                type{Type::List}, children{std::move(childrenarg)}
+                {}
+
+        const Type              type; /**<  Type of node */
+        const std::string       value; /**< String value of node (only for non-Type::List)*/
+        const std::vector<Node> children; /**< Chiidren of node (only for Type::List) */
+};
+
+/**
+ * Parse the string as an s-expressi9on.
+ *
+ * @param expr an s-expression string
+ *
+ * @return the parsed s-expression, or throw Error.
+ */
+Node parse(const std::string& expr);
+
+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::Integer: os << "<integer>"; break;
+        case Sexp::Type::Symbol:  os << "<symbol>"; break;
+        default: throw std::runtime_error ("unknown node type");
+        }
+
+        return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Sexp::Node& node)
+{
+        os << node.type;
+        if (node.type == Sexp::Type::List) {
+                os << '(';
+                for (auto&& elm: node.children)
+                        os <<  elm;
+                os << ')';
+        } else
+                os << '{' << node.value << '}';
+
+        return os;
+}
+
+
+} // Sexp
+
+
+} // Mu
+
+#endif /* MU_SEXP_PARSER_HH__ */
diff --git a/lib/utils/mu-str.c b/lib/utils/mu-str.c
new file mode 100644 (file)
index 0000000..607d322
--- /dev/null
@@ -0,0 +1,453 @@
+/* -*-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 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.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+
+#include <glib.h>
+#include <string.h>
+#include <ctype.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "mu-util.h" /* PATH_MAX */
+#include "mu-str.h"
+
+const char*
+mu_str_size_s  (size_t s)
+{
+       static char      buf[32];
+       char            *tmp;
+
+       tmp = g_format_size_for_display ((goffset)s);
+       strncpy (buf, tmp, sizeof(buf));
+       buf[sizeof(buf) -1] = '\0'; /* just in case */
+       g_free (tmp);
+
+       return buf;
+}
+
+char*
+mu_str_size (size_t s)
+{
+       return g_strdup (mu_str_size_s(s));
+}
+
+
+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;
+}
+
+
+
+
+char*
+mu_str_replace (const char *str, const char *substr, const char *repl)
+{
+       GString         *gstr;
+       const char      *cur;
+
+       g_return_val_if_fail (str, NULL);
+       g_return_val_if_fail (substr, NULL);
+       g_return_val_if_fail (repl, NULL);
+
+       gstr = g_string_sized_new (2 * strlen (str));
+
+       for (cur = str; *cur; ++cur) {
+               if (g_str_has_prefix (cur, substr)) {
+                       g_string_append (gstr, repl);
+                       cur += strlen (substr) - 1;
+               } else
+                       g_string_append_c (gstr, *cur);
+       }
+
+       return g_string_free (gstr, FALSE);
+}
+
+
+
+char*
+mu_str_from_list (const GSList *lst, char sepa)
+{
+       const GSList *cur;
+       char *str;
+
+       g_return_val_if_fail (sepa, NULL);
+
+       for (cur = lst, str = NULL; cur; cur = g_slist_next(cur)) {
+
+               char *tmp;
+               /* two extra dummy '\0' so -Wstack-protector won't complain */
+               char sep[4] = { '\0', '\0', '\0', '\0' };
+               sep[0] = cur->next ? sepa : '\0';
+
+               tmp = g_strdup_printf ("%s%s%s",
+                                      str ? str : "",
+                                      (gchar*)cur->data,
+                                      sep);
+               g_free (str);
+               str = tmp;
+       }
+
+       return str;
+}
+
+GSList*
+mu_str_to_list (const char *str, char sepa, gboolean strip)
+{
+       GSList *lst;
+       gchar **strs, **cur;
+       /* two extra dummy '\0' so -Wstack-protector won't complain */
+       char sep[4] = { '\0', '\0', '\0', '\0' };
+
+       g_return_val_if_fail (sepa, NULL);
+
+       if (!str)
+               return NULL;
+
+       sep[0] = sepa;
+       strs = g_strsplit (str, sep, -1);
+
+       for (cur = strs, lst = NULL; cur && *cur; ++cur) {
+               char *elm;
+               elm = g_strdup(*cur);
+               if (strip)
+                       elm = g_strstrip (elm);
+
+               lst = g_slist_prepend (lst, elm);
+       }
+
+       lst = g_slist_reverse (lst);
+       g_strfreev (strs);
+
+       return lst;
+}
+
+GSList*
+mu_str_esc_to_list (const char *strings)
+{
+       GSList *lst;
+       GString *part;
+       unsigned u;
+       gboolean quoted, escaped;
+
+       g_return_val_if_fail (strings, NULL);
+
+       part = g_string_new (NULL);
+
+       for (u = 0, lst = NULL, quoted = FALSE, escaped = FALSE;
+            u != strlen (strings); ++u) {
+
+               char kar;
+               kar = strings[u];
+
+               if (kar == '\\') {
+                       if (escaped)
+                               g_string_append_c (part, '\\');
+                       escaped = !escaped;
+                       continue;
+               }
+
+               if (quoted && kar != '"') {
+                       g_string_append_c (part, kar);
+                       continue;
+               }
+
+               switch (kar) {
+               case '"':
+                       if (!escaped)
+                               quoted = !quoted;
+                       else
+                               g_string_append_c (part, kar);
+                       continue;
+               case ' ':
+                       if (part->len > 0) {
+                               lst = g_slist_prepend
+                                       (lst, g_string_free (part, FALSE));
+                               part = g_string_new (NULL);
+                       }
+                       continue;
+               default:
+                       g_string_append_c (part, kar);
+               }
+       }
+
+       if (part->len)
+               lst = g_slist_prepend (lst, g_string_free (part, FALSE));
+
+       return g_slist_reverse (lst);
+}
+
+
+void
+mu_str_free_list (GSList *lst)
+{
+       g_slist_foreach (lst, (GFunc)g_free, NULL);
+       g_slist_free (lst);
+}
+
+
+/* this function is critical for sorting performance; therefore, no
+ * regexps, but just some good old c pointer magic */
+const gchar*
+mu_str_subject_normalize (const gchar* str)
+{
+       const char* cur;
+
+       g_return_val_if_fail (str, NULL);
+
+       cur = str;
+       while (isspace(*cur)) ++cur; /* skip space */
+
+       /* starts with Re:? */
+       if (tolower(cur[0]) == 'r' && tolower(cur[1]) == 'e')
+               cur += 2;
+       /* starts with Fwd:? */
+       else if (tolower(cur[0]) == 'f' && tolower(cur[1]) == 'w' &&
+                tolower(cur[2]) == 'd')
+               cur += 3;
+       else /* nope, different string */
+               return str;
+
+       /* we're now past either 'Re' or 'Fwd'. Maybe there's a [<num>] now?
+        * ie., the Re[3]: foo case */
+       if (cur[0] == '[') { /* handle the Re[3]: case */
+               if (isdigit(cur[1])) {
+                       do { ++cur; } while (isdigit(*cur));
+                       if ( cur[0] != ']') {
+                               return str; /* nope: no ending ']' */
+                       } else /* skip ']' and space */
+                               do { ++cur; } while (isspace(*cur));
+               } else /* nope: no number after '[' */
+                       return str;
+       }
+
+       /* now, cur points past either 're' or 'fwd', possibly with
+        * [<num>]; check if it's really a prefix -- after re or fwd
+        * there should either a ':' and possibly some space */
+       if (cur[0] == ':') {
+               do { ++cur; } while (isspace(*cur));
+               /* note: there may still be another prefix, such as
+                * Re[2]: Fwd: foo */
+               return mu_str_subject_normalize (cur);
+       } else
+               return str; /* nope, it was not a prefix */
+}
+
+
+/* note: this function is *not* re-entrant, it returns a static buffer */
+const char*
+mu_str_fullpath_s (const char* path, const char* name)
+{
+       static char buf[PATH_MAX + 1];
+
+       g_return_val_if_fail (path, NULL);
+
+       g_snprintf (buf, sizeof(buf), "%s%c%s", path, G_DIR_SEPARATOR,
+                 name ? name : "");
+
+       return buf;
+}
+
+
+char*
+mu_str_escape_c_literal (const gchar* str, gboolean in_quotes)
+{
+       const char* cur;
+       GString *tmp;
+
+       g_return_val_if_fail (str, NULL);
+
+       tmp = g_string_sized_new (2 * strlen(str));
+
+       if (in_quotes)
+               g_string_append_c (tmp, '"');
+
+       for (cur = str; *cur; ++cur)
+               switch (*cur) {
+               case '\\': tmp = g_string_append   (tmp, "\\\\"); break;
+               case '"':  tmp = g_string_append   (tmp, "\\\""); break;
+               default:   tmp = g_string_append_c (tmp, *cur);
+               }
+
+       if (in_quotes)
+               g_string_append_c (tmp, '"');
+
+       return g_string_free (tmp, FALSE);
+}
+
+
+
+/* turn \0-terminated buf into ascii (which is a utf8 subset); convert
+ *   any non-ascii into '.'
+ */
+char*
+mu_str_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;
+}
+
+char*
+mu_str_utf8ify (const char *buf)
+{
+       char *utf8;
+
+       g_return_val_if_fail (buf, NULL);
+
+       utf8 = g_strdup (buf);
+
+       if (!g_utf8_validate (buf, -1, NULL))
+               mu_str_asciify_in_place (utf8);
+
+       return utf8;
+}
+
+
+
+gchar*
+mu_str_convert_to_utf8 (const char* buffer, const char *charset)
+{
+       GError *err;
+       gchar * utf8;
+
+       g_return_val_if_fail (buffer, NULL);
+       g_return_val_if_fail (charset, NULL );
+
+       err = NULL;
+       utf8 = g_convert_with_fallback (buffer, -1, "UTF-8",
+                                       charset, NULL,
+                                       NULL, NULL, &err);
+       if (!utf8) /* maybe the charset lied; try 8859-15 */
+               utf8 = g_convert_with_fallback (buffer, -1, "UTF-8",
+                                               "ISO8859-15", NULL,
+                                               NULL, NULL, &err);
+       /* final attempt, maybe it was utf-8 already */
+       if (!utf8 && g_utf8_validate (buffer, -1, NULL))
+               utf8 = g_strdup (buffer);
+
+       if (!utf8) {
+               g_warning ("%s: conversion failed from %s: %s",
+                        __func__, charset, err ? err->message : "");
+       }
+
+       g_clear_error (&err);
+
+       return utf8;
+}
+
+
+gchar*
+mu_str_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);
+}
+
+
+char*
+mu_str_remove_ctrl_in_place (char *str)
+{
+       char *orig, *cur;
+
+       g_return_val_if_fail (str, NULL);
+
+       orig = str;
+
+       for (cur = orig; *cur; ++cur) {
+               if (isspace(*cur)) {
+                       /* squash special white space into a simple space */
+                       *orig++ = ' ';
+               } else if (iscntrl(*cur)) {
+                       /* eat it */
+               } else
+                       *orig++ = *cur;
+       }
+
+       *orig = '\0';  /* ensure the updated string has a NULL */
+
+       return str;
+}
diff --git a/lib/utils/mu-str.h b/lib/utils/mu-str.h
new file mode 100644 (file)
index 0000000..f009a11
--- /dev/null
@@ -0,0 +1,221 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_STR_H__
+#define __MU_STR_H__
+
+#include <glib.h>
+#include <time.h>
+#include <sys/types.h>
+
+G_BEGIN_DECLS
+
+/**
+ * @addtogroup MuStr
+ * Various string utilities
+ * @{
+ */
+
+/**
+ * get a display size for a given size_t; uses M for sizes >
+ * 1000*1000, k for smaller sizes. Note: this function use the
+ * 10-based SI units, _not_ the powers-of-2 based ones.
+ *
+ * mu_str_size_s returns a ptr to a static buffer,
+ * while mu_str_size returns dynamically allocated
+ * memory that must be freed after use.
+ *
+ * @param t the size as an size_t
+ *
+ * @return a string representation of the size; see above
+ * for what to do with it
+ */
+const char* mu_str_size_s  (size_t s);
+char*       mu_str_size    (size_t s) G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * Replace all occurrences of substr in str with repl
+ *
+ * @param str a string
+ * @param substr some string to replace
+ * @param repl a replacement string
+ *
+ * @return a newly allocated string with the substr replaced by repl; free with g_free
+ */
+char *mu_str_replace (const char *str, const char *substr, const char *repl);
+
+
+/**
+ * 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;
+
+/**
+ * create a full path from a path + a filename. function is _not_
+ * reentrant.
+ *
+ * @param path a path (!= NULL)
+ * @param name a name (may be NULL)
+ *
+ * @return the path as a statically allocated buffer. don't free.
+ */
+const char* mu_str_fullpath_s (const char* path, const char* name);
+
+/**
+ * escape a string like a string literal in C; ie. replace \ with \\,
+ * and " with \"
+ *
+ * @param str a non-NULL str
+ * @param in_quotes whether the result should be enclosed in ""
+ *
+ * @return the escaped string, newly allocated (free with g_free)
+ */
+char* mu_str_escape_c_literal (const gchar* str, gboolean in_quotes)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * turn a string into plain ascii by replacing each non-ascii
+ * character with a dot ('.'). Replacement is done in-place.
+ *
+ * @param buf a buffer to asciify
+ *
+ * @return the buf ptr (as to allow for function composition)
+ */
+char* mu_str_asciify_in_place (char *buf);
+
+/**
+ * turn string in buf into valid utf8. If this string is not valid
+ * utf8 already, the function massages the offending characters.
+ *
+ * @param buf a buffer to utf8ify
+ *
+ * @return a newly allocated utf8 string
+ */
+char* mu_str_utf8ify (const char *buf);
+
+/**
+ * convert a string in a certain charset into utf8
+ *
+ * @param buffer a buffer to convert
+ * @param charset source character set.
+ *
+ * @return a UTF8 string (which you need to g_free when done with it),
+ * or NULL in case of error
+ */
+gchar* mu_str_convert_to_utf8 (const char* buffer, const char *charset);
+
+
+/**
+ * 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)
+
+/**
+ * convert a GSList of strings to a #sepa-separated list
+ *
+ * @param lst a GSList
+ * @param the separator character
+ *
+ * @return a newly allocated string
+ */
+char* mu_str_from_list (const GSList *lst, char sepa);
+
+/**
+ * convert a #sepa-separated list of strings in to a GSList
+ *
+ * @param str a #sepa-separated list of strings
+ * @param the separator character
+ * @param remove leading/trailing whitespace from the string
+ *
+ * @return a newly allocated GSList (free with mu_str_free_list)
+ */
+GSList* mu_str_to_list (const char *str, char sepa, gboolean strip);
+
+/**
+ * convert a string (with possible escaping) to a list. list items are
+ * separated by one or more spaces. list items can be quoted (using
+ * '"').
+ *
+ * @param str a string
+ *
+ * @return a list of elements or NULL in case of error, free with
+ * mu_str_free_list
+ */
+GSList* mu_str_esc_to_list (const char *str);
+
+/**
+ * free a GSList consisting of allocated strings
+ *
+ * @param lst a GSList
+ */
+void mu_str_free_list (GSList *lst);
+
+/**
+ * strip the subject of Re:, Fwd: etc.
+ *
+ * @param str a subject string
+ *
+ * @return a new string -- this is pointing somewhere inside the @str;
+ * no copy is made, don't free
+ */
+const gchar* mu_str_subject_normalize (const gchar* str);
+
+
+/**
+ * take a list of strings, and return the concatenation of their
+ * quoted forms
+ *
+ * @param params NULL-terminated array of strings
+ *
+ * @return the quoted concatenation of the strings
+ */
+gchar* mu_str_quoted_from_strv (const gchar **params);
+
+
+
+/**
+ * Remove control characters from a string
+ *
+ * @param str a string
+ *
+ * @return the str with control characters removed
+ */
+char* mu_str_remove_ctrl_in_place (char *str);
+
+
+/** @} */
+
+G_END_DECLS
+
+#endif /*__MU_STR_H__*/
diff --git a/lib/utils/mu-util.c b/lib/utils/mu-util.c
new file mode 100644 (file)
index 0000000..9f76c3a
--- /dev/null
@@ -0,0 +1,495 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+/*
+**
+** Copyright (C) 2008-2016 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.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_CONFIG_H*/
+
+#include "mu-util.h"
+#define _XOPEN_SOURCE 500
+
+#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 <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, 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;
+}
+
+
+const char*
+mu_util_cache_dir (void)
+{
+       static char cachedir [PATH_MAX];
+
+       g_snprintf (cachedir, sizeof(cachedir), "%s%cmu-%u",
+                 g_get_tmp_dir(), G_DIR_SEPARATOR,
+                 getuid());
+
+       return cachedir;
+}
+
+
+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, strerror (errno)); */
+               return FALSE;
+       }
+
+       if (stat (path, &statbuf) != 0) {
+               /* g_debug ("Cannot stat %s: %s", path, 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, strerror(errno));
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+
+int
+mu_util_create_writeable_fd (const char* path, mode_t mode,
+                            gboolean overwrite)
+{
+       errno = 0; /* clear! */
+       g_return_val_if_fail (path, -1);
+
+       if (overwrite)
+               return open (path, O_WRONLY|O_CREAT|O_TRUNC, mode);
+       else
+               return open (path, O_WRONLY|O_CREAT|O_EXCL, mode);
+}
+
+
+gboolean
+mu_util_is_local_file (const char* path)
+{
+       /* if it starts with file:// it's a local file (for the
+        * purposes of this function -- if it's on a remote FS it's
+        * still considered local) */
+       if (g_ascii_strncasecmp ("file://", path, strlen("file://")) == 0)
+               return TRUE;
+
+       if (access (path, R_OK) == 0)
+               return TRUE;
+
+       return FALSE;
+}
+
+
+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, gboolean allow_local, gboolean allow_remote,
+             GError **err)
+{
+       gboolean rv;
+       const gchar *argv[3];
+       const char *prog;
+
+       g_return_val_if_fail (path, FALSE);
+       g_return_val_if_fail (mu_util_is_local_file (path) || allow_remote,
+                             FALSE);
+       g_return_val_if_fail (!mu_util_is_local_file (path) || allow_local,
+                             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_with_lstat (const char *path)
+{
+       struct stat statbuf;
+
+       g_return_val_if_fail (path, DT_UNKNOWN);
+
+       if (lstat (path, &statbuf) != 0) {
+               g_warning ("stat failed on %s: %s", path, 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;
+}
+
+gboolean
+mu_util_printerr_encoded (const char *frm, ...)
+{
+       va_list args;
+       gboolean rv;
+
+       g_return_val_if_fail (frm, FALSE);
+
+       va_start (args, frm);
+       rv = print_args (stderr, frm, args);
+       va_end (args);
+
+       return rv;
+}
+
+
+char*
+mu_util_read_password (const char *prompt)
+{
+       char *pass;
+
+       g_return_val_if_fail (prompt, NULL);
+
+       /* note: getpass is obsolete; replace with something better */
+
+       pass = getpass (prompt); /* returns static mem, don't free */
+       if (!pass) {
+               if (errno)
+                       g_warning ("error: %s", strerror(errno));
+               return NULL;
+       }
+
+       return g_strdup (pass);
+}
diff --git a/lib/utils/mu-util.h b/lib/utils/mu-util.h
new file mode 100644 (file)
index 0000000..72be341
--- /dev/null
@@ -0,0 +1,444 @@
+/*
+** 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.
+**
+*/
+
+#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;
+
+
+/**
+ * get our the cache directory, typically, /tmp/mu-<userid>/
+ *
+ * @return the cache directory; don't free
+ */
+const char* mu_util_cache_dir (void) G_GNUC_CONST;
+
+/**
+ * create a writeable file and return its file descriptor (which
+ * you'll need to close(2) when done with it.)
+ *
+ * @param path the full path of the file to create
+ * @param the mode to open (ie. 0644 or 0600 etc., see chmod(3)
+ * @param overwrite should we allow for overwriting existing files?
+ *
+ * @return a file descriptor, or -1 in case of error. If it's a file
+ * system error, 'errno' may contain more info. use 'close()' when done
+ * with the file descriptor
+ */
+int mu_util_create_writeable_fd (const char* path, mode_t mode,
+                                gboolean overwrite)
+       G_GNUC_WARN_UNUSED_RESULT;
+
+
+/**
+ * check if file is local, ie. on the local file system. this means
+ * that it's either having a file URI, *or* that it's an existing file
+ *
+ * @param path a path
+ *
+ * @return TRUE if the file is local, FALSE otherwise
+ */
+gboolean mu_util_is_local_file (const char* path);
+
+
+/**
+ * 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;
+
+/**
+ * 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);
+
+/**
+ * print a formatted string (assumed to be in utf8-format) to stderr,
+ * 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_printerr_encoded (const char *frm, ...) G_GNUC_PRINTF(1,2);
+
+
+/**
+ * read a password from stdin (without echoing), and return it.
+ *
+ * @param prompt the prompt text before the password
+ *
+ * @return the password (free with g_free), or NULL
+ */
+char* mu_util_read_password (const char *prompt)
+       G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT;
+
+/**
+ * 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
+ *
+ * @param path full path of the file to open
+ * @param allow_local allow local files (ie. with file:// prefix or fs paths)
+ * @param allow_remote allow URIs (ie., http, mailto)
+ * @param err receives error information, if any
+ *
+ * @return TRUE if it succeeded, FALSE otherwise
+ */
+gboolean mu_util_play (const char *path, gboolean allow_local,
+                      gboolean allow_remote, 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? */
+       MU_FEATURE_CRYPTO    = 1 << 2   /* do we support crypto (Gmime >= 2.6) */
+};
+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
+* lstat(3)
+ *
+ * @param path full path
+ *
+ * @return DT_REG, DT_DIR, DT_LNK, or DT_UNKNOWN (other values are not
+ * supported currently )
+ */
+unsigned char mu_util_get_dtype_with_lstat (const char *path);
+
+
+/**
+ * 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)
+
+
+
+/**
+ * log something in the log file; note, we use G_LOG_LEVEL_INFO
+ * for such messages
+ */
+#define MU_WRITE_LOG(...)                      \
+       G_STMT_START {                          \
+               g_log (G_LOG_DOMAIN,            \
+                      G_LOG_LEVEL_INFO,        \
+                      __VA_ARGS__);            \
+       } G_STMT_END
+
+
+
+#define MU_G_ERROR_CODE(GE) ((GE)&&(*(GE))?(*(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);
+
+/**
+ * calculate a 64-bit hash for the given string, based on a combination of the
+ * DJB and BKDR hash functions.
+ *
+ * @param a string
+ *
+ * @return the hash
+ */
+static inline guint64
+mu_util_get_hash (const char* str)
+{
+       guint32 djbhash;
+        guint32 bkdrhash;
+        guint32 bkdrseed;
+        guint64 hash;
+
+        djbhash  = 5381;
+        bkdrhash = 0;
+        bkdrseed = 1313;
+
+        for(unsigned u = 0U; str[u]; ++u) {
+               djbhash  = ((djbhash << 5) + djbhash) + str[u];
+               bkdrhash = bkdrhash * bkdrseed + str[u];
+       }
+
+        hash = djbhash;
+        return (hash<<32) | bkdrhash;
+}
+
+
+
+
+
+
+#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.cc b/lib/utils/mu-utils.cc
new file mode 100644 (file)
index 0000000..2354c0c
--- /dev/null
@@ -0,0 +1,471 @@
+/*
+**  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.
+*/
+
+
+#define _XOPEN_SOURCE
+#include <time.h>
+
+#define GNU_SOURCE
+#include <stdio.h>
+#include <stdint.h>
+
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "mu-utils.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;
+}
+
+std::string
+Mu::utf8_clean (const std::string& dirty)
+{
+       GString *gstr = g_string_sized_new (dirty.length());
+
+       for (auto cur = dirty.c_str(); 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);
+       }
+
+       std::string clean(gstr->str, gstr->len);
+       g_string_free (gstr, TRUE);
+
+       clean.erase (0, clean.find_first_not_of(" "));
+       clean.erase (clean.find_last_not_of(" ") + 1); // remove trailing space
+
+       return clean;
+}
+
+std::vector<std::string>
+Mu::split (const std::string& str, const std::string& sepa)
+{
+       char **parts = g_strsplit(str.c_str(), sepa.c_str(), -1);
+       std::vector<std::string> vec;
+       for (auto part = parts; part && *part; ++part)
+               vec.push_back (*part);
+
+       g_strfreev(parts);
+
+       return vec;
+}
+
+std::string
+Mu::quote (const std::string& str)
+{
+       char *s = g_strescape (str.c_str(), NULL);
+       if (!s)
+               return {};
+
+       std::string res (s);
+       g_free (s);
+
+       return "\"" + res + "\"";
+}
+
+ std::string
+ Mu::format (const char *frm, ...)
+ {
+        va_list args;
+
+        va_start (args, frm);
+         auto str = format(frm, args);
+        va_end (args);
+
+        return str;
+ }
+
+std::string
+Mu::format (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;
+}
+
+
+constexpr const auto InternalDateFormat = "%010" G_GINT64_FORMAT;
+constexpr const char InternalDateMin[] = "0000000000";
+constexpr const char InternalDateMax[] = "9999999999";
+static_assert(sizeof(InternalDateMin) == 10 + 1, "invalid");
+static_assert(sizeof(InternalDateMax) == 10 + 1, "invalid");
+
+static std::string
+date_boundary (bool is_first)
+{
+       return is_first ? InternalDateMin : InternalDateMax;
+}
+
+std::string
+Mu::date_to_time_t_string (int64_t t)
+{
+       char buf[sizeof(InternalDateMax)];
+       g_snprintf (buf, sizeof(buf), InternalDateFormat, t);
+
+       return buf;
+}
+
+static std::string
+delta_ymwdhMs (const std::string& expr)
+{
+       char *endptr;
+       auto num = strtol  (expr.c_str(), &endptr, 10);
+       if (num <= 0 || num > 9999 || !endptr || !*endptr)
+               return date_boundary (true);
+
+       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 date_boundary (true);
+       }
+
+       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);
+
+       time_t t = MAX (0, (gint64)g_date_time_to_unix (then));
+
+       g_date_time_unref (then);
+       g_date_time_unref (now);
+
+       return date_to_time_t_string (t);
+}
+
+static std::string
+special_date (const std::string& d, bool is_first)
+{
+       if (d == "now")
+               return date_to_time_t_string (time(NULL));
+
+       else 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 date_to_time_t_string ((time_t)t);
+
+       } else
+               return date_boundary (is_first);
+}
+
+// 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;
+       }
+}
+
+std::string
+Mu::date_to_time_t_string (const std::string& dstr, bool is_first)
+{
+       gint64           t;
+       struct tm        tbuf;
+       GDateTime       *dtime;
+
+       /* one-sided dates */
+       if (dstr.empty())
+               return date_boundary (is_first);
+       else if (dstr == "today" || dstr == "now")
+               return special_date (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);});
+
+       memset (&tbuf, 0, sizeof tbuf);
+       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", &tbuf) &&
+           !strptime (date.c_str(), "%Y%m", &tbuf) &&
+           !strptime (date.c_str(), "%Y", &tbuf))
+               return date_boundary (is_first);
+
+       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);
+       if (!dtime) {
+               g_warning ("invalid %s date '%s'",
+                          is_first ? "lower" : "upper", date.c_str());
+               return date_boundary (is_first);
+       }
+
+       t = g_date_time_to_unix (dtime);
+       g_date_time_unref (dtime);
+
+       if (t < 0 || t > 9999999999)
+               return date_boundary (is_first);
+       else
+               return date_to_time_t_string (t);
+}
+
+constexpr const auto SizeFormat = "%010" G_GINT64_FORMAT;
+
+constexpr const char SizeMin[] = "0000000000";
+constexpr const char SizeMax[] = "9999999999";
+static_assert(sizeof(SizeMin) == 10 + 1, "invalid");
+static_assert(sizeof(SizeMax) == 10 + 1, "invalid");
+
+static std::string
+size_boundary (bool is_first)
+{
+       return is_first ? SizeMin : SizeMax;
+}
+
+std::string
+Mu::size_to_string (int64_t size)
+{
+       char buf[sizeof(SizeMax)];
+       g_snprintf (buf, sizeof(buf), SizeFormat, size);
+
+       return buf;
+}
+
+std::string
+Mu::size_to_string (const std::string& val, bool is_first)
+{
+       std::string      str;
+       GRegex          *rx;
+       GMatchInfo      *minfo;
+
+       /* one-sided ranges */
+       if (val.empty())
+               return size_boundary (is_first);
+
+       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)) {
+               gint64 size;
+               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);
+               str = size_to_string (size);
+       } else
+               str = size_boundary (is_first);
+
+       g_regex_unref (rx);
+       g_match_info_unref (minfo);
+
+       return str;
+}
+
+void
+Mu::assert_equal(const std::string& s1, const std::string& s2)
+{
+        g_assert_cmpstr (s1.c_str(), ==, s2.c_str());
+}
+
+void
+Mu::assert_equal (const Mu::StringVec& v1, const Mu::StringVec& v2)
+{
+        g_assert_cmpuint(v1.size(), ==, v2.size());
+
+        for (auto i = 0U; i != v1.size(); ++i)
+                assert_equal(v1[i], v2[i]);
+}
+
+
+void
+Mu::allow_warnings()
+{
+        g_test_log_set_fatal_handler(
+                [](const char*, GLogLevelFlags, const char*, gpointer) {
+                        return FALSE;
+                },{});
+
+}
diff --git a/lib/utils/mu-utils.hh b/lib/utils/mu-utils.hh
new file mode 100644 (file)
index 0000000..0c35170
--- /dev/null
@@ -0,0 +1,271 @@
+/*
+**  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 __MU_UTILS_HH__
+#define __MU_UTILS_HH__
+
+#include <string>
+#include <sstream>
+#include <vector>
+#include <cstdarg>
+#include <glib.h>
+#include <ostream>
+
+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);
+
+
+/**
+ * Split a string in parts
+ *
+ * @param str a string
+ * @param sepa the separator
+ *
+ * @return the parts.
+ */
+std::vector<std::string> split (const std::string& str,
+                               const std::string& sepa);
+
+/**
+ * Quote & escape a string
+ *
+ * @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 format (const char *frm, va_list args) __attribute__((format(printf, 1, 0)));
+
+
+/**
+ * Convert an date to the corresponding time expressed as a string with a
+ * 10-digit time_t
+ *
+ * @param date the date expressed a YYYYMMDDHHMMSS or any n... of the first
+ * characters.
+ * @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 expressed as a string
+ */
+std::string date_to_time_t_string (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);
+
+
+
+/**
+ * Convert a size string to a size in bytes
+ *
+ * @param sizestr the size string
+ * @param first
+ *
+ * @return the size expressed as a string with the decimal number of bytes
+ */
+std::string size_to_string (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();
+}
+
+
+/**
+ *
+ * don't repeat these catch blocks everywhere...
+ *
+ */
+
+#define MU_XAPIAN_CATCH_BLOCK                                          \
+       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 (...) {                                                 \
+               g_critical ("%s: caught exception", __func__);          \
+        }
+
+#define MU_XAPIAN_CATCH_BLOCK_G_ERROR(GE,E)                                    \
+       catch (const Xapian::DatabaseLockError &xerr) {                         \
+               mu_util_g_set_error ((GE),                                      \
+                                    MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK,      \
+                                    "%s: xapian error '%s'",                   \
+                                    __func__, xerr.get_msg().c_str());         \
+       } catch (const Xapian::DatabaseError &xerr) {                           \
+                mu_util_g_set_error ((GE),MU_ERROR_XAPIAN,                     \
+                                      "%s: xapian error '%s'",                 \
+                                      __func__, xerr.get_msg().c_str());       \
+       } catch (const Xapian::Error &xerr) {                                   \
+               mu_util_g_set_error ((GE),(E),                                  \
+                                        "%s: xapian error '%s'",               \
+                                        __func__, xerr.get_msg().c_str());     \
+       } catch (const std::runtime_error& ex) {                                \
+               mu_util_g_set_error ((GE),(MU_ERROR_INTERNAL),                  \
+                                    "%s: error: %s", __func__, ex.what());     \
+                                                                               \
+       } catch (...) {                                                         \
+               mu_util_g_set_error ((GE),(MU_ERROR_INTERNAL),                  \
+                                    "%s: caught exception", __func__);         \
+       }
+
+
+#define MU_XAPIAN_CATCH_BLOCK_RETURN(R)                                                \
+       catch (const Xapian::Error &xerr) {                                     \
+               g_critical ("%s: xapian error '%s'",                            \
+                           __func__, xerr.get_msg().c_str());                  \
+               return (R);                                                     \
+       } catch (const std::runtime_error& ex) {                                \
+               g_critical("%s: error: %s", __func__, ex.what());               \
+               return (R);                                                     \
+       } catch (...) {                                                         \
+               g_critical ("%s: caught exception", __func__);                  \
+               return (R);                                                     \
+       }
+
+#define MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(GE,E,R)                           \
+       catch (const Xapian::Error &xerr) {                                     \
+               mu_util_g_set_error ((GE),(E),                                  \
+                                    "%s: xapian error '%s'",                   \
+                                    __func__, xerr.get_msg().c_str());         \
+               return (R);                                                     \
+       } catch (const std::runtime_error& ex) {                                \
+               mu_util_g_set_error ((GE),(MU_ERROR_INTERNAL),                  \
+                                    "%s: error: %s", __func__, ex.what());     \
+               return (R);                                                     \
+       } catch (...) {                                                         \
+               if ((GE)&&!(*(GE)))                                             \
+                       mu_util_g_set_error ((GE),                              \
+                                            (MU_ERROR_INTERNAL),               \
+                                            "%s: caught exception", __func__); \
+               return (R);                                                     \
+         }
+
+
+
+
+/// 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; }                             \
+        static inline ET& operator&=(ET& e1, ET e2) { return e1 = e1 & e2;}                                      \
+        static inline ET& operator|=(ET& e1, ET e2) { return e1 = e1 | e2;}
+
+
+/**
+ * For unit tests, assert two std::string's are equal.
+ *
+ * @param s1 string1
+ * @param s2 string2
+ */
+void assert_equal(const std::string& s1, const std::string& s2);
+/**
+ * For unit tests, assert that to containers are the same.
+ *
+ * @param c1 container1
+ * @param c2 container2
+ */
+void assert_equal (const StringVec& v1, const StringVec& v2);
+
+/**
+ * For unit-tests, allow warnings in the current function.
+ *
+ */
+void allow_warnings();
+
+} // namespace Mu
+
+
+#endif /* __MU_UTILS_HH__ */
diff --git a/lib/utils/test-command-parser.cc b/lib/utils/test-command-parser.cc
new file mode 100644 (file)
index 0000000..f18e642
--- /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-command-parser.hh"
+#include "mu-utils.hh"
+
+using namespace Mu;
+
+static void
+test_param_getters()
+{
+        const auto node { Sexp::parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")};
+
+        std::cout << node << "\n";
+
+        g_assert_cmpint(Command::get_int_or(node.children,"bar"), ==, 123);
+        assert_equal(Command::get_string_or(node.children, "bra", "bla"), "bla");
+        assert_equal(Command::get_string_or(node.children, "cuux"), "456");
+
+        g_assert_true(Command::get_bool_or(node.children,"boo") == false);
+        g_assert_true(Command::get_bool_or(node.children,"bah") == true);
+}
+
+
+static bool
+call (const Command::CommandMap& cmap, const std::string& sexp) try
+{
+        const auto node{Sexp::parse(sexp)};
+        g_message ("invoking %s", to_string(node).c_str());
+
+        invoke (cmap, node);
+
+        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::Integer, false, "some integer"}}},
+                            "My command,",
+                            {}});
+
+        //std::cout << cmap << "\n";
+
+        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::Integer, 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::Integer, 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\")"));
+
+}
+
+
+int
+main (int argc, char *argv[]) try
+{
+        g_test_init (&argc, &argv, NULL);
+
+        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);
+
+       return g_test_run ();
+
+
+} catch (const std::runtime_error& re) {
+        std::cerr << re.what() << "\n";
+        return 1;
+}
diff --git a/lib/utils/test-mu-str.c b/lib/utils/test-mu-str.c
new file mode 100644 (file)
index 0000000..75fc71f
--- /dev/null
@@ -0,0 +1,275 @@
+/* -*-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
+test_mu_str_size_01 (void)
+{
+       struct lconv *lc;
+       char *tmp2;
+
+       lc = localeconv();
+
+       g_assert_cmpstr (mu_str_size_s (0), ==, "0 bytes");
+
+       tmp2 = g_strdup_printf ("97%s7 KB", lc->decimal_point);
+       g_assert_cmpstr (mu_str_size_s (100000), ==, tmp2);
+       g_free (tmp2);
+
+       tmp2 = g_strdup_printf ("1%s0 MB", lc->decimal_point);
+       g_assert_cmpstr (mu_str_size_s (1100*1000), ==,  tmp2);
+       g_free (tmp2);
+}
+
+
+
+static void
+test_mu_str_size_02 (void)
+{
+       struct lconv *lc;
+       char *tmp1, *tmp2;
+
+       lc = localeconv();
+
+       tmp2 = g_strdup_printf ("1%s0 MB", lc->decimal_point);
+       tmp1 = mu_str_size (999999);
+       g_assert_cmpstr (tmp1, !=, tmp2);
+
+       g_free (tmp1);
+       g_free (tmp2);
+}
+
+static void
+test_mu_str_esc_to_list (void)
+{
+       int                     i;
+       struct {
+               const char*  str;
+               const char* strs[3];
+       } strings [] = {
+               { "maildir:foo",
+                 {"maildir:foo", NULL, NULL}},
+               { "maildir:sent items",
+                 {"maildir:sent", "items", NULL}},
+               { "\"maildir:sent items\"",
+                 {"maildir:sent items", NULL, NULL}},
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(strings); ++i) {
+               GSList *lst, *cur;
+               unsigned u;
+               lst = mu_str_esc_to_list (strings[i].str);
+               for (cur = lst, u = 0; cur; cur = g_slist_next(cur), ++u)
+                       g_assert_cmpstr ((const char*)cur->data,==,
+                                        strings[i].strs[u]);
+               mu_str_free_list (lst);
+       }
+}
+
+
+
+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_replace (void)
+{
+       unsigned u;
+       struct {
+               const char*  str;
+               const char* sub;
+               const char *repl;
+               const char *exp;
+       } strings [] = {
+               { "hello", "ll", "xx", "hexxo" },
+               { "hello", "hello", "hi", "hi" },
+               { "hello", "foo", "bar", "hello" }
+       };
+
+       for (u = 0; u != G_N_ELEMENTS(strings); ++u) {
+               char *res;
+               res = mu_str_replace (strings[u].str,
+                                     strings[u].sub,
+                                     strings[u].repl);
+               g_assert_cmpstr (res,==,strings[u].exp);
+               g_free (res);
+       }
+}
+
+
+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);
+
+       /* mu_str_size */
+       g_test_add_func ("/mu-str/mu-str-size-01",
+                        test_mu_str_size_01);
+       g_test_add_func ("/mu-str/mu-str-size-02",
+                        test_mu_str_size_02);
+
+       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-replace",
+                        test_mu_str_replace);
+
+       g_test_add_func ("/mu-str/mu-str-esc-to-list",
+                        test_mu_str_esc_to_list);
+
+       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/test-mu-util.c b/lib/utils/test-mu-util.c
new file mode 100644 (file)
index 0000000..f3b0277
--- /dev/null
@@ -0,0 +1,248 @@
+/*
+** 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_with_lstat (MU_TESTMAILDIR), ==, DT_DIR);
+       g_assert_cmpuint (
+               mu_util_get_dtype_with_lstat (MU_TESTMAILDIR2), ==, DT_DIR);
+       g_assert_cmpuint (
+               mu_util_get_dtype_with_lstat (MU_TESTMAILDIR2 "/Foo/cur/mail5"),
+               ==, DT_REG);
+}
+
+
+static void
+test_mu_util_supports (void)
+{
+       gboolean has_guile;
+       gchar *path;
+
+       has_guile = FALSE;
+#ifdef BUILD_GUILE
+       has_guile = TRUE;
+#endif /*BUILD_GUILE*/
+
+       g_assert_cmpuint (mu_util_supports (MU_FEATURE_GUILE),  == ,has_guile);
+       g_assert_cmpuint (mu_util_supports (MU_FEATURE_CRYPTO), == ,TRUE);
+
+       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|
+                                 MU_FEATURE_CRYPTO),
+               ==,
+               has_guile && path ? TRUE : FALSE);
+}
+
+
+static void
+test_mu_util_program_in_path (void)
+{
+       g_assert_cmpuint (mu_util_program_in_path("ls"),==,TRUE);
+}
+
+
+
+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);
+
+       return g_test_run ();
+}
diff --git a/lib/utils/test-sexp-parser.cc b/lib/utils/test-sexp-parser.cc
new file mode 100644 (file)
index 0000000..0dccf96
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+** 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"
+
+using namespace Mu;
+
+static bool
+check_parse (const std::string& expr, const std::string& expected)
+{
+        try {
+                const auto parsed{to_string(Sexp::parse(expr))};
+                g_assert_cmpstr(parsed.c_str(), ==, expected.c_str());
+                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(R"(:foo-123)", "<symbol>{:foo-123}");
+        check_parse(R"("foo")",    "<string>{foo}");
+        check_parse(R"(12345)",    "<integer>{12345}");
+        check_parse(R"(-12345)",   "<integer>{-12345}");
+        check_parse(R"((123 bar "cuux"))",    "<list>(<integer>{123}<symbol>{bar}<string>{cuux})");
+
+        check_parse(R"("\"")", "<string>{\"}");
+        check_parse(R"("\\")", "<string>{\\}");
+}
+
+int
+main (int argc, char *argv[]) try
+{
+        g_test_init (&argc, &argv, NULL);
+
+        if (argc == 2) {
+                std::cout << Sexp::parse(argv[1]) << '\n';
+                return 0;
+        }
+
+        g_test_add_func ("/utils/command-parser/parse", test_parser);
+
+       return g_test_run ();
+
+
+} catch (const std::runtime_error& re) {
+        std::cerr << re.what() << "\n";
+        return 1;
+}
diff --git a/lib/utils/test-utils.cc b/lib/utils/test-utils.cc
new file mode 100644 (file)
index 0000000..b1f666e
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+** 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 <functional>
+
+#include "mu-utils.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 ()
+{
+       g_setenv ("TZ", "Europe/Helsinki", TRUE);
+
+       CaseVec cases = {
+               { "2015-09-18T09:10:23", true,  "1442556623" },
+               { "1972-12-14T09:10:23", true,  "0093165023" },
+               { "1854-11-18T17:10:23", true,  "0000000000" },
+
+               { "2000-02-31T09:10:23", true,  "0951861599" },
+               { "2000-02-29T23:59:59", true,  "0951861599" },
+
+               { "2016",               true,   "1451599200" },
+               { "2016",               false,  "1483221599" },
+
+               { "fnorb",               true,  "0000000000" },
+               { "fnorb",               false, "9999999999" },
+               { "",                    false, "9999999999" },
+               { "",                    true,  "0000000000" }
+       };
+
+       test_cases (cases, [](auto s, auto f){ return date_to_time_t_string(s,f); });
+}
+
+static void
+test_date_ymwdhMs (void)
+{
+       struct {
+               std::string     expr;
+               long            diff;
+               int             tolerance;
+       } tests[] = {
+               { "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 i = 0; i != G_N_ELEMENTS(tests); ++i) {
+               const auto diff = time(NULL) -
+                       strtol(Mu::date_to_time_t_string(tests[i].expr, true).c_str(),
+                              NULL, 10);
+               if (g_test_verbose())
+                       std::cerr << tests[i].expr << ' '
+                                 << diff << ' '
+                                 << tests[i].diff << std::endl;
+
+               g_assert_true (tests[i].diff - diff <= tests[i].tolerance);
+       }
+
+       g_assert_true (strtol(Mu::date_to_time_t_string("-1y", true).c_str(),
+                             NULL, 10) == 0);
+}
+
+static void
+test_size ()
+{
+       CaseVec cases = {
+               { "456", true,  "0000000456" },
+               { "",    false, "9999999999" },
+               { "",    true,  "0000000000" },
+       };
+
+       test_cases (cases, [](auto s, auto f){ return size_to_string(s,f); });
+}
+
+
+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_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, %u", "world", 123) ==
+                      "hello world, 123");
+}
+
+
+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);
+        }
+
+
+}
+
+
+
+int
+main (int argc, char *argv[])
+{
+       g_test_init (&argc, &argv, NULL);
+
+       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/size",  test_size);
+       g_test_add_func ("/utils/flatten",  test_flatten);
+       g_test_add_func ("/utils/clean",  test_clean);
+       g_test_add_func ("/utils/format",  test_format);
+        g_test_add_func ("/utils/define-bitmap",  test_define_bitmap);
+
+       return g_test_run ();
+}
diff --git a/m4/Makefile.am b/m4/Makefile.am
new file mode 100644 (file)
index 0000000..eeb8a05
--- /dev/null
@@ -0,0 +1,47 @@
+## 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
+
+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_14.m4     \
+       ax_file_escapes.m4              \
+       ax_is_release.m4                \
+       ax_lib_readline.m4              \
+       ax_require_defined.m4           \
+       ax_valgrind_check.m4            \
+       guile-2.2.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_14.m4 b/m4/ax_cxx_compile_stdcxx_14.m4
new file mode 100644 (file)
index 0000000..094db0d
--- /dev/null
@@ -0,0 +1,34 @@
+# =============================================================================
+#  https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx_14.html
+# =============================================================================
+#
+# SYNOPSIS
+#
+#   AX_CXX_COMPILE_STDCXX_14([ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+#   Check for baseline language coverage in the compiler for the C++14
+#   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++14.  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>
+#
+#   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 5
+
+AX_REQUIRE_DEFINED([AX_CXX_COMPILE_STDCXX])
+AC_DEFUN([AX_CXX_COMPILE_STDCXX_14], [AX_CXX_COMPILE_STDCXX([14], [$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..0d0822b
--- /dev/null
@@ -0,0 +1,107 @@
+# ===========================================================================
+#     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"
+    for readline_lib in readline edit editline; 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-2.2.m4 b/m4/guile-2.2.m4
new file mode 100644 (file)
index 0000000..89823e9
--- /dev/null
@@ -0,0 +1,394 @@
+## 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. 2.2), falling back to the previous stable version
+# (e.g. 2.0) 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],
+ [PKG_PROG_PKG_CONFIG
+  _guile_versions_to_search="m4_default([$1], [2.2 2.0 1.8])"
+  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. 2.2). 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=2.2
+  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..e055600
--- /dev/null
@@ -0,0 +1,37 @@
+## 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-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/mu-add.1 b/man/mu-add.1
new file mode 100644 (file)
index 0000000..fb4e081
--- /dev/null
@@ -0,0 +1,48 @@
+.TH MU ADD 1 "July 2012" "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                     |
+|    5 | some database update 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..38953be
--- /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 wrongly, 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.
+
+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 (see the \fBmu-find\fR(1) man page for 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-find.1 b/man/mu-find.1
new file mode 100644 (file)
index 0000000..5e46487
--- /dev/null
@@ -0,0 +1,389 @@
+.TH MU FIND 1 "19 April 2015" "User Manuals"
+
+.SH NAME
+
+mu find \- find e-mail messages in the \fBmu\fR database.
+
+mu mfind \- find e-mail messages in the \fBmu\fR database with mu4e defaults.
+
+.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).
+
+\fBmu mfind\fR is a version of \fBmu find\fR that defaults to
+\f--include-related\fR and \fB--skip-dups\fR, just like \fBmu4e\fR does.
+
+.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:2017..
+.fi
+
+would find all messages in 2017 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; the complete list:
+
+.nf
+       t       \fBt\fRo: recipient
+       c       \fBc\fRc: (carbon-copy) recipient
+       h       Bcc: (blind carbon-copy, \fBh\fRidden) 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)
+       p       Message \fBp\fRriority (high, normal, low)
+       s       Message \fBs\fRubject
+       i       Message-\fBi\fRd
+       m       \fBm\fRaildir
+       v       Mailing-list Id
+.fi
+
+
+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). The
+following fields are supported:
+
+.nf
+       cc,c            Cc (carbon-copy) recipient(s)
+       bcc,h           Bcc (blind-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)
+       list,v          Mailing-list id
+.fi
+
+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 --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.
+
+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.
+
+
+\fBWanderlust (old)\fR
+
+Another way to integrate \fBmu\fR and \fBwanderlust\fR is shown below; the
+aforementioned method is recommended, but if that does not work for some
+reason, the below can be an alternative.
+
+.nf
+(defvar mu-wl-mu-program     "/usr/local/bin/mu")
+(defvar mu-wl-search-folder  "search")
+
+(defun mu-wl-search ()
+  "search for messages with `mu', and jump to the results"
+   (let* ((muexpr (read-string "Find messages matching: "))
+         (sfldr  (concat elmo-maildir-folder-path "/"
+                   mu-wl-search-folder))
+         (cmdline (concat mu-wl-mu-program " find "
+                     "--clearlinks --format=links --linksdir='" sfldr "' "
+                    muexpr))
+         (rv (shell-command cmdline)))
+    (cond
+      ((= rv 0)  (message "Query succeeded"))
+      ((= rv 2)  (message "No matches found"))
+      (t (message "Error running query")))
+  (= rv 0)))
+
+(defun mu-wl-search-and-goto ()
+  "search and jump to the folder with the results"
+  (interactive)
+  (when (mu-wl-search)
+    (wl-summary-goto-folder-subr
+      (concat "." mu-wl-search-folder)
+      'force-update nil nil t)
+    (wl-summary-sort-by-date)))
+
+;; querying both in summary and folder
+(define-key wl-summary-mode-map (kbd "Q") ;; => query
+  '(lambda()(interactive)(mu-wl-search-and-goto)))
+(define-key wl-folder-mode-map (kbd "Q") ;; => query
+  '(lambda()(interactive)(mu-wl-search-and-goto)))
+
+.fi
+
+
+.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                  |
+|    2 | no matches (for 'mu find')     |
+|    4 | database is corrupted          |
+.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)
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..c5b6008
--- /dev/null
@@ -0,0 +1,188 @@
+.TH MU-INDEX 1 "February 2020" "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)\.
+
+Note that 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 '!' 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.
+
+The maildir must be on a single file-system; symlinks are not followed.
+
+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.
+
+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
+
+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\-\-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).
+
+As shown, \fBmu\fR has been getting faster with each release, even
+with relatively expensive new features such as text-normalization (for
+case-insensitve/accent-insensitive matching). The profiles are
+dominated by operations in the Xapian database now.
+
+.SH FILES
+\fBmu\fR stores logs of its operations and queries in \fI<muhome>/mu.log\fR
+(by default, this is \fI~/.cache/mu/mu.log\fR). Upon startup, \fBmu\fR checks the
+size of this log file. If it exceeds 1 MB, it will be moved to
+\fI~/.cache/mu/mu.log.old\fR, overwriting any existing file of that name, and start
+with an empty log file. This scheme allows for continued use of \fBmu\fR
+without the need for any manual maintenance of log files.
+
+.SH ENVIRONMENT
+
+\fBmu index\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 index\fR will try \fI~/Maildir\fR.
+
+.SH RETURN VALUE
+
+\fBmu index\fR return 0 upon successful completion, and any other number
+greater than 0 signals an 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-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..43032be
--- /dev/null
@@ -0,0 +1,46 @@
+.TH MU-INFO 1 "February 2020" "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.
+
+.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..34f95d8
--- /dev/null
@@ -0,0 +1,69 @@
+.TH MU-INIT 1 "February 2020" "User Manuals"
+
+.SH NAME
+
+mu init \- initialize the mu message database
+
+.SH SYNOPSIS
+
+.B mu init [options]
+
+.SH DESCRIPTION
+
+\fBmu init\fR is the \fBmu\fR command 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.
+
+.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. The maildir must be on a single file-system; and symbolic links
+are not supported.
+
+.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.
+
+.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.
+
+.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..c3f3f5c
--- /dev/null
@@ -0,0 +1,358 @@
+.TH MU QUERY 7 "28 December 2017" "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.
+
+\fBNOTE:\fR t 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
+.BR mu find
+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. Here is the full table, a shortcut character and a
+description.
+.EX1
+       cc,c            Cc (carbon-copy) recipient(s)
+       bcc,h           Bcc (blind-carbon-copy) recipient(s)
+       from,f          Message sender
+       to,t            To: recipient(s)
+       subject,s       Message subject
+       body,b          Message body
+       maildir,m       Maildir
+       msgid,i         Message-ID
+       prio,p          Message priority (\fIlow\fR, \fInormal\fR or \fIhigh\fR)
+       flag,g          Message Flags
+       date,d          Date range
+       size,z          Message size range
+       embed,e         Search inside embedded text parts
+       file,j          Attachment filename
+       mime,y          MIME-type of one or more message parts
+       tag,x           Tags for the message
+       list,v          Mailing list (e.g. the List-Id value)
+.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 'capibara' anywhere:
+.EX1
+subject:wombat and capibara
+.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)
diff --git a/man/mu-remove.1 b/man/mu-remove.1
new file mode 100644 (file)
index 0000000..36c25c2
--- /dev/null
@@ -0,0 +1,49 @@
+.TH MU REMOVE 1 "July 2012" "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                     |
+|    5 | some database update 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..aa4159b
--- /dev/null
@@ -0,0 +1,83 @@
+.TH MU SCRIPT 1 "June 2013" "User Manuals"
+
+.SH NAME
+
+mu script\- show the available mu scripts, and 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 \fI<muhome>/scripts\fR (which is typically
+\fI~/.mu/scripts\fR). 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..38640f7
--- /dev/null
@@ -0,0 +1,79 @@
+.TH MU VERIFY 1 "June 2015" "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.
+
+\fBmu verify\fR depends on \fBgpg\fR, and uses the one it finds in your
+\fBPATH\fR. If you want to use another one, you need to set \fBMU_GPG_PATH\fB
+to the full path to the desired \fBgpg\fR.
+
+.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).
+
+\" .TP
+\" \fB\-u\fR, \fB\-\-use\-agent\fR attempt to use the GPG-agent (see the the
+\" \fBgnupg-agent(1)\fR documentation). Note that GPG-agent is running many
+\" desktop-evironment; you can check whether this is the case using:
+\" .nf
+\"    $ env | grep GPG_AGENT_INFO
+\" .fi
+
+.SH EXAMPLES
+
+To display aggregated (one-line) information about the signatures 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.
+
+.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.
+
+.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),
+.BR gpg (1)
diff --git a/man/mu-view.1 b/man/mu-view.1
new file mode 100644 (file)
index 0000000..05ecd7d
--- /dev/null
@@ -0,0 +1,54 @@
+.TH MU VIEW 1 "June 2013" "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. Users are strongly recommended to use
+\fBgpg-agent\fR; however, if needed, \fBmu\fR will request the user password
+from the console.
+
+.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 gpg (1),
+.BR gpg-agent (1)
diff --git a/man/mu.1 b/man/mu.1
new file mode 100644 (file)
index 0000000..6998f33
--- /dev/null
+++ b/man/mu.1
@@ -0,0 +1,182 @@
+.TH MU 1 "February 2020" "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 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/mu/Makefile.am b/mu/Makefile.am
new file mode 100644 (file)
index 0000000..b0f8d8a
--- /dev/null
@@ -0,0 +1,120 @@
+## 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)                                          \
+       $(CODE_COVERAGE_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=                                                     \
+       $(JSON_GLIB_CFLAGS)                                     \
+       $(ASAN_CFLAGS)                                          \
+       $(WARN_CFLAGS)                                          \
+       $(CODE_COVERAGE_CFLAGS)                                 \
+       -Wno-switch-enum                                        \
+       -DMU_SCRIPTS_DIR="\"$(pkgdatadir)/scripts/\""
+
+AM_CXXFLAGS=                                                   \
+       $(ASAN_CXXCFLAGS)                                       \
+       $(WARN_CXXFLAGS)                                        \
+       $(CODE_COVERAGE_CFLAGS)
+
+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.c                                          \
+       mu-config.c                                             \
+       mu-config.h                                             \
+       mu-cmd-extract.c                                        \
+       mu-cmd-find.c                                           \
+       mu-cmd-index.c                                          \
+       mu-cmd-server.cc                                        \
+       mu-cmd-script.c                                         \
+       mu-cmd.c                                                \
+       mu-cmd.h
+
+BUILT_SOURCES=                                                 \
+       mu-help-strings.h
+
+mu-help-strings.h: 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)                                            \
+       $(READLINE_LIBS)                                        \
+       $(CODE_COVERAGE_LIBS)
+
+EXTRA_DIST=                                                    \
+       mu-help-strings.awk                                     \
+       mu-help-strings.txt
+
+noinst_PROGRAMS= $(TEST_PROGS)
+
+test_cflags=                                                   \
+       ${AM_CFLAGS}                                            \
+       -DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\"      \
+       -DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\"    \
+       -DMU_TESTMAILDIR3=\"${abs_top_srcdir}/lib/testdir3\"    \
+       -DMU_TESTMAILDIR4=\"${abs_top_srcdir}/lib/testdir4\"    \
+       -DMU_PROGRAM=\"${abs_top_builddir}/mu/mu\"              \
+       -DABS_CURDIR=\"${abs_builddir}\"                        \
+       -DABS_SRCDIR=\"${abs_srcdir}\"
+
+TEST_PROGS += test-mu-query
+test_mu_query_SOURCES= test-mu-query.c dummy.cc
+test_mu_query_CFLAGS=$(test_cflags)
+test_mu_query_LDADD=${top_builddir}/lib/libtestmucommon.la $(CODE_COVERAGE_LIBS)
+
+TEST_PROGS += test-mu-cmd
+test_mu_cmd_SOURCES= test-mu-cmd.c dummy.cc
+test_mu_cmd_CFLAGS=$(test_cflags)
+test_mu_cmd_LDADD=${top_builddir}/lib/libtestmucommon.la $(CODE_COVERAGE_LIBS)
+
+TEST_PROGS += test-mu-cmd-cfind
+test_mu_cmd_cfind_SOURCES= test-mu-cmd-cfind.c dummy.cc
+test_mu_cmd_cfind_CFLAGS=$(test_cflags)
+test_mu_cmd_cfind_LDADD=${top_builddir}/lib/libtestmucommon.la $(CODE_COVERAGE_LIBS)
+
+TEST_PROGS += test-mu-threads
+test_mu_threads_SOURCES= test-mu-threads.c dummy.cc
+test_mu_threads_CFLAGS=$(test_cflags)
+test_mu_threads_LDADD=${top_builddir}/lib/libtestmucommon.la $(CODE_COVERAGE_LIBS)
+
+# we need to use dummy.cc to enforce c++ linking...
+BUILT_SOURCES+=                                                        \
+       dummy.cc
+dummy.cc:
+       touch dummy.cc
+
+TESTS=$(TEST_PROGS)
+include $(top_srcdir)/aminclude_static.am
+
+CLEANFILES=                                                    \
+       $(BUILT_SOURCES)
diff --git a/mu/mu-cmd-cfind.c b/mu/mu-cmd-cfind.c
new file mode 100644 (file)
index 0000000..4157bb0
--- /dev/null
@@ -0,0 +1,470 @@
+/*
+** Copyright (C) 2011-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.
+**
+*/
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "mu-cmd.h"
+#include "mu-contacts.hh"
+#include "mu-runtime.h"
+
+#include "utils/mu-util.h"
+#include "utils/mu-str.h"
+#include "utils/mu-date.h"
+
+/**
+ * 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 (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 char *email, const char *name, time_t tstamp)
+{
+        char *fname, *lname, *now, *timestamp;
+
+        fname    = guess_first_name (name);
+        lname    = guess_last_name (name);
+        now      = mu_date_str ("%Y-%m-%d", time(NULL));
+        timestamp = mu_date_str ("%Y-%m-%d", tstamp);
+
+        g_print ("[\"%s\" \"%s\" nil nil nil nil (\"%s\") "
+                 "((creation-date . \"%s\") (time-stamp . \"%s\")) nil]\n",
+                 fname, lname, email, now, timestamp);
+
+        g_free (now);
+        g_free (timestamp);
+        g_free (fname);
+        g_free (lname);
+}
+
+
+static void
+each_contact_mutt_alias (const char *email, const char *name,
+                         GHashTable *nicks)
+{
+
+        gchar *nick;
+
+        if (!name)
+                return;
+
+        nick = guess_nick (name, nicks);
+        mu_util_print_encoded ("alias %s %s <%s>\n",
+                               nick, name, email);
+        g_free (nick);
+
+}
+
+
+static void
+each_contact_wl (const char *email, const char *name, GHashTable *nicks)
+{
+        gchar *nick;
+
+        if (!name)
+                return;
+
+        nick = guess_nick (name, nicks);
+        mu_util_print_encoded ("%s \"%s\" \"%s\"\n",
+                               email, nick, name);
+        g_free (nick);
+}
+
+
+static void
+each_contact_org_contact (const char *email, const char *name)
+{
+        if (name)
+                mu_util_print_encoded (
+                        "* %s\n:PROPERTIES:\n:EMAIL: %s\n:END:\n\n",
+                        name, email);
+}
+
+
+static void
+print_csv_field (const char *str)
+{
+        char *s;
+
+        if (!str)
+                return;
+
+        s = mu_str_replace (str, "\"", "\"\"");
+        if (strchr (s, ','))
+                mu_util_print_encoded ("\"%s\"", s);
+        else
+                mu_util_print_encoded ("%s", s);
+
+        g_free (s);
+}
+
+static void
+each_contact_csv (const char *email, const char *name)
+{
+        print_csv_field (name);
+        mu_util_print_encoded (",");
+        print_csv_field (email);
+        mu_util_print_encoded ("\n");
+}
+
+
+
+static void
+print_plain (const char *email, const char *name, gboolean color)
+{
+        if (name) {
+                if (color) fputs (MU_COLOR_MAGENTA, stdout);
+                mu_util_fputs_encoded (name, stdout);
+                fputs (" ", stdout);
+        }
+
+        if (color)
+                fputs (MU_COLOR_GREEN, stdout);
+
+        mu_util_fputs_encoded (email, stdout);
+
+        if (color)
+                fputs (MU_COLOR_DEFAULT, stdout);
+
+        fputs ("\n", stdout);
+}
+
+typedef struct {
+        MuConfigFormat format;
+        gboolean       color, personal;
+        time_t         after;
+        GRegex         *rx;
+        GHashTable     *nicks;
+        size_t          n;
+} ECData;
+
+
+static void
+each_contact (const char *full_address,
+              const char *email, const char *name, gboolean personal,
+              time_t last_seen, size_t freq, gint64 tstamp,
+              ECData *ecdata)
+{
+        if (ecdata->personal && !personal)
+                return;
+
+        if (tstamp < ecdata->after)
+                return;
+
+        if (ecdata->rx &&
+            !g_regex_match (ecdata->rx, email, 0, NULL) &&
+            !g_regex_match (ecdata->rx, name ? name : "", 0, NULL))
+                return;
+
+        ++ecdata->n;
+
+        switch (ecdata->format) {
+        case MU_CONFIG_FORMAT_MUTT_ALIAS:
+                each_contact_mutt_alias (email, name, ecdata->nicks);
+                break;
+        case MU_CONFIG_FORMAT_MUTT_AB:
+                mu_util_print_encoded ("%s\t%s\t\n",
+                                       email, name ? name : "");
+                break;
+        case MU_CONFIG_FORMAT_WL:
+                each_contact_wl (email, name, ecdata->nicks);
+                break;
+        case MU_CONFIG_FORMAT_ORG_CONTACT:
+                each_contact_org_contact (email, name);
+                break;
+        case MU_CONFIG_FORMAT_BBDB:
+                each_contact_bbdb (email, name, last_seen);
+                break;
+        case MU_CONFIG_FORMAT_CSV:
+                each_contact_csv (email, name);
+                break;
+        case MU_CONFIG_FORMAT_DEBUG: {
+                char datebuf[32];
+                strftime(datebuf,  sizeof(datebuf), "%F %T",
+                         gmtime(&last_seen));
+                g_print ("%s\n\tname: %s\n\t%s\n\tpersonal: %s\n\tfreq: %zu\n"
+                         "\tlast-seen: %s\n",
+                         email,
+                         name ? name : "<none>",
+                         full_address,
+                         personal ? "yes" : "no",
+                         freq,
+                         datebuf);
+        } break;
+        default:
+                print_plain (email, name, ecdata->color);
+        }
+}
+
+
+static MuError
+run_cmd_cfind (MuStore         *store,
+               const char*      pattern,
+               gboolean         personal,
+               time_t           after,
+               MuConfigFormat   format,
+               gboolean         color,
+               GError         **err)
+{
+        gboolean rv;
+        ECData  ecdata;
+
+        memset(&ecdata, 0, sizeof(ecdata));
+
+        if (pattern) {
+                ecdata.rx = g_regex_new (pattern,
+                                         G_REGEX_CASELESS|G_REGEX_OPTIMIZE,
+                                         0, err);
+                if (!ecdata.rx)
+                        return MU_ERROR_CONTACTS;
+        }
+
+        ecdata.personal = personal;
+        ecdata.n        = 0;
+        ecdata.after    = after;
+        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);
+        rv = mu_contacts_foreach (mu_store_contacts(store),
+                                  (MuContactsForeachFunc)each_contact, &ecdata);
+        g_hash_table_unref (ecdata.nicks);
+
+        if (ecdata.rx)
+                g_regex_unref (ecdata.rx);
+
+        if (ecdata.n == 0) {
+                g_warning ("no matching contacts found");
+                return MU_ERROR_NO_MATCHES;
+        }
+
+        return rv ? MU_OK : MU_ERROR_CONTACTS;
+}
+
+static gboolean
+cfind_params_valid (MuConfig *opts)
+{
+        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_warning ("invalid output format %s",
+                           opts->formatstr ? opts->formatstr : "<none>");
+                return FALSE;
+        }
+
+        /* only one pattern allowed */
+        if (opts->params[1] && opts->params[2]) {
+                g_warning ("usage: mu cfind [options] [<ptrn>]");
+                return FALSE;
+        }
+
+        return TRUE;
+}
+
+MuError
+mu_cmd_cfind (MuStore *store, MuConfig *opts, GError **err)
+{
+        g_return_val_if_fail (store, MU_ERROR_INTERNAL);
+        g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+        g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_CFIND,
+                              MU_ERROR_INTERNAL);
+
+        if (!cfind_params_valid (opts)) {
+                g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_IN_PARAMETERS,
+                             "invalid parameters");
+                return MU_ERROR_IN_PARAMETERS;
+        }
+
+        return run_cmd_cfind (store,
+                              opts->params[1],
+                              opts->personal,
+                              opts->after,
+                              opts->format,
+                              !opts->nocolor,
+                              err);
+}
diff --git a/mu/mu-cmd-extract.c b/mu/mu-cmd-extract.c
new file mode 100644 (file)
index 0000000..f0b90bc
--- /dev/null
@@ -0,0 +1,428 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** Copyright (C) 2010-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 <stdlib.h>
+#include <string.h>
+
+#include "mu-msg.h"
+#include "mu-msg-part.h"
+#include "mu-cmd.h"
+#include "utils/mu-util.h"
+#include "utils/mu-str.h"
+
+
+static gboolean
+save_part (MuMsg *msg, const char *targetdir, guint partidx, MuConfig *opts)
+{
+       GError *err;
+       gchar *filepath;
+       gboolean rv;
+       MuMsgOptions msgopts;
+
+       err = NULL;
+       rv = FALSE;
+
+       msgopts = mu_config_get_msg_options (opts);
+
+       filepath = mu_msg_part_get_path (msg, msgopts, targetdir, partidx, &err);
+       if (!filepath)
+               goto exit;
+
+       if (!mu_msg_part_save (msg, msgopts, filepath, partidx, &err))
+               goto exit;
+
+       if (opts->play)
+               rv = mu_util_play (filepath, TRUE, FALSE, &err);
+       else
+               rv = TRUE;
+exit:
+       if (err) {
+               g_warning ("error with MIME-part: %s", err->message);
+               g_clear_error (&err);
+       }
+
+       g_free (filepath);
+       return rv;
+}
+
+
+
+static gboolean
+save_numbered_parts (MuMsg *msg, MuConfig *opts)
+{
+       gboolean rv;
+       char **parts, **cur;
+
+       parts = g_strsplit (opts->parts, ",", 0);
+
+       for (rv = TRUE, cur = parts; cur && *cur; ++cur) {
+
+               unsigned idx;
+               int i;
+               char *endptr;
+
+               idx = (unsigned)(i = strtol (*cur, &endptr, 10));
+               if (i < 0 || *cur == endptr) {
+                       g_warning ("invalid MIME-part index '%s'", *cur);
+                       rv = FALSE;
+                       break;
+               }
+
+               if (!save_part (msg, opts->targetdir, idx, opts)) {
+                       g_warning ("failed to save MIME-part %d", idx);
+                       rv = FALSE;
+                       break;
+               }
+       }
+
+       g_strfreev (parts);
+       return rv;
+}
+
+static GRegex*
+anchored_regex (const char* pattern)
+{
+       GRegex *rx;
+       GError *err;
+       gchar *anchored;
+
+
+       anchored = g_strdup_printf
+               ("%s%s%s",
+                pattern[0] == '^' ? "" : "^",
+                pattern,
+                pattern[strlen(pattern)-1] == '$' ? "" : "$");
+
+       err = NULL;
+       rx = g_regex_new (anchored, G_REGEX_CASELESS|G_REGEX_OPTIMIZE, 0,
+                         &err);
+       g_free (anchored);
+
+       if (!rx) {
+               g_warning ("error in regular expression '%s': %s",
+                          pattern, err->message ? err->message : "error");
+               g_error_free (err);
+               return NULL;
+       }
+
+       return rx;
+}
+
+
+static gboolean
+save_part_with_filename (MuMsg *msg, const char *pattern, MuConfig *opts)
+{
+       GSList *lst, *cur;
+       GRegex *rx;
+       gboolean rv;
+       MuMsgOptions msgopts;
+
+       msgopts = mu_config_get_msg_options (opts);
+
+       /* 'anchor' the pattern with '^...$' if not already */
+       rx = anchored_regex (pattern);
+       if (!rx)
+               return FALSE;
+
+       lst = mu_msg_find_files (msg, msgopts, rx);
+       g_regex_unref (rx);
+       if (!lst) {
+               g_warning ("no matching attachments found");
+               return FALSE;
+       }
+
+       for (cur = lst, rv = TRUE; cur; cur = g_slist_next (cur))
+               rv = rv && save_part (msg, opts->targetdir,
+                                     GPOINTER_TO_UINT(cur->data), opts);
+       g_slist_free (lst);
+
+       return rv;
+}
+
+struct _SaveData {
+       gboolean                 result;
+       guint                    saved_num;
+       MuConfig                 *opts;
+};
+typedef struct _SaveData        SaveData;
+
+
+static gboolean
+ignore_part (MuMsg *msg, MuMsgPart *part, SaveData *sd)
+{
+       /* something went wrong somewhere; stop */
+       if (!sd->result)
+               return TRUE;
+
+       /* only consider leaf parts */
+       if (!(part->part_type & MU_MSG_PART_TYPE_LEAF))
+               return TRUE;
+
+       /* filter out non-attachments? */
+       if (!sd->opts->save_all &&
+           !(mu_msg_part_maybe_attachment (part)))
+               return TRUE;
+
+       return FALSE;
+}
+
+
+static void
+save_part_if (MuMsg *msg, MuMsgPart *part, SaveData *sd)
+{
+       gchar *filepath;
+       gboolean rv;
+       GError *err;
+       MuMsgOptions msgopts;
+
+       if (ignore_part (msg, part, sd))
+               return;
+
+       rv       = FALSE;
+       filepath = NULL;
+       err      = NULL;
+
+       msgopts = mu_config_get_msg_options (sd->opts);
+       filepath = mu_msg_part_get_path (msg, msgopts,
+                                        sd->opts->targetdir,
+                                        part->index, &err);
+       if (!filepath)
+               goto exit;
+
+       if (!mu_msg_part_save (msg, msgopts, filepath, part->index, &err))
+               goto exit;
+
+       if (sd->opts->play)
+               rv = mu_util_play (filepath, TRUE, FALSE, &err);
+       else
+               rv = TRUE;
+
+       ++sd->saved_num;
+exit:
+       if (err)
+               g_warning ("error saving MIME part: %s", err->message);
+
+       g_free (filepath);
+       g_clear_error (&err);
+
+       sd->result = rv;
+
+}
+
+static gboolean
+save_certain_parts (MuMsg *msg, MuConfig *opts)
+{
+       SaveData sd;
+       MuMsgOptions msgopts;
+
+       sd.result           = TRUE;
+       sd.saved_num        = 0;
+       sd.opts             = opts;
+
+       msgopts = mu_config_get_msg_options (opts);
+       mu_msg_part_foreach (msg, msgopts,
+                            (MuMsgPartForeachFunc)save_part_if, &sd);
+
+       if (sd.saved_num == 0) {
+               g_warning ("no %s extracted from this message",
+                          opts->save_attachments ? "attachments" : "parts");
+               sd.result = FALSE;
+       }
+
+       return sd.result;
+}
+
+
+static gboolean
+save_parts (const char *path, const char *filename, MuConfig *opts)
+{
+       MuMsg* msg;
+       gboolean rv;
+       GError *err;
+
+       err = NULL;
+       msg = mu_msg_new_from_file (path, NULL, &err);
+       if (!msg) {
+               if (err) {
+                       g_warning ("error: %s", err->message);
+                       g_error_free (err);
+               }
+               return FALSE;
+       }
+
+       /* note, mu_cmd_extract already checks whether what's in opts
+        * is somewhat, so no need for extensive checking here */
+
+       /* should we save some explicit parts? */
+       if (opts->parts)
+               rv = save_numbered_parts (msg, opts);
+       else if (filename)
+               rv = save_part_with_filename (msg, filename, opts);
+       else
+               rv = save_certain_parts (msg, opts);
+
+       mu_msg_unref (msg);
+
+       return rv;
+}
+
+#define color_maybe(C) do{ if (color) fputs ((C),stdout);}while(0)
+
+static const char*
+disp_str (MuMsgPartType ptype)
+{
+       if (ptype & MU_MSG_PART_TYPE_ATTACHMENT)
+               return "attach";
+       if (ptype & MU_MSG_PART_TYPE_INLINE)
+               return "inline";
+       return "<none>";
+}
+
+static void
+each_part_show (MuMsg *msg, MuMsgPart *part, gboolean color)
+{
+       /* index */
+       g_print ("  %u ", part->index);
+
+       /* filename */
+       color_maybe (MU_COLOR_GREEN); {
+               gchar *fname;
+               fname = mu_msg_part_get_filename (part, FALSE);
+               mu_util_fputs_encoded (fname ? fname : "<none>", stdout);
+               g_free (fname);
+       }
+       /* content-type */
+       color_maybe (MU_COLOR_BLUE);
+       mu_util_print_encoded (
+               " %s/%s ",
+               part->type ? part->type : "<none>",
+               part->subtype ? part->subtype : "<none>");
+
+       /* /\* disposition *\/ */
+       color_maybe (MU_COLOR_MAGENTA);
+       mu_util_print_encoded ("[%s]",  disp_str(part->part_type));
+
+       /* size */
+       if (part->size > 0) {
+               color_maybe (MU_COLOR_CYAN);
+               g_print (" (%s)", mu_str_size_s (part->size));
+       }
+
+       color_maybe (MU_COLOR_DEFAULT);
+       fputs ("\n", stdout);
+}
+
+
+static gboolean
+show_parts (const char* path, MuConfig *opts, GError **err)
+{
+       MuMsg *msg;
+       MuMsgOptions msgopts;
+
+       msg = mu_msg_new_from_file (path, NULL, err);
+       if (!msg)
+               return FALSE;
+
+       msgopts = mu_config_get_msg_options (opts);
+
+       /* TODO: update this for crypto */
+       g_print ("MIME-parts in this message:\n");
+       mu_msg_part_foreach
+               (msg, msgopts,
+                (MuMsgPartForeachFunc)each_part_show,
+                GUINT_TO_POINTER(!opts->nocolor));
+
+       mu_msg_unref (msg);
+
+       return TRUE;
+
+}
+
+
+static gboolean
+check_params (MuConfig *opts, GError **err)
+{
+       size_t param_num;
+
+       param_num = mu_config_param_num (opts);
+
+       if (param_num < 2) {
+               mu_util_g_set_error
+                       (err, MU_ERROR_IN_PARAMETERS,
+                        "parameters missing");
+               return FALSE;
+       }
+
+       if (opts->save_attachments || opts->save_all)
+               if (opts->parts || param_num == 3) {
+                       mu_util_g_set_error
+                               (err, MU_ERROR_IN_PARAMETERS,
+                                "--save-attachments and --save-all don't "
+                                "accept a filename pattern or --parts");
+                       return FALSE;
+               }
+
+       if (opts->save_attachments && opts->save_all) {
+               mu_util_g_set_error
+                       (err, MU_ERROR_IN_PARAMETERS,
+                        "only one of --save-attachments and"
+                        " --save-all is allowed");
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+MuError
+mu_cmd_extract (MuConfig *opts, GError **err)
+{
+       int rv;
+
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_EXTRACT,
+                             MU_ERROR_INTERNAL);
+
+       if (!check_params (opts, err))
+               return MU_ERROR_IN_PARAMETERS;
+
+       if (!opts->params[2] && !opts->parts &&
+           !opts->save_attachments && !opts->save_all)
+               /* show, don't save */
+               rv = show_parts (opts->params[1], opts, err);
+       else {
+               rv = mu_util_check_dir(opts->targetdir, FALSE, TRUE);
+               if (!rv)
+                       mu_util_g_set_error
+                               (err, MU_ERROR_FILE_CANNOT_WRITE,
+                                "target '%s' is not a writable directory",
+                                opts->targetdir);
+               else
+                       rv = save_parts (opts->params[1],
+                                        opts->params[2],
+                                        opts); /* save */
+       }
+
+       return rv ? MU_OK : MU_ERROR;
+}
diff --git a/mu/mu-cmd-find.c b/mu/mu-cmd-find.c
new file mode 100644 (file)
index 0000000..47e2d13
--- /dev/null
@@ -0,0 +1,768 @@
+/*
+** 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 <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <signal.h>
+
+#include "mu-msg.h"
+#include "mu-maildir.h"
+#include "mu-index.h"
+#include "mu-query.h"
+#include "mu-msg-iter.h"
+#include "mu-bookmarks.h"
+#include "mu-runtime.h"
+
+#include "utils/mu-util.h"
+#include "utils/mu-str.h"
+#include "utils/mu-date.h"
+
+#include "mu-cmd.h"
+#include "mu-threader.h"
+
+#ifdef HAVE_JSON_GLIB
+#include <json-glib/json-glib.h>
+#endif /*HAVE_JSON_GLIB*/
+
+typedef gboolean (OutputFunc) (MuMsg *msg, MuMsgIter *iter,
+                              MuConfig *opts, GError **err);
+
+static gboolean
+print_internal (MuQuery *query, const gchar *expr, gboolean xapian,
+               gboolean warn, GError **err)
+{
+       char *str;
+
+       if (xapian)
+               str = mu_query_internal_xapian (query, expr, err);
+       else
+               str = mu_query_internal (query, expr, warn, err);
+
+       if (str) {
+               g_print ("%s\n", str);
+               g_free (str);
+       }
+
+       return str != NULL;
+}
+
+
+/* returns MU_MSG_FIELD_ID_NONE if there is an error */
+static MuMsgFieldId
+sort_field_from_string (const char* fieldstr, GError **err)
+{
+       MuMsgFieldId mfid;
+
+       mfid = mu_msg_field_id_from_name (fieldstr, FALSE);
+
+       /* not found? try a shortcut */
+       if (mfid == MU_MSG_FIELD_ID_NONE &&
+           strlen(fieldstr) == 1)
+               mfid = mu_msg_field_id_from_shortcut(fieldstr[0],
+                                                    FALSE);
+       if (mfid == MU_MSG_FIELD_ID_NONE)
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_IN_PARAMETERS,
+                            "not a valid sort field: '%s'\n", fieldstr);
+       return mfid;
+}
+
+static MuMsg*
+get_message (MuMsgIter *iter, time_t after)
+{
+       MuMsg *msg;
+
+       if (mu_msg_iter_is_done (iter))
+               return NULL;
+
+       msg = mu_msg_iter_get_msg_floating (iter);
+       if (!msg)
+               return NULL; /* error */
+
+       if (!mu_msg_is_readable (msg)) {
+               mu_msg_iter_next (iter);
+               return get_message (iter, after);
+       }
+
+       if (after != 0 && after > mu_msg_get_timestamp (msg)) {
+               mu_msg_iter_next (iter);
+               return get_message (iter, after);
+       }
+
+       return msg;
+}
+
+static MuMsgIter*
+run_query (MuQuery *xapian, const gchar *query, MuConfig *opts,  GError **err)
+{
+       MuMsgIter *iter;
+       MuMsgFieldId sortid;
+       MuQueryFlags qflags;
+
+       sortid = MU_MSG_FIELD_ID_NONE;
+       if (opts->sortfield) {
+               sortid = sort_field_from_string (opts->sortfield, err);
+               if (sortid == MU_MSG_FIELD_ID_NONE) /* error occurred? */
+                       return FALSE;
+       }
+
+       qflags = MU_QUERY_FLAG_NONE;
+       if (opts->reverse)
+               qflags |= MU_QUERY_FLAG_DESCENDING;
+       if (opts->skip_dups)
+               qflags |= MU_QUERY_FLAG_SKIP_DUPS;
+       if (opts->include_related)
+               qflags |= MU_QUERY_FLAG_INCLUDE_RELATED;
+       if (opts->threads)
+               qflags |= MU_QUERY_FLAG_THREADS;
+
+       iter = mu_query_run (xapian, query, sortid, opts->maxnum, qflags, err);
+       return iter;
+}
+
+static gboolean
+exec_cmd (MuMsg *msg, MuMsgIter *iter, MuConfig *opts,  GError **err)
+{
+       gint status;
+       char *cmdline, *escpath;
+       gboolean rv;
+
+       escpath = g_shell_quote (mu_msg_get_path (msg));
+       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 (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 gchar*
+get_query (MuConfig *opts, GError **err)
+{
+       gchar   *query, *bookmarkval;
+
+       /* params[0] is 'find', actual search params start with [1] */
+       if (!opts->bookmark && !opts->params[1]) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_IN_PARAMETERS,
+                            "error in parameters");
+               return NULL;
+       }
+
+       bookmarkval = NULL;
+       if (opts->bookmark) {
+               bookmarkval = resolve_bookmark (opts, err);
+               if (!bookmarkval)
+                       return NULL;
+       }
+
+       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 query;
+}
+
+static MuQuery*
+get_query_obj (MuStore *store, GError **err)
+{
+       MuQuery *mquery;
+       unsigned count;
+
+       count = mu_store_count (store, err);
+
+       if (count == (unsigned)-1)
+               return NULL;
+
+       if (count == 0) {
+               g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_NEEDS_REINDEX,
+                            "the database is empty");
+               return NULL;
+       }
+
+       mquery = mu_query_new (store, err);
+       if (!mquery)
+               return NULL;
+
+       return mquery;
+}
+
+static gboolean
+prepare_links (MuConfig *opts, GError **err)
+{
+       /* note, mu_maildir_mkdir simply ignores whatever part of the
+        * mail dir already exists */
+
+       if (!mu_maildir_mkdir (opts->linksdir, 0700, TRUE, err)) {
+               mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_MKDIR,
+                                    "error creating %s", opts->linksdir);
+               return FALSE;
+       }
+
+       if (opts->clearlinks &&
+           !mu_maildir_clear_links (opts->linksdir, err)) {
+                       mu_util_g_set_error (err, MU_ERROR_FILE,
+                                            "error clearing links under %s",
+                                            opts->linksdir);
+                       return FALSE;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+output_link (MuMsg *msg, MuMsgIter *iter, MuConfig *opts,  GError **err)
+{
+       if (mu_msg_iter_is_first (iter) && !prepare_links (opts, err))
+               return FALSE;
+
+       return mu_maildir_link (mu_msg_get_path (msg),
+                               opts->linksdir, err);
+}
+
+static void
+ansi_color_maybe (MuMsgFieldId mfid, gboolean color)
+{
+       const char* ansi;
+
+       if (!color)
+               return; /* nothing to do */
+
+       switch (mfid) {
+
+       case MU_MSG_FIELD_ID_FROM:
+               ansi = MU_COLOR_CYAN; break;
+
+       case MU_MSG_FIELD_ID_TO:
+       case MU_MSG_FIELD_ID_CC:
+       case MU_MSG_FIELD_ID_BCC:
+               ansi = MU_COLOR_BLUE; break;
+
+       case MU_MSG_FIELD_ID_SUBJECT:
+               ansi = MU_COLOR_GREEN; break;
+
+       case MU_MSG_FIELD_ID_DATE:
+               ansi = MU_COLOR_MAGENTA; break;
+
+       default:
+               if (mu_msg_field_type(mfid) == MU_MSG_FIELD_TYPE_STRING)
+                       ansi = MU_COLOR_YELLOW;
+               else
+                       ansi = MU_COLOR_RED;
+       }
+
+       fputs (ansi, stdout);
+}
+
+static void
+ansi_reset_maybe (MuMsgFieldId mfid, gboolean color)
+{
+       if (!color)
+               return; /* nothing to do */
+
+       fputs (MU_COLOR_DEFAULT, stdout);
+
+}
+
+static const char*
+field_string_list (MuMsg *msg, MuMsgFieldId mfid)
+{
+       char *str;
+       const GSList *lst;
+       static char buf[80];
+
+       lst = mu_msg_get_field_string_list (msg, mfid);
+       if (!lst)
+               return NULL;
+
+       str = mu_str_from_list (lst, ',');
+       if (str) {
+               strncpy (buf, str, sizeof(buf)-1);
+               buf[sizeof(buf)-1]='\0';
+               g_free (str);
+               return buf;
+       }
+
+       return NULL;
+}
+
+static const char*
+display_field (MuMsg *msg, MuMsgFieldId mfid)
+{
+       gint64 val;
+
+       switch (mu_msg_field_type(mfid)) {
+       case MU_MSG_FIELD_TYPE_STRING: {
+               const gchar *str;
+               str = mu_msg_get_field_string (msg, mfid);
+               return str ? str : "";
+       }
+       case MU_MSG_FIELD_TYPE_INT:
+
+               if (mfid == MU_MSG_FIELD_ID_PRIO) {
+                       val = mu_msg_get_field_numeric (msg, mfid);
+                       return mu_msg_prio_name ((MuMsgPrio)val);
+               } else if (mfid == MU_MSG_FIELD_ID_FLAGS) {
+                       val = mu_msg_get_field_numeric (msg, mfid);
+                       return mu_str_flags_s ((MuFlags)val);
+               } else  /* as string */
+                       return mu_msg_get_field_string (msg, mfid);
+
+       case MU_MSG_FIELD_TYPE_TIME_T:
+               val = mu_msg_get_field_numeric (msg, mfid);
+               return mu_date_str_s ("%c", (time_t)val);
+
+       case MU_MSG_FIELD_TYPE_BYTESIZE:
+               val = mu_msg_get_field_numeric (msg, mfid);
+               return mu_str_size_s ((unsigned)val);
+       case MU_MSG_FIELD_TYPE_STRING_LIST: {
+               const char *str;
+               str = field_string_list (msg, mfid);
+               return str ? str : "";
+       }
+       default:
+               g_return_val_if_reached (NULL);
+       }
+}
+
+static void
+print_summary (MuMsg *msg, MuConfig *opts)
+{
+       const char* body;
+       char *summ;
+       MuMsgOptions msgopts;
+
+       msgopts = mu_config_get_msg_options (opts);
+       body = mu_msg_get_body_text(msg, msgopts);
+
+       if (body)
+               summ = mu_str_summarize (body, (unsigned)opts->summary_len);
+       else
+               summ = NULL;
+
+       g_print ("Summary: ");
+       mu_util_fputs_encoded (summ ? summ : "<none>", stdout);
+       g_print ("\n");
+
+       g_free (summ);
+}
+
+static void
+thread_indent (MuMsgIter *iter)
+{
+       const MuMsgIterThreadInfo *ti;
+       const char* threadpath;
+       int i;
+       gboolean is_root, first_child, empty_parent, is_dup;
+
+       ti = mu_msg_iter_get_thread_info (iter);
+       if (!ti) {
+               g_warning ("cannot get thread-info for message %u",
+                          mu_msg_iter_get_docid (iter));
+               return;
+       }
+
+       threadpath = ti->threadpath;
+       /* fputs (threadpath, stdout); */
+       /* fputs ("  ", stdout); */
+
+       is_root      = ti->prop & MU_MSG_ITER_THREAD_PROP_ROOT;
+       first_child  = ti->prop & MU_MSG_ITER_THREAD_PROP_FIRST_CHILD;
+       empty_parent = ti->prop & MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT;
+       is_dup       = ti->prop & MU_MSG_ITER_THREAD_PROP_DUP;
+
+       /* FIXME: count the colons... */
+       for (i = 0; *threadpath; ++threadpath)
+               i += (*threadpath == ':') ? 1 : 0;
+
+       /* indent */
+       while (i --> 0)
+               fputs ("  ", stdout);
+
+       if (!is_root) {
+               fputs (first_child ? "`" : "|", stdout);
+               fputs (empty_parent ? "*> " : is_dup ? "=> " : "-> ", stdout);
+       }
+}
+
+static void
+output_plain_fields (MuMsg *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) {
+
+               MuMsgFieldId mfid;
+               mfid =  mu_msg_field_id_from_shortcut (*myfields, FALSE);
+
+               if (mfid == MU_MSG_FIELD_ID_NONE ||
+                   (!mu_msg_field_xapian_value (mfid) &&
+                    !mu_msg_field_xapian_contact (mfid)))
+                 nonempty += printf ("%c", *myfields);
+
+               else {
+                       ansi_color_maybe (mfid, color);
+                       nonempty += mu_util_fputs_encoded
+                         (display_field (msg, mfid), stdout);
+                       ansi_reset_maybe (mfid, color);
+               }
+       }
+
+       if (nonempty)
+               fputs ("\n", stdout);
+}
+
+static gboolean
+output_plain (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err)
+{
+       /* we reuse the color (whatever that may be)
+        * for message-priority for threads, too */
+       ansi_color_maybe (MU_MSG_FIELD_ID_PRIO, !opts->nocolor);
+       if (opts->threads)
+               thread_indent (iter);
+
+       output_plain_fields (msg, opts->fields, !opts->nocolor, opts->threads);
+
+       if (opts->summary_len > 0)
+               print_summary (msg, opts);
+
+       return TRUE;
+}
+
+static gboolean
+output_sexp (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err)
+{
+       char *sexp;
+       const MuMsgIterThreadInfo *ti;
+
+       ti   = opts->threads ? mu_msg_iter_get_thread_info (iter) : NULL;
+       sexp = mu_msg_to_sexp (msg, mu_msg_iter_get_docid (iter),
+                              ti, MU_MSG_OPTION_HEADERS_ONLY);
+       fputs (sexp, stdout);
+       g_free (sexp);
+
+       return TRUE;
+}
+
+static gboolean
+output_json (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err)
+{
+#ifdef HAVE_JSON_GLIB
+       JsonNode                        *node;
+       const MuMsgIterThreadInfo       *ti;
+       char                            *s;
+
+       if (mu_msg_iter_is_first(iter))
+               g_print ("[\n");
+
+       ti   = opts->threads ? mu_msg_iter_get_thread_info (iter) : NULL;
+       node = mu_msg_to_json (msg, mu_msg_iter_get_docid (iter),
+                              ti, MU_MSG_OPTION_HEADERS_ONLY);
+
+       s = json_to_string (node, TRUE);
+       json_node_free (node);
+
+       fputs (s, stdout);
+       g_free (s);
+
+       if (mu_msg_iter_is_last(iter))
+               fputs("]\n", stdout);
+       else
+               fputs (",\n", stdout);
+
+       return TRUE;
+#else
+       g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_IN_PARAMETERS,
+                    "this mu was built without json support");
+       return FALSE;
+#endif /*HAVE_JSON_GLIB*/
+
+}
+
+static void
+print_attr_xml (const char* elm, const char *str)
+{
+       gchar *esc;
+
+       if (mu_str_is_empty(str))
+               return; /* empty: don't include */
+
+       esc = g_markup_escape_text (str, -1);
+       g_print ("\t\t<%s>%s</%s>\n", elm, esc, elm);
+       g_free (esc);
+}
+
+static gboolean
+output_xml (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err)
+{
+       if (mu_msg_iter_is_first(iter)) {
+               g_print ("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
+               g_print ("<messages>\n");
+       }
+
+       g_print ("\t<message>\n");
+       print_attr_xml ("from", mu_msg_get_from (msg));
+       print_attr_xml ("to", mu_msg_get_to (msg));
+       print_attr_xml ("cc", mu_msg_get_cc (msg));
+       print_attr_xml ("subject", mu_msg_get_subject (msg));
+       g_print ("\t\t<date>%u</date>\n",
+                (unsigned)mu_msg_get_date (msg));
+       g_print ("\t\t<size>%u</size>\n", (unsigned)mu_msg_get_size (msg));
+       print_attr_xml ("msgid", mu_msg_get_msgid (msg));
+       print_attr_xml ("path", mu_msg_get_path (msg));
+       print_attr_xml ("maildir", mu_msg_get_maildir (msg));
+       g_print ("\t</message>\n");
+
+       if (mu_msg_iter_is_last(iter))
+               g_print ("</messages>\n");
+
+       return TRUE;
+}
+
+static OutputFunc*
+get_output_func (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 gboolean
+output_query_results (MuMsgIter *iter, MuConfig *opts, GError **err)
+{
+       int              count;
+       gboolean         rv;
+       OutputFunc      *output_func;
+
+       output_func = get_output_func (opts, err);
+       if (!output_func)
+               return FALSE;
+
+       for (count = 0, rv = TRUE; !mu_msg_iter_is_done(iter);
+            mu_msg_iter_next (iter)) {
+
+               MuMsg *msg;
+
+               if (count == opts->maxnum)
+                       break;
+               msg = get_message (iter, opts->after);
+               if (!msg)
+                       break;
+               /* { */
+               /*      const char* thread_id; */
+               /*      thread_id = mu_msg_iter_get_thread_id (iter); */
+               /*      g_print ("%s ", thread_id ? thread_id : "<none>"); */
+
+               /* } */
+               rv = output_func (msg, iter, opts, err);
+               if (!rv)
+                       break;
+               else
+                       ++count;
+       }
+
+       if (rv && count == 0) {
+               mu_util_g_set_error (err, MU_ERROR_NO_MATCHES,
+                                    "no matches for search expression");
+               return FALSE;
+       }
+
+       return rv;
+}
+
+static gboolean
+process_query (MuQuery *xapian, const gchar *query, MuConfig *opts, GError **err)
+{
+       MuMsgIter *iter;
+       gboolean rv;
+
+       iter = run_query (xapian, query, opts, err);
+       if (!iter)
+               return FALSE;
+
+       rv = output_query_results (iter, opts, err);
+       mu_msg_iter_destroy (iter);
+
+       return rv;
+}
+
+static gboolean
+execute_find (MuStore *store, MuConfig *opts, GError **err)
+{
+       char            *query_str;
+       MuQuery         *oracle;
+       gboolean         rv;
+
+       oracle = get_query_obj (store, err);
+       if (!oracle)
+               return FALSE;
+
+       query_str = get_query (opts, err);
+       if (!query_str) {
+               mu_query_destroy (oracle);
+               return FALSE;
+       }
+
+       if (opts->format == MU_CONFIG_FORMAT_XQUERY)
+               rv = print_internal (oracle, query_str, TRUE, FALSE, err);
+       else if (opts->format == MU_CONFIG_FORMAT_MQUERY)
+               rv = print_internal (oracle, query_str, FALSE,
+                                    opts->verbose, err);
+       else
+               rv = process_query (oracle, query_str, opts, err);
+
+       mu_query_destroy (oracle);
+       g_free (query_str);
+
+       return rv;
+}
+
+static gboolean
+format_params_valid (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 (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;
+}
+
+MuError
+mu_cmd_find (MuStore *store, MuConfig *opts, GError **err)
+{
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_FIND,
+                             MU_ERROR_INTERNAL);
+
+       if (opts->exec)
+               opts->format = MU_CONFIG_FORMAT_EXEC; /* pseudo format */
+
+       if (!query_params_valid (opts, err) ||
+           !format_params_valid(opts, err))
+               return MU_G_ERROR_CODE (err);
+
+       if (!execute_find (store, opts, err))
+               return MU_G_ERROR_CODE(err);
+       else
+               return MU_OK;
+}
diff --git a/mu/mu-cmd-index.c b/mu/mu-cmd-index.c
new file mode 100644 (file)
index 0000000..5508463
--- /dev/null
@@ -0,0 +1,318 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** Copyright (C) 2008-2016 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 "mu-cmd.h"
+
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include "mu-msg.h"
+#include "mu-index.h"
+#include "mu-store.hh"
+#include "mu-runtime.h"
+
+#include "utils/mu-util.h"
+#include "utils/mu-log.h"
+
+static gboolean MU_CAUGHT_SIGNAL;
+
+static void
+sig_handler (int sig)
+{
+       if (!MU_CAUGHT_SIGNAL && sig == SIGINT) { /* Ctrl-C */
+               g_print ("\n");
+               g_warning ("shutting down gracefully, "
+                          "press again to kill immediately");
+       }
+
+       MU_CAUGHT_SIGNAL = TRUE;
+}
+
+static void
+install_sig_handler (void)
+{
+       struct sigaction action;
+       int i, sigs[] = { SIGINT, SIGHUP, SIGTERM };
+
+       MU_CAUGHT_SIGNAL = FALSE;
+
+       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], strerror (errno));;
+}
+
+
+static gboolean
+check_params (MuConfig *opts, GError **err)
+{
+       /* param[0] == 'index'  there should be no param[1] */
+       if (opts->params[1]) {
+               mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "unexpected parameter");
+               return FALSE;
+       }
+
+       if (opts->max_msg_size < 0) {
+               mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "the maximum message size must >= 0");
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static MuError
+index_msg_silent_cb (MuIndexStats* stats, void *user_data)
+{
+       return MU_CAUGHT_SIGNAL ? MU_STOP: MU_OK;
+}
+
+
+
+static void
+print_stats (MuIndexStats* stats, gboolean clear, gboolean color)
+{
+       const char *kars="-\\|/";
+       char output[120];
+
+       static unsigned i = 0;
+
+       if (clear)
+               fputs ("\r", stdout);
+
+       if (color)
+               g_snprintf
+                       (output, sizeof(output),
+                        MU_COLOR_YELLOW "%c " MU_COLOR_DEFAULT
+                        "processing mail; "
+                        "processed: " MU_COLOR_GREEN "%u; " MU_COLOR_DEFAULT
+                        "updated/new: " MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT
+                        ", cleaned-up: " MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT,
+                        (unsigned)kars[++i % 4],
+                        (unsigned)stats->_processed,
+                        (unsigned)stats->_updated,
+                        (unsigned)stats->_cleaned_up);
+       else
+               g_snprintf
+                       (output, sizeof(output),
+                        "%c processing mail; processed: %u; "
+                        "updated/new: %u, cleaned-up: %u",
+                        (unsigned)kars[++i % 4],
+                        (unsigned)stats->_processed,
+                        (unsigned)stats->_updated,
+                        (unsigned)stats->_cleaned_up);
+
+       fputs (output, stdout);
+       fflush (stdout);
+}
+
+
+struct _IndexData {
+       gboolean color;
+};
+typedef struct _IndexData IndexData;
+
+
+static MuError
+index_msg_cb  (MuIndexStats* stats, IndexData *idata)
+{
+       if (stats->_processed % 75)
+               return MU_OK;
+
+       print_stats (stats, TRUE, idata->color);
+
+       return MU_CAUGHT_SIGNAL ? MU_STOP: MU_OK;
+}
+
+static void
+show_time (unsigned t, unsigned processed, gboolean color)
+{
+       if (color) {
+               if (t)
+                       g_print ("elapsed: "
+                                  MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT
+                                  " second(s), ~ "
+                                  MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT
+                                  " msg/s",
+                                  t, processed/t);
+               else
+                       g_print ("elapsed: "
+                                  MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT
+                                  " second(s)", t);
+       } else {
+               if (t)
+                       g_print ("elapsed: %u second(s), ~ %u msg/s",
+                                  t, processed/t);
+               else
+                       g_print ("elapsed: %u second(s)", t);
+       }
+
+       g_print ("\n");
+}
+
+/* when logging to console, print a newline before doing so; this
+ * makes it more clear when something happens during the
+ * indexing/cleanup progress output */
+#define newline_before_on()                                              \
+       mu_log_options_set(mu_log_options_get() | MU_LOG_OPTIONS_NEWLINE)
+#define newline_before_off()                                             \
+       mu_log_options_set(mu_log_options_get() & ~MU_LOG_OPTIONS_NEWLINE)
+
+static MuError
+cleanup_missing (MuIndex *midx, MuConfig *opts, MuIndexStats *stats,
+                GError **err)
+{
+       MuError rv;
+       time_t t;
+       IndexData idata;
+       gboolean show_progress;
+
+       if (!opts->quiet)
+               g_print ("cleaning up messages [%s]\n",
+                        mu_runtime_path (MU_RUNTIME_PATH_XAPIANDB));
+
+       show_progress = !opts->quiet && isatty(fileno(stdout));
+       mu_index_stats_clear (stats);
+
+       t = time (NULL);
+       idata.color = !opts->nocolor;
+       newline_before_on();
+       rv = mu_index_cleanup
+               (midx, stats,
+                show_progress ?
+                (MuIndexCleanupDeleteCallback)index_msg_cb :
+                (MuIndexCleanupDeleteCallback)index_msg_silent_cb,
+                &idata, err);
+       newline_before_off();
+
+       if (!opts->quiet) {
+               print_stats (stats, TRUE, !opts->nocolor);
+               g_print ("\n");
+               show_time ((unsigned)(time(NULL)-t),stats->_processed,
+                          !opts->nocolor);
+       }
+
+       return (rv == MU_OK || rv == MU_STOP) ? MU_OK: MU_G_ERROR_CODE(err);
+}
+
+static MuError
+cmd_index (MuIndex *midx, MuConfig *opts, MuIndexStats *stats, GError **err)
+{
+       IndexData       idata;
+       MuError         rv;
+       gboolean        show_progress;
+
+       show_progress = !opts->quiet && isatty(fileno(stdout));
+       idata.color   = !opts->nocolor;
+
+       newline_before_on();
+
+       rv = mu_index_run (midx,
+                          opts->rebuild,
+                          opts->lazycheck, stats,
+                          show_progress ?
+                          (MuIndexMsgCallback)index_msg_cb :
+                          (MuIndexMsgCallback)index_msg_silent_cb,
+                          NULL, &idata);
+       newline_before_off();
+
+       if (rv == MU_OK || rv == MU_STOP) {
+               MU_WRITE_LOG ("index: processed: %u; updated/new: %u",
+                             stats->_processed, stats->_updated);
+       } else
+               mu_util_g_set_error (err, rv, "error while indexing");
+
+       return rv;
+}
+
+
+static MuIndex*
+init_mu_index (MuStore *store, MuConfig *opts, GError **err)
+{
+       MuIndex *midx;
+
+       if (!check_params (opts, err))
+               return NULL;
+
+       midx = mu_index_new (store, err);
+       if (!midx)
+               return NULL;
+
+       mu_index_set_max_msg_size (midx, opts->max_msg_size);
+
+       return midx;
+}
+
+MuError
+mu_cmd_index (MuStore *store, MuConfig *opts, GError **err)
+{
+       MuIndex         *midx;
+       MuIndexStats     stats;
+       gboolean         rv;
+       time_t           t;
+
+       g_return_val_if_fail (opts, FALSE);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_INDEX,
+                             FALSE);
+
+       /* create, and do error handling if needed */
+       midx = init_mu_index (store, opts, err);
+       if (!midx)
+               return MU_G_ERROR_CODE(err);
+
+       mu_index_stats_clear (&stats);
+       install_sig_handler ();
+
+       if (!opts->quiet)
+               mu_store_print_info (store, opts->nocolor);
+
+       t = time (NULL);
+       rv = cmd_index (midx, opts, &stats, err);
+
+       if (rv == MU_OK && !opts->nocleanup) {
+               if (!opts->quiet)
+                       g_print ("\n");
+               rv = cleanup_missing (midx, opts, &stats, err);
+       }
+
+       if (!opts->quiet)  {
+               print_stats (&stats, TRUE, !opts->nocolor);
+               g_print ("\n");
+               show_time ((unsigned)(time(NULL)-t),
+                          stats._processed, !opts->nocolor);
+       }
+
+       mu_index_destroy (midx);
+
+       return rv;
+}
diff --git a/mu/mu-cmd-script.c b/mu/mu-cmd-script.c
new file mode 100644 (file)
index 0000000..e1180c1
--- /dev/null
@@ -0,0 +1,204 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** 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.
+**
+*/
+
+#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.h"
+#include "mu-script.h"
+#include "mu-runtime.h"
+
+
+#include "utils/mu-util.h"
+#include "utils/mu-str.h"
+
+
+#define MU_GUILE_EXT          ".scm"
+#define MU_GUILE_DESCR_PREFIX ";; INFO: "
+
+#define COL(C) ((color)?C:"")
+
+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 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 = g_strdup_printf ("%s%c%s",
+                                   muhome, G_DIR_SEPARATOR, "scripts");
+
+       /* 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 */
+}
+
+
+
+static gboolean
+check_params (MuConfig *opts, GError **err)
+{
+       if (!mu_util_supports (MU_FEATURE_GUILE)) {
+               mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "the 'script' command is not available "
+                                    "in this version of mu");
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+
+MuError
+mu_cmd_script (MuConfig *opts, GError **err)
+{
+       MuScriptInfo *msi;
+       GSList *scripts;
+
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_SCRIPT,
+                             MU_ERROR_INTERNAL);
+
+       if (!check_params (opts, err))
+               return MU_ERROR;
+
+       scripts = get_script_info_list (opts->muhome, err);
+       if (err && *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);
+       return (err && *err) ? MU_ERROR : MU_OK;
+}
diff --git a/mu/mu-cmd-server.cc b/mu/mu-cmd-server.cc
new file mode 100644 (file)
index 0000000..bce9025
--- /dev/null
@@ -0,0 +1,1334 @@
+/*
+** 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 "mu-cmd.h"
+
+#include <iostream>
+#include <string>
+#include <algorithm>
+#include <atomic>
+#include <cstring>
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "mu-runtime.h"
+#include "mu-cmd.h"
+#include "mu-maildir.h"
+#include "mu-query.h"
+#include "mu-index.h"
+#include "mu-store.hh"
+#include "mu-msg-part.h"
+#include "mu-contacts.hh"
+
+#include "utils/mu-str.h"
+#include "utils/mu-utils.hh"
+#include "utils/mu-command-parser.hh"
+
+using namespace Mu;
+using namespace Command;
+using namespace Sexp;
+
+using DocId = unsigned;
+
+static std::atomic<bool> MuTerminate{false};
+
+static void
+sig_handler (int sig)
+{
+        MuTerminate = true;
+}
+
+static void
+install_sig_handler (void)
+{
+        struct sigaction action;
+        int i, sigs[] = { SIGINT, SIGHUP, SIGTERM, SIGPIPE };
+
+        MuTerminate = false;
+
+        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 G_GNUC_PRINTF(1, 2)
+print_expr (const char* frm, ...)
+{
+        char *expr, *expr_orig;
+        va_list ap;
+        ssize_t rv;
+        size_t exprlen, lenlen;
+        char cookie[16];
+        static int outfd = 0;
+
+#if defined(__CYGWIN__ )&& !defined (_WIN32)
+        const size_t writestep = 4096 * 16;
+        size_t bytestowrite = 0;
+#endif
+
+        if (outfd == 0)
+                outfd = fileno (stdout);
+
+        expr    = NULL;
+
+        va_start (ap, frm);
+        exprlen = g_vasprintf (&expr, frm, ap);
+        va_end (ap);
+
+        /* this cookie tells the frontend where to expect the next
+         * expression */
+
+        cookie[0] = COOKIE_PRE;
+        lenlen = sprintf(cookie + 1, "%x",
+                         (unsigned)exprlen + 1); /* + 1 for \n */
+        cookie[lenlen + 1] = COOKIE_POST;
+
+        /* write the cookie, ie.
+         *   COOKIE_PRE <len-of-following-sexp-in-hex> COOKIE_POST
+         */
+        rv = write (outfd, cookie, lenlen + 2);
+        if (rv != -1) {
+                expr_orig = expr;
+#if defined (__CYGWIN__) && !defined(_WIN32)
+                /* CYGWIN doesn't like big packets */
+                while (exprlen > 0) {
+                        bytestowrite = exprlen > writestep ? writestep : exprlen;
+                        rv = write(outfd, expr, bytestowrite);
+                        expr += bytestowrite;
+                        exprlen -= bytestowrite;
+                }
+#else
+                rv = write (outfd, expr, exprlen);
+#endif
+                g_free (expr_orig);
+        }
+        if (rv != -1)
+                rv = write (outfd, "\n", 1);
+        if (rv == -1) {
+                g_critical ("%s: write() failed: %s",
+                           __func__, g_strerror(errno));
+                /* terminate ourselves */
+                raise (SIGTERM);
+        }
+}
+
+
+G_GNUC_PRINTF(2,3) static MuError
+print_error (MuError errcode, const char* frm, ...)
+{
+        char    *msg;
+        va_list  ap;
+
+        va_start (ap, frm);
+        g_vasprintf (&msg, frm, ap);
+        va_end (ap);
+
+        print_expr ("(:error %u :message %s)", errcode, quote(msg).c_str());
+        g_free (msg);
+
+        return errcode;
+}
+
+static unsigned
+print_sexps (MuMsgIter *iter, unsigned maxnum)
+{
+        unsigned u;
+        u = 0;
+
+        while (!mu_msg_iter_is_done (iter) && u < maxnum) {
+
+                MuMsg *msg;
+                msg = mu_msg_iter_get_msg_floating (iter);
+
+                if (mu_msg_is_readable (msg)) {
+                        char *sexp;
+                        const MuMsgIterThreadInfo* ti;
+                        ti   = mu_msg_iter_get_thread_info (iter);
+                        sexp = mu_msg_to_sexp (msg,
+                                               mu_msg_iter_get_docid (iter),
+                                               ti, MU_MSG_OPTION_HEADERS_ONLY);
+                        print_expr ("%s", sexp);
+                        g_free (sexp);
+                        ++u;
+                }
+                mu_msg_iter_next (iter);
+        }
+        return u;
+}
+
+
+struct Context {
+        Context(){}
+        Context (MuConfig *opts) {
+                const auto dbpath{mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB)};
+                GError *gerr{};
+                store = mu_store_new_writable (dbpath, NULL);
+                if (!store) {
+                        const auto mu_init = format("mu init %s%s",
+                                                    opts->muhome ? "--muhome=" : "",
+                                                    opts->muhome ? opts->muhome : "");
+
+                        if (gerr) {
+                                if ((MuError)gerr->code == MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK)
+                                        print_error(MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK,
+                                                    "mu database already locked; "
+                                                    "some other mu running?");
+                                else
+                                        print_error((MuError)gerr->code,
+                                                    "cannot open database @ %s:%s; already running? "
+                                                    "if not, please try '%s", dbpath,
+                                                    gerr->message ? gerr->message : "something went wrong",
+                                                    mu_init.c_str());
+                        } else
+                                print_error(MU_ERROR,
+                                            "cannot open database @ %s; already running? if not, please try '%s'",
+                                            dbpath, mu_init.c_str());
+
+                        throw Mu::Error (Error::Code::Store, &gerr/*consumed*/,
+                                         "failed to open database @ %s; already running? if not, please try '%s'",
+                                         dbpath, mu_init.c_str());
+                }
+
+                query = mu_query_new (store, &gerr);
+                if (!query)
+                        throw Error(Error::Code::Store, &gerr, "failed to create query");
+        }
+
+        ~Context() {
+                if (query)
+                        mu_query_destroy(query);
+                if (store) {
+                        mu_store_flush(store);
+                        mu_store_unref(store);
+                }
+        }
+
+        Context(const Context&) = delete;
+
+        MuStore *store{};
+        MuQuery *query{};
+        bool do_quit{};
+
+        CommandMap command_map;
+};
+
+
+static MuMsgOptions
+message_options (const Parameters& params)
+{
+        const auto extract_images{get_bool_or(params, "extract-images", false)};
+        const auto decrypt{get_bool_or(params, "decrypt", false)};
+        const auto verify{get_bool_or(params, "verify", false)};
+
+        int opts{MU_MSG_OPTION_NONE};
+        if (extract_images)
+                opts |= MU_MSG_OPTION_EXTRACT_IMAGES;
+        if (verify)
+                opts |= MU_MSG_OPTION_VERIFY  | MU_MSG_OPTION_USE_AGENT;
+        if (decrypt)
+                opts |= MU_MSG_OPTION_DECRYPT | MU_MSG_OPTION_USE_AGENT;
+
+        return (MuMsgOptions)opts;
+}
+
+/* '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)
+ */
+static void
+add_handler (Context& context, const Parameters& params)
+{
+        const auto path{get_string_or(params, "path")};
+
+        GError *gerr{};
+        const auto docid{mu_store_add_path (context.store, path.c_str(), &gerr)};
+        if (docid == MU_STORE_INVALID_DOCID)
+                throw Error(Error::Code::Store, &gerr, "failed to add message at %s",
+                            path.c_str());
+
+        print_expr ("(:info add :path %s :docid %u)", quote(path).c_str(), docid);
+
+        auto msg{mu_store_get_msg(context.store, docid, &gerr)};
+        if (!msg)
+                throw Error(Error::Code::Store, &gerr, "failed to get message at %s",
+                            path.c_str());
+
+        auto sexp{mu_msg_to_sexp (msg, docid, NULL, MU_MSG_OPTION_VERIFY)};
+        print_expr ("(:update %s :move nil)", sexp);
+        mu_msg_unref(msg);
+        g_free (sexp);
+}
+
+
+struct _PartInfo {
+        GSList      *attlist;
+        MuMsgOptions opts;
+};
+typedef struct _PartInfo PartInfo;
+
+static void
+each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo)
+{
+        char   *att, *cachefile;
+
+        /* exclude things that don't look like proper attachments,
+         * unless they're images */
+        if (!mu_msg_part_maybe_attachment(part))
+                return;
+
+        GError *gerr{};
+        cachefile = mu_msg_part_save_temp (msg,
+                                           (MuMsgOptions)(pinfo->opts|MU_MSG_OPTION_OVERWRITE),
+                                           part->index, &gerr);
+        if (!cachefile)
+                throw Error (Error::Code::File, &gerr, "failed to save part");
+
+        att = g_strdup_printf ("(:file-name %s :mime-type \"%s/%s\")",
+                               quote(cachefile).c_str(), part->type, part->subtype);
+        pinfo->attlist = g_slist_append (pinfo->attlist, att);
+
+        g_free (cachefile);
+
+}
+
+
+/* take the attachments of msg, save them as tmp files, and return
+ * as sexp (as a string) describing them
+ *
+ * ((:name <filename> :mime-type <mime-type> :disposition
+ *   <attachment|inline>) ... )
+ *
+ */
+static gchar*
+include_attachments (MuMsg *msg, MuMsgOptions opts)
+{
+        GSList  *cur;
+        GString *gstr;
+        PartInfo pinfo;
+
+        pinfo.attlist = NULL;
+        pinfo.opts    = opts;
+        mu_msg_part_foreach (msg, opts,
+                             (MuMsgPartForeachFunc)each_part,
+                             &pinfo);
+
+        gstr = g_string_sized_new (512);
+        gstr = g_string_append_c (gstr, '(');
+        for (cur = pinfo.attlist; cur; cur = g_slist_next (cur))
+                g_string_append (gstr, (gchar*)cur->data);
+        gstr = g_string_append_c (gstr, ')');
+
+        mu_str_free_list (pinfo.attlist);
+
+        return g_string_free (gstr, FALSE);
+}
+
+enum { NEW, REPLY, FORWARD, EDIT, RESEND, INVALID_TYPE };
+static unsigned
+compose_type (const char *typestr)
+{
+        if (g_str_equal (typestr, "reply"))
+                return REPLY;
+        else if (g_str_equal (typestr, "forward"))
+                return FORWARD;
+        else if (g_str_equal (typestr, "edit"))
+                return EDIT;
+        else if (g_str_equal (typestr, "resend"))
+                return RESEND;
+        else if (g_str_equal (typestr, "new"))
+                return NEW;
+        else
+                return INVALID_TYPE;
+}
+
+/* '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 void
+compose_handler (Context& context, const Parameters& params)
+{
+        const auto typestr{get_symbol_or(params, "type")};
+        const auto ctype{compose_type(typestr.c_str())};
+        if (ctype == INVALID_TYPE)
+                throw Error(Error::Code::InvalidArgument, "invalid compose type");
+
+        // message optioss below checks extract-images / extract-encrypted
+
+        char *sexp{}, *atts{};
+        if (ctype == REPLY || ctype == FORWARD || ctype == EDIT || ctype == RESEND) {
+
+                GError *gerr{};
+                const unsigned docid{(unsigned)get_int_or(params, "docid")};
+                auto msg{mu_store_get_msg (context.store, docid, &gerr)};
+                if (!msg)
+                        throw Error{Error::Code::Store, &gerr, "failed to get message %u", docid};
+
+                const auto opts{message_options(params)};
+                sexp = mu_msg_to_sexp (msg, docid, NULL, opts);
+                atts = (ctype == FORWARD) ? include_attachments (msg, opts) : NULL;
+                mu_msg_unref (msg);
+        }
+        print_expr ("(:compose %s :original %s :include %s)",
+                    typestr.c_str(), sexp ? sexp : "nil", atts ? atts : "nil");
+
+        g_free (sexp);
+        g_free (atts);
+}
+
+
+struct SexpData {
+        GString  *gstr;
+        gboolean  personal;
+        time_t    last_seen;
+        gint64    tstamp;
+        size_t    rank;
+};
+
+
+static void
+each_contact_sexp (const char* full_address,
+                   const char *email, const char *name, gboolean personal,
+                   time_t last_seen, unsigned freq,
+                   gint64 tstamp, SexpData *sdata)
+{
+        sdata->rank++;
+
+        /* since the last time we got some contacts */
+        if (sdata->tstamp > tstamp)
+                return;
+
+        /* (maybe) only include 'personal' contacts */
+        if (sdata->personal && !personal)
+                return;
+
+        /* only include newer-than-x contacts */
+        if (sdata->last_seen > last_seen)
+                return;
+
+        /* only include *real* e-mail addresses (ignore local
+         * addresses... there's little to complete there anyway...) */
+        if (!email || !strstr (email, "@"))
+                return;
+
+        g_string_append_printf (sdata->gstr, "(%s . %zu)\n",
+                                quote(full_address).c_str(), sdata->rank);
+}
+
+/**
+ * get all contacts as an s-expression
+ *
+ * @param self contacts object
+ * @param personal_only whether to restrict the list to 'personal' email
+ * addresses
+ *
+ * @return the sexp
+ */
+static char*
+contacts_to_sexp (const MuContacts *contacts, bool personal,
+                  int64_t last_seen, gint64 tstamp)
+{
+
+        g_return_val_if_fail (contacts, NULL);
+
+        SexpData sdata{};
+        sdata.personal  = personal;
+        sdata.last_seen = last_seen;
+        sdata.tstamp    = tstamp;
+        sdata.rank      = 0;
+
+        /* make a guess for the initial size */
+        sdata.gstr = g_string_sized_new (mu_contacts_count(contacts) * 128);
+        g_string_append (sdata.gstr, "(:contacts (");
+
+        const auto cutoff{g_get_monotonic_time()};
+        mu_contacts_foreach (contacts, (MuContactsForeachFunc)each_contact_sexp, &sdata);
+        /* pass a string, elisp doesn't like 64-bit nums */
+        g_string_append_printf (sdata.gstr,
+                                ") :tstamp \"%" G_GINT64_FORMAT  "\")", cutoff);
+
+        return g_string_free (sdata.gstr, FALSE);
+}
+
+
+static void
+contacts_handler (Context& context, 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 after{afterstr.empty() ? 0 :
+                        g_ascii_strtoll(date_to_time_t_string(afterstr, true).c_str(), {}, 10)};
+        const auto tstamp = g_ascii_strtoll (tstampstr.c_str(), NULL, 10);
+
+        const auto contacts{mu_store_contacts(context.store)};
+        if (!contacts)
+                throw Error{Error::Code::Internal, "failed to get contacts"};
+
+        /* dump the contacts cache as a giant sexp */
+        auto sexp = contacts_to_sexp (contacts, personal, after, tstamp);
+        print_expr ("%s\n", sexp);
+        g_free (sexp);
+}
+
+static void
+save_part (MuMsg *msg, unsigned docid, unsigned index,
+           MuMsgOptions opts, const Parameters& params)
+{
+        const auto path{get_string_or(params, "path")};
+        if (path.empty())
+                throw Error{Error::Code::Command, "missing path"};
+
+        GError *gerr{};
+        if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | (int)MU_MSG_OPTION_OVERWRITE),
+                               path.c_str(), index, &gerr))
+                throw Error{Error::Code::File, &gerr, "failed to save part"};
+
+        print_expr ("(:info save :message %s)", quote(path + " has been saved").c_str());
+}
+
+
+static void
+open_part (MuMsg *msg, unsigned docid, unsigned index, MuMsgOptions opts)
+{
+        GError *gerr{};
+        char *targetpath{mu_msg_part_get_cache_path (msg, opts, index, &gerr)};
+        if (!targetpath)
+                throw Error{Error::Code::File, &gerr, "failed to get cache-path"};
+
+        if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING),
+                                targetpath, index, &gerr)) {
+                g_free(targetpath);
+                throw Error{Error::Code::File, &gerr, "failed to save to cache-path"};
+        }
+
+        if (!mu_util_play (targetpath, TRUE,/*allow local*/
+                           FALSE/*allow remote*/, &gerr)) {
+                g_free(targetpath);
+                throw Error{Error::Code::File, &gerr, "failed to play"};
+        }
+
+        print_expr ("(:info open :message %s)",
+                    quote(std::string{targetpath} + " has been opened").c_str());
+        g_free (targetpath);
+}
+
+static void
+temp_part (MuMsg *msg, unsigned docid, unsigned index,
+           MuMsgOptions opts, const Parameters& params)
+{
+        const auto what{get_symbol_or(params, "what")};
+        if (what.empty())
+                throw Error{Error::Code::Command, "missing 'what'"};
+
+        const auto param{get_string_or(params, "param")};
+
+        GError *gerr{};
+        char *path{mu_msg_part_get_cache_path (msg, opts, index, &gerr)};
+        if (!path)
+                throw Error{Error::Code::File, &gerr, "could not get cache path"};
+
+        if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING),
+                               path, index, &gerr)) {
+                g_free(path);
+                throw Error{Error::Code::File, &gerr, "saving failed"};
+        }
+
+        const auto qpath{quote(path)};
+        g_free(path);
+
+        if (!param.empty())
+                print_expr ("(:temp %s"
+                            " :what \"%s\""
+                            " :docid %u"
+                            " :param %s"
+                            ")",
+                            qpath.c_str(), what.c_str(), docid, quote(param).c_str());
+        else
+                print_expr ("(:temp %s :what \"%s\" :docid %u)",
+                            qpath.c_str(), what.c_str(), docid);
+}
+
+
+
+/* 'extract' extracts some mime part from a message */
+static void
+extract_handler (Context& context, const Parameters& params)
+{
+        const auto docid{get_int_or(params, "docid")};
+        const auto index{get_int_or(params, "index")};
+        const auto opts{message_options(params)};
+
+        GError *gerr{};
+        auto msg{mu_store_get_msg (context.store, docid, &gerr)};
+        if (!msg)
+                throw Error{Error::Code::Store, "failed to get message"};
+
+        try {
+                const auto action{get_symbol_or(params, "action")};
+                if (action == "save")
+                        save_part (msg, docid, index, opts, params);
+                else if (action == "open")
+                        open_part (msg, docid, index, opts);
+                else if (action == "temp")
+                        temp_part (msg, docid, index, opts, params);
+                else {
+                        throw Error{Error::Code::InvalidArgument,
+                                        "unknown action '%s'", action.c_str()};
+                }
+
+        } catch (...) {
+                mu_msg_unref (msg);
+                throw;
+        }
+}
+
+
+/* get a *list* of all messages with the given message id */
+static std::vector<DocId>
+docids_for_msgid (MuQuery *query, const std::string& msgid, size_t max=100)
+{
+        if (msgid.size() > MU_STORE_MAX_TERM_LENGTH - 1) {
+                throw Error(Error::Code::InvalidArgument,
+                                  "invalid message-id '%s'", msgid.c_str());
+        }
+
+        const auto xprefix{mu_msg_field_xapian_prefix(MU_MSG_FIELD_ID_MSGID)};
+        /*XXX this is a bit dodgy */
+        auto tmp{g_ascii_strdown(msgid.c_str(), -1)};
+        auto rawq{g_strdup_printf("%c%s", xprefix, tmp)};
+        g_free(tmp);
+
+        GError *gerr{};
+        auto iter{mu_query_run (query, rawq, MU_MSG_FIELD_ID_NONE, max, MU_QUERY_FLAG_RAW, &gerr)};
+        g_free (rawq);
+        if (!iter)
+                throw Error(Error::Code::Store, &gerr, "failed to run msgid-query");
+        if (mu_msg_iter_is_done (iter))
+                throw Error(Error::Code::NotFound,
+                                  "could not find message(s) for msgid %s", msgid.c_str());
+        std::vector<DocId> docids;
+        do {
+                docids.emplace_back(mu_msg_iter_get_docid (iter));
+        } while (mu_msg_iter_next (iter));
+        mu_msg_iter_destroy (iter);
+
+        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 (MuStore *store, unsigned docid)
+{
+        GError *gerr{};
+        auto msg{mu_store_get_msg (store, docid, &gerr)};
+        if (!msg)
+                throw Error(Error::Code::Store, &gerr, "could not get message from store");
+
+        auto p{mu_msg_get_path(msg)};
+        if (!p) {
+                mu_msg_unref(msg);
+                throw Error(Error::Code::Store,
+                            "could not get path for message %u", docid);
+        }
+
+        std::string msgpath{p};
+        mu_msg_unref (msg);
+
+        return msgpath;
+}
+
+
+static std::vector<DocId>
+determine_docids (MuQuery *query, 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 { (unsigned)docid };
+        else
+                return docids_for_msgid (query, msgid.c_str());
+}
+
+
+static void
+find_handler (Context& context, const Parameters& params)
+{
+        const auto query{get_string_or(params, "query")};
+        const auto threads{get_bool_or(params, "threads", false)};
+        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)};
+
+        MuMsgFieldId sort_field{MU_MSG_FIELD_ID_NONE};
+        if (!sortfieldstr.empty()) {
+                sort_field = mu_msg_field_id_from_name (
+                        sortfieldstr.c_str() + 1, FALSE); // skip ':'
+                if (sort_field == MU_MSG_FIELD_ID_NONE)
+                        throw Error{Error::Code::InvalidArgument, "invalid sort field %s",
+                                        sortfieldstr.c_str()};
+        }
+
+        int qflags{MU_QUERY_FLAG_NONE/*UNREADABLE*/};
+        if (descending)
+                qflags |= MU_QUERY_FLAG_DESCENDING;
+        if (skip_dups)
+                qflags |= MU_QUERY_FLAG_SKIP_DUPS;
+        if (include_related)
+                qflags |= MU_QUERY_FLAG_INCLUDE_RELATED;
+        if (threads)
+                qflags |= MU_QUERY_FLAG_THREADS;
+
+        GError *gerr{};
+        auto miter{mu_query_run(context.query, query.c_str(), sort_field, maxnum,
+                                (MuQueryFlags)qflags, &gerr)};
+        if (!miter)
+                throw Error(Error::Code::Query, &gerr, "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. */
+        print_expr ("(:erase t)");
+        const auto foundnum{print_sexps (miter, maxnum)};
+        print_expr ("(:found %u)", foundnum);
+        mu_msg_iter_destroy (miter);
+}
+
+
+static void
+help_handler (Context& context, const Parameters& params)
+{
+        const auto command{get_symbol_or(params, "command", "")};
+        const auto full{get_bool_or(params, "full")};
+
+        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 information about the 'quit' command\n;;\n";
+                std::cout << ";; The following commands are available:\n";
+        }
+
+        std::vector<std::string> names;
+        for (auto&& name_cmd: context.command_map)
+                names.emplace_back(name_cmd.first);
+        std::sort(names.begin(), names.end());
+
+        for (auto&& name: names) {
+                const auto& info{context.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 MuError
+index_msg_cb (MuIndexStats *stats, void *user_data)
+{
+        if (MuTerminate)
+                return MU_STOP;
+
+        if (stats->_processed % 1000)
+                return MU_OK;
+
+        print_expr ("(:info index :status running "
+                    ":processed %u :updated %u)",
+                    stats->_processed, stats->_updated);
+
+        return MU_OK;
+}
+
+
+static MuError
+index_and_maybe_cleanup (MuIndex *index, bool cleanup, bool lazy_check)
+{
+        MuIndexStats stats{}, stats2{};
+        mu_index_stats_clear (&stats);
+        auto rv = mu_index_run (index, FALSE, lazy_check, &stats,
+                                index_msg_cb, NULL, NULL);
+        if (rv != MU_OK && rv != MU_STOP)
+                throw Error{Error::Code::Store, "indexing failed"};
+
+        mu_index_stats_clear (&stats2);
+        if (cleanup) {
+                GError *gerr{};
+                rv = mu_index_cleanup (index, &stats2, NULL, NULL, &gerr);
+                if (rv != MU_OK && rv != MU_STOP)
+                        throw Error{Error::Code::Store, &gerr, "cleanup failed"};
+        }
+
+        print_expr ("(:info index :status complete "
+                    ":processed %u :updated %u :cleaned-up %u)",
+                    stats._processed, stats._updated, stats2._cleaned_up);
+
+        return rv;
+}
+
+
+static void
+index_handler (Context& context, const Parameters& params)
+{
+        GError *gerr{};
+        const auto cleanup{get_bool_or(params,   "cleanup")};
+        const auto lazy_check{get_bool_or(params,  "lazy-check")};
+        auto index{mu_index_new (context.store, &gerr)};
+        if (!index)
+                throw Error(Error::Code::Index, &gerr, "failed to create index object");
+
+        try {
+                index_and_maybe_cleanup (index, cleanup, lazy_check);
+        } catch (...) {
+                mu_index_destroy(index);
+                throw;
+        }
+        mu_index_destroy(index);
+        mu_store_flush(context.store);
+}
+
+static void
+mkdir_handler (Context& context, const Parameters& params)
+{
+        const auto path{get_string_or(params, "path")};
+
+        GError *gerr{};
+        if (!mu_maildir_mkdir(path.c_str(), 0755, FALSE, &gerr))
+                throw Error{Error::Code::File, &gerr, "failed to create maildir"};
+
+        print_expr ("(:info mkdir :message \"%s has been created\")", path.c_str());
+}
+
+
+static MuFlags
+get_flags (const std::string& path, const std::string& flagstr)
+{
+        if (flagstr.empty())
+                return MU_FLAG_NONE; /* ie., ignore flags */
+        else {
+                /* if there's a '+' or '-' sign in the string, it must
+                 * be a flag-delta */
+                if (strstr (flagstr.c_str(), "+") || strstr (flagstr.c_str(), "-")) {
+                        auto oldflags = mu_maildir_get_flags_from_path (path.c_str());
+                        return mu_flags_from_str_delta (flagstr.c_str(), oldflags, MU_FLAG_TYPE_ANY);
+                } else
+                        return  mu_flags_from_str (flagstr.c_str(), MU_FLAG_TYPE_ANY,
+                                                   TRUE /*ignore invalid*/);
+        }
+}
+
+static void
+do_move (MuStore *store, DocId docid, MuMsg *msg, const std::string& maildirarg,
+         MuFlags flags, bool new_name, bool no_view)
+{
+        bool different_mdir{};
+        auto maildir{maildirarg};
+        if (maildir.empty()) {
+                maildir = mu_msg_get_maildir (msg);
+                different_mdir = FALSE;
+        } else /* are we moving to a different mdir, or is it just flags? */
+                different_mdir = maildir != mu_msg_get_maildir(msg);
+
+        GError* gerr{};
+        if (!mu_msg_move_to_maildir (msg, maildir.c_str(), flags, TRUE, new_name, &gerr))
+                throw Error{Error::Code::File, &gerr, "failed to move message"};
+
+        /* after mu_msg_move_to_maildir, path will be the *new* path, and flags and maildir fields
+         * will be updated as wel */
+        auto rv = mu_store_update_msg (store, docid, msg, &gerr);
+        if (rv == MU_STORE_INVALID_DOCID)
+                throw Error{Error::Code::Store, &gerr, "failed to store updated message"};
+
+        char *sexp = mu_msg_to_sexp (msg, docid, NULL, MU_MSG_OPTION_VERIFY);
+        /* note, the :move t thing is a hint to the frontend that it
+         * could remove the particular header */
+        print_expr ("(:update %s :move %s :maybe-view %s)", sexp,
+                    different_mdir ? "t" : "nil",
+                    no_view ? "nil" : "t");
+        g_free (sexp);
+}
+
+static void
+move_docid (MuStore *store, DocId docid, const std::string& flagstr,
+            bool new_name, bool no_view)
+{
+        if (docid == MU_STORE_INVALID_DOCID)
+                throw Error{Error::Code::InvalidArgument, "invalid docid"};
+
+        GError *gerr{};
+        auto msg{mu_store_get_msg (store, docid, &gerr)};
+
+        try {
+                if (!msg)
+                        throw Error{Error::Code::Store, &gerr, "failed to get message from store"};
+
+                const auto flags = flagstr.empty() ? mu_msg_get_flags (msg) :
+                        get_flags (mu_msg_get_path(msg), flagstr);
+                if (flags == MU_FLAG_INVALID)
+                        throw Error{Error::Code::InvalidArgument, "invalid flags '%s'", flagstr.c_str()};
+
+                do_move (store, docid, msg, "", flags, new_name, no_view);
+
+        } catch (...) {
+                if (msg)
+                        mu_msg_unref (msg);
+                throw;
+        }
+
+        mu_msg_unref (msg);
+}
+
+/*
+ * '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>)
+ *
+ */
+static void
+move_handler (Context& context, const Parameters& params)
+{
+        auto maildir{get_string_or(params, "maildir")};
+        const auto flagstr{get_string_or(params, "flags")};
+        const auto rename{get_bool_or (params, "rename")};
+        const auto no_view{get_bool_or (params, "noupdate")};
+        const auto docids{determine_docids (context.query, 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)
+                        move_docid(context.store, docid, flagstr, rename, no_view);
+                return;
+        }
+        auto docid{docids.at(0)};
+
+        GError *gerr{};
+        auto msg{mu_store_get_msg(context.store, docid, &gerr)};
+        if (!msg)
+                throw Error{Error::Code::InvalidArgument, &gerr, "could not create message"};
+
+        /* if maildir was not specified, take the current one */
+        if (maildir.empty())
+                maildir = mu_msg_get_maildir (msg);
+
+        /* determine the real target flags, which come from the flags-parameter
+         * we received (ie., flagstr), if any, plus the existing message
+         * flags. */
+        MuFlags flags{};
+        if (!flagstr.empty())
+                flags = get_flags (mu_msg_get_path(msg), flagstr.c_str());
+        else
+                flags = mu_msg_get_flags (msg);
+
+        if (flags == MU_FLAG_INVALID) {
+                mu_msg_unref(msg);
+                throw Error{Error::Code::InvalidArgument, "invalid flagse"};
+        }
+
+        try {
+                do_move (context.store, docid, msg, maildir, flags, rename, no_view);
+        } catch (...) {
+                mu_msg_unref(msg);
+                throw;
+        }
+
+        mu_msg_unref(msg);
+}
+
+static void
+ping_handler (Context& context, const Parameters& params)
+{
+        GError *gerr{};
+        const auto storecount = mu_store_count(context.store, &gerr);
+        if (storecount == (unsigned)-1)
+                throw Error{Error::Code::Store, &gerr, "failed to read store"};
+
+        const auto queries  = get_string_vec (params, "queries");
+        const auto qresults = [&]() -> std::string {
+                if (queries.empty())
+                        return {};
+
+                std::string res{":queries ("};
+                for (auto&& q: queries) {
+                        const auto count{mu_query_count_run (context.query, q.c_str())};
+                        const auto unreadq{format("flag:unread AND (%s)", q.c_str())};
+                        const auto unread{mu_query_count_run (context.query, unreadq.c_str())};
+                        res += format("(:query %s :count %zu :unread %zu)", quote(q).c_str(),
+                                      count, unread);
+                }
+                return res + ")";
+        }();
+
+        const auto personal = [&]() ->std::string {
+                auto addrs{mu_store_personal_addresses (context.store)};
+                std::string res;
+                if (addrs && g_strv_length(addrs) != 0) {
+                        res = ":personal-addresses (";
+                        for (int i = 0; addrs[i]; ++i)
+                                res += quote(addrs[i]) + ' ';
+                        res += ")";
+                }
+                g_strfreev(addrs);
+                return res;
+        }();
+
+        print_expr ("(:pong \"mu\" :props ("
+                    ":version \"" VERSION "\" "
+                    "%s "
+                    ":database-path %s "
+                    ":root-maildir %s "
+                    ":doccount %u "
+                    "%s))",
+                    personal.c_str(),
+                    quote(mu_store_database_path(context.store)).c_str(),
+                    quote(mu_store_root_maildir(context.store)).c_str(),
+                    storecount,
+                    qresults.c_str());
+}
+
+static void
+quit_handler (Context& context, const Parameters& params)
+{
+        context.do_quit = true;
+}
+
+
+static void
+remove_handler (Context& context, const Parameters& params)
+{
+        const auto docid{get_int_or(params, "docid")};
+        const auto path{path_from_docid (context.store, docid)};
+
+        if (::unlink (path.c_str()) != 0 && errno != ENOENT)
+                throw Error(Error::Code::File, "could not delete %s: %s",
+                                  path.c_str(), strerror (errno));
+
+        if (!mu_store_remove_path (context.store, path.c_str()))
+                throw Error(Error::Code::Store,
+                            "failed to remove message @ %s (%d) from store",
+                            path.c_str(), docid);
+
+        print_expr ("(:remove %u)", docid);
+}
+
+
+static void
+sent_handler (Context& context, const Parameters& params)
+{
+        GError *gerr{};
+        const auto path{get_string_or(params, "path")};
+        const auto docid{mu_store_add_path(context.store, path.c_str(), &gerr)};
+        if (docid == MU_STORE_INVALID_DOCID)
+                throw Error{Error::Code::Store, &gerr, "failed to add path"};
+
+        print_expr ("(:sent t :path %s :docid %u)", quote(path).c_str(), docid);
+}
+
+
+static void
+view_handler (Context& context, const Parameters& params)
+{
+        DocId docid{};
+        const auto path{get_string_or(params, "path")};
+
+        GError *gerr{};
+        MuMsg *msg{};
+
+        if (!path.empty())
+                msg   = mu_msg_new_from_file (path.c_str(), NULL, &gerr);
+        else {
+                docid = determine_docids(context.query, params).at(0);
+                msg   = mu_store_get_msg (context.store, docid, &gerr);
+        }
+
+        if (!msg)
+                throw Error{Error::Code::Store, &gerr, "failed to find message for view"};
+
+        auto sexp{mu_msg_to_sexp(msg, docid, {}, message_options(params))};
+        mu_msg_unref(msg);
+
+        print_expr ("(:view %s)\n", sexp);
+        g_free (sexp);
+}
+
+
+static CommandMap
+make_command_map (Context& context)
+{
+      CommandMap cmap;
+
+      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(context, params);}});
+
+      cmap.emplace("compose",
+                   CommandInfo{
+                           ArgMap{{"type", ArgInfo{Type::Symbol, true,
+                                            "type of composition: reply/forward/edit/resend/new"}},
+                                   {"docid", ArgInfo{Type::Integer, false,"document id of parent-message, if any"}},
+                                   {"decrypt", ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)" }}},
+                           "get contact information",
+                           [&](const auto& params){compose_handler(context, 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" }}},
+                           "get contact information",
+                           [&](const auto& params){contacts_handler(context, params);}});
+
+      cmap.emplace("extract",
+                   CommandInfo{
+                           ArgMap{{"docid", ArgInfo{Type::Integer, true,  "document for the message" }},
+                                   {"index", ArgInfo{Type::Integer, true,  "index for the part to operate on" }},
+                                   {"action", ArgInfo{Type::Symbol, true, "what to do with the part" }},
+                                   {"decrypt", ArgInfo{Type::Symbol, false,
+                                            "whether to decrypt encrypted parts (if any)" }},
+                                   {"path",    ArgInfo{Type::String, false, "part for saving (for action: save)" }},
+                                   {"what",    ArgInfo{Type::Symbol, false, "what to do with the part (feedback)" }},
+                                   {"param",   ArgInfo{Type::String, false, "parameter for 'what'" }}},
+                           "extract mime-parts from a message",
+                           [&](const auto& params){extract_handler(context, 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" }},
+                                   {"maxnum",  ArgInfo{Type::Integer, 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(context, params);}});
+
+      cmap.emplace("help",
+                   CommandInfo{
+                           ArgMap{ {"command", ArgInfo{Type::Symbol, false,
+                                                   "command to get information for" }},
+                                   {"full", ArgInfo{Type::Symbol, false,
+                                            "whether to include information about parameters" }}},
+                           "get information about one or all commands",
+                           [&](const auto& params){help_handler(context, 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(context, params);}});
+
+      cmap.emplace("move",
+                   CommandInfo{
+                           ArgMap{{"docid",  ArgInfo{Type::Integer, 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(context, 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(context, 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(context, params);}});
+
+      cmap.emplace("quit",
+                   CommandInfo{{},
+                           "quit the mu server",
+                           [&](const auto& params){quit_handler(context, params);}});
+
+      cmap.emplace("remove",
+                   CommandInfo{
+                           ArgMap{ {"docid", ArgInfo{Type::Integer, true,
+                                                   "document-id for the message to remove" }}},
+                           "remove a message from filesystem and database",
+                          [&](const auto& params){remove_handler(context, 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(context, params);}});
+
+      cmap.emplace("view",
+                   CommandInfo{
+                           ArgMap{{"docid",  ArgInfo{Type::Integer, false, "document-id"}},
+                                   {"msgid",  ArgInfo{Type::String, false, "message-id"}},
+                                   {"path",   ArgInfo{Type::String, false, "message filesystem path"}},
+
+                                   {"extract-images", ArgInfo{Type::Symbol, false,
+                                                           "whether to extract images for this messages (if any)"}},
+                                   {"decrypt", ArgInfo{Type::Symbol, false,
+                                                           "whether to decrypt encrypted parts (if any)" }},
+                                   {"verify", ArgInfo{Type::Symbol, false,
+                                                           "whether to verify signatures (if any)" }}
+
+                           },
+                           "view a message. exactly one of docid/msgid/path must be specified",
+                           [&](const auto& params){view_handler(context, params);}});
+      return cmap;
+}
+
+
+static std::string
+read_line(bool& do_quit)
+{
+        std::string line;
+        std::cout << ";; mu> ";
+        if (!std::getline(std::cin, line))
+                do_quit = true;
+        return line;
+}
+
+MuError
+mu_cmd_server (MuConfig *opts, GError **err) try
+{
+        if (opts->commands) {
+                Context ctx{};
+                auto cmap = make_command_map(ctx);
+                invoke(cmap, Sexp::parse("(help :full t)"));
+                return MU_OK;
+        }
+
+        Context context{opts};
+        context.command_map = make_command_map (context);
+
+        if (opts->eval) { // evaluate command-line command & exit
+                auto call{Sexp::parse(opts->eval)};
+                invoke(context.command_map, call);
+                return MU_OK;
+        }
+
+        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";
+
+        while (!MuTerminate && !context.do_quit) {
+
+                std::string line;
+                try {
+                        line = read_line(context.do_quit);
+                        if (line.find_first_not_of(" \t") == std::string::npos)
+                                continue; // skip whitespace-only lines
+
+                        auto call{Sexp::parse(line)};
+
+                        invoke(context.command_map, call);
+
+                } catch (const Error& er) {
+                        std::cerr << ";; error: " << er.what() << "\n";
+                        print_error ((MuError)er.code(), "%s (line was:'%s')",
+                                     er.what(), line.c_str());
+                }
+        }
+
+        return MU_OK;
+
+} catch (const Error& er) {
+        g_set_error(err, MU_ERROR_DOMAIN, MU_ERROR, "%s", er.what());
+        return MU_ERROR;
+} catch (...) {
+        g_set_error(err, MU_ERROR_DOMAIN, MU_ERROR, "%s", "caught exception");
+        return MU_ERROR;
+}
diff --git a/mu/mu-cmd.c b/mu/mu-cmd.c
new file mode 100644 (file)
index 0000000..6c4c53b
--- /dev/null
@@ -0,0 +1,732 @@
+/*
+** 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 <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "mu-msg.h"
+#include "mu-msg-part.h"
+#include "mu-cmd.h"
+#include "mu-maildir.h"
+#include "mu-contacts.hh"
+#include "mu-runtime.h"
+#include "mu-flags.h"
+
+#include "utils/mu-log.h"
+#include "utils/mu-util.h"
+#include "utils/mu-str.h"
+#include "utils/mu-date.h"
+
+#define VIEW_TERMINATOR '\f' /* form-feed */
+
+static gboolean
+view_msg_sexp (MuMsg *msg, MuConfig *opts)
+{
+       char *sexp;
+
+       sexp = mu_msg_to_sexp (msg, 0, NULL, mu_config_get_msg_options(opts));
+       fputs (sexp, stdout);
+       g_free (sexp);
+
+       return TRUE;
+}
+
+
+static void
+each_part (MuMsg *msg, MuMsgPart *part, gchar **attach)
+{
+       char *fname, *tmp;
+
+       if (!mu_msg_part_maybe_attachment (part))
+               return;
+
+       fname = mu_msg_part_get_filename (part, FALSE);
+       if (!fname)
+               return;
+
+       tmp = *attach;
+       *attach = g_strdup_printf ("%s%s'%s'",
+                                  *attach ? *attach : "",
+                                  *attach ? ", " : "",
+                                  fname);
+       g_free (tmp);
+}
+
+/* return comma-sep'd list of attachments */
+static gchar *
+get_attach_str (MuMsg *msg, MuConfig *opts)
+{
+       gchar           *attach;
+       MuMsgOptions     msgopts;
+
+       msgopts = mu_config_get_msg_options(opts) |
+               MU_MSG_OPTION_CONSOLE_PASSWORD;
+
+       attach = NULL;
+       mu_msg_part_foreach (msg, msgopts,
+                            (MuMsgPartForeachFunc)each_part, &attach);
+       return attach;
+}
+
+#define color_maybe(C) do { if(color) fputs ((C),stdout);} while(0)
+
+static void
+print_field (const char* field, const char *val, gboolean color)
+{
+       if (!val)
+               return;
+
+       color_maybe (MU_COLOR_MAGENTA);
+       mu_util_fputs_encoded (field, stdout);
+       color_maybe (MU_COLOR_DEFAULT);
+       fputs (": ", stdout);
+
+       if (val) {
+               color_maybe (MU_COLOR_GREEN);
+               mu_util_fputs_encoded (val, 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 (MuMsg *msg, MuConfig *opts)
+{
+       const char *body;
+       gboolean color;
+
+       color = !opts->nocolor;
+       body = mu_msg_get_body_text (msg,
+                                    mu_config_get_msg_options(opts) |
+                                    MU_MSG_OPTION_CONSOLE_PASSWORD);
+       if (!body) {
+               if (mu_msg_get_flags (msg) & MU_FLAG_ENCRYPTED) {
+                       color_maybe (MU_COLOR_CYAN);
+                       g_print ("[No body found; "
+                                "message has encrypted parts]\n");
+               } else {
+                       color_maybe (MU_COLOR_MAGENTA);
+                       g_print ("[No body found]\n");
+               }
+               color_maybe (MU_COLOR_DEFAULT);
+               return;
+       }
+
+       if (opts->summary_len != 0) {
+               gchar *summ;
+               summ = mu_str_summarize (body, opts->summary_len);
+               print_field ("Summary", summ, color);
+               g_free (summ);
+       } else {
+               mu_util_print_encoded ("%s", body);
+               if (!g_str_has_suffix (body, "\n"))
+                       g_print ("\n");
+       }
+}
+
+
+/* we ignore fields for now */
+/* summary_len == 0 means "no summary */
+static gboolean
+view_msg_plain (MuMsg *msg, MuConfig *opts)
+{
+       gchar *attachs;
+       time_t date;
+       const GSList *lst;
+       gboolean color;
+
+       color = !opts->nocolor;
+
+       print_field ("From",    mu_msg_get_from (msg),    color);
+       print_field ("To",      mu_msg_get_to (msg),      color);
+       print_field ("Cc",      mu_msg_get_cc (msg),      color);
+       print_field ("Bcc",     mu_msg_get_bcc (msg),     color);
+       print_field ("Subject", mu_msg_get_subject (msg), color);
+
+       if ((date = mu_msg_get_date (msg)))
+               print_field ("Date", mu_date_str_s ("%c", date),
+                            color);
+
+       if ((lst = mu_msg_get_tags (msg))) {
+               gchar *tags;
+               tags = mu_str_from_list (lst,',');
+               print_field ("Tags", tags, color);
+               g_free (tags);
+       }
+
+       if ((attachs = get_attach_str (msg, opts))) {
+               print_field ("Attachments", attachs, color);
+               g_free (attachs);
+       }
+
+       body_or_summary (msg, opts);
+
+       return TRUE;
+}
+
+
+static gboolean
+handle_msg (const char *fname, MuConfig *opts, GError **err)
+{
+       MuMsg *msg;
+       gboolean rv;
+
+       msg = mu_msg_new_from_file (fname, NULL, err);
+       if (!msg)
+               return FALSE;
+
+       switch (opts->format) {
+       case MU_CONFIG_FORMAT_PLAIN:
+               rv = view_msg_plain (msg, opts);
+               break;
+       case MU_CONFIG_FORMAT_SEXP:
+               rv = view_msg_sexp (msg, opts);
+               break;
+       default:
+               g_critical ("bug: should not be reached");
+               rv = FALSE;
+       }
+
+       mu_msg_unref (msg);
+
+       return rv;
+}
+
+static gboolean
+view_params_valid (MuConfig *opts, GError **err)
+{
+       /* note: params[0] will be 'view' */
+       if (!opts->params[0] || !opts->params[1]) {
+               mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "error in parameters");
+               return FALSE;
+       }
+
+       switch (opts->format) {
+       case MU_CONFIG_FORMAT_PLAIN:
+       case MU_CONFIG_FORMAT_SEXP:
+               break;
+       default:
+               mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "invalid output format");
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+
+static MuError
+cmd_view (MuConfig *opts, GError **err)
+{
+       int i;
+       gboolean rv;
+
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_VIEW,
+                             MU_ERROR_INTERNAL);
+
+       rv = view_params_valid (opts, err);
+       if (!rv)
+               goto leave;
+
+       for (i = 1; opts->params[i]; ++i) {
+
+               rv = handle_msg (opts->params[i], opts, err);
+               if (!rv)
+                       break;
+
+               /* add a separator between two messages? */
+               if (opts->terminator)
+                       g_print ("%c", VIEW_TERMINATOR);
+       }
+
+leave:
+       if (!rv)
+               return err && *err ? (*err)->code : MU_ERROR;
+
+       return MU_OK;
+}
+
+static MuError
+cmd_mkdir (MuConfig *opts, GError **err)
+{
+       int i;
+
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_MKDIR,
+                             MU_ERROR_INTERNAL);
+
+       if (!opts->params[1]) {
+               mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "missing directory parameter");
+               return MU_ERROR_IN_PARAMETERS;
+       }
+
+       for (i = 1; opts->params[i]; ++i)
+               if (!mu_maildir_mkdir (opts->params[i], opts->dirmode,
+                                      FALSE, err))
+                       return err && *err ? (*err)->code :
+                               MU_ERROR_FILE_CANNOT_MKDIR;
+       return MU_OK;
+}
+
+
+static gboolean
+check_file_okay (const char *path, gboolean cmd_add)
+{
+       if (!g_path_is_absolute (path)) {
+               g_warning ("path is not absolute: %s", path);
+               return FALSE;
+       }
+
+       if (cmd_add && access(path, R_OK) != 0) {
+               g_warning ("path is not readable: %s: %s",
+                          path, strerror (errno));
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+
+typedef gboolean (*ForeachMsgFunc) (MuStore *store, const char *path,
+                                   GError **err);
+
+
+static MuError
+foreach_msg_file (MuStore *store, MuConfig *opts,
+                 ForeachMsgFunc foreach_func, GError **err)
+{
+       unsigned        u;
+       gboolean        all_ok;
+
+       /* note: params[0] will be 'add' */
+       if (!opts->params[0] || !opts->params[1]) {
+               g_print ("usage: mu %s <file> [<files>]\n",
+                        opts->params[0] ? opts->params[0] : "<cmd>");
+               mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "missing parameters");
+               return MU_ERROR_IN_PARAMETERS;
+       }
+
+       for (u = 1, all_ok = TRUE; opts->params[u]; ++u) {
+
+               const char* path;
+
+               path = opts->params[u];
+
+               if (!check_file_okay (path, TRUE)) {
+                       all_ok = FALSE;
+                       MU_WRITE_LOG ("not a valid message file: %s", path);
+                       continue;
+               }
+
+               if (!foreach_func (store, path, err)) {
+                       all_ok = FALSE;
+                       MU_WRITE_LOG ("error with %s: %s", path,
+                                     (err&&*err) ? (*err)->message :
+                                     "something went wrong");
+                       g_clear_error (err);
+                       continue;
+               }
+       }
+
+       if (!all_ok) {
+               mu_util_g_set_error (err, MU_ERROR_XAPIAN_STORE_FAILED,
+                                    "%s failed for some message(s)",
+                                    opts->params[0]);
+               return MU_ERROR_XAPIAN_STORE_FAILED;
+       }
+
+       return MU_OK;
+}
+
+
+static gboolean
+add_path_func (MuStore *store, const char *path, GError **err)
+{
+       return mu_store_add_path (store, path, err);
+}
+
+
+static MuError
+cmd_add (MuStore *store, MuConfig *opts, GError **err)
+{
+       g_return_val_if_fail (store, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_ADD,
+                             MU_ERROR_INTERNAL);
+
+       return foreach_msg_file (store, opts, add_path_func, err);
+}
+
+static gboolean
+remove_path_func (MuStore *store, const char *path, GError **err)
+{
+       if (!mu_store_remove_path (store, path)) {
+               mu_util_g_set_error (err, MU_ERROR_XAPIAN_REMOVE_FAILED,
+                                    "failed to remove %s", path);
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static MuError
+cmd_remove (MuStore *store, MuConfig *opts, GError **err)
+{
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_REMOVE,
+                             MU_ERROR_INTERNAL);
+
+       return foreach_msg_file (store, opts, remove_path_func, err);
+}
+
+static gboolean
+tickle_func (MuStore *store, const char *path, GError **err)
+{
+       MuMsg           *msg;
+       gboolean         rv;
+
+       msg = mu_msg_new_from_file (path, NULL, err);
+       if (!msg)
+               return FALSE;
+
+       rv = mu_msg_tickle (msg, err);
+       mu_msg_unref (msg);
+
+       return rv;
+}
+
+
+static MuError
+cmd_tickle (MuStore *store, MuConfig *opts, GError **err)
+{
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_TICKLE,
+                             MU_ERROR_INTERNAL);
+
+       return foreach_msg_file (store, opts, tickle_func, err);
+}
+
+struct _VData {
+       MuMsgPartSigStatus combined_status;
+       char *report;
+       gboolean oneline;
+};
+typedef struct _VData VData;
+
+static void
+each_sig (MuMsg *msg, MuMsgPart *part, VData *vdata)
+{
+       MuMsgPartSigStatusReport *report;
+
+       report = part->sig_status_report;
+       if (!report)
+               return;
+
+       if (vdata->oneline)
+               vdata->report = g_strdup_printf
+                       ("%s%s%s",
+                        vdata->report ? vdata->report : "",
+                        vdata->report ? "; " : "",
+                        report->report);
+       else
+               vdata->report = g_strdup_printf
+                       ("%s%s\t%s",
+                        vdata->report ? vdata->report : "",
+                        vdata->report ? "\n" : "",
+                        report->report);
+
+       if (vdata->combined_status == MU_MSG_PART_SIG_STATUS_BAD ||
+           vdata->combined_status == MU_MSG_PART_SIG_STATUS_ERROR)
+               return;
+
+       vdata->combined_status = report->verdict;
+}
+
+
+static void
+print_verdict (VData *vdata, gboolean color, gboolean verbose)
+{
+       g_print ("verdict: ");
+
+       switch (vdata->combined_status) {
+       case MU_MSG_PART_SIG_STATUS_UNSIGNED:
+               g_print ("no signature found");
+               break;
+       case MU_MSG_PART_SIG_STATUS_GOOD:
+               color_maybe (MU_COLOR_GREEN);
+               g_print ("signature(s) verified");
+               break;
+       case MU_MSG_PART_SIG_STATUS_BAD:
+               color_maybe (MU_COLOR_RED);
+               g_print ("bad signature");
+               break;
+       case MU_MSG_PART_SIG_STATUS_ERROR:
+               color_maybe (MU_COLOR_RED);
+               g_print ("verification failed");
+               break;
+       case MU_MSG_PART_SIG_STATUS_FAIL:
+               color_maybe(MU_COLOR_RED);
+               g_print ("error in verification process");
+               break;
+       default: g_return_if_reached ();
+       }
+
+       color_maybe (MU_COLOR_DEFAULT);
+       if (vdata->report && verbose)
+               g_print ("%s%s\n",
+                        (vdata->oneline) ? ";" : "\n",
+                        vdata->report);
+       else
+               g_print ("\n");
+}
+
+
+static MuError
+cmd_verify (MuConfig *opts, GError **err)
+{
+       MuMsg *msg;
+       MuMsgOptions msgopts;
+       VData vdata;
+
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+       g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_VERIFY,
+                             MU_ERROR_INTERNAL);
+
+       if (!opts->params[1]) {
+               mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "missing message-file parameter");
+               return MU_ERROR_IN_PARAMETERS;
+       }
+
+       msg = mu_msg_new_from_file (opts->params[1], NULL, err);
+       if (!msg)
+               return MU_ERROR;
+
+       msgopts = mu_config_get_msg_options (opts)
+               | MU_MSG_OPTION_VERIFY
+               | MU_MSG_OPTION_CONSOLE_PASSWORD;
+
+       vdata.report  = NULL;
+       vdata.combined_status = MU_MSG_PART_SIG_STATUS_UNSIGNED;
+       vdata.oneline = FALSE;
+
+       mu_msg_part_foreach (msg, msgopts,
+                            (MuMsgPartForeachFunc)each_sig, &vdata);
+
+       if (!opts->quiet)
+               print_verdict (&vdata, !opts->nocolor, opts->verbose);
+
+       mu_msg_unref (msg);
+       g_free (vdata.report);
+
+       return vdata.combined_status == MU_MSG_PART_SIG_STATUS_GOOD ?
+               MU_OK : MU_ERROR;
+}
+
+static MuError
+cmd_info (MuStore *store, MuConfig *opts, GError **err)
+{
+       mu_store_print_info (store, opts->nocolor);
+
+       return MU_OK;
+}
+
+
+static MuError
+cmd_init (MuConfig *opts, GError **err)
+{
+       MuStore    *store;
+       const char *path;
+
+        /* not provided, nor could we find a good default */
+        if (!opts->maildir) {
+                mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "missing --maildir parameter and could "
+                                     "not determine default");
+               return MU_ERROR_IN_PARAMETERS;
+        }
+
+       path  = mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB);
+       store = mu_store_new_create (path,
+                                    opts->maildir,
+                                    (const char**)opts->my_addresses,
+                                    err);
+       if (!store)
+               return MU_G_ERROR_CODE(err);
+
+       if (!opts->quiet) {
+               mu_store_print_info (store, opts->nocolor);
+               g_print ("\nstore created.\n"
+                        "use 'mu index' to fill the database "
+                         "with your messages.\n"
+                        "see mu-index(1) for details\n");
+       }
+
+       mu_store_unref (store);
+       return MU_OK;
+}
+
+
+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");
+}
+
+typedef MuError (*store_func) (MuStore *, MuConfig *, GError **err);
+
+
+static MuError
+with_store (store_func func, MuConfig *opts, gboolean read_only, GError **err)
+{
+       MuError          merr;
+       MuStore         *store;
+       const char      *path;
+
+       path  = mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB);
+
+       if (read_only)
+               store = mu_store_new_readable (path, err);
+       else
+               store = mu_store_new_writable (path, err);
+
+       if (!store)
+               return MU_G_ERROR_CODE(err);
+
+       merr = func (store, opts, err);
+       mu_store_unref (store);
+
+       return merr;
+}
+
+static MuError
+with_readonly_store (store_func func, MuConfig *opts, GError **err)
+{
+       return with_store (func, opts, TRUE, err);
+}
+
+static MuError
+with_writable_store (store_func func, MuConfig *opts, GError **err)
+{
+       return with_store (func, opts, FALSE, err);
+}
+
+static gboolean
+check_params (MuConfig *opts, GError **err)
+{
+       if (!opts->params||!opts->params[0]) {/* no command? */
+               show_usage ();
+               mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS,
+                                    "error in parameters");
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static void
+set_log_options (MuConfig *opts)
+{
+       MuLogOptions logopts;
+
+       logopts = MU_LOG_OPTIONS_NONE;
+
+       if (opts->quiet)
+               logopts |= MU_LOG_OPTIONS_QUIET;
+       if (!opts->nocolor)
+               logopts |= MU_LOG_OPTIONS_COLOR;
+       if (opts->log_stderr)
+               logopts |= MU_LOG_OPTIONS_STDERR;
+       if (opts->debug)
+               logopts |= MU_LOG_OPTIONS_DEBUG;
+}
+
+MuError
+mu_cmd_execute (MuConfig *opts, GError **err)
+{
+       MuError merr;
+
+       g_return_val_if_fail (opts, MU_ERROR_INTERNAL);
+
+       if (!check_params(opts, err))
+               return MU_G_ERROR_CODE(err);
+
+       set_log_options (opts);
+
+       switch (opts->cmd) {
+
+               /* already handled in mu-config.c */
+       case MU_CONFIG_CMD_HELP: return MU_OK;
+
+        /* no store needed */
+
+       case MU_CONFIG_CMD_MKDIR:   merr = cmd_mkdir   (opts, err); break;
+       case MU_CONFIG_CMD_SCRIPT:  merr = mu_cmd_script  (opts, err); break;
+       case MU_CONFIG_CMD_VIEW:    merr = cmd_view    (opts, err); break;
+       case MU_CONFIG_CMD_VERIFY:  merr = cmd_verify  (opts, err); break;
+       case MU_CONFIG_CMD_EXTRACT: merr = mu_cmd_extract (opts, err); break;
+
+       /* read-only store */
+
+       case MU_CONFIG_CMD_CFIND:
+               merr = with_readonly_store (mu_cmd_cfind, opts, err); break;
+       case MU_CONFIG_CMD_FIND:
+               merr = with_readonly_store (mu_cmd_find, opts, err);  break;
+       case MU_CONFIG_CMD_INFO:
+               merr = with_readonly_store (cmd_info, opts, err);  break;
+
+       /* writable store */
+
+       case MU_CONFIG_CMD_ADD:
+               merr = with_writable_store (cmd_add, opts, err);      break;
+       case MU_CONFIG_CMD_REMOVE:
+               merr = with_writable_store (cmd_remove, opts, err);   break;
+       case MU_CONFIG_CMD_TICKLE:
+               merr = with_writable_store (cmd_tickle, opts, err);   break;
+       case MU_CONFIG_CMD_INDEX:
+               merr = with_writable_store (mu_cmd_index, opts, err);   break;
+
+       /* commands instantiate store themselves */
+       case MU_CONFIG_CMD_INIT:
+               merr = cmd_init (opts,err); break;
+       case MU_CONFIG_CMD_SERVER:
+               merr = mu_cmd_server (opts, err); break;
+
+       default:
+               merr = MU_ERROR_IN_PARAMETERS; break;
+       }
+
+       return merr;
+}
diff --git a/mu/mu-cmd.h b/mu/mu-cmd.h
new file mode 100644 (file)
index 0000000..b65cd15
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+** 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_CMD_H__
+#define __MU_CMD_H__
+
+#include <glib.h>
+#include <mu-config.h>
+#include <mu-store.hh>
+
+G_BEGIN_DECLS
+
+/**
+ * execute the 'find' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return MU_OK (0) if the command succeeds and
+ * >MU_OK (0) results, MU_EXITCODE_NO_MATCHES if the command
+ * succeeds but there no matches, some error code for all other errors
+ */
+MuError mu_cmd_find (MuStore *store, MuConfig *opts, GError **err);
+
+
+/**
+ * execute the 'extract' command
+ *
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return MU_OK (0) if the command succeeds,
+ * some error code otherwise
+ */
+MuError mu_cmd_extract (MuConfig *opts, GError **err);
+
+
+/**
+ * execute the 'script' command
+ *
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return MU_OK (0) if the command succeeds,
+ * some error code otherwise
+ */
+MuError mu_cmd_script (MuConfig *opts, GError **err);
+
+/**
+ * execute the cfind command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return MU_OK (0) if the command succeeds,
+ * some error code otherwise
+ */
+MuError mu_cmd_cfind (MuStore *store, MuConfig *opts, GError **err);
+
+/**
+ * execute some mu command, based on 'opts'
+ *
+ * @param opts configuration option
+ * @param err receives error information, or NULL
+ *
+ * @return MU_OK if all went wall, some error code otherwise
+ */
+MuError mu_cmd_execute (MuConfig *opts, GError **err);
+
+/**
+ * execute the 'index' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return MU_OK (0) if the command succeeded,
+ * some error code otherwise
+ */
+MuError mu_cmd_index (MuStore *store, MuConfig *opt, GError **err);
+
+/**
+ * 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
+ */
+MuError mu_cmd_server (MuConfig *opts, GError **err);
+
+G_END_DECLS
+
+#endif /*__MU_CMD_H__*/
diff --git a/mu/mu-config.c b/mu/mu-config.c
new file mode 100644 (file)
index 0000000..89538cd
--- /dev/null
@@ -0,0 +1,809 @@
+/*
+** 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 <string.h>            /* memset */
+#include <unistd.h>
+#include <stdio.h>
+
+#include "mu-config.h"
+#include "mu-cmd.h"
+
+
+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 (void)
+{
+       /* If muhome is not set, we use the XDG Base Directory Specification
+        * locations. */
+       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 (void)
+{
+       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", 0, 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, 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 (void)
+{
+       if (!MU_CONFIG.maildir)
+               MU_CONFIG.maildir = mu_util_guess_maildir();
+
+       expand_dir (MU_CONFIG.maildir);
+}
+
+static GOptionGroup*
+config_options_group_init (void)
+{
+       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>"},
+                {NULL, 0, 0, 0, NULL, NULL, NULL}
+       };
+
+       og = g_option_group_new("init", "Options for the 'index' 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 (void)
+{
+       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, 0, 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 (void)
+{
+       /* note, when no fields are specified, we use
+        * date-from-subject, and sort descending by date. If fields
+        * *are* specified, we sort in ascending order. */
+       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 (void)
+{
+       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, 0, 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 (void)
+{
+       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, 0, 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 (void)
+{
+       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 (void)
+{
+       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>"},
+               {NULL, 0, 0, 0, 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 (void)
+{
+       GOptionGroup *og;
+       GOptionEntry entries[] = {
+               {G_OPTION_REMAINING, 0,0, G_OPTION_ARG_STRING_ARRAY,
+                &MU_CONFIG.params, "script parameters", NULL},
+               {NULL, 0, 0, 0, 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 (void)
+{
+       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 (void)
+{
+       static GOptionEntry entries[] = {
+               {"auto-retrieve", 'r', 0, G_OPTION_ARG_NONE,
+                &MU_CONFIG.auto_retrieve,
+                "attempt to retrieve keys online (false)", NULL},
+               {"use-agent", 'a', 0, G_OPTION_ARG_NONE, &MU_CONFIG.use_agent,
+                "attempt to use the GPG agent (false)", NULL},
+               {"decrypt", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.decrypt,
+                "attempt to decrypt the message", NULL},
+               {NULL, 0, 0, 0, NULL, NULL, NULL}
+       };
+
+       return entries;
+}
+
+static GOptionGroup *
+config_options_group_view (void)
+{
+       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)",
+                "<term>"},
+               {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr,
+                "output format ('plain'(*), 'sexp')", "<format>"},
+               {NULL, 0, 0, 0, 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 (void)
+{
+       if (!MU_CONFIG.targetdir)
+               MU_CONFIG.targetdir = g_strdup (".");
+
+       expand_dir (MU_CONFIG.targetdir);
+}
+
+
+static GOptionGroup*
+config_options_group_extract (void)
+{
+       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, 0, 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 (void)
+{
+       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 (void)
+{
+       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, 0, 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  },
+               { "tickle",  MU_CONFIG_CMD_TICKLE  },
+               { "verify",  MU_CONFIG_CMD_VERIFY  },
+               { "view",    MU_CONFIG_CMD_VIEW    }
+       };
+
+       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",
+                         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 gchar*
+get_help_string (MuConfigCmd cmd, gboolean long_help)
+{
+       unsigned u;
+
+       /* this include gets us MU_HELP_STRINGS */
+#include "mu-help-strings.h"
+
+       for (u = 0; u != G_N_ELEMENTS(MU_HELP_STRINGS); ++u)
+               if (cmd == MU_HELP_STRINGS[u].cmd) {
+                       if (long_help)
+                               return MU_HELP_STRINGS[u].long_help;
+                       else
+                               return MU_HELP_STRINGS[u].usage ;
+               }
+
+       g_return_val_if_reached ("");
+       return "";
+}
+
+
+void
+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 (void)
+{
+       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, TRUE);
+       rv  = TRUE;
+
+       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_config_init (int *argcp, char ***argvp, GError **err)
+{
+       g_return_val_if_fail (argcp && argvp, NULL);
+
+       memset (&MU_CONFIG, 0, sizeof(MU_CONFIG));
+
+       MU_CONFIG.maxnum = -1; /* By default, output all matching entries. */
+
+       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_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->params);
+
+       memset (opts, 0, sizeof(MU_CONFIG));
+}
+
+size_t
+mu_config_param_num (MuConfig *opts)
+{
+       size_t n;
+
+       g_return_val_if_fail (opts && opts->params, 0);
+       for (n = 0; opts->params[n]; ++n);
+
+       return n;
+}
+
+
+MuMsgOptions
+mu_config_get_msg_options (MuConfig *muopts)
+{
+       MuMsgOptions opts;
+
+       opts = MU_MSG_OPTION_NONE;
+
+       if (muopts->decrypt)
+               opts |= MU_MSG_OPTION_DECRYPT;
+       if (muopts->verify)
+               opts |= MU_MSG_OPTION_VERIFY;
+       if (muopts->use_agent)
+               opts |= MU_MSG_OPTION_USE_AGENT;
+       if (muopts->auto_retrieve)
+               opts |= MU_MSG_OPTION_AUTO_RETRIEVE;
+       if (muopts->overwrite)
+               opts |= MU_MSG_OPTION_OVERWRITE;
+
+       return opts;
+}
diff --git a/mu/mu-config.h b/mu/mu-config.h
new file mode 100644 (file)
index 0000000..43418ae
--- /dev/null
@@ -0,0 +1,260 @@
+/*
+** 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_CONFIG_H__
+#define __MU_CONFIG_H__
+
+#include <glib.h>
+#include <sys/types.h> /* for mode_t */
+#include <mu-msg-fields.h>
+#include <mu-msg.h>
+#include <utils/mu-util.h>
+
+G_BEGIN_DECLS
+
+/* 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_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_TICKLE,
+       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; /* spew out debug 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 */
+       /* options for indexing */
+
+       gboolean        nocleanup;      /* don't cleanup del'd mails from db */
+       gboolean        rebuild;        /* empty the database before indexing */
+       gboolean        lazycheck;      /* don't check dirs with up-to-date
+                                        * timestamps */
+       int             max_msg_size;   /* maximum size for message files */
+
+       /* 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 */
+
+       gboolean         summary;       /* OBSOLETE: use summary_len */
+       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         use_agent;       /* attempt to use the gpg-agent */
+       gboolean         decrypt;         /* try to decrypt the
+                                          * message body, if any */
+       gboolean         verify;          /* try to crypto-verify the
+                                          * message */
+
+       /* 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 (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 (MuConfig *conf);
+
+
+/**
+ * determine MuMsgOptions for command line args
+ *
+ * @param opts a MuConfig struct
+ *
+ * @return the corresponding MuMsgOptions
+ */
+MuMsgOptions mu_config_get_msg_options (MuConfig *opts);
+
+
+/**
+ * print help text for the current command
+ *
+ * @param cmd the command to show help for
+ */
+void mu_config_show_help (MuConfigCmd cmd);
+
+G_END_DECLS
+
+#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..81f4606
--- /dev/null
@@ -0,0 +1,69 @@
+## 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 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;
+#      srand();
+#      guard=int(100000*rand());
+#      print "#ifndef __" guard "__"
+       print "/* Do not edit - auto-generated. */"
+       print "static const struct {"
+       print "\tMuConfigCmd cmd;"
+       print "\tconst char *usage;"
+       print "\tconst char *long_help;"
+       print "} MU_HELP_STRINGS[] = {"
+}
+
+
+/^#BEGIN/ {
+       print "\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;
+       print "\n\t},\n"
+}
+
+
+!/^#/  {
+       if (in_string==1) {
+               printf "\n\t\"" $0 "\\n\""
+       }
+}
+
+
+END {
+       print "};"
+#      print "#endif /*" guard "*/"
+       print "/* the end */"
+}
diff --git a/mu/mu-help-strings.txt b/mu/mu-help-strings.txt
new file mode 100644 (file)
index 0000000..a18a93a
--- /dev/null
@@ -0,0 +1,198 @@
+#-*-mode:org-*-
+#
+# 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 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_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
+  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
+
+#BEGIN MU_CONFIG_CMD_TICKLE
+#STRING
+mu tickle [options] <file>
+#STRING
+Give a message a new unique name. Useful for some external tools.
+#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..5528b16
--- /dev/null
+++ b/mu/mu.cc
@@ -0,0 +1,131 @@
+/*
+** 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.
+**
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <glib-object.h>
+#include <locale.h>
+
+#include "mu-config.h"
+#include "mu-cmd.h"
+#include "mu-runtime.h"
+
+
+static void
+show_version (void)
+{
+       const char* blurb =
+               "mu (mail indexer/searcher) version " VERSION "\n"
+               "Copyright (C) 2008-2020 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 void
+handle_error (MuConfig *conf, MuError merr, GError **err)
+{
+       if (!(err && *err))
+               return;
+
+       if (*err)
+               g_printerr ("error: %s (%u)\n",
+                           (*err)->message,
+                           (*err)->code);
+
+       switch ((*err)->code) {
+       case MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK:
+               g_printerr ("maybe mu is already running?\n");
+               break;
+       case MU_ERROR_XAPIAN_NEEDS_REINDEX:
+               g_printerr ("database needs (re)indexing.\n"
+                           "try 'mu index' "
+                           "(see mu-index(1) for details)\n");
+               return;
+       case MU_ERROR_IN_PARAMETERS:
+               if (conf && mu_config_cmd_is_valid(conf->cmd))
+                       mu_config_show_help (conf->cmd);
+               break;
+       case MU_ERROR_SCRIPT_NOT_FOUND:
+               g_printerr ("see the mu manpage for commands, or "
+                           "'mu script' for the scripts\n");
+               break;
+       case MU_ERROR_XAPIAN_CANNOT_OPEN:
+               g_printerr("Please (re)initialize mu with 'mu init' "
+                          "see mu-init(1) for details\n");
+               return;
+       case MU_ERROR_XAPIAN_SCHEMA_MISMATCH:
+               g_printerr("Please (re)initialize mu with 'mu init' "
+                          "see mu-init(1) for details\n");
+               return;
+       default:
+               break; /* nothing to do */
+       }
+}
+
+
+int
+main (int argc, char *argv[])
+{
+       GError          *err;
+       MuError          rv;
+       MuConfig        *conf;
+
+       setlocale (LC_ALL, "");
+
+       err = NULL;
+       rv  = MU_OK;
+
+       conf = mu_config_init (&argc, &argv, &err);
+       if (!conf) {
+               rv = err ? (MuError)err->code : MU_ERROR;
+               goto cleanup;
+       } else if (conf->version) {
+               show_version ();
+               goto cleanup;
+       }
+
+       /* nothing to do */
+       if (conf->cmd == MU_CONFIG_CMD_NONE)
+               return 0;
+
+       if (!mu_runtime_init (conf->muhome, PACKAGE_NAME)) {
+               mu_config_uninit (conf);
+               return 1;
+       }
+
+       rv = mu_cmd_execute (conf, &err);
+
+cleanup:
+       handle_error (conf, rv, &err);
+       g_clear_error (&err);
+
+       mu_config_uninit (conf);
+       mu_runtime_uninit ();
+
+       return rv;
+}
diff --git a/mu/test-mu-cmd-cfind.c b/mu/test-mu-cmd-cfind.c
new file mode 100644 (file)
index 0000000..335bb08
--- /dev/null
@@ -0,0 +1,362 @@
+/* -*- mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+**
+** 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 "test-mu-common.h"
+#include "mu-store.hh"
+#include "mu-query.h"
+
+static gchar *CONTACTS_CACHE = NULL;
+
+static gchar*
+fill_contacts_cache (void)
+{
+       gchar *cmdline, *tmpdir;
+       GError *err;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       cmdline = g_strdup_printf (
+               "/bin/sh -c '"
+               "%s init  --muhome=%s --maildir=%s --quiet; "
+               "%s index --muhome=%s  --quiet'",
+               MU_PROGRAM,  tmpdir, MU_TESTMAILDIR,
+               MU_PROGRAM,  tmpdir);
+
+       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);
+       return tmpdir;
+}
+
+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);
+       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);
+
+       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);
+
+       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);
+
+       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);
+
+       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);
+
+       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);
+
+       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,
+                                ==,
+                                "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[])
+{
+       int rv;
+       g_test_init (&argc, &argv, NULL);
+
+       if (!set_en_us_utf8_locale())
+               return 0; /* don't error out... */
+
+       CONTACTS_CACHE = fill_contacts_cache ();
+
+       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);
+
+       g_log_set_handler (NULL,
+                          G_LOG_LEVEL_MASK | G_LOG_LEVEL_WARNING|
+                          G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       rv = g_test_run ();
+
+       g_free (CONTACTS_CACHE);
+       CONTACTS_CACHE = NULL;
+
+       return rv;
+}
diff --git a/mu/test-mu-cmd.c b/mu/test-mu-cmd.c
new file mode 100644 (file)
index 0000000..095afe1
--- /dev/null
@@ -0,0 +1,910 @@
+/* -*- mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+**
+** 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 <string.h>
+#include <errno.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "test-mu-common.h"
+#include "mu-store.hh"
+#include "mu-query.h"
+
+/* tests for the command line interface, uses testdir2 */
+
+static gchar *DBPATH; /* global */
+
+static gchar*
+fill_database (void)
+{
+       gchar *cmdline, *tmpdir;
+       GError *err;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       cmdline = g_strdup_printf (
+               "/bin/sh -c '"
+               "%s init  --muhome=%s --maildir=%s --quiet; "
+               "%s index --muhome=%s  --quiet'",
+               MU_PROGRAM,  tmpdir, MU_TESTMAILDIR2,
+               MU_PROGRAM,  tmpdir);
+       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);
+       return tmpdir;
+}
+
+
+static unsigned
+newlines_in_output (const char* str)
+{
+       int count;
+
+       count = 0;
+
+       while (str && *str) {
+               if (*str == '\n')
+                       ++count;
+               ++str;
+       }
+
+       return count;
+}
+
+static void
+search (const char* query, unsigned expected)
+{
+       gchar *cmdline, *output, *erroutput;
+
+       cmdline = g_strdup_printf ("%s find --muhome=%s %s",
+                                  MU_PROGRAM, DBPATH, query);
+
+       if (g_test_verbose())
+               g_printerr ("\n$ %s\n", cmdline);
+
+       g_assert (g_spawn_command_line_sync (cmdline,
+                                            &output, &erroutput,
+                                            NULL, NULL));
+       if (g_test_verbose())
+               g_print ("\nOutput:\n%s", output);
+
+       g_assert_cmpuint (newlines_in_output(output),==,expected);
+
+
+       /* 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);
+}
+
+/* index testdir2, and make sure it adds two documents */
+static void
+test_mu_index (void)
+{
+       MuStore *store;
+       gchar *xpath;
+
+       xpath = g_strdup_printf ("%s%c%s", DBPATH, G_DIR_SEPARATOR, "xapian");
+       g_printerr ("*** %s\n", DBPATH);
+       store = mu_store_new_readable (xpath, NULL);
+       g_assert (store);
+
+       g_assert_cmpuint (mu_store_count (store, NULL), ==, 13);
+       mu_store_unref (store);
+
+       g_free (xpath);
+}
+
+
+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: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, 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, 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));
+       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.3 KB)\n"
+                        "  3 custer.jpg image/jpeg [inline] (21.1 KB)\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", 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_true (size >= 15958 && size <= 15960);
+       g_assert_cmpint (get_file_size(att2),==,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_true (size >= 15958 && 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);
+       /* g_print ("\n[%s] (%d)\n", output, len); */
+       g_assert (len > 349);
+
+       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);
+       /* g_print ("\n[%s](%u)\n", output, len); */
+       g_assert_cmpuint (len,>,150);
+
+       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);
+       /* g_print ("\n[%s](%u)\n", output, len); */
+       g_assert_cmpuint (len,>,150);
+
+       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);
+       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;
+       g_test_init (&argc, &argv, NULL);
+
+       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);
+
+       g_log_set_handler (NULL,
+                          G_LOG_LEVEL_MASK | G_LOG_LEVEL_WARNING|
+                          G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       DBPATH = fill_database ();
+       rv = g_test_run ();
+       g_free (DBPATH);
+
+       return rv;
+}
diff --git a/mu/test-mu-query.c b/mu/test-mu-query.c
new file mode 100644 (file)
index 0000000..f68ec81
--- /dev/null
@@ -0,0 +1,767 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+
+/*
+** 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, 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.
+**
+*/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <locale.h>
+
+#include "test-mu-common.h"
+#include "mu-query.h"
+#include "utils/mu-str.h"
+#include "mu-store.hh"
+
+static char* DB_PATH1 = NULL;
+static char* DB_PATH2 = NULL;
+
+static gchar*
+fill_database (const char *testdir)
+{
+       gchar *cmdline, *tmpdir, *xpath;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       cmdline = g_strdup_printf (
+               "/bin/sh -c '"
+               "%s init  --muhome=%s --maildir=%s --quiet ; "
+               "%s index --muhome=%s  --quiet'",
+               MU_PROGRAM,  tmpdir, testdir,
+               MU_PROGRAM,  tmpdir);
+
+
+       if (g_test_verbose())
+               g_printerr ("\n%s\n", cmdline);
+
+       g_assert (g_spawn_command_line_sync (cmdline, NULL, NULL,
+                                            NULL, NULL));
+       g_free (cmdline);
+
+       xpath= g_strdup_printf ("%s%c%s", tmpdir,
+                               G_DIR_SEPARATOR, "xapian");
+       g_free (tmpdir);
+
+       return xpath;
+}
+
+
+
+static void
+assert_no_dups (MuMsgIter *iter)
+{
+       GHashTable *hash;
+
+       hash = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                     (GDestroyNotify)g_free, NULL);
+
+       mu_msg_iter_reset (iter);
+       while (!mu_msg_iter_is_done(iter)) {
+               MuMsg *msg;
+               msg = mu_msg_iter_get_msg_floating (iter);
+               /* make sure there are no duplicates */
+               g_assert (!g_hash_table_lookup (hash, mu_msg_get_path (msg)));
+               g_hash_table_insert (hash, g_strdup (mu_msg_get_path(msg)),
+                                    GUINT_TO_POINTER(TRUE));
+               mu_msg_iter_next (iter);
+       }
+       mu_msg_iter_reset (iter);
+       g_hash_table_destroy (hash);
+}
+
+
+/* note: this also *moves the iter* */
+static guint
+run_and_count_matches_with_query_flags (const char *xpath, const char *query,
+                                       MuQueryFlags flags)
+{
+       MuQuery  *mquery;
+       MuMsgIter *iter;
+       MuStore *store;
+       guint count1, count2;
+       GError *err;
+
+       err = NULL;
+       store = mu_store_new_readable (xpath, &err);
+       if (err) {
+               g_printerr ("error: %s\n", err->message);
+               g_clear_error (&err);
+               err = NULL;
+       }
+       g_assert (store);
+
+       mquery = mu_query_new (store, &err);
+       if (err) {
+               g_printerr ("error: %s\n", err->message);
+               g_clear_error (&err);
+               err = NULL;
+       }
+
+       g_assert (mquery);
+
+       mu_store_unref (store);
+
+       if (g_test_verbose()) {
+               char *x;
+               g_print ("\n==> query: %s\n", query);
+               x = mu_query_internal (mquery, query, FALSE, NULL);
+               g_print ("==> mquery: '%s'\n", x);
+               g_free (x);
+               x = mu_query_internal_xapian (mquery, query, NULL);
+               g_print ("==> xquery: '%s'\n", x);
+               g_free (x);
+       }
+
+       iter = mu_query_run (mquery, query, MU_MSG_FIELD_ID_NONE, -1,
+                            flags, NULL);
+       mu_query_destroy (mquery);
+       g_assert (iter);
+
+       assert_no_dups (iter);
+
+       /* run query twice, to test mu_msg_iter_reset */
+       for (count1 = 0; !mu_msg_iter_is_done(iter);
+            mu_msg_iter_next(iter), ++count1);
+
+       mu_msg_iter_reset (iter);
+
+       assert_no_dups (iter);
+
+       for (count2 = 0; !mu_msg_iter_is_done(iter);
+            mu_msg_iter_next(iter), ++count2);
+
+       mu_msg_iter_destroy (iter);
+
+       g_assert_cmpuint (count1, ==, count2);
+
+       return count1;
+}
+
+static guint
+run_and_count_matches (const char *xpath, const char *query)
+{
+       return run_and_count_matches_with_query_flags (
+               xpath, query, MU_QUERY_FLAG_NONE);
+}
+
+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 },
+               //      { "",                   18 },
+               { "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)
+{
+       MuQuery *query;
+       MuMsgIter *iter;
+       MuMsg *msg;
+       MuStore *store;
+       GError *err;
+       gchar *summ;
+
+       store = mu_store_new_readable (DB_PATH1, NULL);
+       g_assert (store);
+
+       query = mu_query_new (store, NULL);
+       mu_store_unref (store);
+
+       iter = mu_query_run (query, "fünkÿ", MU_MSG_FIELD_ID_NONE,
+                            -1, MU_QUERY_FLAG_NONE, NULL);
+       err = NULL;
+       msg = mu_msg_iter_get_msg_floating (iter); /* don't unref */
+       if (!msg) {
+               g_warning ("error getting message: %s", err->message);
+               g_error_free (err);
+               g_assert_not_reached ();
+       }
+
+       g_assert_cmpstr (mu_msg_get_subject(msg),==,
+                        "Greetings from Lothlórien");
+       /* TODO: fix this again */
+
+       summ = mu_str_summarize (mu_msg_get_body_text
+                                (msg, MU_MSG_OPTION_NONE), 5);
+       g_assert_cmpstr (summ,==,
+                        "Let's write some fünkÿ text using umlauts. Foo.");
+       g_free (summ);
+
+       mu_msg_iter_destroy (iter);
+       mu_query_destroy (query);
+}
+
+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},
+               { "queensryche", 1},
+               { "Queensrÿche", 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_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)
+{
+       gchar *xpath;
+       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 ("Europe/Helsinki");
+
+       xpath = fill_database (MU_TESTMAILDIR);
+       g_assert (xpath != NULL);
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint (run_and_count_matches
+                                 (xpath, queries[i].query),
+                                 ==, queries[i].count);
+
+       g_free (xpath);
+       set_tz (old_tz);
+
+}
+
+static void
+test_mu_query_dates_sydney (void)
+{
+       gchar *xpath;
+       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 ("Australia/Sydney");
+
+       xpath = fill_database (MU_TESTMAILDIR);
+       g_assert (xpath != NULL);
+
+       for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+               g_assert_cmpuint (run_and_count_matches
+                                 (xpath, queries[i].query),
+                                 ==, queries[i].count);
+
+       g_free (xpath);
+       set_tz (old_tz);
+
+}
+
+static void
+test_mu_query_dates_la (void)
+{
+       gchar *xpath;
+       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 ("America/Los_Angeles");
+
+       xpath = fill_database (MU_TESTMAILDIR);
+       g_assert (xpath != NULL);
+
+       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);
+       }
+
+       g_free (xpath);
+       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},
+               { "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)
+{
+       gchar *xpath;
+
+       xpath = fill_database (MU_TESTMAILDIR);
+       g_assert (xpath != NULL);
+
+       g_assert_cmpuint (run_and_count_matches_with_query_flags
+                         (xpath, "msgid:uwsireh25.fsf@one.dot.net",
+                         MU_QUERY_FLAG_NONE),
+                         ==, 1);
+
+       g_assert_cmpuint (run_and_count_matches_with_query_flags
+                         (xpath, "msgid:uwsireh25.fsf@one.dot.net",
+                          MU_QUERY_FLAG_INCLUDE_RELATED),
+                         ==, 3);
+
+       g_free (xpath);
+}
+
+
+
+int
+main (int argc, char *argv[])
+{
+       int rv;
+
+       setlocale (LC_ALL, "");
+
+       g_test_init (&argc, &argv, NULL);
+
+       DB_PATH1 = fill_database (MU_TESTMAILDIR);
+       g_assert (DB_PATH1);
+
+       DB_PATH2 = fill_database (MU_TESTMAILDIR2);
+       g_assert (DB_PATH2);
+
+       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);
+
+       if (!g_test_verbose())
+           g_log_set_handler (NULL,
+                              G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL|
+                              G_LOG_FLAG_RECURSION,
+                              (GLogFunc)black_hole, NULL);
+
+       rv = g_test_run ();
+
+       g_free (DB_PATH1);
+       g_free (DB_PATH2);
+
+       return rv;
+}
diff --git a/mu/test-mu-runtime.c b/mu/test-mu-runtime.c
new file mode 100644 (file)
index 0000000..b48e03e
--- /dev/null
@@ -0,0 +1,98 @@
+/* 
+** 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 "test-mu-common.h"
+#include "src/mu-runtime.h"
+
+static void
+test_mu_runtime_init (void)
+{
+       gchar* tmpdir;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       g_assert (tmpdir);
+       g_assert (mu_runtime_init (tmpdir, "test-mu-runtime") == TRUE); 
+       mu_runtime_uninit ();
+
+       g_assert (mu_runtime_init (tmpdir, "test-mu-runtime") == TRUE); 
+       mu_runtime_uninit ();
+
+       g_free (tmpdir);        
+}
+
+
+static void
+test_mu_runtime_data (void)
+{
+       gchar *homedir, *xdir, *bmfile;
+
+       homedir = test_mu_common_get_random_tmpdir();
+       g_assert (homedir);
+
+       xdir = g_strdup_printf ("%s%c%s", homedir, 
+                                  G_DIR_SEPARATOR, "xapian" );
+        
+       bmfile = g_strdup_printf ("%s%c%s", homedir, 
+                                  G_DIR_SEPARATOR, "bookmarks");
+
+       g_assert (mu_runtime_init (homedir, "test-mu-runtime") == TRUE);        
+       
+       g_assert_cmpstr (homedir, ==, mu_runtime_path (MU_RUNTIME_PATH_MUHOME));
+       g_assert_cmpstr (xdir, ==, mu_runtime_path (MU_RUNTIME_PATH_XAPIANDB));
+       g_assert_cmpstr (bmfile, ==, mu_runtime_path (MU_RUNTIME_PATH_BOOKMARKS));
+
+       mu_runtime_uninit ();
+
+       g_free (homedir);
+       g_free (xdir);
+       g_free (bmfile);        
+}
+
+
+
+int
+main (int argc, char *argv[])
+{
+       g_test_init (&argc, &argv, NULL);
+
+       /* mu_runtime_init/uninit */
+       g_test_add_func ("/mu-runtime/mu-runtime-init",
+                        test_mu_runtime_init);
+       g_test_add_func ("/mu-runtime/mu-runtime-data",
+                        test_mu_runtime_data);
+
+       g_log_set_handler (NULL,
+                          G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+       
+       return g_test_run ();
+}
+
+
diff --git a/mu/test-mu-threads.c b/mu/test-mu-threads.c
new file mode 100644 (file)
index 0000000..3bef734
--- /dev/null
@@ -0,0 +1,462 @@
+/* -*-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.
+**
+*/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "test-mu-common.h"
+#include "mu-query.h"
+#include "utils/mu-str.h"
+
+struct _tinfo {
+       const char *threadpath;
+       const char *msgid;
+       const char *subject;
+};
+typedef struct _tinfo tinfo;
+
+static void
+assert_tinfo_equal (const tinfo *expected, const tinfo *actual)
+{
+       g_assert_cmpstr (expected->threadpath,==,actual->threadpath);
+       g_assert_cmpstr (expected->subject,==,actual->subject);
+       g_assert_cmpstr (expected->msgid,==,actual->msgid);
+}
+
+static void
+tinfo_init_from_iter (tinfo *item, MuMsgIter *iter)
+{
+       MuMsg *msg;
+       const MuMsgIterThreadInfo *ti;
+
+       msg = mu_msg_iter_get_msg_floating (iter);
+       g_assert (msg);
+
+       ti = mu_msg_iter_get_thread_info (iter);
+       if (!ti)
+               g_print ("%s: thread info not found\n", mu_msg_get_msgid (msg));
+       g_assert (ti);
+
+       item->threadpath = ti->threadpath;
+       item->subject = mu_msg_get_subject (msg);
+       item->msgid = mu_msg_get_msgid (msg);
+
+       if (g_test_verbose())
+               g_print ("%s %s %s\n",
+                        item->threadpath, item->subject, item->msgid);
+
+}
+
+static void
+foreach_assert_tinfo_equal (MuMsgIter *iter, const tinfo items[], guint n_items)
+{
+       guint u;
+
+       u = 0;
+       while (!mu_msg_iter_is_done (iter) && u < n_items) {
+               tinfo ti;
+
+               tinfo_init_from_iter (&ti, iter);
+
+               g_assert (u < n_items);
+               assert_tinfo_equal (&items[u], &ti);
+
+               ++u;
+               mu_msg_iter_next (iter);
+       }
+       g_assert (u == n_items);
+}
+
+static gchar*
+fill_database (const char *testdir)
+{
+       gchar *cmdline, *tmpdir, *xpath;
+
+       tmpdir = test_mu_common_get_random_tmpdir();
+       cmdline = g_strdup_printf (
+               "/bin/sh -c '"
+               "%s init  --muhome=%s --maildir=%s --quiet ; "
+               "%s index --muhome=%s  --quiet'",
+               MU_PROGRAM,  tmpdir, testdir,
+               MU_PROGRAM,  tmpdir);
+
+       if (g_test_verbose())
+               g_print ("%s\n", cmdline);
+
+       g_assert (g_spawn_command_line_sync (cmdline, NULL, NULL,
+                                            NULL, NULL));
+       g_free (cmdline);
+       xpath= g_strdup_printf ("%s%c%s", tmpdir,
+                               G_DIR_SEPARATOR, "xapian");
+       g_free (tmpdir);
+
+       return xpath;
+}
+
+/* note: this also *moves the iter* */
+static MuMsgIter*
+run_and_get_iter_full (const char *xpath, const char *query,
+                      MuMsgFieldId sort_field, MuQueryFlags flags)
+{
+       MuQuery  *mquery;
+       MuStore *store;
+       MuMsgIter *iter;
+
+       store = mu_store_new_readable (xpath, NULL);
+       g_assert (store);
+
+       mquery = mu_query_new (store, NULL);
+       mu_store_unref (store);
+       g_assert (query);
+
+       flags |= MU_QUERY_FLAG_THREADS;
+       iter = mu_query_run (mquery, query, sort_field, -1, flags, NULL);
+       mu_query_destroy (mquery);
+       g_assert (iter);
+
+       return iter;
+}
+
+static MuMsgIter*
+run_and_get_iter (const char *xpath, const char *query)
+{
+       return run_and_get_iter_full (xpath, query, MU_MSG_FIELD_ID_DATE,
+                                     MU_QUERY_FLAG_NONE);
+}
+
+static void
+test_mu_threads_01 (void)
+{
+       gchar *xpath;
+       MuMsgIter *iter;
+
+       const tinfo items [] = {
+               {"0",   "root0@msg.id",  "root0"},
+               {"0:0", "child0.0@msg.id", "Re: child 0.0"},
+               {"0:1",   "child0.1@msg.id", "Re: child 0.1"},
+               {"0:1:0", "child0.1.0@msg.id", "Re: child 0.1.0"},
+               {"1",   "root1@msg.id", "root1"},
+               {"2",   "root2@msg.id", "root2"},
+               /* next one's been promoted 2.0.0 => 2.0 */
+               {"2:0", "child2.0.0@msg.id", "Re: child 2.0.0"},
+               /* next one's been promoted 3.0.0.0.0 => 3 */
+               {"3", "child3.0.0.0.0@msg.id", "Re: child 3.0.0.0"},
+
+               /* two children of absent root 4.0 */
+               {"4:0", "child4.0@msg.id", "Re: child 4.0"},
+               {"4:1", "child4.1@msg.id", "Re: child 4.1"}
+       };
+
+       xpath = fill_database (MU_TESTMAILDIR3);
+       g_assert (xpath != NULL);
+
+       iter = run_and_get_iter (xpath, "abc");
+       g_assert (iter);
+       g_assert (!mu_msg_iter_is_done(iter));
+
+       foreach_assert_tinfo_equal (iter, items, G_N_ELEMENTS (items));
+
+       g_free (xpath);
+       mu_msg_iter_destroy (iter);
+}
+
+static void
+test_mu_threads_rogue (void)
+{
+       gchar *xpath;
+       MuMsgIter *iter;
+       tinfo *items;
+
+       tinfo items1 [] = {
+               {"0",     "cycle0@msg.id",     "cycle0"},
+               {"0:0",   "cycle0.0@msg.id",   "cycle0.0"},
+               {"0:0:0", "cycle0.0.0@msg.id", "cycle0.0.0"},
+               {"0:1",   "rogue0@msg.id",     "rogue0"},
+       };
+
+       tinfo items2 [] = {
+               {"0",     "cycle0.0@msg.id",   "cycle0.0"},
+               {"0:0",   "cycle0@msg.id",     "cycle0"},
+               {"0:0:0", "rogue0@msg.id",     "rogue0" },
+               {"0:1",   "cycle0.0.0@msg.id", "cycle0.0.0"}
+       };
+
+       xpath = fill_database (MU_TESTMAILDIR3);
+       g_assert (xpath != NULL);
+
+       iter = run_and_get_iter (xpath, "def");
+       g_assert (iter);
+       g_assert (!mu_msg_iter_is_done(iter));
+
+       /* due to the random order in files can be indexed, there are two possible ways
+        * for the threads to be built-up; both are okay */
+       if (g_strcmp0 (mu_msg_get_msgid(mu_msg_iter_get_msg_floating (iter)),
+                      "cycle0@msg.id") == 0)
+               items = items1;
+       else
+               items = items2;
+
+       foreach_assert_tinfo_equal (iter, items, G_N_ELEMENTS (items1));
+
+       g_free (xpath);
+       mu_msg_iter_destroy (iter);
+}
+
+static MuMsgIter*
+query_testdir (const char *query, MuMsgFieldId sort_field,  gboolean descending)
+{
+       MuMsgIter *iter;
+       gchar *xpath;
+       MuQueryFlags flags;
+
+       flags = MU_QUERY_FLAG_NONE;
+       if (descending)
+               flags |= MU_QUERY_FLAG_DESCENDING;
+
+       xpath = fill_database (MU_TESTMAILDIR3);
+       g_assert (xpath != NULL);
+
+       iter = run_and_get_iter_full (xpath, query, sort_field, flags);
+       g_assert (iter != NULL);
+       g_assert (!mu_msg_iter_is_done (iter));
+
+       g_free (xpath);
+       return iter;
+}
+
+static void
+check_sort_by_subject (const char *query, const tinfo expected[],
+                      guint n_expected, gboolean descending)
+{
+       MuMsgIter *iter;
+
+       iter = query_testdir (query, MU_MSG_FIELD_ID_SUBJECT, descending);
+       foreach_assert_tinfo_equal (iter, expected, n_expected);
+       mu_msg_iter_destroy (iter);
+}
+
+static void
+check_sort_by_subject_asc (const char *query, const tinfo expected[],
+                          guint n_expected)
+{
+       check_sort_by_subject (query, expected, n_expected, FALSE);
+}
+
+static void
+check_sort_by_subject_desc (const char *query, const tinfo expected[],
+                           guint n_expected)
+{
+       check_sort_by_subject (query, expected, n_expected, TRUE);
+}
+
+static void
+test_mu_threads_sort_1st_child_promotes_thread (void)
+{
+       const char *query = "maildir:/sort/1st-child-promotes-thread";
+
+       const tinfo expected_asc [] = {
+               { "0", "A@msg.id", "A"},
+               { "1", "C@msg.id", "C"},
+               { "2", "B@msg.id", "B"},
+               { "2:0", "D@msg.id", "D"},
+       };
+       const tinfo expected_desc [] = {
+               { "0", "B@msg.id", "B"},
+               { "0:0", "D@msg.id", "D"},
+               { "1", "C@msg.id", "C"},
+               { "2", "A@msg.id", "A"},
+       };
+
+       check_sort_by_subject_asc (query, expected_asc,
+                                  G_N_ELEMENTS (expected_asc));
+       check_sort_by_subject_desc (query, expected_desc,
+                                   G_N_ELEMENTS (expected_desc));
+}
+
+static void
+test_mu_threads_sort_2nd_child_promotes_thread (void)
+{
+       const char *query = "maildir:/sort/2nd-child-promotes-thread";
+
+       const tinfo expected_asc [] = {
+               { "0", "A@msg.id", "A"},
+               { "1", "D@msg.id", "D"},
+               { "2", "B@msg.id", "B"},
+               { "2:0", "C@msg.id", "C"},
+               { "2:1", "E@msg.id", "E"},
+       };
+       const tinfo expected_desc [] = {
+               { "0", "B@msg.id", "B"},
+               { "0:0", "C@msg.id", "C"},
+               { "0:1", "E@msg.id", "E"},
+               { "1", "D@msg.id", "D"},
+               { "2", "A@msg.id", "A"},
+       };
+
+       check_sort_by_subject_asc (query, expected_asc,
+                                  G_N_ELEMENTS (expected_asc));
+       check_sort_by_subject_desc (query, expected_desc,
+                                   G_N_ELEMENTS (expected_desc));
+}
+
+static void
+test_mu_threads_sort_orphan_promotes_thread (void)
+{
+       const char *query = "maildir:/sort/2nd-child-promotes-thread NOT B";
+
+       /* B lost, C & E orphaned but not promoted */
+       const tinfo expected_asc [] = {
+               { "0", "A@msg.id", "A"},
+               { "1", "D@msg.id", "D"},
+               { "2:0", "C@msg.id", "C"},
+               { "2:1", "E@msg.id", "E"},
+       };
+       const tinfo expected_desc [] = {
+               { "0:0", "C@msg.id", "C"},
+               { "0:1", "E@msg.id", "E"},
+               { "1", "D@msg.id", "D"},
+               { "2", "A@msg.id", "A"},
+       };
+
+       check_sort_by_subject_asc (query, expected_asc,
+                                  G_N_ELEMENTS (expected_asc));
+       check_sort_by_subject_desc (query, expected_desc,
+                                   G_N_ELEMENTS (expected_desc));
+}
+
+/* Won't normally happen when sorting by date. */
+static void
+test_mu_threads_sort_child_does_not_promote_thread (void)
+{
+       const char *query = "maildir:/sort/child-does-not-promote-thread";
+
+       const tinfo expected_asc [] = {
+               { "0", "Y@msg.id", "Y"},
+               { "0:0", "A@msg.id", "A"},
+               { "1", "X@msg.id", "X"},
+               { "2", "Z@msg.id", "Z"},
+       };
+       const tinfo expected_desc [] = {
+               { "0", "Z@msg.id", "Z"},
+               { "1", "X@msg.id", "X"},
+               { "2", "Y@msg.id", "Y"},
+               { "2:0", "A@msg.id", "A"},
+       };
+
+       check_sort_by_subject_asc (query, expected_asc,
+                                  G_N_ELEMENTS (expected_asc));
+       check_sort_by_subject_desc (query, expected_desc,
+                                   G_N_ELEMENTS (expected_desc));
+}
+
+static void
+test_mu_threads_sort_grandchild_promotes_thread (void)
+{
+       const char *query = "maildir:/sort/grandchild-promotes-thread";
+
+       const tinfo expected_asc [] = {
+               { "0", "A@msg.id", "A"},
+               { "1", "D@msg.id", "D"},
+               { "2", "B@msg.id", "B"},
+               { "2:0", "C@msg.id", "C"},
+               { "2:0:0", "E@msg.id", "E"},
+       };
+       const tinfo expected_desc [] = {
+               { "0", "B@msg.id", "B"},
+               { "0:0", "C@msg.id", "C"},
+               { "0:0:0", "E@msg.id", "E"},
+               { "1", "D@msg.id", "D"},
+               { "2", "A@msg.id", "A"},
+       };
+
+       check_sort_by_subject_asc (query, expected_asc,
+                                  G_N_ELEMENTS (expected_asc));
+       check_sort_by_subject_desc (query, expected_desc,
+                                   G_N_ELEMENTS (expected_desc));
+}
+
+static void
+test_mu_threads_sort_granchild_promotes_only_subthread (void)
+{
+       const char *query = "maildir:/sort/grandchild-promotes-only-subthread";
+
+       const tinfo expected_asc [] = {
+               { "0", "A@msg.id", "A"},
+               { "1", "B@msg.id", "B"},
+               { "1:0", "C@msg.id", "C"},
+               { "1:1", "D@msg.id", "D"},
+               { "1:1:0", "F@msg.id", "F"},
+               { "1:2", "E@msg.id", "E"},
+               { "2", "G@msg.id", "G"},
+       };
+       const tinfo expected_desc [] = {
+               { "0", "G@msg.id", "G"},
+               { "1", "B@msg.id", "B"},
+               { "1:0", "C@msg.id", "C"},
+               { "1:1", "D@msg.id", "D"},
+               { "1:1:0", "F@msg.id", "F"},
+               { "1:2", "E@msg.id", "E"},
+               { "2", "A@msg.id", "A"},
+       };
+
+       check_sort_by_subject_asc (query, expected_asc,
+                                  G_N_ELEMENTS (expected_asc));
+       check_sort_by_subject_desc (query, expected_desc,
+                                   G_N_ELEMENTS (expected_desc));
+}
+int
+main (int argc, char *argv[])
+{
+       int rv;
+
+       g_test_init (&argc, &argv, NULL);
+
+       g_test_add_func ("/mu-query/test-mu-threads-01", test_mu_threads_01);
+       g_test_add_func ("/mu-query/test-mu-threads-rogue", test_mu_threads_rogue);
+       g_test_add_func ("/mu-query/test-mu-threads-sort-1st-child-promotes-thread",
+                        test_mu_threads_sort_1st_child_promotes_thread);
+       g_test_add_func ("/mu-query/test-mu-threads-sort-2nd-child-promotes-thread",
+                        test_mu_threads_sort_2nd_child_promotes_thread);
+       g_test_add_func ("/mu-query/test-mu-threads-orphan-promotes-thread",
+                        test_mu_threads_sort_orphan_promotes_thread);
+       g_test_add_func ("/mu-query/test-mu-threads-sort-child-does-not-promote-thread",
+                        test_mu_threads_sort_child_does_not_promote_thread);
+       g_test_add_func ("/mu-query/test-mu-threads-sort-grandchild-promotes-thread",
+                        test_mu_threads_sort_grandchild_promotes_thread);
+       g_test_add_func ("/mu-query/test-mu-threads-sort-grandchild-promotes-only-subthread",
+                        test_mu_threads_sort_granchild_promotes_only_subthread);
+
+       g_log_set_handler (NULL,
+                          G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION,
+                          (GLogFunc)black_hole, NULL);
+
+       rv = g_test_run ();
+
+       return rv;
+}
diff --git a/mu4e/Makefile.am b/mu4e/Makefile.am
new file mode 100644 (file)
index 0000000..6d13ff7
--- /dev/null
@@ -0,0 +1,62 @@
+## 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-compose.el         \
+       mu4e-context.el         \
+       mu4e-contrib.el         \
+       mu4e-draft.el           \
+       mu4e-headers.el         \
+       mu4e-icalendar.el       \
+       mu4e-lists.el           \
+       mu4e-main.el            \
+       mu4e-mark.el            \
+       mu4e-message.el         \
+       mu4e-meta.el            \
+       mu4e-org.el             \
+       mu4e-proc.el            \
+       mu4e-speedbar.el        \
+       mu4e-utils.el           \
+       mu4e-vars.el            \
+       mu4e-view.el            \
+       mu4e.el                 \
+       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/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..a4940c8
--- /dev/null
@@ -0,0 +1,371 @@
+;;; mu4e-actions.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2019 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Example actions for messages, attachments (see chapter 'Actions' in the
+;; manual)
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'ido)
+
+(require 'mu4e-utils)
+(require 'mu4e-message)
+(require 'mu4e-meta)
+
+(declare-function mu4e~proc-extract     "mu4e-proc")
+(declare-function mu4e-headers-search   "mu4e-headers")
+
+(defvar mu4e-headers-include-related)
+(defvar mu4e-headers-show-threads)
+(defvar mu4e-view-show-addresses)
+(defvar mu4e-view-date-format)
+
+\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))))))
+
+;;; To PDF
+
+(defvar mu4e-msg2pdf
+  (let ((exec-path (cons (concat mu4e-builddir "/toys/msg2pdf/") exec-path)))
+    (locate-file "msg2pdf" exec-path exec-suffixes))
+  "Path to the msg2pdf toy.")
+
+(defun mu4e-action-view-as-pdf (msg)
+  "Convert MSG to pdf, then show it.
+Works for the message view."
+  (unless (file-executable-p mu4e-msg2pdf)
+    (mu4e-error "Program msg2pdf not found; please set `mu4e-msg2pdf'"))
+  (let* ((pdf
+          (shell-command-to-string
+           (concat mu4e-msg2pdf " "
+                   (shell-quote-argument (mu4e-message-field msg :path))
+                   " 2> /dev/null")))
+         (pdf (and pdf (> (length pdf) 5)
+                   (substring pdf 0 -1)))) ;; chop \n
+    (unless (and pdf (file-exists-p pdf))
+      (mu4e-warn "Failed to create PDF file"))
+    (find-file pdf)))
+
+;;; To HTML
+
+(defun mu4e~action-header-to-html (msg field)
+  "Convert the FIELD of MSG to an HTML string."
+  (mapconcat
+   (lambda(c)
+     (let* ((name (when (car c)
+                    (replace-regexp-in-string "[[:cntrl:]]" "" (car c))))
+            (email (when (cdr c)
+                     (replace-regexp-in-string "[[:cntrl:]]" "" (cdr c))))
+            (addr (if mu4e-view-show-addresses
+                      (if name (format "%s <%s>" name email) email)
+                    (or name email))) ;; name may be nil
+            ;; Escape HTML entities
+            (addr (replace-regexp-in-string "&" "&amp;" addr))
+            (addr (replace-regexp-in-string "<" "&lt;" addr))
+            (addr (replace-regexp-in-string ">" "&gt;" addr)))
+       addr))
+   (mu4e-message-field msg field) ", "))
+
+(defun mu4e~write-body-to-html (msg)
+  "Write MSG's body (either html or text) to a temporary file;
+return the filename."
+  (let* ((html (mu4e-message-field msg :body-html))
+         (txt (mu4e-message-field msg :body-txt))
+         (tmpfile (mu4e-make-temp-file "html"))
+         (attachments (cl-remove-if (lambda (part)
+                                      (or (null (plist-get part :attachment))
+                                          (null (plist-get part :cid))))
+                                    (mu4e-message-field msg :parts))))
+    (unless (or html txt)
+      (mu4e-error "No body part for this message"))
+    (with-temp-buffer
+      (insert "<head><meta charset=\"UTF-8\"></head>\n")
+      (insert (concat "<p><strong>From</strong>: "
+                      (mu4e~action-header-to-html msg :from) "</br>"))
+      (insert (concat "<strong>To</strong>: "
+                      (mu4e~action-header-to-html msg :to) "</br>"))
+      (insert (concat "<strong>Date</strong>: "
+                      (format-time-string mu4e-view-date-format (mu4e-message-field msg :date)) "</br>"))
+      (insert (concat "<strong>Subject</strong>: " (mu4e-message-field msg :subject) "</p>"))
+      (insert (or html (concat "<pre>" txt "</pre>")))
+      (write-file tmpfile)
+      ;; rewrite attachment urls
+      (mapc (lambda (attachment)
+              (goto-char (point-min))
+              (while (re-search-forward (format "src=\"cid:%s\""
+                                                (plist-get attachment :cid)) nil t)
+                (if (plist-get attachment :temp)
+                    (replace-match (format "src=\"%s\""
+                                           (plist-get attachment :temp)))
+                  (replace-match (format "src=\"%s%s\"" temporary-file-directory
+                                         (plist-get attachment :name)))
+                  (let ((tmp-attachment-name
+                         (format "%s%s" temporary-file-directory
+                                 (plist-get attachment :name))))
+                    (mu4e~proc-extract 'save (mu4e-message-field msg :docid)
+                                       (plist-get attachment :index)
+                                       mu4e-decryption-policy tmp-attachment-name)
+                    (mu4e-remove-file-later tmp-attachment-name)))))
+            attachments)
+      (save-buffer)
+      tmpfile)))
+
+(defun mu4e-action-view-in-browser (msg)
+  "View the body of MSG in a web browser.
+You can influence the browser to use with the variable
+`browse-url-generic-program', and see the discussion of privacy
+aspects in `(mu4e) Displaying rich-text messages'."
+  (browse-url (concat "file://"
+                      (mu4e~write-body-to-html msg))))
+
+(defun mu4e-action-view-with-xwidget (msg)
+  "View the body of MSG inside xwidget-webkit.
+This is only available in Emacs 25+; also see the discussion of
+privacy aspects in `(mu4e) Displaying rich-text messages'."
+  (unless (fboundp 'xwidget-webkit-browse-url)
+    (mu4e-error "No xwidget support available"))
+  (xwidget-webkit-browse-url
+   (concat "file://" (mu4e~write-body-to-html msg)) t))
+
+;;; To speech
+
+(defconst mu4e-text2speech-command "festival --tts"
+  "Program that speaks out text it receives on standard input.")
+
+(defun mu4e-action-message-to-speech (msg)
+  "Pronounce MSG's body text using `mu4e-text2speech-command'."
+  (unless (mu4e-message-field msg :body-txt)
+    (mu4e-warn "No text body for this message"))
+  (with-temp-buffer
+    (insert (mu4e-message-field msg :body-txt))
+    (shell-command-on-region (point-min) (point-max)
+                             mu4e-text2speech-command)))
+
+;;; 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 (car-safe sender)) (email (cdr-safe 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)))))
+    (message "%S" org-capture-templates)
+    (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)))))
+
+(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-headers-show-threads t)
+            (mu4e-headers-include-related t))
+        (mu4e-headers-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-compose.el b/mu4e/mu4e-compose.el
new file mode 100644 (file)
index 0000000..021a691
--- /dev/null
@@ -0,0 +1,944 @@
+;;; mu4e-compose.el -- part of mu4e, the mu mail user agent for emacs -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  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 'rfc2368)
+
+(require 'mu4e-utils)
+(require 'mu4e-vars)
+(require 'mu4e-proc)
+(require 'mu4e-actions)
+(require 'mu4e-message)
+(require 'mu4e-draft)
+(require 'mu4e-context)
+
+;;; Composing / Sending messages
+
+(defgroup mu4e-compose nil
+  "Customizations for composing/sending messages."
+  :group 'mu4e)
+
+(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-reply-encrypted-policy 'sign-and-encrypt
+  "Policy for signing/encrypting replies to encrypted messages.
+We have the following choices:
+
+- `sign': sign the reply
+- `sign-and-encrypt': sign and encrypt the reply
+- `encrypt': encrypt the reply, but don't sign it.
+-  anything else: do nothing."
+  :type '(choice
+          (const :tag "Sign the reply" sign)
+          (const :tag "Sign and encrypt the reply" sign-and-encrypt)
+          (const :tag "Encrypt the reply" encrypt)
+          (const :tag "Don't do anything" nil))
+  :safe 'symbolp
+  :group 'mu4e-compose)
+
+(defcustom mu4e-compose-crypto-reply-plain-policy nil
+  "Policy for signing/encrypting replies to messages received unencrypted.
+We have the following choices:
+
+- `sign': sign the reply
+- `sign-and-encrypt': sign and encrypt the reply
+- `encrypt': encrypt the reply, but don't sign it.
+-  anything else: do nothing."
+  :type '(choice
+          (const :tag "Sign the reply" sign)
+          (const :tag "Sign and encrypt the reply" sign-and-encrypt)
+          (const :tag "Encrypt the reply" encrypt)
+          (const :tag "Don't do anything" nil))
+  :safe 'symbolp
+  :group 'mu4e-compose)
+
+(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 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)
+
+(defvar mu4e-compose-type nil
+  "The compose-type for this buffer.
+This is a symbol, `new', `forward', `reply' or `edit'.")
+
+;;; 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
+          (cl-case sent-behavior
+            (delete nil)
+            (trash (mu4e-get-trash-folder mu4e-compose-parent-message))
+            (sent (mu4e-get-sent-folder mu4e-compose-parent-message))
+            (otherwise
+             (mu4e-error "Unsupported value '%S'
+      `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~proc-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~proc-mkdir mdir-path)))
+                (write-file file) ;; writing maildirs files is easy
+                (mu4e~proc-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
+            (lambda()
+              ;; replace the date
+              (save-excursion
+                (message-remove-header "Date")
+                (message-generate-headers '(Date Message-ID))
+                (save-match-data
+                  (mu4e~draft-remove-mail-header-separator)))) nil t)
+  (add-hook 'after-save-hook
+            (lambda ()
+              (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~proc-add (buffer-file-name)))) nil t))
+\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 pred))
+   ((eq action t)
+    (all-completions str mu4e~contacts 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 "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)))
+  (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-in-modeline)
+    (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)
+    (make-local-variable 'message-default-charset)
+    ;; Set to nil to enable `electric-quote-local-mode' to work:
+    (set (make-variable-buffer-local '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)
+    ;; if the default charset is not set, use UTF-8
+    (unless message-default-charset
+      (setq message-default-charset 'utf-8))
+    (mu4e~compose-register-message-save-hooks)
+    ;; offer completion for e-mail addresses
+    (when mu4e-compose-complete-addresses
+      (unless mu4e~contacts   ;; 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
+              (lambda () ;; 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)) nil t)
+    ;; when the message has been sent.
+    (add-hook 'message-sent-hook
+              (lambda () ;;  mu4e~compose-mark-after-sending
+                (setq mu4e-sent-func 'mu4e-sent-handler)
+                (mu4e~proc-sent (buffer-file-name))) 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-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
+                  (cl-case compose-type
+                    (reply       "*reply*")
+                    (forward     "*forward*")
+                    (otherwise   "*draft*")))))
+    (rename-buffer (generate-new-buffer-name
+                    (truncate-string-to-width str mu4e~compose-buffer-max-name-length)
+                    (buffer-name)))))
+
+(defun mu4e~compose-crypto-reply (parent compose-type)
+  "Possibly encrypt or sign a message based on PARENT and COMPOSE-TYPE.
+When composing a reply to an encrypted message, we can
+automatically encrypt that reply. When the message is unencrypted,
+we can decide what we want to do."
+  (if (and  (eq compose-type 'reply)
+            (and parent (member 'encrypted (mu4e-message-field parent :flags))))
+      (cl-case mu4e-compose-crypto-reply-encrypted-policy
+        (sign (mml-secure-message-sign))
+        (encrypt (mml-secure-message-encrypt))
+        (sign-and-encrypt (mml-secure-message-sign-encrypt)))
+    (cl-case mu4e-compose-crypto-reply-plain-policy
+      (sign (mml-secure-message-sign))
+      (encrypt (mml-secure-message-encrypt))
+      (sign-and-encrypt (mml-secure-message-sign-encrypt)))))
+
+(cl-defun mu4e~compose-handler (compose-type &optional original-msg includes)
+  "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> :disposition <disposition>)
+for the attachments to include; file-name refers to
+a file which our backend has conveniently saved for us (as a
+tempfile)."
+
+  ;; 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 messages with all the basic headers set.
+  (let ((winconf (current-window-configuration)))
+    (condition-case nil
+        (mu4e-draft-open compose-type original-msg)
+      (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-reply 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)
+        (mml-attach-file
+         (plist-get att :file-name) (plist-get att :mime-type)))))
+
+  (mu4e~compose-set-friendly-buffer-name compose-type)
+
+  ;; now jump to some useful positions, and start writing that mail!
+  (if (member compose-type '(new forward))
+      (message-goto-to)
+    ;; otherwise, it depends...
+    (cl-case message-cite-reply-position
+      ((above traditional)
+       (message-goto-body))
+      (t
+       (when (message-goto-signature)
+         (forward-line -2)))))
+
+  ;; 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)
+  ;; 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-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~proc-remove docid))
+  ;; kill any remaining buffers for the draft file, or they will hang around...
+  ;; this seems a bit hamfisted...
+  (dolist (buf (buffer-list))
+    (when (and (buffer-file-name buf)
+               (string= (buffer-file-name buf) path))
+      (if message-kill-buffer-on-exit
+          (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-t 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. 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.
+
+Now, 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's 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 (cl-first refs))))))
+          ;; remove the <>
+          (when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to))
+            (mu4e~proc-move (match-string 1 in-reply-to) nil "+R-N"))
+          (when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from))
+            (mu4e~proc-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~proc-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.)
+
+;;;###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~start))
+
+  ;; 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)
+
+  (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
+  (when other-headers
+    (dolist (h other-headers other-headers)
+      (if (symbolp (car h)) (setcar h (symbol-name (car h))))
+      (message-add-header (concat (capitalize (car h)) ": " (cdr h) "\n"  ))
+      ))
+
+  ;; 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-context.el b/mu4e/mu4e-context.el
new file mode 100644 (file)
index 0000000..231bb1b
--- /dev/null
@@ -0,0 +1,183 @@
+;;; mu4e-context.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  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 'cl-lib)
+(require 'mu4e-utils)
+
+(defvar smtpmail-smtp-user)
+(defvar mu4e-view-date-format)
+
+(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.")
+
+(defvar mu4e~context-current nil
+  "The current context; for internal use. Use
+  `mu4e-context-switch' to change it.")
+
+(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, or \"\" 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."
+  (when mu4e-contexts
+    (let* ((names (cl-map 'list (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 context to a context with NAME which is 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 (cl-map 'list (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)
+  "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 with a match-func that returns t. 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?
+       (cl-find-if (lambda (context)
+                     (when (mu4e-context-match-func context)
+                       (funcall (mu4e-context-match-func context) msg)))
+                   mu4e-contexts)
+       ;; no context found yet; consult policy
+       (cl-case 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: ")))
+         (otherwise 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))))
+
+;;; _
+(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..9ce3b24
--- /dev/null
@@ -0,0 +1,222 @@
+;;; mu4e-contrib.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013-2020 Dirk-Jan C. Binnema
+
+;; This file is not part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  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)
+
+;; Contributed by sabof
+(defvar bookmark-make-record-function)
+
+;;; 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 messages within current query results and ask user to execute which action."
+  (interactive)
+  (mu4e-headers-mark-for-each-if
+   (cons 'something nil)
+   (lambda (_msg _param) t))
+  (mu4e-mark-execute-all))
+
+;;; Bookmark handlers
+;;
+;;  Allow bookmarking a mu4e buffer in regular emacs bookmarks.
+
+;; Probably this can be moved to mu4e-view.el.
+(add-hook 'mu4e-view-mode-hook
+          (lambda ()
+            (set (make-local-variable 'bookmark-make-record-function)
+                 'mu4e-view-bookmark-make-record)))
+;; And this can be moved to mu4e-headers.el.
+(add-hook 'mu4e-headers-mode-hook
+          (lambda ()
+            (set (make-local-variable 'bookmark-make-record-function)
+                 'mu4e-view-bookmark-make-record)))
+
+(defun mu4e-view-bookmark-make-record ()
+  "Make a bookmark entry for a mu4e buffer. Note that this is an
+emacs bookmark, not to be confused with `mu4e-bookmarks'."
+  (let* ((msg     (mu4e-message-at-point))
+         (maildir (plist-get msg :maildir))
+         (date    (format-time-string "%Y%m%d" (plist-get msg :date)))
+         (query   (format "maildir:%s date:%s" maildir date))
+         (docid   (plist-get msg :docid))
+         (mode    (symbol-name major-mode))
+         (subject (or (plist-get msg :subject) "No subject")))
+    `(,subject
+      ,@(bookmark-make-record-default 'no-file 'no-context)
+      (location . (,query . ,docid))
+      (mode . ,mode)
+      (handler . mu4e-bookmark-jump))))
+
+(defun mu4e-bookmark-jump (bookmark)
+  "Handler function for record returned by `mu4e-view-bookmark-make-record'.
+BOOKMARK is a bookmark name or a bookmark record."
+  (let* ((path  (bookmark-prop-get bookmark 'location))
+         (mode  (bookmark-prop-get bookmark 'mode))
+         (docid (cdr path))
+         (query (car path)))
+    (call-interactively 'mu4e)
+    (mu4e-headers-search query)
+    (sit-for 0.5)
+    (mu4e~headers-goto-docid docid)
+    (mu4e~headers-highlight docid)
+    (unless (string= mode "mu4e-headers-mode")
+      (call-interactively 'mu4e-headers-view-message)
+      (run-with-timer 0.1 nil
+                      (lambda (bmk)
+                        (bookmark-default-handler
+                         `("" (buffer . ,(current-buffer)) .
+                           ,(bookmark-get-bookmark-record bmk))))
+                      bookmark))))
+
+;;; 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)
+  "Mark message as spam."
+  (interactive)
+  (let* ((path (shell-quote-argument (mu4e-message-field msg :path)))
+         (command (format mu4e-register-as-spam-cmd path))) ;; re-register msg as spam
+    (shell-command command))
+  (mu4e-mark-at-point 'delete nil))
+
+(defun mu4e-register-msg-as-ham (msg)
+  "Mark message as ham."
+  (interactive)
+  (let* ((path (shell-quote-argument(mu4e-message-field msg :path)))
+         (command (format mu4e-register-as-ham-cmd path))) ;; re-register msg as ham
+    (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)
+  "Mark message 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 message 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))
+
+;;; 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 eshell/mu4e-attach (&rest args)
+  "Attach files to a mu4e message using eshell. 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..8e8a0c2
--- /dev/null
@@ -0,0 +1,606 @@
+;;; mu4e-draft.el -- part of mu4e, the mu mail user agent for emacs -*- lexical-binding: t -*-
+;;
+;; Copyright (C) 2011-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file, various functions to create draft messages
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'mu4e-vars)
+(require 'mu4e-utils)
+(require 'mu4e-message)
+(require 'message) ;; mail-header-separator
+
+;;; Options
+
+(defcustom mu4e-compose-dont-reply-to-self nil
+  "If non-nil, don't include self.
+\(that is, member of `(mu4e-personal-addresses)') in replies."
+  :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)
+
+(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~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 (addrcell)
+       (let ((name (car addrcell))
+             (email (cdr addrcell)))
+         (if name
+             (format "%s <%s>" (mu4e~rfc822-quoteit name) email)
+           (format "%s" email))))
+     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 (cdr cell1) ""))
+   (downcase (or (cdr 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)
+           (cl-member-if
+            (lambda (addr)
+              (string= (downcase addr) (downcase (cdr to-cell))))
+            (mu4e-personal-addresses)))
+         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 (cdr 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 (cdr 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)
+                 (cl-member-if
+                  (lambda (addr)
+                    (string= (downcase addr) (downcase (cdr cc-cell))))
+                  (mu4e-personal-addresses)))
+               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")))))
+
+;;; 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 'rfc822-quoted-string). Then whether there is a quote
+inside the phrase (returning 'rfc822-containing-quote).
+The reverse of the RFC atext definition is then tested.
+If it matches, nil is returned, if not, it is an 'rfc822-atom, which
+is returned."
+  (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-quoteit (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)))))
+
+
+(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
+    (if user-full-name
+        (format "%s <%s>" (mu4e~rfc822-quoteit user-full-name) user-mail-address)
+      (format "%s" 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:2,%s"
+            (format-time-string "%s" (current-time))
+            (random 65535) (random 65535) (random 65535) (random 65535)
+            hostname (or flagstr ""))))
+
+(defun mu4e~draft-common-construct ()
+  "Construct the common headers for each message."
+  (concat
+   (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))))
+         (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)" (cdar from))     . 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)
+  "Open the the draft file at PATH."
+  (if mu4e-compose-in-new-frame
+      (find-file-other-frame path)
+    (find-file path)))
+
+(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)
+  "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)))
+
+      (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)))
+
+      ((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)
+         (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-headers.el b/mu4e/mu4e-headers.el
new file mode 100644 (file)
index 0000000..d3fa289
--- /dev/null
@@ -0,0 +1,1868 @@
+;;; mu4e-headers.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  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 'mule-util) ;; seems _some_ people need this for truncate-string-ellipsis
+
+(require 'mu4e-utils)    ;; utility functions
+(require 'mu4e-proc)
+(require 'mu4e-vars)
+(require 'mu4e-mark)
+(require 'mu4e-compose)
+(require 'mu4e-actions)
+(require 'mu4e-message)
+
+(eval-when-compile (require 'mu4e-view))
+
+(declare-function mu4e-view       "mu4e-view")
+(declare-function mu4e~main-view  "mu4e-main")
+
+;;; Options
+
+(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-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-results-limit 500
+  "Maximum number of results to show; this affects performance
+quite a bit, especially when `mu4e-headers-include-related' is
+non-nil. Set to -1 for no limits, and you temporarily (for one
+query) ignore the limit by pressing a C-u before invoking the
+search.
+
+Note that there are a few complications when
+`mu4e-headers-include-related' is enabled: mu perform *two*
+queries; the first one with this limit set, and then a second
+(unlimited) query for all messages that are related to the first
+matches. We then limit this second result as well, favoring the
+messages that were found in the first set (the \"leaders\").
+"
+  :type '(choice (const :tag "Unlimited" -1)
+                 (integer :tag "Limit"))
+  :group 'mu4e-headers)
+
+(make-obsolete-variable 'mu4e-search-results-limit
+                        'mu4e-headers-results-limit "0.9.9.5-dev6")
+
+(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 applied to headers before they are shown;
+if function is nil or evaluates to nil, show the header,
+otherwise don't. 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))))
+
+Note that this is merely a display filter.")
+
+(defcustom mu4e-headers-visible-flags
+  '(draft flagged new passed replied seen trashed attach encrypted signed unread)
+  "An ordered list of flags to show in the headers buffer. Each
+element is a symbol in the list (DRAFT FLAGGED NEW PASSED
+REPLIED SEEN TRASHED ATTACH ENCRYPTED SIGNED UNREAD)."
+  :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 "Unread" unread))
+  :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)
+
+(defcustom mu4e-headers-search-bookmark-hook nil
+  "Hook run just after we invoke 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, e.g.
+`mu4e-headers-show-threads', `mu4e-headers-include-related',
+`mu4e-headers-skip-duplicates` or `mu4e-headers-results-limit'.
+"
+  :type 'hook
+  :group 'mu4e-headers)
+
+(defcustom mu4e-headers-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-headers-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-headers)
+
+;;; Public variables
+
+(defvar mu4e-headers-sort-field :date
+  "Field to sort the headers by. Must be a symbol,
+one of: `:date', `:subject', `:size', `:prio', `:from', `:to.',
+`:list'")
+
+(defvar mu4e-headers-sort-direction 'descending
+  "Direction to sort by; a symbol either `descending' (sorting
+  Z->A) or `ascending' (sorting A->Z).")
+
+;;;; 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
+(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.")
+
+;;;; Graph drawing
+
+(defvar mu4e-headers-thread-child-prefix '("├>" . "┣▶ ")
+  "Prefix for messages in sub threads that do have a following sibling.")
+
+(defvar mu4e-headers-thread-last-child-prefix '("└>" . "┗▶ ")
+  "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.
+
+This prefix should have the same length as `mu4e-headers-thread-blank-prefix'.")
+
+(defvar mu4e-headers-thread-blank-prefix '(" " . "  ")
+  "Prefix to separate non connected messages.
+
+This prefix should 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.")
+
+;;;; 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.")
+
+(defvar mu4e-headers-show-threads t
+  "Whether to show threads in the headers list.")
+
+(defvar mu4e-headers-full-search nil
+  "Whether to show all results.
+If this is nil show results up to `mu4e-headers-results-limit')")
+
+;;; 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-view-win nil
+  "The view window connected to this headers view.")
+
+(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'.")
+
+;;; Clear
+
+(defun mu4e~headers-clear (&optional msg)
+  "Clear the header buffer and related data structures."
+  (when (buffer-live-p (mu4e-get-headers-buffer))
+    (let ((inhibit-read-only t))
+      (with-current-buffer (mu4e-get-headers-buffer)
+        (mu4e~mark-clear)
+        (erase-buffer)
+        (when msg
+          (goto-char (point-min))
+          (insert (propertize msg 'face 'mu4e-system-face 'intangible t)))))))
+
+;;; 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)))))
+
+(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))
+             (point (mu4e~headers-docid-pos docid)))
+
+        (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 :thread
+                     (mu4e~headers-field-for-docid docid :thread))
+
+          ;; 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))
+            (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
+            (mu4e~headers-header-handler msg point))
+
+          (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 &optional skip-hook)
+  "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.
+
+If SKIP-HOOK is absent or nil, `mu4e-message-changed-hook' will be invoked."
+  (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)))
+  (unless skip-hook
+    (run-hooks 'mu4e-message-changed-hook)))
+
+\f
+;;; Misc
+
+(defun mu4e~headers-contact-str (contacts)
+  "Turn the list of contacts CONTACTS (with elements (NAME . EMAIL)
+into a string."
+  (mapconcat
+   (lambda (ct)
+     (let ((name (car ct)) (email (cdr ct)))
+       (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))
+      ('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))
+          (empty-parent (plist-get thread :empty-parent))
+          (first-child  (plist-get thread :first-child))
+          (last-child   (plist-get thread :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 (and empty-parent first-child)
+                        (if last-child 'single-orphan 'orphan)
+                      (if last-child 'last-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 the 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)."
+  (let ((str "")
+        (get-prefix
+         (lambda (cell)  (if mu4e-use-fancy-chars (cdr cell) (car cell)))))
+    (dolist (flag mu4e-headers-visible-flags)
+      (when (member flag flags)
+        (setq str
+              (concat str
+                      (cl-case flag
+                        ('draft     (funcall get-prefix mu4e-headers-draft-mark))
+                        ('flagged   (funcall get-prefix mu4e-headers-flagged-mark))
+                        ('new       (funcall get-prefix mu4e-headers-new-mark))
+                        ('passed    (funcall get-prefix mu4e-headers-passed-mark))
+                        ('replied   (funcall get-prefix mu4e-headers-replied-mark))
+                        ('seen      (funcall get-prefix mu4e-headers-seen-mark))
+                        ('trashed   (funcall get-prefix mu4e-headers-trashed-mark))
+                        ('attach    (funcall get-prefix mu4e-headers-attach-mark))
+                        ('encrypted (funcall get-prefix mu4e-headers-encrypted-mark))
+                        ('signed    (funcall get-prefix mu4e-headers-signed-mark))
+                        ('unread    (funcall get-prefix mu4e-headers-unread-mark)))))))
+    str))
+
+;;; Special headers
+
+(defconst mu4e-headers-from-or-to-prefix '("" . "To ")
+  "Prefix for the :from-or-to field.
+It's a cons cell with the car element being the From: prefix, the
+cdr element the To: prefix.")
+
+(defun mu4e~headers-from-or-to (msg)
+  "When the from address for message MSG is one of the the user's addresses,
+\(as per `mu4e-personal-addresses'), show the To address;
+otherwise ; show the from address; prefixed with the appropriate
+`mu4e-headers-from-or-to-prefix'."
+  (let ((addr (cdr-safe (car-safe (mu4e-message-field msg :from)))))
+    (if (mu4e-user-mail-address-p addr)
+        (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.
+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 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 :thread))
+         (subj (mu4e-msg-field msg :subject)))
+    (concat ;; prefix subject with a thread indicator
+     (mu4e~headers-thread-prefix tinfo)
+     (if (or (not tinfo) (zerop (plist-get tinfo :level))
+             (plist-get tinfo :empty-parent))
+         (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)
+    ""))
+
+(defun mu4e~headers-custom-field (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-apply-basic-properties (msg field val _width)
+  (cl-case field
+    (:subject
+     (concat ;; prefix subject with a thread indicator
+      (mu4e~headers-thread-prefix (mu4e-message-field msg :thread))
+      ;;  "["(plist-get (mu4e-message-field msg :thread) :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 (mu4e~headers-thread-subject msg))
+    ((: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))
+    (:mailing-list (mu4e~headers-mailing-list val))
+    (: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 msg field))))
+
+(defun mu4e~headers-field-truncate-to-width (_msg _field val width)
+  "Truncate VAL to WIDTH."
+  (if width
+      (truncate-string-to-width val width 0 ?\s truncate-string-ellipsis)
+    val))
+
+(defvar mu4e~headers-field-handler-functions
+  '(mu4e~headers-field-apply-basic-properties
+    mu4e~headers-field-truncate-to-width))
+
+(defun 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-message-field msg (car f-w))))
+    (dolist (func mu4e~headers-field-handler-functions)
+      (setq val (funcall func msg field val width)))
+    val))
+
+(defvar mu4e~headers-line-handler-functions
+  '(mu4e~headers-line-apply-flag-face))
+
+(defun mu4e~headers-line-apply-flag-face (msg line)
+  "Adjust LINE's face property based on FLAGS."
+  (let* ((flags (mu4e-message-field msg :flags))
+         (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)
+                ((memq 'replied flags) 'mu4e-replied-face)
+                ((memq 'passed flags)  'mu4e-forwarded-face)
+                (t                     'mu4e-header-face))))
+    ;; hmmm, this only works with emacs 24.4+
+    (when (fboundp 'add-face-text-property)
+      (add-face-text-property 0 (length line) face t line))
+    line))
+
+(defun mu4e~headers-line-handler (msg line)
+  (dolist (func mu4e~headers-line-handler-functions)
+    (setq line (funcall func msg line)))
+  line)
+
+;; note: this function is very performance-sensitive
+(defun mu4e~headers-header-handler (msg &optional point)
+  "Create a one line description of MSG in this buffer, at POINT,
+if provided, or at the end of the buffer otherwise."
+  (when (buffer-live-p (mu4e-get-headers-buffer))
+    (with-current-buffer (mu4e-get-headers-buffer)
+      (let ((line (mu4e~message-header-description msg)))
+        (when line
+          (mu4e~headers-add-header line (mu4e-message-field msg :docid)
+                                   point msg))))))
+
+(defun mu4e~message-header-description (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))
+    (let ((line (mapconcat
+                 (lambda (f-w) (mu4e~headers-field-handler f-w msg))
+                 mu4e-headers-fields " ")))
+      (mu4e~headers-line-handler msg line))))
+
+(defconst mu4e~searching      "Searching...")
+(defconst mu4e~no-matches     "No matching messages found")
+(defconst mu4e~end-of-results "End of search results")
+
+(defvar mu4e~headers-view-target nil
+  "Whether to automatically view (open) the target message (as
+  per `mu4e~headers-msgid-target').")
+
+(defvar mu4e~headers-msgid-target nil
+  "Message-id to jump to after the search has finished.")
+
+
+(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)))
+          (insert (propertize str 'face 'mu4e-system-face 'intangible t))
+          (unless (zerop count)
+            (mu4e-message "Found %d matching message%s"
+                          count (if (= 1 count) "" "s")))))
+      ;; if we need to jump to some specific message, do so now
+      (goto-char (point-min))
+      (when mu4e~headers-msgid-target
+        (if (eq (current-buffer) (window-buffer))
+            (mu4e-headers-goto-message-id mu4e~headers-msgid-target)
+          (let* ((pos (mu4e-headers-goto-message-id mu4e~headers-msgid-target)))
+            (when pos
+              (set-window-point (get-buffer-window) pos)))))
+      (when (and mu4e~headers-view-target (mu4e-message-at-point 'noerror))
+        ;; view the message at point when there is one.
+        (mu4e-headers-view-message))
+      (setq mu4e~headers-view-target nil
+            mu4e~headers-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  (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-headers-search)
+          (define-key map "S" 'mu4e-headers-search-edit)
+
+          (define-key map "/" 'mu4e-headers-search-narrow)
+
+          (define-key map "j" 'mu4e~headers-jump-to-maildir)
+
+          (define-key map (kbd "<M-left>")  'mu4e-headers-query-prev)
+          (define-key map (kbd "\\")        'mu4e-headers-query-prev)
+          (define-key map (kbd "<M-right>") 'mu4e-headers-query-next)
+
+          (define-key map "b" 'mu4e-headers-search-bookmark)
+          (define-key map "B" 'mu4e-headers-search-bookmark-edit)
+
+          (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)
+          (define-key map "V" 'mu4e-headers-toggle-skip-duplicates)
+
+          (define-key map "q" 'mu4e~headers-quit-buffer)
+          (define-key map "g" 'mu4e-headers-rerun-search) ;; 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)
+
+          ;; 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)
+
+          (define-key map ";" 'mu4e-context-switch)
+
+          ;; 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)
+
+          ;; 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-headers-show-threads)
+                                                mu4e-headers-show-threads))))
+
+            (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-headers-rerun-search))
+            (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* ((field (car item)) (width (cdr item))
+               (info (cdr (assoc field
+                                 (append mu4e-header-info mu4e-header-info-custom))))
+               (require-full (plist-get info :require-full))
+               (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)
+               (sortfield (when sortable (if (booleanp sortable) field sortable)))
+               (help (plist-get info :help))
+               ;; triangle to mark the sorted-by column
+               (arrow
+                (when (and sortable (eq sortfield mu4e-headers-sort-field))
+                  (if (eq mu4e-headers-sort-direction 'descending) downarrow uparrow)))
+               (name (concat (plist-get info :shortname) arrow))
+               (map (make-sparse-keymap)))
+          (when require-full
+            (mu4e-error "Field %S is not supported in mu4e-headers-mode" field))
+          (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))))
+
+
+(defvar mu4e-headers-mode-abbrev-table nil)
+
+(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
+             (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-headers-rerun-search))))
+
+(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)
+
+  (mu4e-context-in-modeline)
+
+  ;; maybe update the current headers upon indexing changes
+  (add-hook 'mu4e-index-updated-hook 'mu4e~headers-maybe-auto-update)
+  (add-hook 'mu4e-index-updated-hook
+            (lambda() (run-hooks 'mu4e-message-changed-hook)) t)
+  (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
+  (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))))
+
+(defun mu4e~headers-add-header (str docid point &optional msg)
+  "Add header STR with DOCID to the buffer at POINT if non-nil, or
+at (point-max) otherwise. If MSG is not nil, add it as the
+text-property `msg'."
+  (when (buffer-live-p (mu4e-get-headers-buffer))
+    (with-current-buffer (mu4e-get-headers-buffer)
+      (let ((inhibit-read-only t))
+        (save-excursion
+          (goto-char (if point point (point-max)))
+          (insert
+           (propertize
+            (concat
+             (mu4e~headers-docid-cookie docid)
+             mu4e~mark-fringe
+             str "\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)))))
+
+;;; Queries & searching
+
+(defcustom mu4e-query-rewrite-function 'identity
+  "Function that 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)
+
+(defun mu4e~headers-search-execute (expr ignore-history)
+  "Search in the mu database for EXPR, and switch to the output
+buffer for the results. If IGNORE-HISTORY is true, do *not* update
+the query history stack."
+  ;; note: we don't want to update the history if this query comes from
+  ;; `mu4e~headers-query-next' or `mu4e~headers-query-prev'.
+  ;;(mu4e-hide-other-mu4e-buffers)
+  (let* ((buf (get-buffer-create mu4e~headers-buffer-name))
+         (inhibit-read-only t)
+         (rewritten-expr (funcall mu4e-query-rewrite-function expr))
+         (maxnum (unless mu4e-headers-full-search mu4e-headers-results-limit)))
+    (with-current-buffer buf
+      (mu4e-headers-mode)
+      (unless ignore-history
+        ;; save the old present query to the history list
+        (when mu4e~headers-last-query
+          (mu4e~headers-push-query mu4e~headers-last-query 'past)))
+      (setq
+       mode-name "mu4e-headers"
+       mu4e~headers-last-query rewritten-expr)
+      (make-local-variable 'global-mode-string)
+      (add-to-list 'global-mode-string
+                   '(:eval
+                     (concat
+                      (propertize
+                       (mu4e~quote-for-modeline mu4e~headers-last-query)
+                       '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)
+                        "")))))
+
+    ;; 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-headers-search-hook expr)
+    (mu4e~headers-clear mu4e~searching)
+    (mu4e~proc-find
+     rewritten-expr
+     mu4e-headers-show-threads
+     mu4e-headers-sort-field
+     mu4e-headers-sort-direction
+     maxnum
+     mu4e-headers-skip-duplicates
+     mu4e-headers-include-related)))
+
+(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
+          (let* ((new-win-func
+                  (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)))))
+            (cond ((with-demoted-errors "Unable to split window: %S"
+                     (eval new-win-func)))
+                  (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"    . :mailing-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 (car contact)) (email (cdr 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* ((thread (or (mu4e-message-field msg :thread)
+                     (mu4e-error "No thread info found")))
+         (path  (or (plist-get thread :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))
+         (last-marked-point))
+    (mu4e-headers-for-each
+     (lambda (mymsg)
+       (let ((my-thread-id (mu4e~headers-get-thread-info mymsg 'thread-id)))
+         (if subthread
+             ;; subthread matching; mymsg's thread path should have path as its
+             ;; prefix
+             (when (string-match (concat "^" path)
+                                 (mu4e~headers-get-thread-info mymsg '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 my-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
+;;; The query past / present / future
+
+(defvar mu4e~headers-query-past nil
+  "Stack of queries before the present one.")
+(defvar mu4e~headers-query-future nil
+  "Stack of queries after the present one.")
+(defvar mu4e~headers-query-stack-size 20
+  "Maximum size for the query stacks.")
+
+(defun mu4e~headers-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. Functional also removes duplicates, limits the
+stack size."
+  (let ((stack
+         (cl-case where
+           (past   mu4e~headers-query-past)
+           (future mu4e~headers-query-future))))
+    ;; only add if not the same item
+    (unless (and stack (string= (car stack) query))
+      (push query stack)
+      ;; limit the stack to `mu4e~headers-query-stack-size' elements
+      (when (> (length stack) mu4e~headers-query-stack-size)
+        (setq stack (cl-subseq stack 0 mu4e~headers-query-stack-size)))
+      ;; remove all duplicates of the new element
+      (cl-remove-if (lambda (elm) (string= elm (car stack))) (cdr stack))
+      ;; update the stacks
+      (cl-case where
+        (past   (setq mu4e~headers-query-past   stack))
+        (future (setq mu4e~headers-query-future stack))))))
+
+(defun mu4e~headers-pop-query (whence)
+  "Pop a query from the stack.
+WHENCE is a symbol telling us where to get it from, either `future'
+or `past'."
+  (cl-case whence
+    (past
+     (unless mu4e~headers-query-past
+       (mu4e-warn "No more previous queries"))
+     (pop mu4e~headers-query-past))
+    (future
+     (unless mu4e~headers-query-future
+       (mu4e-warn "No more next queries"))
+     (pop mu4e~headers-query-future))))
+
+\f
+;;; Interactive functions
+
+(defvar mu4e~headers-search-hist nil
+  "History list of searches.")
+
+(defun mu4e-headers-search (&optional expr prompt edit
+                                      ignore-history msgid show)
+  "Search in the mu database for EXPR, and 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."
+  ;; note: we don't want to update the history if this query comes from
+  ;; `mu4e~headers-query-next' or `mu4e~headers-query-prev'."
+  (interactive)
+  (let* ((prompt (mu4e-format (or prompt "Search for: ")))
+         (expr
+          (if edit
+              (read-string prompt expr)
+            (or expr
+                (read-string prompt nil 'mu4e~headers-search-hist)))))
+    (mu4e-mark-handle-when-leaving)
+    (mu4e~headers-search-execute expr ignore-history)
+    (setq mu4e~headers-msgid-target msgid
+          mu4e~headers-view-target show)))
+
+(defun mu4e-headers-search-edit ()
+  "Edit the last search expression."
+  (interactive)
+  (mu4e-headers-search mu4e~headers-last-query nil t))
+
+(defun mu4e-headers-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-headers-search-bookmark-hook expr)
+    (mu4e-headers-search expr (when edit "Edit bookmark: ") edit)))
+
+(defun mu4e-headers-search-bookmark-edit ()
+  "Edit an existing bookmark before executing it."
+  (interactive)
+  (mu4e-headers-search-bookmark nil t))
+
+(defun mu4e-headers-search-narrow (filter )
+  "Narrow the last search by appending search expression FILTER to
+the last search expression. Note that you can go back to previous
+query (effectively, 'widen' it), with `mu4e-headers-query-prev'."
+  (interactive
+   (let ((filter
+          (read-string (mu4e-format "Narrow down to: ")
+                       nil 'mu4e~headers-search-hist nil t)))
+     (list filter)))
+  (unless mu4e~headers-last-query
+    (mu4e-warn "There's nothing to filter"))
+  (mu4e-headers-search
+   (format "(%s) AND (%s)" mu4e~headers-last-query filter)))
+
+(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))
+
+(defvar mu4e~headers-loading-buf nil
+  "A buffer for loading a message view.")
+
+(defun mu4e~decrypt-p (msg)
+  "Should we decrypt this message?"
+  (unless mu4e-view-use-gnus ;; we don't decrypt in the gnus-view case
+    (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))))
+
+(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))
+         (docid (or (mu4e-message-field msg :docid)
+                    (mu4e-warn "No message at point")))
+         (decrypt (mu4e~decrypt-p msg))
+         (verify  (not mu4e-view-use-gnus))
+         (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)
+    ;; Note, ideally in the 'gnus' case, we don't need to call the server to get
+    ;; the body etc., we only need the path which we already have.
+    ;;
+    ;; However, for now we still need the body for e.g. view-in-browser so let's
+    ;; not yet do that.
+
+    ;; (if mu4e-view-use-gnus
+    ;;     (mu4e-view msg)
+    ;;   (mu4e~proc-view docid mu4e-view-show-images decrypt))
+    (mu4e~proc-view docid mu4e-view-show-images decrypt verify)))
+
+(defun mu4e-headers-rerun-search ()
+  "Rerun 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-headers-search mu4e~headers-last-query nil nil t msgid)))
+
+(defun mu4e~headers-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~headers-pop-query whence))
+        (where (if (eq whence 'future) 'past 'future)))
+    (when query
+      (mu4e~headers-push-query mu4e~headers-last-query where)
+      (mu4e-headers-search query nil nil t))))
+
+(defun mu4e-headers-query-next ()
+  "Execute the previous query from the query stacks."
+  (interactive)
+  (mu4e~headers-query-navigate 'future))
+
+(defun mu4e-headers-query-prev ()
+  "Execute the previous query from the query stacks."
+  (interactive)
+  (mu4e~headers-query-navigate 'past))
+
+;; forget the past so we don't repeat it :/
+(defun mu4e-headers-forget-queries ()
+  "Forget all the complete query history."
+  (interactive)
+  (setq ;; note: don't forget the present one
+   mu4e~headers-query-past nil
+   mu4e~headers-query-future nil)
+  (mu4e-message "Query history cleared"))
+
+(defun mu4e~headers-move (lines)
+  "Move point LINES lines forward (if LINES is positive) or
+backward (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))
+  (let* ((_succeeded (zerop (forward-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)
+      docid)))
+
+(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)
+  "Show the messages in maildir (user is prompted to ask what
+maildir)."
+  (interactive
+   (let ((maildir (mu4e-ask-maildir "Jump to maildir: ")))
+     (list maildir)))
+  (when maildir
+    (mu4e-mark-handle-when-leaving)
+    (mu4e-headers-search (format "maildir:\"%s\"" maildir))))
+
+(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))))
+
+;;; _
+(provide 'mu4e-headers)
+;;; mu4e-headers.el ends here
diff --git a/mu4e/mu4e-icalendar.el b/mu4e/mu4e-icalendar.el
new file mode 100644 (file)
index 0000000..92153b8
--- /dev/null
@@ -0,0 +1,189 @@
+;;; mu4e-icalendar.el --- reply to iCalendar meeting requests (part of mu4e)  -*- lexical-binding: t; -*- -*- 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  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)
+
+;; 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)
+
+(eval-when-compile (require 'mu4e-mark))
+(eval-when-compile (require 'mu4e-vars))
+
+;;;###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-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))
+         (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)))))
+
+        (with-current-buffer (get-buffer-create gnus-icalendar-reply-bufname)
+          (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))
+          (mu4e-icalendar-reply-ical msg event status (buffer-name)))
+
+        ;; Back in article buffer
+        (setq-local gnus-icalendar-reply-status status)
+        (when gnus-icalendar-org-enabled-p
+          (gnus-icalendar--update-org-event 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."
+  (delete-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-reply-ical (original-msg event status buffer-name)
+  "Reply to ORIGINAL-MSG containing invitation EVENT with STATUS.
+See `gnus-icalendar-event-reply-from-buffer' for the possible
+STATUS values.  BUFFER-NAME is the name of the buffer holding the
+response in icalendar format."
+  (let ((message-signature nil))
+    (let ((mu4e-compose-cite-function #'mu4e~icalendar-delete-citation)
+          (mu4e-sent-messages-behavior 'delete)
+          (mu4e-compose-reply-recipients 'sender))
+      (mu4e~compose-handler 'reply original-msg))
+    ;; Make sure the recipient is the organizer
+    (let ((organizer (gnus-icalendar-event:organizer event)))
+      (unless (string= organizer "")
+        (message-remove-header "To")
+        (message-goto-to)
+        (insert organizer)))
+    ;; Not (message-goto-body) to possibly skip mll sign directive
+    ;; inserted by `mu4e-compose-mode-hook':
+    (goto-char (point-max))
+    (mml-insert-multipart "alternative")
+    (mml-insert-part "text/plain")
+    (let ((reply-event (gnus-icalendar-event-from-buffer
+                        buffer-name (mu4e-personal-addresses))))
+      (insert (gnus-icalendar-event->gnus-calendar reply-event status)))
+    (forward-line 1); move past closing tag
+    (mml-attach-buffer buffer-name "text/calendar; method=REPLY; charset=utf-8")
+    (message-remove-header "Subject")
+    (message-goto-subject)
+    (insert (capitalize (symbol-name status))
+            ": " (gnus-icalendar-event:summary event))
+    (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
+       (lambda () (setq mu4e-sent-func
+                   (mu4e~icalendar-trash-message original-msg)))
+       t t))))
+
+
+(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..8355ff4
--- /dev/null
@@ -0,0 +1,102 @@
+;;; mu4e-lists.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2016 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  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:
+
+(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 regex patterns 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)))
+
+;;; _
+(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..debcbf4
--- /dev/null
@@ -0,0 +1,326 @@
+;;; mu4e-main.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'smtpmail)      ;; the queueing stuff (silence elint)
+(require 'mu4e-utils)    ;; utility functions
+(require 'mu4e-context)  ;; the context
+(require 'mu4e-vars)  ;; the context
+(require 'cl-lib)
+
+;;; Mode
+
+(defvar mu4e-main-buffer-hide-personal-addresses nil
+  "Whether to hid the personal address in the main view. This
+  can be useful to avoid the noise when there are many.
+
+  This also hides the warning if your `user-mail-address' is not
+part of the personal addresses.")
+
+(defvar mu4e-main-buffer-name " *mu4e-main*"
+  "Name of the mu4e main view buffer. The default name starts
+with SPC and therefore is not visible in buffer list.")
+
+(defvar mu4e-main-mode-map
+  (let ((map (make-sparse-keymap)))
+
+    (define-key map "b" 'mu4e-headers-search-bookmark)
+    (define-key map "B" 'mu4e-headers-search-bookmark-edit)
+
+    (define-key map "s" 'mu4e-headers-search)
+    (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 ";" 'mu4e-context-switch)
+
+    (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.")
+
+(defvar mu4e-main-mode-abbrev-table nil)
+(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-in-modeline)
+  (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)
+                       (- (length newstr) 1) 'mouse-face 'highlight newstr)
+    newstr))
+
+
+(defun mu4e~main-bookmarks ()
+  ;; TODO: it's a bit uncool to hard-code the "b" shortcut...
+  (cl-loop with bmks = (mu4e-bookmarks)
+           with longest = (cl-loop for b in bmks
+                                   maximize (length (plist-get b :name)))
+           with queries = (plist-get mu4e~server-props :queries)
+           for bm in bmks
+           for key = (string (plist-get bm :key))
+           for name = (plist-get bm :name)
+           for query = (plist-get bm :query)
+           for qcounts = (and (stringp query)
+                              (cl-loop for q in queries
+                                       when (string= (plist-get q :query) query)
+                                       collect q))
+           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 (length name)) ? )
+                          (propertize (number-to-string unread)
+                                      'face 'mu4e-header-key-face)
+                          count))
+                     "")
+                   "\n")))
+
+
+(defun mu4e~key-val (key val &optional unit)
+  "Return a key / value pair."
+  (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))
+
+(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)
+        (pos (point)))
+    ;; Maybe refresh infos from server.
+    (if refresh
+        (mu4e~start 'mu4e~main-redraw-buffer)
+      (mu4e~main-redraw-buffer))))
+
+(defun mu4e~main-redraw-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-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 "  Misc\n\n" 'face 'mu4e-title-face)
+
+       (mu4e~main-action-str "\t* [;]Switch context\n" 'mu4e-context-switch)
+
+       (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 "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-buffer-hide-personal-addresses ""
+         (mu4e~key-val "personal addresses" (if addrs (mapconcat #'identity addrs ", "  ) "none"))))
+
+      (if mu4e-main-buffer-hide-personal-addresses ""
+        (when (and user-mail-address (not (member user-mail-address addrs)))
+          (mu4e-message (concat
+                         "Note: `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 ()
+  "mu4e main view in the minibuffer."
+  (interactive)
+  (let ((key
+         (read-key
+          (mu4e-format
+           "%s"
+           (concat
+            (mu4e~main-action-str "[j]ump " 'mu4e-jump-to-maildir)
+            (mu4e~main-action-str "[s]earch " 'mu4e-search)
+            (mu4e~main-action-str "[C]ompose " 'mu4e-compose-new)
+            (mu4e~main-action-str "[b]ookmarks " 'mu4e-headers-search-bookmark)
+            (mu4e~main-action-str "[;]Switch context " 'mu4e-context-switch)
+            (mu4e~main-action-str "[U]pdate " 'mu4e-update-mail-and-index)
+            (mu4e~main-action-str "[N]ews " 'mu4e-news)
+            (mu4e~main-action-str "[A]bout " 'mu4e-about)
+            (mu4e~main-action-str "[H]elp " 'mu4e-display-manual))))))
+    (unless (member key '(?\C-g ?\C-\[))
+      (let ((mu4e-command (lookup-key mu4e-main-mode-map (string key) t)))
+        (if mu4e-command
+            (condition-case err
+                (let ((mu4e-hide-index-messages t))
+                  (call-interactively mu4e-command))
+              (error (when (cadr err) (message (cadr err)))))
+          (message (mu4e-format "key %s not bound to a command" (string key))))
+        (when (or (not mu4e-command) (eq mu4e-command '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..6ae0fc0
--- /dev/null
@@ -0,0 +1,468 @@
+;;; mu4e-mark.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  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 'cl-lib)
+(require 'mu4e-proc)
+(require 'mu4e-utils)
+(require 'mu4e-message)
+
+;; 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 an Emacs feature called 'overlays', which aren't
+particularly fast).")
+
+;;; 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."
+  (cl-find-if
+   (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~proc-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~proc-remove docid)))
+    (flag
+     :char ("+" . "✚")
+     :prompt "+flag"
+     :show-target (lambda (target) "flag")
+     :action (lambda (docid msg target)
+               (mu4e~proc-move docid nil "+F-u-N")))
+    (move
+     :char ("m" . "▷")
+     :prompt "move"
+     :ask-target  mu4e~mark-get-move-target
+     :action (lambda (docid msg target)
+               (mu4e~proc-move docid (mu4e~mark-check-target target) "-N")))
+    (read
+     :char    ("!" . "◼")
+     :prompt "!read"
+     :show-target (lambda (target) "read")
+     :action (lambda (docid msg target) (mu4e~proc-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~proc-move docid
+                                                        (mu4e~mark-check-target target) "+T-N")))
+    (unflag
+     :char    ("-" . "➖")
+     :prompt "-unflag"
+     :show-target (lambda (target) "unflag")
+     :action (lambda (docid msg target) (mu4e~proc-move docid nil "-F-N")))
+    (untrash
+     :char   ("=" . "▲")
+     :prompt "=untrash"
+     :show-target (lambda (target) "untrash")
+     :action (lambda (docid msg target) (mu4e~proc-move docid nil "-T")))
+    (unread
+     :char    ("?" . "◻")
+     :prompt "?unread"
+     :show-target (lambda (target) "unread")
+     :action (lambda (docid msg target) (mu4e~proc-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 'target' '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 overlays
+        (remove-overlays (line-beginning-position) (line-end-position))
+        ;; 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)
+              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~proc-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 (cl-remove-if (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)
+  (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..cf75f94
--- /dev/null
@@ -0,0 +1,352 @@
+;;; mu4e-message.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Functions to get data from mu4e-message plist structure
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'mu4e-vars)
+(require 'mu4e-utils)
+(require 'flow-fill)
+
+(defvar mu4e~view-message)
+(defvar shr-inhibit-images)
+
+(defcustom mu4e-html2text-command
+  (if (fboundp 'shr-insert-document)
+      'mu4e-shr2text
+    (progn (require 'html2text) 'html2text))
+  "Either a shell command or a function that converts from html to plain text.
+
+If it is a shell command, the command reads html from standard
+input and outputs plain text on standard output. If you use the
+htmltext program, it's recommended you use \"html2text -utf8
+-width 72\". Alternatives are the python-based html2markdown, w3m
+and on MacOS you may want to use textutil.
+
+It can also be a function, which takes a messsage-plist as
+argument and is expected to return the textified html as output.
+
+For backward compatibility, it can also be a parameterless
+function which is run in the context of a buffer with the html
+and expected to transform this (like the `html2text' function).
+
+In all cases, the output is expected to be in UTF-8 encoding.
+
+Newer emacs has the shr renderer, and when it's available
+conversion defaults to `mu4e-shr2text'; otherwise, the default is
+emacs' built-in `html2text' function."
+  :type '(choice string function)
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-prefer-html nil
+  "Whether to base the body display on the html-version.
+If the e-mail message has no html-version the plain-text version
+is always used."
+  :type 'boolean
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-html-plaintext-ratio-heuristic 5
+  "Ratio between the length of the html and the plain text part.
+Below this ratio mu4e will consider the plain text part to be
+'This messages requires html' text bodies. You can neutralize
+it (always show the text version) by using
+`most-positive-fixnum'."
+  :type 'integer
+  :group 'mu4e-view)
+
+(defvar mu4e-message-body-rewrite-functions '(mu4e-message-outlook-cleanup)
+  "List of functions to transform the message body text.
+The functions take two parameters, MSG and TXT, which are the
+message-plist and the text, which is the plain-text version,
+ossibly converted from html and/or transformed by earlier rewrite
+functions.")
+
+;;; Message fields
+
+(defsubst mu4e-message-field-raw (msg field)
+  "Retrieve FIELD from message plist MSG.
+FIELD is one of :from, :to, :cc, :bcc, :subject, :data,
+:message-id, :path, :maildir, :priority, :attachments,
+:references, :in-reply-to, :body-txt, :body-html
+
+Returns nil if the field does not exist.
+
+A message plist looks something like:
+\(:docid 32461
+ :from ((\"Nikola Tesla\" . \"niko@example.com\"))
+ :to ((\"Thomas Edison\" . \"tom@example.com\"))
+ :cc ((\"Rupert The Monkey\" . \"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)
+ :attachments
+     ((:index 2 :name \"photo.jpg\" :mime-type \"image/jpeg\" :size 147331)
+      (:index 3 :name \"book.pdf\" :mime-type \"application/pdf\" :size 192220))
+ :references  (\"238C8384574032D81EE81AF0114E4E74@123213.mail.example.com\"
+ \"6BDC23465F79238203498230942D81EE81AF0114E4E74@123213.mail.example.com\")
+ :in-reply-to \"238203498230942D81EE81AF0114E4E74@123213.mail.example.com\"
+ :body-txt \"Hi Tom, ...\"
+\)).
+Some notes on the format:
+- The address fields are lists of pairs (NAME . EMAIL), where NAME can be nil.
+- 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."
+  (let ((msg (or (get-text-property (point) 'msg) mu4e~view-message)))
+    (if msg
+        msg
+      (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))
+
+(defvar mu4e~message-body-html nil
+  "Whether the body text uses HTML.")
+
+(defun mu4e~message-use-html-p (msg prefer-html)
+  "Do we want to PREFER-HTML for MSG?
+Determine whether we want
+to use html or text. The decision is based on PREFER-HTML and
+whether the message supports the given representation."
+  (let* ((txt (mu4e-message-field msg :body-txt))
+         (html (mu4e-message-field msg :body-html))
+         (txt-len (length txt))
+         (html-len (length html))
+         (txt-limit (* mu4e-view-html-plaintext-ratio-heuristic txt-len))
+         (txt-limit (if (>= txt-limit 0) txt-limit most-positive-fixnum)))
+    (cond
+                                        ; user prefers html --> use html if there is
+     (prefer-html (> html-len 0))
+     ;; otherwise (user prefers text) still use html if there is not enough
+     ;; text
+     ((< txt-limit html-len) t)
+     ;; otherwise, use text
+     (t nil))))
+
+(defun mu4e~message-body-has-content-type-param (msg param)
+  "Does the MSG have a content-type parameter PARAM?"
+  (cdr
+   (assoc param (mu4e-message-field msg :body-txt-params))))
+
+(defun mu4e~safe-iequal (a b)
+  "Is string A equal to a downcased B?"
+  (and b (equal (downcase b) a)))
+
+(defun mu4e-message-body-text (msg &optional prefer-html)
+  "Get the body in text form for message MSG.
+This is either :body-txt, or if not available, :body-html
+converted to text, using `mu4e-html2text-command' is non-nil, it
+will use that. Normally, this function prefers the text part,
+unless PREFER-HTML is non-nil."
+  (setq mu4e~message-body-html (mu4e~message-use-html-p msg prefer-html))
+  (let ((body
+         (if mu4e~message-body-html
+             ;; use an htmml body
+             (cond
+              ((stringp mu4e-html2text-command)
+               (mu4e~html2text-shell msg mu4e-html2text-command))
+              ((functionp mu4e-html2text-command)
+               (if (help-function-arglist mu4e-html2text-command)
+                   (funcall mu4e-html2text-command msg)
+                 ;; oldskool parameterless mu4e-html2text-command
+                 (mu4e~html2text-wrapper mu4e-html2text-command msg)))
+              (t (mu4e-error "Invalid `mu4e-html2text-command'")))
+           ;; use a text body
+           (or (with-temp-buffer
+                 (insert (or (mu4e-message-field msg :body-txt) ""))
+                 (if (mu4e~safe-iequal "flowed"
+                                       (mu4e~message-body-has-content-type-param
+                                        msg "format"))
+                     (fill-flowed nil
+                                  (mu4e~safe-iequal
+                                   "yes"
+                                   (mu4e~message-body-has-content-type-param
+                                    msg "delsp"))))
+                 (buffer-string)) ""))))
+    (dolist (func mu4e-message-body-rewrite-functions)
+      (setq body (funcall func msg body)))
+    body))
+
+(defun mu4e-message-outlook-cleanup (_msg body)
+  "Clean-up MSG's BODY.
+Esp. MS-Outlook-originating message may not advertise the correct
+encoding (e.g. 'iso-8859-1' instead of 'windows-1252'), thus
+giving us these funky chars. here, we either remove them, or
+replace with."
+  (with-temp-buffer
+    (insert body)
+    (goto-char (point-min))
+    (while (re-search-forward "[\r \92]" nil t)
+      (replace-match
+       (cond
+        ((string= (match-string 0) "\92") "'")
+        ((string= (match-string 0) " ") " ")
+        (t ""))))
+    (buffer-string)))
+
+(defun mu4e-message-contact-field-matches (msg cfield rx)
+  "Does MSG's contact-field CFIELD matche 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."
+  (if (and cfield (listp cfield))
+      (or (mu4e-message-contact-field-matches msg (car cfield) rx)
+          (mu4e-message-contact-field-matches msg (cdr cfield) rx))
+    (when cfield
+      (if (listp rx)
+          ;; if rx is a list, try each one of them for a match
+          (cl-find-if
+           (lambda (a-rx) (mu4e-message-contact-field-matches msg cfield a-rx))
+           rx)
+        ;; not a list, check the rx
+        (cl-find-if
+         (lambda (ct)
+           (let ((name (car ct)) (email (cdr ct)))
+             (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 e-mail address in
+`(mu4e-personal-addresses)'. Returns the contact cell that
+matched, or nil."
+  (cl-find-if
+   (lambda (cc-cell)
+     (cl-member-if
+      (lambda (addr)
+        (string= (downcase addr) (downcase (cdr cc-cell))))
+      (mu4e-personal-addresses)))
+   (mu4e-message-field msg cfield)))
+
+(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)
+(defalias 'mu4e-body-text 'mu4e-message-body-text) ;; backward compatibility
+
+(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
+
+(defun mu4e~html2text-wrapper (func msg)
+  "Apply FUNC on a temporary buffer with html from MSG.
+Return the buffer contents."
+  (with-temp-buffer
+    (insert (or (mu4e-message-field msg :body-html) ""))
+    (funcall func)
+    (or (buffer-string) "")))
+
+(defun mu4e-shr2text (msg)
+  "Convert html in MSG to text using the shr engine.
+This can be used in `mu4e-html2text-command' in a new enough
+Emacs. Based on code by Titus von der Malsburg."
+  (mu4e~html2text-wrapper
+   (lambda ()
+     (let (
+           ;; When HTML emails contain references to remote images,
+           ;; retrieving these images leaks information. For example,
+           ;; the sender can see when I opened the email and from which
+           ;; computer (IP address). For this reason, it is preferable
+           ;; to not retrieve images.
+           ;; See this discussion on mu-discuss:
+           ;; https://groups.google.com/forum/#!topic/mu-discuss/gr1cwNNZnXo
+           (shr-inhibit-images t))
+       (shr-render-region (point-min) (point-max)))) msg))
+
+(defun mu4e~html2text-shell (msg _cmd)
+  "Convert html2 text in MSG using a shell function CMD."
+  (mu4e~html2text-wrapper
+   (lambda ()
+     (let* ((tmp-file (mu4e-make-temp-file "html")))
+       (write-region (point-min) (point-max) tmp-file)
+       (erase-buffer)
+       (call-process-shell-command mu4e-html2text-command tmp-file t t)
+       (delete-file tmp-file))) msg))
+
+;;; _
+(provide 'mu4e-message)
+;;; mu4e-message.el ends here
diff --git a/mu4e/mu4e-meta.el.in b/mu4e/mu4e-meta.el.in
new file mode 100644 (file)
index 0000000..47d242d
--- /dev/null
@@ -0,0 +1,11 @@
+;; 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-meta)
diff --git a/mu4e/mu4e-org.el b/mu4e/mu4e-org.el
new file mode 100644 (file)
index 0000000..cb0a80a
--- /dev/null
@@ -0,0 +1,162 @@
+;;; mu4e-org -- Org-links to mu4e messages/queries -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The expect version here is org 8.x.
+
+;;; Code:
+
+(require 'org)
+
+(defgroup mu4e-org nil
+  "Settings for the org-mode related functionality in mu4e."
+  :group 'mu4e
+  :group '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.")
+(define-obsolete-variable-alias 'org-mu4e-link-query-in-headers-mode
+  'mu4e-org-link-query-in-headers-mode "1.3.6")
+
+(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 use in org capture templates.
+
+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 'org-mu4e)
+(define-obsolete-variable-alias 'org-mu4e-link-desc-func
+  'mu4e-org-link-desc-func "1.3.6")
+
+(defun mu4e~org-store-link-query ()
+  "Store a link to a mu4e query."
+  (let* ((query  (mu4e-last-query))
+         (date (format-time-string (org-time-stamp-format)))
+         ;; seems we get an error when there's no date...
+         (link (concat "mu4e:query:" query)))
+    (org-store-link-props
+     :type "mu4e"
+     :query query
+     :date date)
+    (org-add-link-props
+     :link link
+     :description (format "mu4e-query: '%s'" query))
+    link))
+
+(defun mu4e~org-first-address (msg field)
+  "Get address field FIELD from MSG as a string or nil."
+  (let* ((val (plist-get msg field))
+         (name (when val (car (car val))))
+         (addr (when val (cdr (car val)))))
+    (when val
+      (if name
+          (format "%s <%s>" name addr)
+        (format "%s" addr)))))
+
+(defun mu4e~org-store-link-message ()
+  "Store a link to a mu4e message."
+  (unless (fboundp 'mu4e-message-at-point)
+    (error "Please load mu4e before mu4e-org"))
+  (let* ((msg      (mu4e-message-at-point))
+         (msgid   (or (plist-get msg :message-id) "<none>"))
+         (date    (plist-get msg :date))
+         (date    (format-time-string (org-time-stamp-format) date))
+         ;; seems we get an error when there's no date...
+         (link    (concat "mu4e:msgid:" msgid)))
+    (org-store-link-props
+     :type        "mu4e"
+     :message-id  msgid
+     :to          (mu4e~org-first-address msg :to)
+     :from        (mu4e~org-first-address msg :from)
+     :date        date
+     :subject     (plist-get msg :subject))
+    (org-add-link-props
+     :link link
+     :description (funcall mu4e-org-link-desc-func msg))
+    link))
+
+(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."
+  (unless (fboundp 'mu4e-message-at-point)
+    (error "Please load mu4e before mu4e-org"))
+  (if (and (eq major-mode 'mu4e-headers-mode)
+           mu4e-org-link-query-in-headers-mode)
+      (mu4e~org-store-link-query)
+    (when (mu4e-message-at-point t)
+      (mu4e~org-store-link-message))))
+
+;; org-add-link-type is obsolete as of org-mode 9. Instead we will use the
+;; org-link-set-parameters method
+(if (fboundp 'org-link-set-parameters)
+    (org-link-set-parameters "mu4e"
+                             :follow #'mu4e-org-open
+                             :store  #'mu4e-org-store-link)
+  (org-add-link-type "mu4e" 'mu4e-org-open)
+  (add-hook 'org-store-link-functions 'mu4e-org-store-link))
+
+(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-headers-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 'org-mu4e-store-and-capture "1.3.6")
+
+;;; _
+(provide 'mu4e-org)
+;;; mu4e-org.el ends here
diff --git a/mu4e/mu4e-proc.el b/mu4e/mu4e-proc.el
new file mode 100644 (file)
index 0000000..bee6c71
--- /dev/null
@@ -0,0 +1,488 @@
+;;; mu4e-proc.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'mu4e-vars)
+(require 'mu4e-utils)
+(require 'mu4e-meta)
+
+;;; Internal vars
+
+(defvar mu4e~proc-buf nil
+  "Buffer (string) for data received from the backend.")
+(defconst mu4e~proc-name " *mu4e-proc*"
+  "Name of the server process, buffer.")
+(defvar mu4e~proc-process nil
+  "The mu-server process.")
+
+;; dealing with the length cookie that precedes expressions
+(defconst mu4e~cookie-pre "\376"
+  "Each expression starts with a length cookie:
+<`mu4e~cookie-pre'><length-in-hex><`mu4e~cookie-post'>.")
+(defconst mu4e~cookie-post "\377"
+  "Each expression starts with a length cookie:
+<`mu4e~cookie-pre'><length-in-hex><`mu4e~cookie-post'>.")
+(defconst mu4e~cookie-matcher-rx
+  (concat mu4e~cookie-pre "\\([[:xdigit:]]+\\)" mu4e~cookie-post)
+  "Regular expression matching the length cookie.
+Match 1 will be the length (in hex).")
+
+;;; Functions
+
+(defun mu4e~proc-running-p  ()
+  "Whether the mu process is running."
+  (and mu4e~proc-process
+       (memq (process-status mu4e~proc-process)
+             '(run open listen connect stop))
+       t))
+
+(defsubst mu4e~proc-eat-sexp-from-buf ()
+  "'Eat' the next s-expression from `mu4e~proc-buf'.
+Note: this is a string, not an emacs-buffer. `mu4e~proc-buf gets
+its contents from the mu-servers in the following form:
+   <`mu4e~cookie-pre'><length-in-hex><`mu4e~cookie-post'>
+Function returns this sexp, or nil if there was none.
+`mu4e~proc-buf' is updated as well, with all processed sexp data
+removed."
+  (ignore-errors ;; the server may die in the middle...
+    ;; mu4e~cookie-matcher-rx:
+    ;;  (concat mu4e~cookie-pre "\\([[:xdigit:]]+\\)]" mu4e~cookie-post)
+    (let ((b (string-match mu4e~cookie-matcher-rx mu4e~proc-buf))
+          (sexp-len) (objcons))
+      (when b
+        (setq sexp-len (string-to-number (match-string 1 mu4e~proc-buf) 16))
+        ;; does mu4e~proc-buf contain the full sexp?
+        (when (>= (length mu4e~proc-buf) (+ sexp-len (match-end 0)))
+          ;; clear-up start
+          (setq mu4e~proc-buf (substring mu4e~proc-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~proc-buf 0 sexp-len)
+                          'utf-8 t)))
+          (when objcons
+            (setq mu4e~proc-buf (substring mu4e~proc-buf sexp-len))
+            (car objcons)))))))
+
+
+(defun mu4e~proc-filter (_proc str)
+  "Filter string STR from PROC.
+This processes the 'mu server' output. It accumulates the
+strings into valid sexps by checking of the ';;eox' `end-of-sexp'
+marker, and then evaluating them.
+
+The server output is as follows:
+
+   1. an error
+      (:error 2 :message \"unknown command\")
+      ;; eox
+   => passed to `mu4e-error-func'.
+
+   2a. a message sexp looks something like:
+ \(
+  :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/1312254065_3.32282.pluto,4cd5bd4e9:2,\"
+  :priority high
+  :flags (new unread)
+  :attachments ((2 \"hello.jpg\" \"image/jpeg\") (3 \"laah.mp3\" \"audio/mp3\"))
+  :body-txt \" <message body>\"
+\)
+;; eox
+   => this will be passed to `mu4e-header-func'.
+
+  2b. After the list of message sexps 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'.
+
+  4. a database update looks like:
+  (:update <msg-sexp> :move <nil-or-t>)
+
+   => 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'."
+  (mu4e-log 'misc "* Received %d byte(s)" (length str))
+  (setq mu4e~proc-buf (concat mu4e~proc-buf str)) ;; update our buffer
+  (let ((sexp (mu4e~proc-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 :date)
+          (funcall mu4e-header-func sexp))
+
+         ;; 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)
+          (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)))
+
+         ;; do something with a temporary file
+         ((plist-get sexp :temp)
+          (funcall mu4e-temp-func
+                   (plist-get sexp :temp)   ;; name of the temp file
+                   (plist-get sexp :what)   ;; what to do with it
+                   ;; (pipe|emacs|open-with...)
+                   (plist-get sexp :docid)  ;; docid of the message
+                   (plist-get sexp :param)));; parameter for the action
+
+         ;; 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~proc-eat-sexp-from-buf))))))
+
+(defun mu4e~escape (str)
+  "Escape string STR for transport.
+Put it in quotes, and escape existing quotation. In particular,
+backslashes and double-quotes."
+  (let ((esc (replace-regexp-in-string "\\\\" "\\\\\\\\" str)))
+    (format "\"%s\"" (replace-regexp-in-string "\"" "\\\\\"" esc))))
+
+(defun mu4e~proc-start ()
+  "Start the mu server process."
+  (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary))
+    (mu4e-error
+     (format
+      "`mu4e-mu-binary' (%S) not found; please set to the path to the mu executable"
+      mu4e-mu-binary)))
+  (let* ((process-connection-type nil) ;; use a pipe
+         (args (when mu4e-mu-home `(,(format"--muhome=%s" mu4e-mu-home))))
+         (args (cons "server" args)))
+    (setq mu4e~proc-buf "")
+    (setq mu4e~proc-process (apply 'start-process
+                                   mu4e~proc-name mu4e~proc-name
+                                   mu4e-mu-binary args))
+    ;; register a function for (:info ...) sexps
+    (unless mu4e~proc-process
+      (mu4e-error "Failed to start the mu4e backend"))
+    (set-process-query-on-exit-flag mu4e~proc-process nil)
+    (set-process-coding-system mu4e~proc-process 'binary 'utf-8-unix)
+    (set-process-filter mu4e~proc-process 'mu4e~proc-filter)
+    (set-process-sentinel mu4e~proc-process 'mu4e~proc-sentinel)))
+
+(defun mu4e~proc-kill ()
+  "Kill the mu server process."
+  (let* ((buf (get-buffer mu4e~proc-name))
+         (proc (and (buffer-live-p buf) (get-buffer-process buf))))
+    (when proc
+      (let ((delete-exited-processes t))
+        (mu4e~call-mu '(quit)))
+      ;; try sending SIGINT (C-c) to process, so it can exit gracefully
+      (ignore-errors
+        (signal-process proc 'SIGINT))))
+  (setq
+   mu4e~proc-process nil
+   mu4e~proc-buf nil))
+
+;; error codes are defined in src/mu-util
+;;(defconst mu4e-xapian-empty 19 "Error code: xapian is empty/non-existent")
+
+(defun mu4e~proc-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~proc-process nil)
+    (setq mu4e~proc-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~call-mu (form)
+  "Call 'mu' with some command."
+  (unless (mu4e~proc-running-p) (mu4e~proc-start))
+  (let* ((print-length nil) (print-level nil)
+         (cmd (format "%S" form)))
+    (mu4e-log 'to-server "%s" cmd)
+    (process-send-string mu4e~proc-process (concat cmd "\n"))))
+
+(defun mu4e~docid-msgid-param (docid-or-msgid)
+  "Construct a backend parameter based on DOCID-OR-MSGID."
+  (if (stringp docid-or-msgid)
+      `(:msgid ,(mu4e~escape docid-or-msgid))
+    `(:docid ,docid-or-msgid)))
+
+(defun mu4e~proc-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~call-mu `(add :path ,path)))
+
+(defun mu4e~proc-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~call-mu `(compose
+                  :type ,type
+                  :decrypt ,(and decrypt t)
+                  :docid   ,docid)))
+
+(defun mu4e~proc-contacts (personal after tstamp)
+  "Ask for contacts with PERSONAL AFTER 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)."
+  (mu4e~call-mu `(contacts
+                  :personal ,(and personal t)
+                  :after    ,(or after nil)
+                  :tstamp   ,(or tstamp nil))))
+
+(defun mu4e~proc-extract (action docid index decrypt
+                                 &optional path what param)
+  "Perform ACTION  on part with DOCID INDEX DECRYPT PATH WHAT PARAM.
+Use a message with DOCID and perform ACTION on it (as symbol,
+either `save', `open', `temp') which mean: * save: save the part
+to PATH (a path) (non-optional for save)$ * open: open the part
+with the default application registered for doing so * temp: save
+to a temporary file, then respond with
+       (:temp <path> :what <what> :param <param>)."
+  (mu4e~call-mu `(extract
+                  :action ,action
+                  :docid ,docid
+                  :index ,index
+                  :decrypt ,(and decrypt t)
+                  :path ,path
+                  :what ,what
+                  :param ,param)))
+
+(defun mu4e~proc-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 will be called for, resp., a message (header row)
+or an error."
+  (mu4e~call-mu `(find
+                  :query ,query
+                  :threads ,threads
+                  :sortfield ,sortfield
+                  :descending ,(if (eq sortdir 'descending) t nil)
+                  :maxnum ,maxnum
+                  :skip-dups ,skip-dups
+                  :include-related ,include-related)))
+
+(defun mu4e~proc-index (&optional cleanup lazy-check)
+  "Index messages with possible CLEANUP and LAZY-CHECK."
+  (mu4e~call-mu `(index :cleanup ,cleanup :lazy-check ,lazy-check)))
+
+(defun mu4e~proc-mkdir (path)
+  "Create a new maildir-directory at filesystem PATH."
+  ;;(mu4e~proc-send-command "cmd:mkdir path:%s"  (mu4e~escape path))
+  (mu4e~call-mu `(mkdir :path ,path)))
+
+
+(defun mu4e~proc-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, don't 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~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~proc-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~call-mu `(ping :queries ,queries)))
+
+(defun mu4e~proc-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~call-mu `(remove :docid ,docid)))
+
+(defun mu4e~proc-sent (path)
+  "Add the message at PATH to the database.
+
+ if this works, we will receive (:info add :path <path> :docid
+<docid> :fcc <path>)."
+  (mu4e~call-mu `(sent :path ,path)))
+
+(defun mu4e~proc-view (docid-or-msgid &optional images decrypt verify)
+  "Get a message DOCID-OR-MSGID.
+Optionally, if IMAGES is non-nil, backend will any images
+attached to the message, and return them as temp files. DECRYPT and VERIFY
+if necessary. The result will be delivered to the function
+registered as `mu4e-view-func'."
+  (mu4e~call-mu `(view
+                  :docid ,(if (stringp docid-or-msgid) nil docid-or-msgid)
+                  :msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil)
+                  :extract-images ,(if images t nil)
+                  :decrypt ,(and decrypt t)
+                  :verify  ,(and verify t))))
+
+(defun mu4e~proc-view-path (path &optional images decrypt)
+  "View message at PATH..
+Optionally, if IMAGES is non-nil, backend will any images
+attached to the message, and return them as temp files. The
+result will be delivered to the function registered as
+`mu4e-view-func'. Optionally DECRYPT and VERIFY."
+  (mu4e~call-mu `(view
+                  :path ,path
+                  :extract-images ,(and images t)
+                  :decrypt        ,(and decrypt t)
+                  :verify         ,(and verify t))))
+
+;;; _
+(provide 'mu4e-proc)
+;;; mu4e-proc.el ends here
diff --git a/mu4e/mu4e-speedbar.el b/mu4e/mu4e-speedbar.el
new file mode 100644 (file)
index 0000000..8ef6d5e
--- /dev/null
@@ -0,0 +1,134 @@
+;;; mu4e-speedbar --- Speedbar support for mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2020 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-utils)
+
+(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
+            (lambda()
+              (when (buffer-live-p speedbar-buffer)
+                (with-current-buffer speedbar-buffer
+                  (let ((inhibit-read-only t))
+                    (mu4e-speedbar-buttons))))))
+  (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))))
+
+;; Make sure our special speedbar major mode is loaded
+(if (featurep 'speedbar)
+    (mu4e-speedbar-install-variables)
+  (add-hook 'speedbar-load-hook '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-headers-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-headers-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-utils.el b/mu4e/mu4e-utils.el
new file mode 100644 (file)
index 0000000..4558250
--- /dev/null
@@ -0,0 +1,1247 @@
+;;; mu4e-utils.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Utility functions used in the mu4e
+
+;;; Code:
+
+(eval-when-compile
+  (require 'org nil 'noerror))
+(require 'cl-lib)
+(require 'cl-seq nil 'noerror)
+(require 'mu4e-vars)
+(require 'mu4e-meta)
+(require 'mu4e-lists)
+(require 'doc-view)
+
+;; keep the byte-compiler happy
+(declare-function mu4e~proc-mkdir     "mu4e-proc")
+(declare-function mu4e~proc-ping      "mu4e-proc")
+(declare-function mu4e~proc-contacts  "mu4e-proc")
+(declare-function mu4e~proc-kill      "mu4e-proc")
+(declare-function mu4e~proc-index     "mu4e-proc")
+(declare-function mu4e~proc-add       "mu4e-proc")
+(declare-function mu4e~proc-mkdir     "mu4e-proc")
+(declare-function mu4e~proc-running-p "mu4e-proc")
+
+(declare-function mu4e~context-autoswitch "mu4e-context")
+(declare-function mu4e-context-determine  "mu4e-context")
+(declare-function mu4e-context-vars       "mu4e-context")
+(declare-function show-all "org")
+
+;; the following is taken from org.el; we copy it here since we don't want to
+;; depend on org-mode directly (it causes byte-compilation errors) TODO: a
+;; cleaner solution....
+(defconst mu4e~ts-regexp0
+  (concat
+   "\\(\\([0-9]\\{4\\}\\)-\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\)"
+   "\\( +[^]+0-9>\r\n -]+\\)?\\( +\\([0-9]\\{1,2\\}\\):"
+   "\\([0-9]\\{2\\}\\)\\)?\\)")
+  "Regular expression matching time strings for analysis.
+This one does not require the space after the date, so it can be
+used on a string that terminates immediately after the date.")
+
+(defun mu4e-parse-time-string (s &optional nodefault)
+  "Parse the standard Org-mode time string.
+This should be a lot faster than the normal `parse-time-string'.
+If time is not given, defaults to 0:00.  However, with optional
+NODEFAULT, hour and minute fields will be nil if not given."
+  (if (string-match mu4e~ts-regexp0 s)
+      (list 0
+            (if (or (match-beginning 8) (not nodefault))
+                (string-to-number (or (match-string 8 s) "0")))
+            (if (or (match-beginning 7) (not nodefault))
+                (string-to-number (or (match-string 7 s) "0")))
+            (string-to-number (match-string 4 s))
+            (string-to-number (match-string 3 s))
+            (string-to-number (match-string 2 s))
+            nil nil nil)
+    (mu4e-error "Not a standard mu4e time string: %s" s)))
+
+;;; Various
+
+(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)))
+
+(defun mu4e-user-mail-address-p (addr)
+  "If ADDR is one of user's e-mail addresses return t, nil otherwise.
+User's addresses are set in `(mu4e-personal-addresses)'.  Case
+insensitive comparison is used."
+  (when (and addr (mu4e-personal-addresses)
+             (cl-find addr (mu4e-personal-addresses)
+                      :test (lambda (s1 s2)
+                              (eq t (compare-strings s1 nil nil s2 nil nil t)))))
+    t))
+
+(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))))
+
+;;; Folders (1/2)
+
+;; 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. See `mu4e-drafts-folder'."
+  (mu4e~get-folder 'mu4e-drafts-folder msg))
+
+(defun mu4e-get-refile-folder (&optional msg)
+  "Get the folder for refiling. See `mu4e-refile-folder'."
+  (mu4e~get-folder 'mu4e-refile-folder msg))
+
+(defun mu4e-get-sent-folder (&optional msg)
+  "Get the sent folder. See `mu4e-sent-folder'."
+  (mu4e~get-folder 'mu4e-sent-folder msg))
+
+(defun mu4e-get-trash-folder (&optional msg)
+  "Get the sent folder. See `mu4e-trash-folder'."
+  (mu4e~get-folder 'mu4e-trash-folder msg))
+
+;;; Self-destructing files
+
+(defun mu4e-remove-file-later (filename)
+  "Remove FILENAME in a few seconds."
+  (run-at-time "30 sec" nil
+               (lambda () (ignore-errors (delete-file filename)))))
+
+(defun mu4e-make-temp-file (ext)
+  "Create a temporary file with extension EXT. The file will
+self-destruct in a few seconds, enough to open it in another
+program."
+  (let ((tmpfile (make-temp-file "mu4e-" nil (concat "." ext))))
+    (mu4e-remove-file-later tmpfile)
+    tmpfile))
+
+;;; Folders (2/2)
+;;
+;; 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' (which can be either a string or a function,
+see its docstring)."
+  (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"))))
+
+;;; Maildir (1/2)
+
+(defun mu4e~guess-maildir (path)
+  "Guess the maildir for some 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 "%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~proc-mkdir dir) t)
+   (t nil)))
+
+;;; 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)
+  "Like `message', but prefixed with mu4e.
+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))))
+
+(defun mu4e-index-message (frm &rest args)
+  "Like `mu4e-message', but specifically for
+index-messages. Doesn't display anything if
+`mu4e-hide-index-messages' is non-nil. "
+  (unless mu4e-hide-index-messages
+    (apply 'mu4e-message frm args)))
+
+(defun mu4e-error (frm &rest args)
+  "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))
+  (error "%s" (apply 'mu4e-format frm args)))
+
+;; the user-error function is only available in emacs-trunk
+(unless (fboundp 'user-error)
+  (defalias 'user-error 'error))
+
+(defun mu4e-warn (frm &rest args)
+  "Create [mu4e]-prefixed warning based on format FRM and ARGS.
+Does a local-exit and does not return. In emacs versions below
+24.2, the functions is the same as `mu4e-error'."
+  (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 mu4e's
+version of `read-char-choice', that 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 will return 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
+           (cl-map 'list (lambda(elm) (string-to-char (car elm))) options)))
+         (chosen
+          (cl-find-if
+           (lambda (option) (eq response (string-to-char (car option))))
+           options)))
+    (if chosen
+        (cdr chosen)
+      (mu4e-warn "Unknown shortcut '%c'" response))))
+
+;;; Maildir (1/2)
+
+(defun mu4e~get-maildirs-1 (path mdir)
+  "Get maildirs under path, recursively, as 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-root-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', recursively, as 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
+               (cl-find-if (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', but check for existence of the maildir,
+and 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~proc-mkdir fullpath)))
+    mdir))
+
+;;; Bookmarks
+ (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 character
+KAR, or raise an error if none is found."
+  (let* ((chosen-bm
+          (or (cl-find-if
+               (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 NAME and
+shortcut-character KEY in the list of `mu4e-bookmarks'. This
+replaces any existing bookmark with KEY."
+  (setq mu4e-bookmarks
+        (cl-remove-if
+         (lambda (bm)
+           (= (plist-get bm :key) key))
+         (mu4e-bookmarks)))
+  (cl-pushnew `(:name  ,name
+                       :query ,query
+                       :key   ,key)
+              mu4e-bookmarks :test 'equal))
+
+\f
+;;; Converting flags->string and vice-versa
+
+(defun mu4e~flags-to-string-raw (flags)
+  "Convert a list of flags into a string as seen in Maildir
+message files; flags are symbols draft, flagged, new, passed,
+replied, seen, trashed and the string is the concatenation of the
+uppercased first letters of these flags, as per [1]. Other flags
+than the ones listed here are ignored.
+Also see `mu4e-flags-to-string'.
+\[1\]: http://cr.yp.to/proto/maildir.html"
+  (when flags
+    (let ((kar (cl-case (car flags)
+                 ('draft     ?D)
+                 ('flagged   ?F)
+                 ('new       ?N)
+                 ('passed    ?P)
+                 ('replied   ?R)
+                 ('seen      ?S)
+                 ('trashed   ?T)
+                 ('attach    ?a)
+                 ('encrypted ?x)
+                 ('signed    ?s)
+                 ('unread    ?u))))
+      (concat (and kar (string kar))
+              (mu4e~flags-to-string-raw (cdr flags))))))
+
+(defun mu4e-flags-to-string (flags)
+  "Remove duplicates and sort the output of `mu4e~flags-to-string-raw'."
+  (concat
+   (sort (cl-remove-duplicates
+          (append (mu4e~flags-to-string-raw flags) nil)) '>)))
+
+(defun mu4e~string-to-flags-1 (str)
+  "Convert a string with message flags as seen in Maildir
+messages into a list of flags in; flags are symbols draft,
+flagged, new, passed, replied, seen, trashed and the string is
+the concatenation of the uppercased first letters of these flags,
+as per [1]. Other letters than the ones listed here are ignored.
+Also see `mu4e-flags-to-string'.
+\[1\]: http://cr.yp.to/proto/maildir.html."
+  (when (/= 0 (length str))
+    (let ((flag
+           (cl-case (string-to-char str)
+             (?D   'draft)
+             (?F   'flagged)
+             (?P   'passed)
+             (?R   'replied)
+             (?S   'seen)
+             (?T   'trashed))))
+      (append (when flag (list flag))
+              (mu4e~string-to-flags-1 (substring str 1))))))
+
+(defun mu4e-string-to-flags (str)
+  "Convert a string with message flags as seen in Maildir messages
+into a list of flags in; flags are symbols draft, flagged, new,
+passed, replied, seen, trashed and the string is the concatenation
+of the uppercased first letters of these flags, as per [1]. Other
+letters than the ones listed here are ignored.  Also see
+`mu4e-flags-to-string'.  \[1\]:
+http://cr.yp.to/proto/maildir.html "
+  ;;  "Remove duplicates from the output of `mu4e~string-to-flags-1'"
+  (cl-remove-duplicates (mu4e~string-to-flags-1 str)))
+
+;;; Various
+
+(defun mu4e-display-size (size)
+  "Get a 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 (propertize "?" 'face 'mu4e-system-face))))
+
+
+(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 (cl-case major-mode
+          ('mu4e-main-mode "(mu4e)Main view")
+          ('mu4e-headers-mode "(mu4e)Headers view")
+          ('mu4e-view-mode "(mu4e)Message view")
+          (t               "mu4e"))))
+
+;;; Misc
+
+(defun mu4e-last-query ()
+  "Get the most recent query or nil if there is none."
+  (when (buffer-live-p (mu4e-get-headers-buffer))
+    (with-current-buffer  (mu4e-get-headers-buffer)
+      mu4e~headers-last-query)))
+
+(defun mu4e-get-view-buffer ()
+  (get-buffer mu4e~view-buffer-name))
+
+(defun mu4e-get-headers-buffer ()
+  (get-buffer mu4e~headers-buffer-name))
+
+(defun mu4e-select-other-view ()
+  "When the headers view is selected, select the message view (if
+that has a live window), and vice versa."
+  (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"))))
+
+
+(defconst mu4e-output-buffer-name "*mu4e-output*"
+  "*internal* Name of the mu4e output buffer.")
+
+(defun mu4e-process-file-through-pipe (path pipecmd)
+  "Process file at PATH through a pipe with PIPECMD."
+  (let ((buf (get-buffer-create mu4e-output-buffer-name)))
+    (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)))
+
+(defvar mu4e~lists-hash nil
+  "Hashtable of mailing-list-id => shortname, based on
+  `mu4e~mailing-lists' and `mu4e-user-mailing-lists'.")
+
+(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)
+        (cl-member-if
+         (lambda (pattern)
+           (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)))
+
+(defvar mu4e-index-updated-hook nil
+  "Hook run when the indexing process had one or more updated messages.
+This can be used as a simple way to invoke some action when new
+messages appear, but note that an update in the index does not
+necessarily mean a new message.")
+
+(defvar mu4e-message-changed-hook nil
+  "Hook run when there is a message changed in db. 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.")
+
+(make-obsolete-variable 'mu4e-msg-changed-hook
+                        'mu4e-message-changed-hook "0.9.19")
+
+(defvar mu4e~contacts-tstamp "0"
+  "Timestamp for the most recent contacts update." )
+
+;;; Some handler functions for server messages
+
+(defun mu4e-info-handler (info)
+  "Handler function for (:info ...) sexps received from the server
+process."
+  (let* ((type (plist-get info :info))
+         (processed (plist-get info :processed))
+         (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... processed %d, updated %d" processed updated)
+        (progn
+          (mu4e-index-message
+           "Indexing completed; processed %d, updated %d, cleaned-up %d"
+           processed updated cleaned-up)
+          ;; call the updated hook if anything changed.
+          (unless (zerop (+ updated cleaned-up))
+            (run-hooks 'mu4e-index-updated-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-error-handler (errcode errmsg)
+  "Handler function for showing an error."
+  ;; don't use mu4e-error here; it's running in the process filter context
+  (cl-case errcode
+    (4 (user-error "No matches for this search query."))
+    (t (error "Error %d: %s" errcode errmsg))))
+
+\f
+;;; Contacts
+
+(defun mu4e~update-contacts (contacts &optional tstamp)
+  "Receive a sorted list of CONTACTS.
+Each of the contacts has the form
+  (FULL_EMAIL_ADDRESS . RANK) and fill the hash
+`mu4e~contacts' with it, with each contact mapped to an integer
+for their ranking.
+
+This is used by the completion function in mu4e-compose."
+  ;; We have our nicely sorted list, map them to a list
+  ;; of increasing integers. We use that map in the composer
+  ;; to sort them there. It would have been so much easier if emacs
+  ;; allowed us to use the sorted-list as-is, but no such luck.
+  (let ((n 0))
+    (unless mu4e~contacts
+      (setq mu4e~contacts (make-hash-table :test 'equal :weakness nil
+                                           :size (length contacts))))
+    (dolist (contact contacts)
+      (cl-incf n)
+      (let ((address
+             (if (functionp mu4e-contact-process-function)
+                 (funcall mu4e-contact-process-function (car contact))
+               (car contact))))
+        (when address ;; note the explicit deccode; the strings we get are  utf-8,
+          ;; but emacs doesn't know yet.
+          (puthash (decode-coding-string address 'utf-8) (cdr contact) mu4e~contacts))))
+
+    (setq mu4e~contacts-tstamp (or tstamp "0"))
+
+    (unless (zerop n)
+      (mu4e-index-message "Contacts updated: %d; total %d"
+                          n (hash-table-count mu4e~contacts)))))
+
+(defun mu4e-contacts-info ()
+  "Display information about the cache used for contacts
+completion; 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
+      (insert (format "number of contacts cached: %d\n\n"
+                      (hash-table-count mu4e~contacts)))
+      (maphash (lambda(key _val)
+                 (insert (format "%S\n" key))) mu4e~contacts)))
+  (pop-to-buffer "*mu4e-contacts-info*"))
+
+(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-props
+    (unless (string= (mu4e-server-version) mu4e-mu-version)
+      (mu4e-error "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))))))
+
+(defun mu4e-running-p ()
+  "Whether mu4e is running.
+Checks whether the server process is live."
+  (mu4e~proc-running-p))
+
+;;; Starting / getting mail / updating the index
+
+(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 to match a password query in the `mu4e-get-mail-command' output.")
+
+(defun mu4e~request-contacts-maybe ()
+  "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
+    (setq mu4e-contacts-func 'mu4e~update-contacts)
+    (mu4e~proc-contacts
+     mu4e-compose-complete-only-personal
+     mu4e-compose-complete-only-after
+     mu4e~contacts-tstamp)))
+
+(defun mu4e~pong-handler (data func)
+  "Handle 'pong' responses from the mu server."
+  (setq mu4e~server-props (plist-get data :props)) ;; save info from the server
+  (let ((doccount (plist-get mu4e~server-props :doccount)))
+    (mu4e~check-requirements)
+    (when func (funcall func))
+    (when (zerop doccount)
+      (mu4e-message "Store is empty; (re)indexing. This may take a while.") ;
+      (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)
+  "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, execute function FUNC (if
+non-nil). Otherwise, check various requireme`'nts, then start mu4e.
+When successful, call 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~proc-ping
+   (mapcar
+    ;; send it a list of queries we'd like to see read/unread info for.
+    (lambda(bm) (plist-get bm :query))
+    (seq-filter (lambda (bm) ;; exclude bookmarks that are not strings,
+                  ;; and with these flags.
+                  (and (stringp (plist-get bm :query))
+                       (not (or (plist-get bm :hide) (plist-get bm :hide-unread)))))
+                (mu4e-bookmarks))))
+  ;; maybe request the list of contacts, automatically refreshed after
+  ;; reindexing
+  (unless mu4e~contacts (mu4e~request-contacts-maybe)))
+
+(defun mu4e-clear-caches ()
+  "Clear any cached resources."
+  (setq
+   mu4e-maildir-list nil
+   mu4e~contacts nil
+   mu4e~contacts-tstamp "0"))
+
+(defun mu4e~stop ()
+  "Stop the mu4e session."
+  (when mu4e~update-timer
+    (cancel-timer mu4e~update-timer)
+    (setq mu4e~update-timer nil))
+  (mu4e-clear-caches)
+  (mu4e~proc-kill)
+  ;; kill all mu4e buffers
+  (mapc
+   (lambda (buf)
+     (with-current-buffer buf
+       (when (member major-mode
+                     '(mu4e-headers-mode mu4e-view-mode mu4e-main-mode))
+         (kill-buffer))))
+   (buffer-list)))
+
+\f
+;;; Indexing & Updating
+
+(defvar mu4e~progress-reporter nil
+  "Internal, the progress reporter object.")
+
+(defun mu4e~get-mail-process-filter (proc msg)
+  "Filter the output of `mu4e-get-mail-command'.
+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-update-index ()
+  "Update the mu4e index."
+  (interactive)
+  (mu4e~proc-index  mu4e-index-cleanup mu4e-index-lazy-check))
+
+(defvar mu4e~update-buffer nil
+  "Internal, store 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 of the
+frame to display buffer 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."
+  (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)
+  "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."
+  (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")
+
+\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*"
+  "*internal* Name of the logging buffer.")
+(defun mu4e-log (type frm &rest args)
+  "Write a message of TYPE with format-string FRM and ARGS in
+*mu4e-log* buffer, if the variable mu4e-debug is non-nil. Type is
+either 'to-server, 'from-server or 'misc. This function is meant for debugging."
+  (when mu4e-debug
+    (with-current-buffer (get-buffer-create mu4e~log-buffer-name)
+      (view-mode)
+      (setq buffer-undo-list t)
+      (let* ((inhibit-read-only t)
+             (tstamp (propertize (format-time-string "%Y-%m-%d %T.%3N"
+                                                     (current-time))
+                                 'face 'font-lock-string-face))
+             (msg-face
+              (cl-case 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)
+                (otherwise   (mu4e-error "Unsupported log type"))))
+             (msg (propertize (apply 'format frm args) 'face msg-face)))
+        (goto-char (point-max))
+        (insert tstamp
+                (cl-case type
+                  (from-server " <- ")
+                  (to-server   " -> ")
+                  (error       " !! ")
+                  (otherwise   " "))
+                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 between enabling/disabling debug-mode (in debug-mode,
+mu4e logs some of its internal workings to a log-buffer. See
+`mu4e-visit-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)
+  (let ((buf (get-buffer mu4e~log-buffer-name)))
+    (unless (buffer-live-p buf)
+      (mu4e-warn "No debug log available"))
+    (switch-to-buffer buf)))
+
+
+(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)))
+
+;;; Misc 2
+
+(defvar mu4e-imagemagick-identify "identify"
+  "Name/path of the Imagemagick 'identify' program.")
+
+(defun mu4e-display-image (imgpath &optional maxwidth maxheight)
+  "Display image IMG at point; optionally specify MAXWIDTH and
+MAXHEIGHT. Function tries to use imagemagick if available (ie.,
+emacs was compiled with inmagemagick support); otherwise MAXWIDTH
+and MAXHEIGHT are ignored."
+  (let* ((have-im (and (fboundp 'imagemagick-types)
+                       (imagemagick-types))) ;; hmm, should check for specific type
+         (identify (and have-im maxwidth
+                        (executable-find mu4e-imagemagick-identify)))
+         (props (and identify (shell-command-to-string
+                               (format "%s -format '%%w' %s"
+                                       identify (shell-quote-argument imgpath)))))
+         (width (and props (string-to-number props)))
+         (img (if have-im
+                  (if (> (or width 0) (or maxwidth 0))
+                      (create-image imgpath 'imagemagick nil :width maxwidth)
+                    (create-image imgpath 'imagemagick))
+                (create-image imgpath))))
+    (when img
+      (save-excursion
+        (insert "\n")
+        (let ((size (image-size img))) ;; inspired by gnus..
+          (insert-char ?\n
+                       (max 0 (round (- (window-height) (or maxheight (cdr size)) 1) 2)))
+          (insert-char ?\.
+                       (max 0 (round (- (window-width)  (or maxwidth (car size))) 2)))
+          (insert-image img))))))
+
+
+(defun mu4e-hide-other-mu4e-buffers ()
+  "Bury mu4e-buffers (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)))
+
+
+(defun mu4e-get-time-date (prompt)
+  "Determine the emacs time value for the time/date entered by user
+  after PROMPT. Formats are all that are accepted by
+  `parse-time-string'."
+  (let ((timestr (read-string (mu4e-format "%s" prompt))))
+    (apply 'encode-time (mu4e-parse-time-string timestr))))
+
+\f
+;;; Mu4e-org-mode
+
+(define-derived-mode mu4e-org-mode org-mode "mu4e:org"
+  "Major mode for mu4e documents, derived from
+  `org-mode'.")
+
+(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 the mu4e 'about' page."
+  (interactive)
+  (mu4e-info (concat mu4e-doc-dir "/NEWS.org")))
+
+;;; Misc 3
+
+(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~proc-add path))
+
+
+(defun mu4e~fontify-cited ()
+  "Colorize message content based on the citation level. This is
+used in the view and compose modes."
+  (save-excursion
+    (goto-char (point-min))
+    (when (search-forward-regexp "^\n" nil t) ;; search the first empty line
+      (while (re-search-forward mu4e-cited-regexp nil t)
+        (let* ((level (string-width (replace-regexp-in-string
+                                     "[^>]" "" (match-string 0))))
+               (face  (unless (zerop level)
+                        (intern-soft (format "mu4e-cited-%d-face" level)))))
+          (when face
+            (add-text-properties (line-beginning-position 1)
+                                 (line-end-position 1) `(face ,face))))))))
+
+(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
+                    (re-search-forward "\\(^-\\{30\\}.*$\\)" nil t) ;; 30 by RFC1153
+                    (point-max))))
+          (add-text-properties p end '(face mu4e-footer-face)))))))
+
+;;; Misc 4
+
+(defun mu4e~quote-for-modeline (str)
+  "Quote a string to be used literally in the modeline. The
+string will be shortened to fit if its length exceeds
+`mu4e-modeline-max-width'."
+  (let* (;; Adjust the max-width to include the length of the " ..." string
+         (width (let ((w (- mu4e-modeline-max-width 4)))
+                  (if (> w 0) w 0)))
+         (str (let* ((l (length str))
+                     ;; If the input str is longer than the max-width, then will shorten
+                     (w (if (> l width) width l))
+                     ;; If the input str is longer than the max-width, then append " ..."
+                     (a (if (> l width) " ..." "")))
+                (concat (substring str 0 w) a))))
+    ;; Escape the % character
+    (replace-regexp-in-string "%" "%%" str t t)))
+
+(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)))
+
+;;; _
+(provide 'mu4e-utils)
+;;; mu4e-utils.el ends here
diff --git a/mu4e/mu4e-vars.el b/mu4e/mu4e-vars.el
new file mode 100644 (file)
index 0000000..4af0468
--- /dev/null
@@ -0,0 +1,1094 @@
+;;; mu4e-vars.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'mu4e-meta)
+(require 'message)
+
+(declare-function mu4e-error "mu4e-utils")
+
+;;; Customization
+
+(defgroup mu4e nil
+  "mu4e - mu for emacs"
+  :group 'mail)
+
+(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."
+  :group 'mu4e
+  :type '(choice (const :tag "Default location" nil)
+                 (directory :tag "Specify location"))
+  :safe 'stringp)
+
+(defcustom mu4e-mu-binary (executable-find "mu")
+  "Name of the mu-binary to use.
+If it cannot be found in your PATH, you can specify the full
+path."
+  :type 'file
+  :group 'mu4e
+  :safe 'stringp)
+
+(make-obsolete-variable 'mu4e-maildir
+                        "determined by server; see `mu4e-root-maildir'." "1.3.8")
+
+(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)
+
+(defcustom mu4e-get-mail-command "true"
+  "Shell command to run to retrieve 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."
+  :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 t, mu only uses
+the directory timestamps to decide whether it needs to check the
+messages beneath it, which would miss messages that are modified
+outside mu. On the other hand, it's significantly faster."
+  :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)
+
+(defcustom mu4e-headers-include-related t
+  "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
+  "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-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)
+
+(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
+  :safe 'stringp)
+
+;; 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")
+
+(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-date-format-long "%c"
+  "Date format to use in the message view.
+Follows the format of `format-time-string'."
+  :type 'string
+  :group 'mu4e)
+
+(defcustom mu4e-modeline-max-width 30
+  "Determines the maximum length of the modeline string.
+If the string exceeds this limit, it will be truncated to fit."
+  :type 'integer
+  :group 'mu4e)
+
+(defvar mu4e-debug nil
+  "When set to non-nil, log debug information to the *mu4e-log* buffer.")
+
+;; for backward compatibility, when a bookmark was defined with defstruct.
+(cl-defun make-mu4e-bookmark (&key name query key)
+  "Create a mu4e proplist with the following elements:
+- `name': the user-visible name of the bookmark
+- `key': a single key to search for this bookmark
+- `query': the query for this bookmark. Either a literal string or a function
+   that evaluates to a string."
+  `(:name ,name :query ,query :key ,key))
+(make-obsolete 'make-mu4e-bookmark "`unneeded; `mu4e-bookmarks'
+are plists" "1.3.7")
+
+(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, 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 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 if
+`:query' is a function.
+
+Queries used to determine the unread/all counts do _not_ apply
+`mu4e-query-rewrite-function'; nor do they discard duplicate or
+unreadable messages (for efficiency). Thus, the numbers shown may
+differ from the number you get from a 'real' query."
+  :type '(repeat (plist))
+  :version "1.3.9"
+  :group 'mu4e)
+
+
+(defun mu4e-bookmarks ()
+  "Get `mu4e-bookmarks' in the (new) format, converting from the
+old format if needed."
+  (cl-map 'list
+          (lambda (item)
+            (if (and (listp item) (= (length item) 3))
+                `(:name  ,(nth 1 item)
+                         :query ,(nth 0 item)
+                         :key   ,(nth 2 item))
+              item))
+          mu4e-bookmarks))
+
+
+(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
+ * 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)
+
+(defcustom mu4e-view-max-specpdl-size 4096
+  "The value of `max-specpdl-size' for displaying messages with Gnus."
+  :type 'integer
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-show-images nil
+  "If non-nil, automatically display images in the view buffer."
+  :type 'boolean
+  :group 'mu4e-view)
+
+(make-obsolete-variable 'mu4e-show-images
+                        'mu4e-view-show-images "0.9.9.x")
+
+(defcustom mu4e-confirm-quit t
+  "Whether to confirm to quit mu4e."
+  :type 'boolean
+  :group 'mu4e)
+
+(defcustom mu4e-cited-regexp
+  "^\\(\\([[:alpha:]]+\\)\\|\\( *\\)\\)\\(\\(>+ ?\\)+\\)"
+  "Regex that determines whether a line is a citation.
+This recognizes lines starting with numbers of '>'
+and spaces as well as citations of the type \"John> ... \"."
+  :type 'string
+  :group 'mu4e)
+
+(defcustom mu4e-completing-read-function 'ido-completing-read
+  "Function to be used to receive user-input during completion.
+This is used to receive the name of the maildir to switch to via
+`mu4e~headers-jump-to-maildir'.
+
+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-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)
+
+;;;; Crypto
+
+(defgroup mu4e-crypto nil
+  "Crypto-related settings."
+  :group 'mu4e)
+
+(make-obsolete-variable 'mu4e-auto-retrieve-keys  "no longer used." "1.3.1")
+
+(defcustom mu4e-decryption-policy t
+  "Policy for dealing with encrypted parts.
+The setting is a symbol:
+ * t:     try to decrypt automatically
+ * `ask': ask before decrypting anything
+ * nil:   don't try to decrypt anything.
+
+Note that this is not used when `mu4e-view-use-gnus' is enabled."
+  :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-crypto)
+
+;;;; Address completion
+;;
+;; We put these options here rather than in mu4e-compose, because
+;; mu4e-utils needs them.
+
+(defgroup mu4e-compose nil
+  "Message-composition related settings."
+  :group 'mu4e)
+
+(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 "2014-01-01"
+  "Consider only contacts last seen after this date.
+
+Date must be a string of the form YYY-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 timses. Set to nil to not have any
+time-based restriction."
+  :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 nil
+  "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)
+
+(defcustom mu4e-compose-reply-recipients 'ask
+  "Which recipients to use when replying to a message.
+May be '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.")
+
+;;;; 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)
+
+
+;;;; Folders
+
+(defgroup mu4e-folders nil
+  "Special folders."
+  :group 'mu4e)
+
+(defcustom mu4e-drafts-folder "/drafts"
+  "Your 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"
+  "Your 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"
+  "Your 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"
+  "Your 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.
+
+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)
+
+
+(defun mu4e-maildir-shortcuts ()
+  "Get `mu4e-maildir-shortcuts' in the (new) format, converting
+from the old format if needed."
+  (cl-map 'list
+          (lambda (item) ;; convert from old format?
+            (if (and (consp item) (not (consp (cdr item))))
+                `(:maildir  ,(car item) :key ,(cdr item))
+              item))
+          mu4e-maildir-shortcuts))
+
+(defcustom mu4e-display-update-status-in-modeline nil
+  "Non-nil value will display the update status in the modeline."
+  :group 'mu4e
+  :type 'boolean)
+
+;;; 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 :bold t))
+  "Face for an unread message header."
+  :group 'mu4e-faces)
+
+(defface mu4e-moved-face
+  '((t :inherit font-lock-comment-face :slant italic))
+  "Face for a message header that has been moved to some folder.
+\(It's still visible in the search results, since we cannot
+be sure it no longer matches)."
+  :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 :bold t))
+  "Face for a flagged message header."
+  :group 'mu4e-faces)
+
+(defface mu4e-replied-face
+  '((t :inherit font-lock-builtin-face :bold nil))
+  "Face for a replied message header."
+  :group 'mu4e-faces)
+
+(defface mu4e-forwarded-face
+  '((t :inherit font-lock-builtin-face :bold nil))
+  "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-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 :bold t))
+  "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 :bold t))
+  "Face for a header title in the headers view."
+  :group 'mu4e-faces)
+
+(defface mu4e-context-face
+  '((t :inherit mu4e-title-face :bold t))
+  "Face for displaying the context in the modeline."
+  :group 'mu4e-faces)
+
+(defface mu4e-modeline-face
+  '((t :inherit font-lock-string-face :bold t))
+  "Face for the query in the mode-line."
+  :group 'mu4e-faces)
+
+(defface mu4e-view-body-face
+  '((t :inherit default))
+  "Face for the body in the message-view."
+  :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 :bold t))
+  "Face for the number tags for URLs."
+  :group 'mu4e-faces)
+
+(defface mu4e-attach-number-face
+  '((t :inherit font-lock-variable-name-face :bold t))
+  "Face for the number tags for attachments."
+  :group 'mu4e-faces)
+
+(defface mu4e-cited-1-face
+  '((t :inherit font-lock-builtin-face :bold nil :italic t))
+  "Face for cited message parts (level 1)."
+  :group 'mu4e-faces)
+
+(defface mu4e-cited-2-face
+  '((t :inherit font-lock-preprocessor-face :bold nil :italic t))
+  "Face for cited message parts (level 2)."
+  :group 'mu4e-faces)
+
+(defface mu4e-cited-3-face
+  '((t :inherit font-lock-variable-name-face :bold nil :italic t))
+  "Face for cited message parts (level 3)."
+  :group 'mu4e-faces)
+
+(defface mu4e-cited-4-face
+  '((t :inherit font-lock-keyword-face :bold nil :italic t))
+  "Face for cited message parts (level 4)."
+  :group 'mu4e-faces)
+
+(defface mu4e-cited-5-face
+  '((t :inherit font-lock-comment-face :bold nil :italic t))
+  "Face for cited message parts (level 5)."
+  :group 'mu4e-faces)
+
+(defface mu4e-cited-6-face
+  '((t :inherit font-lock-comment-delimiter-face :bold nil :italic t))
+  "Face for cited message parts (level 6)."
+  :group 'mu4e-faces)
+
+(defface mu4e-cited-7-face
+  '((t :inherit font-lock-type-face :bold nil :italic t))
+  "Face for cited message parts (level 7)."
+  :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 :bold t :slant normal))
+  "Face for things that are okay."
+  :group 'mu4e-faces)
+
+(defface mu4e-warning-face
+  '((t :inherit font-lock-warning-face :bold t :slant normal))
+  "Face for warnings / error."
+  :group 'mu4e-faces)
+
+(defface mu4e-compose-separator-face
+  '((t :inherit message-separator :slant italic))
+  "Face for the separator between headers / message in
+mu4e-compose-mode."
+  :group 'mu4e-faces)
+
+(defface mu4e-compose-header-face
+  '((t :inherit message-separator :slant italic))
+  "Face for the separator between headers / message 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
+  '((:attachments
+     . (:name "Attachments"
+        :shortname "Atts"
+        :help "Message attachments"
+        :require-full t
+        :sortable nil))
+    (: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))
+    (:date
+     . (:name "Date"
+        :shortname "Date"
+        :help "Date/time when the message was written"
+        :sortable t))
+    (:human-date
+     . (:name "Date"
+        :shortname "Date"
+        :help "Date/time when the message was written."
+        :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))
+    (:signature
+     . (:name "Signature"
+        :shortname "Sgn"
+        :help "Check for the cryptographic signature"
+        :require-full t
+        :sortable nil))
+    (:decryption
+     . (:name "Decryption"
+        :shortname "Dec"
+        :help "Check the cryptographic decryption status"
+        :require-full t
+        :sortable nil))
+    (: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))
+    (:user-agent
+     . (:name "User-Agent"
+        :shortname "UA"
+        :help "Program used for writing this message"
+        :require-full t
+        :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.
+
+Fields which have the property `:require-full' set to
+non-nil require a full message; in practice this means that you
+cannot use such fieds as part of `mu4e-headers-fields', but only
+in `mu4e-view-fields.'
+
+Note, `:sortable' is not supported for custom header fields.")
+
+(defvar mu4e-header-info-custom
+  '( (: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 (see
+`mu4e-view-use-gnus'), 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.")
+
+;;; Run-time variables
+;;;; Headers
+
+(defconst mu4e~headers-buffer-name "*mu4e-headers*"
+  "Name of the buffer for message headers.")
+
+(defvar mu4e~headers-last-query nil
+  "The present (most recent) query.")
+
+;;;; View
+
+(defconst mu4e~view-buffer-name "*mu4e-view*"
+  "Name for the message view buffer.")
+
+(defconst mu4e~view-embedded-buffer-name " *mu4e-embedded-view*"
+  "Name for the embedded message view buffer.")
+
+;;;; Other
+
+(defvar mu4e~contacts nil
+  "Hash that maps contacts (ie. 'name <e-mail>') to an integer for sorting.
+We need to keep this information around to quickly re-sort
+subsets of the contacts in the completions function in
+mu4e-compose.")
+
+(defvar mu4e~server-props nil
+  "Information  we receive from the mu4e server process \(in the 'pong-handler').")
+
+(defun mu4e-root-maildir()
+  "Get the root maildir."
+  (let ((root-maildir (and mu4e~server-props
+                           (plist-get mu4e~server-props :root-maildir))))
+    (unless root-maildir
+      (mu4e-error "root maildir unknown; did you start mu4e?"))
+    root-maildir))
+
+(defun mu4e-database-path()
+  "Get the mu4e database path"
+  (let ((path (and mu4e~server-props
+                   (plist-get mu4e~server-props :database-path))))
+    (unless path
+      (mu4e-error "database-path unknown; did you start mu4e?"))
+    path))
+
+(defun mu4e-personal-addresses()
+  "Get the user's personal addresses, if any."
+  (when mu4e~server-props (plist-get mu4e~server-props :personal-addresses)))
+
+(defun mu4e-server-version()
+  "Get the server version, which should match mu4e's."
+  (let ((version (and mu4e~server-props (plist-get mu4e~server-props :version))))
+    (unless version
+      (mu4e-error "version unknown; did you start mu4e?"))
+    version))
+
+\f
+;;; Handler functions
+;;
+;; The handler functions define what happens when we receive a certain
+;; message from the server.  Here we register our handler functions;
+;; these connect server messages to functions to handle them.
+;;
+;; These bindings form mu4e's central nervous system so it's not
+;; really recommended to override them (they reference various
+;; internal bits, which could change).
+
+(defun mu4e~default-handler (&rest args)
+  "Dummy handler function with arbitrary ARGS."
+  (error "Not handled: %S" args))
+
+(defvar mu4e-error-func 'mu4e-error-handler
+  "Function called for each error received.
+The function is passed an error plist as argument. See
+`mu4e~proc-filter' for the format.")
+
+(defvar mu4e-update-func 'mu4e~headers-update-handler
+  "Function called for each :update sexp returned.
+The function is passed a msg sexp as argument.
+See `mu4e~proc-filter' for the format.")
+
+(defvar mu4e-remove-func  'mu4e~headers-remove-handler
+  "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  'mu4e~default-handler
+  "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  'mu4e~headers-view-handler
+  "Function called for each single-message sexp.
+The function is passed a message sexp as argument. See
+`mu4e~proc-filter' for the format.")
+
+(defvar mu4e-header-func  'mu4e~headers-header-handler
+  "Function called for each message-header received.
+The function is passed a msg plist as argument. See
+`mu4e~proc-filter' for the format.")
+
+(defvar mu4e-found-func  'mu4e~headers-found-handler
+  "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~proc-filter' for the format.")
+
+(defvar mu4e-erase-func 'mu4e~headers-clear
+  "Function called we receive an :erase sexp.
+This before new headers are displayed, to clear the current
+headers buffer. See `mu4e~proc-filter' for the format.")
+
+(defvar mu4e-compose-func 'mu4e~compose-handler
+  "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~proc-filter' for the format of <msg-plist>.")
+
+(defvar mu4e-info-func  'mu4e-info-handler
+  "Function called for each (:info type ....) sexp received.
+from the server process.")
+
+(defvar mu4e-pong-func 'mu4e~default-handler
+  "Function called for each (:pong type ....) sexp received.")
+
+(defvar mu4e-contacts-func 'mu4e-contacts-func
+  "A function called for each (:contacts (<list-of-contacts>)
+sexp received from the server process.")
+
+(defvar mu4e-temp-func 'mu4e~view-temp-handler
+  "A function called for each (:temp <file> <cookie>) sexp.")
+
+;;; _
+(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..a34a1d6
--- /dev/null
@@ -0,0 +1,1856 @@
+;;; mu4e-view.el -- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2020 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  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 'mu4e-utils) ;; utility functions
+(require 'mu4e-vars)
+(require 'mu4e-mark)
+(require 'mu4e-proc)
+(require 'mu4e-compose)
+(require 'mu4e-actions)
+(require 'mu4e-message)
+
+(eval-when-compile (require 'gnus-art))
+(require 'comint)
+(require 'browse-url)
+(require 'button)
+(require 'epa)
+(require 'epg)
+(require 'thingatpt)
+(require 'calendar)
+
+(declare-function mu4e-view-mode "mu4e-view")
+(defvar gnus-icalendar-additional-identities)
+(defvar mu4e~headers-view-win)
+
+;;; Options
+
+(defgroup mu4e-view nil
+  "Settings for the message view."
+  :group 'mu4e)
+
+(defcustom mu4e-view-use-gnus nil
+  "Whether to (experimentally) use Gnus' article view.
+\(instead of mu4e's internal viewer)."
+  :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'."
+  :type (list 'symbol)
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-show-addresses nil
+  "Whether to initially show full e-mail addresses for contacts.
+Otherwise, just show their names."
+  :type 'boolean
+  :group 'mu4e-view)
+
+(make-obsolete-variable 'mu4e-view-wrap-lines nil "0.9.9-dev7")
+(make-obsolete-variable 'mu4e-view-hide-cited nil "0.9.9-dev7")
+
+(defcustom mu4e-view-date-format "%c"
+  "Date format to use in the message view.
+In the format of `format-time-string'."
+  :type 'string
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-image-max-width 800
+  "The maximum width for images to display.
+This is only effective if you're using an Emacs with Imagemagick
+support, and `mu4e-view-show-images' is non-nil."
+  :type 'integer
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-image-max-height 600
+  "The maximum height for images to display.
+This is only effective if you're using an Emacs with Imagemagick
+support, and `mu4e-view-show-images' is non-nil."
+  :type 'integer
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-scroll-to-next t
+  "Move to the next message when calling
+`mu4e-view-scroll-up-or-next' (typically bound to SPC) when at
+the end of a message. Otherwise, don't move to the next message."
+  :type 'boolean
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-auto-mark-as-read t
+  "Automatically mark messages are 'read' when you read
+them. This is typically the expected behavior, but can be turned
+off, for example when using a read-only file-system."
+  :type 'boolean
+  :group 'mu4e-view)
+
+(defcustom mu4e-save-multiple-attachments-without-asking nil
+  "If non-nil, saving multiple attachments asks once for a
+directory and saves all attachments in the chosen directory."
+  :type 'boolean
+  :group 'mu4e-view)
+
+(defcustom mu4e-view-actions
+  '( ("capture message"  . mu4e-action-capture-message)
+     ("view as pdf"      . mu4e-action-view-as-pdf)
+     ("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-attachment-assoc nil
+  "Alist of (EXTENSION . PROGRAM).
+Specify which PROGRAM to use to open attachment with EXTENSION.
+Args EXTENSION and PROGRAM should be specified as strings."
+  :group 'mu4e-view
+  :type '(alist :key-type string :value-type string))
+
+(defcustom mu4e-view-attachment-actions
+  '( ("ssave" . mu4e-view-save-attachment-single)
+     ("Ssave multi" . mu4e-view-save-attachment-multi)
+     ("wopen-with" . mu4e-view-open-attachment-with)
+     ("ein-emacs"  . mu4e-view-open-attachment-emacs)
+     ("dimport-in-diary"  . mu4e-view-import-attachment-diary)
+     ("kimport-public-key" . mu4e-view-import-public-key)
+     ("|pipe"      . mu4e-view-pipe-attachment))
+  "List of actions to perform on message attachments.
+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 two arguments: the message
+  plist and the attachment number.
+The first letter of NAME is used as a shortcut character."
+  :group 'mu4e-view
+  :type '(alist :key-type string :value-type function))
+
+;;; Keymaps
+
+(defvar mu4e-view-header-field-keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map [mouse-1] 'mu4e~view-header-field-fold)
+    (define-key map (kbd "TAB") 'mu4e~view-header-field-fold)
+    map)
+  "Keymap used for header fields.")
+
+(defvar mu4e-view-contacts-header-keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map [mouse-2] 'mu4e~view-compose-contact)
+    (define-key map "C"  'mu4e~view-compose-contact)
+    (define-key map "c"  'mu4e~view-copy-contact)
+    map)
+  "Keymap used for the contacts in the header fields.")
+
+(defvar mu4e-view-clickable-urls-keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map [mouse-1] 'mu4e~view-browse-url-from-binding)
+    (define-key map [?\M-\r] 'mu4e~view-browse-url-from-binding)
+    map)
+  "Keymap used for the urls inside the body.")
+
+(defvar mu4e-view-attachments-header-keymap
+  (let ((map (make-sparse-keymap)))
+    (define-key map [mouse-1] 'mu4e~view-open-attach-from-binding)
+    (define-key map  [?\M-\r] 'mu4e~view-open-attach-from-binding)
+    (define-key map [mouse-2] 'mu4e~view-save-attach-from-binding)
+    (define-key map (kbd "<S-return>") 'mu4e~view-save-attach-from-binding)
+    map)
+  "Keymap used in the \"Attachments\" header field.")
+
+;;; Variables
+
+;; 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-local mu4e~view-message nil
+  "The message being viewed in view mode.")
+(put 'mu4e~view-message 'permanent-local t)
+
+(defvar mu4e-view-fill-headers t
+  "If non-nil, automatically fill the headers when viewing them.")
+
+(defvar mu4e~view-cited-hidden nil "Whether cited lines are hidden.")
+(put 'mu4e~view-cited-hidden 'permanent-local t)
+
+(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~path-parent-docid-map (make-hash-table :test 'equal)
+  "A map of msg paths --> parent-docids.
+This is to determine what is the parent docid for embedded
+message extracted at some path.")
+(put 'mu4e~path-parent-docid-map 'permanent-local t)
+
+(defvar mu4e~view-attach-map nil
+  "A mapping of user-visible attachment number to the actual part index.")
+(put 'mu4e~view-attach-map 'permanent-local t)
+
+(defvar mu4e~view-rendering nil)
+
+(defvar mu4e~view-html-text nil
+  "Should we prefer html or text just this once? A symbol `text'
+or `html' or nil.")
+
+;;; Main
+
+(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-headers-search (concat "msgid:" msgid) nil nil t msgid t))
+
+(define-obsolete-function-alias 'mu4e-view-message-with-msgid
+  'mu4e-view-message-with-message-id "0.9.17")
+
+(defun mu4e~view-custom-field (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-view-message-text (msg)
+  "Return the message to display (as a string), based on the MSG plist."
+  (concat
+   (mapconcat
+    (lambda (field)
+      (let ((fieldval (mu4e-message-field msg field)))
+        (cl-case field
+          (:subject    (mu4e~view-construct-header field fieldval))
+          (:path       (mu4e~view-construct-header field fieldval))
+          (:maildir    (mu4e~view-construct-header field fieldval))
+          (:user-agent (mu4e~view-construct-header field fieldval))
+          ((:flags :tags) (mu4e~view-construct-flags-tags-header
+                           field fieldval))
+
+          ;; contact fields
+          (:to       (mu4e~view-construct-contacts-header msg field))
+          (:from     (mu4e~view-construct-contacts-header msg field))
+          (:cc       (mu4e~view-construct-contacts-header msg field))
+          (:bcc      (mu4e~view-construct-contacts-header msg field))
+
+          ;; if we (`user-mail-address' are the From, show To, otherwise,
+          ;; show From
+          (:from-or-to
+           (let* ((from (mu4e-message-field msg :from))
+                  (from (and from (cdar from))))
+             (if (mu4e-user-mail-address-p from)
+                 (mu4e~view-construct-contacts-header msg :to)
+               (mu4e~view-construct-contacts-header msg :from))))
+          ;; date
+          (:date
+           (let ((datestr
+                  (when fieldval (format-time-string mu4e-view-date-format
+                                                     fieldval))))
+             (if datestr (mu4e~view-construct-header field datestr) "")))
+          ;; size
+          (:size
+           (mu4e~view-construct-header field (mu4e-display-size fieldval)))
+          (:mailing-list
+           (mu4e~view-construct-header field fieldval))
+          (:message-id
+           (mu4e~view-construct-header field fieldval))
+          ;; attachments
+          (:attachments (mu4e~view-construct-attachments-header msg))
+          ;; pgp-signatures
+          (:signature   (mu4e~view-construct-signature-header msg))
+          ;; pgp-decryption
+          (:decryption  (mu4e~view-construct-decryption-header msg))
+          (t (mu4e~view-construct-header field
+                                         (mu4e~view-custom-field msg field))))))
+    mu4e-view-fields "")
+   "\n"
+   (let* ((prefer-html
+           (cond
+            ((eq mu4e~view-html-text 'html) t)
+            ((eq mu4e~view-html-text 'text) nil)
+            (t mu4e-view-prefer-html)))
+          (body (mu4e-message-body-text msg prefer-html)))
+     (setq mu4e~view-html-text nil)
+     (when (fboundp 'add-face-text-property)
+       (add-face-text-property 0 (length body) 'mu4e-view-body-face t body))
+     body)))
+
+(defun mu4e~view-embedded-winbuf ()
+  "Get a buffer (shown in a window) for the embedded message."
+  (let* ((buf (get-buffer-create mu4e~view-embedded-buffer-name))
+         (win (or (get-buffer-window buf) (split-window-vertically))))
+    (select-window win)
+    (switch-to-buffer buf)))
+
+(defun mu4e~delete-all-overlays ()
+  "`delete-all-overlays' with compatibility fallback."
+  (if (functionp 'delete-all-overlays)
+      (delete-all-overlays)
+    (remove-overlays)))
+
+(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.
+
+Depending on the value of `mu4e-view-use-gnus', either use mu4e's
+internal display mode, or a display mode based on Gnus'
+article-mode."
+  (mu4e~view-define-mode)
+
+  ;; XXX(djcb): only called for the side-effect of setting up
+  ;; `mu4e~view-attach-map'. Instead, we should split that function
+  ;; into setting up the map, and actually producing the header.
+  (mu4e~view-construct-attachments-header msg)
+
+  ;; When MSG is unread, mu4e~view-mark-as-read-maybe will trigger
+  ;; another call to mu4e-view (via mu4e~headers-update-handler as
+  ;; the reply handler to mu4e~proc-move)
+  (unless (mu4e~view-mark-as-read-maybe msg)
+    (if mu4e-view-use-gnus
+        (mu4e~view-gnus msg)
+      (mu4e~view-internal msg))))
+
+(defun mu4e~view-internal (msg)
+  "Display MSG using mu4e's internal view mode."
+  (let* ((embedded ;; is it as an embedded msg (ie. message/rfc822 att)?
+          (when (gethash (mu4e-message-field msg :path)
+                         mu4e~path-parent-docid-map) t))
+         (buf (if embedded
+                  (mu4e~view-embedded-winbuf)
+                (get-buffer-create mu4e~view-buffer-name))))
+    (with-current-buffer buf
+      (let ((inhibit-read-only t))
+        (when (or embedded (not (mu4e~view-mark-as-read-maybe msg)))
+          (erase-buffer)
+          (mu4e~delete-all-overlays)
+          (insert (mu4e-view-message-text msg))
+          (goto-char (point-min))
+          (mu4e~fontify-cited)
+          (mu4e~fontify-signature)
+          (mu4e~view-make-urls-clickable)
+          (mu4e~view-show-images-maybe msg)
+          (when (not embedded) (setq mu4e~view-message msg))
+          (mu4e-view-mode)
+          (when embedded (local-set-key "q" 'kill-buffer-and-window))))
+      (switch-to-buffer buf))))
+
+;; 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-gnus (msg)
+  "View MSG using Gnus' article mode. Experimental."
+  (require 'gnus-art)
+  (let ((marked-read (mu4e~view-mark-as-read-maybe msg))
+        (path (mu4e-message-field msg :path))
+        (inhibit-read-only t)
+        (mm-decrypt-option 'known)
+        (gnus-article-emulate-mime t)
+        (gnus-buttonized-mime-types (append (list "multipart/signed"
+                                                  "multipart/encrypted")
+                                            gnus-buttonized-mime-types)))
+    (switch-to-buffer (get-buffer-create mu4e~view-buffer-name))
+    (erase-buffer)
+    (unless marked-read
+      ;; when we're being marked as read, no need to start rendering
+      ;; the messages; just the minimal so (update... ) can find us.
+      (insert-file-contents-literally path)
+      (unless (message-fetch-field "Content-Type" t)
+        ;; For example, for messages in `mu4e-drafts-folder'
+        (let ((coding (or (default-value 'buffer-file-coding-system)
+                          'prefer-utf-8)))
+          (recode-region (point-min) (point-max) coding 'no-conversion)))
+      (setq
+       gnus-summary-buffer (get-buffer-create " *appease-gnus*")
+       gnus-original-article-buffer (current-buffer))
+      (run-hooks 'gnus-article-decode-hook)
+      (let ((mu4e~view-rendering t) ; customize gnus in mu4e
+            (max-specpdl-size mu4e-view-max-specpdl-size)
+            (gnus-blocked-images ".") ;; don't load external images.
+            ;; Possibly add headers (before "Attachments")
+            (gnus-display-mime-function (mu4e~view-gnus-display-mime msg))
+            (gnus-icalendar-additional-identities (mu4e-personal-addresses)))
+        (gnus-article-prepare-display))
+      (setq mu4e~gnus-article-mime-handles gnus-article-mime-handles)
+      (setq mu4e~view-message msg)
+      (mu4e-view-mode)
+      (setq gnus-article-decoded-p gnus-article-decode-hook)
+      (set-buffer-modified-p nil)
+      (add-hook 'kill-buffer-hook
+                (lambda() ;; cleanup the mm-* buffers that the view spawns
+                  (when mu4e~gnus-article-mime-handles
+                    (mm-destroy-parts mu4e~gnus-article-mime-handles)
+                    (setq mu4e~gnus-article-mime-handles nil))))
+      (read-only-mode))))
+
+(defun mu4e~view-gnus-display-mime (msg)
+  "Same as `gnus-display-mime' but add a 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)))
+            (cl-case field
+              ((:path :maildir :user-agent :mailing-list :message-id)
+               (mu4e~view-gnus-insert-header field fieldval))
+              ((: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)))
+              ((:subject :to :from :cc :bcc :from-or-to :date :attachments
+                         :signature :decryption)) ; handled by Gnus
+              (t
+               (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 in Gnus article view."
+  (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 the custom FIELD in Gnus article view."
+  (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)
+  "Do not trigger an error when displaying an ical attachment
+with no 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-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)))
+
+(defun mu4e~view-construct-header (field val &optional dont-propertize-val)
+  "Return header field FIELD (as in `mu4e-header-info') with value
+VAL if VAL is non-nil. If DONT-PROPERTIZE-VAL is non-nil, do not
+add text-properties to VAL."
+  (let* ((info (cdr (assoc field
+                           (append mu4e-header-info mu4e-header-info-custom))))
+         (key (plist-get info :name))
+         (val (if val (propertize val 'field 'mu4e-header-field-value
+                                  'front-sticky '(field))))
+         (help (plist-get info :help)))
+    (if (and val (> (length val) 0))
+        (with-temp-buffer
+          (insert (propertize (concat key ":")
+                              'field 'mu4e-header-field-key
+                              'front-sticky '(field)
+                              'keymap mu4e-view-header-field-keymap
+                              'face 'mu4e-header-key-face
+                              'help-echo help) " "
+                              (if dont-propertize-val
+                                  val
+                                (propertize val 'face 'mu4e-header-value-face)) "\n")
+          (when mu4e-view-fill-headers
+            ;; temporarily set the fill column <margin> positions to the right, so
+            ;; we can indent the following lines correctly
+            (let* ((margin 1)
+                   (fill-column (max (- fill-column margin) 0)))
+              (fill-region (point-min) (point-max))
+              (goto-char (point-min))
+              (while (and (zerop (forward-line 1)) (not (looking-at "^$")))
+                (indent-to-column margin))))
+          (buffer-string))
+      "")))
+
+(defun mu4e~view-header-field-fold ()
+  "Fold/unfold headers' value if there are more than one line."
+  (interactive)
+  (let ((name-pos (field-beginning))
+        (value-pos (1+ (field-end))))
+    (if (and name-pos value-pos
+             (eq (get-text-property name-pos 'field) 'mu4e-header-field-key))
+        (save-excursion
+          (let* ((folded))
+            (mapc (lambda (o)
+                    (when (overlay-get o 'mu4e~view-header-field-folded)
+                      (delete-overlay o)
+                      (setq folded t)))
+                  (overlays-at value-pos))
+            (unless folded
+              (let* ((o (make-overlay value-pos (field-end value-pos)))
+                     (vals (split-string (field-string value-pos) "\n" t))
+                     (val (if (= (length vals) 1)
+                              (car vals)
+                            (concat (substring (car vals) 0 -3) "..."))))
+                (overlay-put o 'mu4e~view-header-field-folded t)
+                (overlay-put o 'display val))))))))
+
+(defun mu4e~view-compose-contact (&optional point)
+  "Compose a message for the address at point."
+  (interactive)
+  (unless (get-text-property (or point (point)) 'email)
+    (mu4e-error "No address at point"))
+  (mu4e~compose-mail (get-text-property (or point (point)) 'long)))
+
+(defun mu4e~view-copy-contact (&optional full)
+  "Compose a message for the address at (point)."
+  (interactive "P")
+  (let ((email (get-text-property (point) 'email))
+        (long (get-text-property (point) 'long)))
+    (unless email (mu4e-error "No address at point"))
+    (kill-new (if full long email))
+    (mu4e-message "Address copied.")))
+
+(defun mu4e~view-construct-contacts-header (msg field)
+  "Add a header for a contact field (ie., :to, :from, :cc, :bcc)."
+  (mu4e~view-construct-header field
+                              (mapconcat
+                               (lambda(c)
+                                 (let* ((name (when (car c)
+                                                (replace-regexp-in-string "[[:cntrl:]]" "" (car c))))
+                                        (email (when (cdr c)
+                                                 (replace-regexp-in-string "[[:cntrl:]]" "" (cdr c))))
+                                        (short (or name email)) ;; name may be nil
+                                        (long (if name (format "%s <%s>" name email) email)))
+                                   (propertize
+                                    (if mu4e-view-show-addresses long short)
+                                    'long long
+                                    'short short
+                                    'email email
+                                    'keymap mu4e-view-contacts-header-keymap
+                                    'face 'mu4e-contact-face
+                                    'mouse-face 'highlight
+                                    'help-echo (format "<%s>\n%s" email
+                                                       "[mouse-2] or C to compose a mail for this recipient"))))
+                               (mu4e-message-field msg field) ", ") t))
+
+(defun mu4e~view-construct-flags-tags-header (field val)
+  "Construct a Flags: header."
+  (mu4e~view-construct-header
+   field
+   (mapconcat
+    (lambda (flag)
+      (propertize
+       (if (symbolp flag)
+           (symbol-name flag)
+         flag)
+       'face 'mu4e-special-header-value-face))
+    val
+    (propertize ", " 'face 'mu4e-header-value-face)) t))
+
+(defun mu4e~view-construct-signature-header (msg)
+  "Construct a Signature: header, if there are any signed parts."
+  (let* ((parts (mu4e-message-field msg :parts))
+         (verdicts
+          (cl-remove-if 'null
+                        (mapcar (lambda (part) (mu4e-message-part-field part :signature))
+                                parts)))
+         (signers
+          (mapconcat 'identity
+                     (cl-remove-if 'null
+                                   (mapcar (lambda (part) (mu4e-message-part-field part :signers))
+                                           parts)) ", "))
+         (val (when verdicts
+                (mapconcat
+                 (lambda (v)
+                   (propertize (symbol-name v)
+                               'face (if (eq v 'verified)
+                                         'mu4e-ok-face 'mu4e-warning-face)))
+                 verdicts ", ")))
+         (btn (when val
+                (with-temp-buffer
+                  (insert-text-button "Details"
+                                      'action (lambda (b)
+                                                (mu4e-view-verify-msg-popup
+                                                 (button-get b 'msg))))
+                  (buffer-string))))
+         (val (when val (concat val " " signers " (" btn ")"))))
+    (mu4e~view-construct-header :signature val t)))
+
+(defun mu4e~view-construct-decryption-header (msg)
+  "Construct a Decryption: header, if there are any encrypted parts."
+  (let* ((parts (mu4e-message-field msg :parts))
+         (verdicts
+          (cl-remove-if 'null
+                        (mapcar (lambda (part)
+                                  (mu4e-message-part-field part :decryption))
+                                parts)))
+         (succeeded (cl-remove-if (lambda (v) (eq v 'failed)) verdicts))
+         (failed (cl-remove-if (lambda (v) (eq v 'succeeded)) verdicts))
+         (succ (when succeeded
+                 (propertize
+                  (concat (number-to-string (length succeeded))
+                          " part(s) decrypted")
+                  'face 'mu4e-ok-face)))
+         (fail (when failed
+                 (propertize
+                  (concat (number-to-string (length failed))
+                          " part(s) failed")
+                  'face 'mu4e-warning-face)))
+         (val (concat succ fail)))
+    (mu4e~view-construct-header :decryption val t)))
+
+(defun mu4e~view-open-attach-from-binding ()
+  "Open the attachment at point, or click location."
+  (interactive)
+  (let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg))
+         ( attnum (mu4e~view-get-property-from-event 'mu4e-attnum)))
+    (when (and msg attnum)
+      (mu4e-view-open-attachment msg attnum))))
+
+(defun mu4e~view-save-attach-from-binding ()
+  "Save the attachment at point, or click location."
+  (interactive)
+  (let* (( msg (mu4e~view-get-property-from-event 'mu4e-msg))
+         ( attnum (mu4e~view-get-property-from-event 'mu4e-attnum)))
+    (when (and msg attnum)
+      (mu4e-view-save-attachment-single msg attnum))))
+
+(defun mu4e~view-construct-attachments-header (msg)
+  "Display attachment information; the field looks like something like:
+        :parts ((:index 1 :name \"1.part\" :mime-type \"text/plain\"
+                 :type (leaf) :attachment nil :size 228)
+                (:index 2 :name \"analysis.doc\"
+                 :mime-type \"application/msword\"
+                 :type (leaf attachment) :attachment nil :size 605196))"
+  (setq mu4e~view-attach-map ;; buffer local
+        (make-hash-table :size 64 :weakness nil))
+  (let* ((id 0)
+         (partcount (length (mu4e-message-field msg :parts)))
+         (attachments
+          ;; we only list parts that look like attachments, ie. that have a
+          ;; non-nil :attachment property; we record a mapping between
+          ;; user-visible numbers and the part indices
+          (cl-remove-if-not
+           (lambda (part)
+             (let* ((mtype (or (mu4e-message-part-field part :mime-type)
+                               "application/octet-stream"))
+                    (partsize (or (mu4e-message-part-field part :size) 0))
+                    (attachtype (mu4e-message-part-field part :type))
+                    (isattach
+                     (or ;; we consider parts marked either
+                      ;; "attachment" or "inline" as attachment.
+                      (member 'attachment attachtype)
+                      ;; list inline parts as attachment (so they can be
+                      ;; saved), unless they are text/plain, which are
+                      ;; usually just message footers in mailing lists
+                      ;;
+                      ;; however, slow bigger text parts as attachments,
+                      ;; except when they're the only part... it's
+                      ;; complicated.
+                      (and (member 'inline attachtype)
+                           (or
+                            (and (> partcount 1) (> partsize 256))
+                            (not (string-match "^text/plain" mtype)))))))
+               (or ;; remove if it's not an attach *or* if it's an
+                ;; image/audio/application type (but not a signature)
+                isattach
+                (string-match "^\\(image\\|audio\\)" mtype)
+                (string= "message/rfc822" mtype)
+                (string= "text/calendar" mtype)
+                (and (string-match "^application" mtype)
+                     (not (string-match "signature" mtype))))))
+           (mu4e-message-field msg :parts)))
+         (attstr
+          (mapconcat
+           (lambda (part)
+             (let ((index (mu4e-message-part-field part :index))
+                   (name (mu4e-message-part-field part :name))
+                   (size (mu4e-message-part-field part :size)))
+               (cl-incf id)
+               (puthash id index mu4e~view-attach-map)
+
+               (concat
+                (propertize (format "[%d]" id)
+                            'face 'mu4e-attach-number-face)
+                (propertize name 'face 'mu4e-link-face
+                            'keymap mu4e-view-attachments-header-keymap
+                            'mouse-face 'highlight
+                            'help-echo (concat
+                                        "[mouse-1] or [M-RET] opens the attachment\n"
+                                        "[mouse-2] or [S-RET] offers to save it")
+                            'mu4e-msg msg
+                            'mu4e-attnum id
+                            )
+                (when (and size (> size 0))
+                  (propertize (format "(%s)" (mu4e-display-size size))
+                              'face 'mu4e-header-key-face)))))
+           attachments ", ")))
+    (when attachments
+      (mu4e~view-construct-header :attachments attstr t))))
+
+(defun mu4e-view-for-each-part (msg func)
+  "Apply FUNC to each part in MSG.
+FUNC should be a function taking two arguments:
+ 1. the message MSG, and
+ 2. a plist describing the attachment. The plist looks like:
+         (:index 1 :name \"test123.doc\"
+          :mime-type \"application/msword\" :attachment t :size 1234)."
+  (dolist (part (mu4e-msg-field msg :parts))
+    (funcall func msg part)))
+
+(defmacro mu4e~native-def (def)
+  "Definition DEF only available in 'native' mode."
+  `(lambda(prefix-argument) (interactive "P")
+     (if mu4e-view-use-gnus
+         (mu4e-warn "binding not supported with the gnus-based view")
+       (if prefix-argument (,def prefix-argument) (,def)))))
+
+(defvar mu4e-view-mode-map nil
+  "Keymap for \"*mu4e-view*\" buffers.")
+(unless mu4e-view-mode-map
+  (setq mu4e-view-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 "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 "s" 'mu4e-headers-search)
+          (define-key map "S" 'mu4e-view-search-edit)
+          (define-key map "/" 'mu4e-view-search-narrow)
+
+          (define-key map (kbd "<M-left>")  'mu4e-headers-query-prev)
+          (define-key map (kbd "<M-right>") 'mu4e-headers-query-next)
+
+          (define-key map "b" 'mu4e-headers-search-bookmark)
+          (define-key map "B" 'mu4e-headers-search-bookmark-edit)
+
+          (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 "v" 'mu4e-view-verify-msg-popup)
+
+          (define-key map "j" 'mu4e~headers-jump-to-maildir)
+
+          (define-key map "g" (mu4e~native-def mu4e-view-go-to-url))
+          (define-key map "k" (mu4e~native-def mu4e-view-save-url))
+          (define-key map "f" (mu4e~native-def 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)
+
+          ;; some gnus things we do not support
+          (define-key map "G" 'ignore)
+          (define-key map "I" 'ignore)
+          (define-key map "J" 'ignore)
+          (define-key map "K" 'ignore)
+          (define-key map "L" 'ignore)
+          (define-key map "N" 'ignore)
+          (define-key map "V" 'ignore)
+          (define-key map "X" 'ignore)
+          (define-key map "Y" 'ignore)
+          (define-key map "Z" 'ignore)
+
+          (define-key map "." 'mu4e-view-raw-message)
+          (define-key map "|" 'mu4e-view-pipe)
+          (define-key map "a" 'mu4e-view-action)
+
+          (define-key map ";" 'mu4e-context-switch)
+
+          ;; 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 "SPC") 'mu4e-view-scroll-up-or-next)
+          (define-key map (kbd "<home>") 'beginning-of-buffer)
+          (define-key map (kbd "<end>") 'end-of-buffer)
+          (define-key map (kbd "RET")
+            (lambda()
+              (interactive)
+              (if (and mu4e-view-use-gnus
+                       (eq (get-text-property (point) 'gnus-callback) 'gnus-button-push))
+                  (widget-button-press (point))
+                (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 to view mode (if it's visible)
+          (define-key map "y" 'mu4e-select-other-view)
+
+          ;; attachments
+          (define-key map "e" (mu4e~native-def mu4e-view-save-attachment))
+          (define-key map "o" (mu4e~native-def mu4e-view-open-attachment))
+          (define-key map "A" (mu4e~native-def mu4e-view-attachment-action))
+
+          ;; 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 "w" 'visual-line-mode)
+          (define-key map "#" (mu4e~native-def mu4e-view-toggle-hide-cited))
+          (define-key map "h" (mu4e~native-def mu4e-view-toggle-html))
+          (define-key map (kbd "M-q")
+            (lambda()
+              (interactive)
+              (if mu4e-view-use-gnus
+                  (article-fill-long-lines)
+                (mu4e-view-fill-long-lines))))
+
+          ;; 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))
+            (unless mu4e-view-use-gnus
+              (define-key menumap [toggle-html]
+                '("Toggle view-html" . mu4e-view-toggle-html)))
+            (define-key menumap [raw-view]
+              '("View raw message" . mu4e-view-raw-message))
+            (define-key menumap [pipe]
+              '("Pipe through shell" . mu4e-view-pipe))
+
+            (unless mu4e-view-use-gnus
+              (define-key menumap [sepa8] '("--"))
+              (define-key menumap [open-att]
+                '("Open attachment" . mu4e-view-open-attachment))
+              (define-key menumap [extract-att]
+                '("Extract attachment" . mu4e-view-save-attachment))
+              (define-key menumap [save-url]
+                '("Save URL to kill-ring" . mu4e-view-save-url))
+              (define-key menumap [fetch-url]
+                '("Fetch URL" . mu4e-view-fetch-url))
+              (define-key menumap [goto-url]
+                '("Visit URL" . mu4e-view-go-to-url)))
+
+            (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)))
+          map))
+
+  (fset 'mu4e-view-mode-map mu4e-view-mode-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)
+
+(defvar mu4e-view-mode-abbrev-table nil)
+
+(defun mu4e~view-mode-body ()
+  "Body of the mode-function."
+  (use-local-map mu4e-view-mode-map)
+  (mu4e-context-in-modeline)
+  (setq buffer-undo-list t);; don't record undo info
+  ;; autopair mode gives error when pressing RET
+  ;; turn it off
+  (when (boundp 'autopair-dont-activate)
+    (setq autopair-dont-activate t)))
+
+(defun mu4e~view-define-mode ()
+  "Define the major-mode for the mu4e-view."
+  (if mu4e-view-use-gnus
+      (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."
+        ;; remove some gnus stuff that does not apply
+        (define-key mu4e-view-mode-map [menu-bar Treatment] nil)
+        (define-key mu4e-view-mode-map [menu-bar Article] nil)
+        (define-key mu4e-view-mode-map [menu-bar post] nil)
+        (define-key mu4e-view-mode-map [menu-bar commands] nil)
+        (setq mu4e~view-buffer-name gnus-article-buffer)
+        (mu4e~view-mode-body))
+    (define-derived-mode mu4e-view-mode special-mode "mu4e:view"
+      "Major mode for viewing an e-mail message in mu4e."
+      (mu4e~view-mode-body))))
+
+(defun mu4e~view-mark-as-read-maybe (msg)
+  "Clear the message MSG New/Unread status and set it to Seen.
+If the message is not New/Unread, do nothing. Evaluates to t if it
+triggers any changes, nil otherwise. If this function does any
+changes, it triggers a refresh."
+  (when (and mu4e-view-auto-mark-as-read msg)
+    (let ((flags (mu4e-message-field msg :flags))
+          (msgid (mu4e-message-field msg :message-id))
+          (docid (mu4e-message-field msg :docid)))
+      ;; attached (embedded) messages don't have docids; leave them alone if it
+      ;; is a new message
+      (when (and docid (or (member 'unread flags) (member 'new flags)))
+        ;; mark /all/ messages with this message-id as read, so all copies of
+        ;; this message will be marked as read. We don't want an update though,
+        ;; we want a full message, so images etc. work correctly.
+        (mu4e~proc-move msgid nil "+S-u-N" 'noview)
+        (mu4e~proc-view docid mu4e-view-show-images
+                        (mu4e~decrypt-p msg) (not mu4e-view-use-gnus))
+        t))))
+
+(defun mu4e~view-browse-url-func (url)
+  "Return a function that executes `browse-url' with URL.
+The browser that is called depends on
+`browse-url-browser-function' and `browse-url-mailto-function'."
+  (save-match-data
+    (if (string-match "^mailto:" url)
+        (lambda ()
+          (interactive)
+          (browse-url-mail url))
+      (lambda ()
+        (interactive)
+        (browse-url url)))))
+
+(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-show-images-maybe (msg)
+  "Show attached images, if `mu4e-show-images' is non-nil."
+  (when (and (display-images-p) mu4e-view-show-images)
+    (mu4e-view-for-each-part msg
+                             (lambda (_msg part)
+                               (when (string-match "^image/"
+                                                   (or (mu4e-message-part-field part :mime-type)
+                                                       "application/object-stream"))
+                                 (let ((imgfile (mu4e-message-part-field part :temp)))
+                                   (when (and imgfile (file-exists-p imgfile))
+                                     (save-excursion
+                                       (goto-char (point-max))
+                                       (mu4e-display-image imgfile
+                                                           mu4e-view-image-max-width
+                                                           mu4e-view-image-max-height)))))))))
+
+(defvar mu4e~view-beginning-of-url-regexp
+  "https?\\://\\|mailto:"
+  "Regexp that matches the beginning of http:/https:/mailto:
+URLs; match-string 1 will contain the matched URL, if any.")
+
+;; this is fairly simplistic...
+(defun mu4e~view-make-urls-clickable ()
+  "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-clickable-urls-keymap
+                      help-echo
+                      "[mouse-1] or [M-RET] to open the link"))
+              (overlay-put ov 'after-string
+                           (propertize (format "\u200B[%d]" num)
+                                       'face 'mu4e-url-number-face)))))))))
+
+\f
+(defun mu4e~view-hide-cited ()
+  "Toggle hiding of cited lines in the message body."
+  (save-excursion
+    (let ((inhibit-read-only t))
+      (goto-char (point-min))
+      (flush-lines mu4e-cited-regexp)
+      (setq mu4e~view-cited-hidden t))))
+
+(defmacro mu4e~view-in-headers-context (&rest body)
+  "Evaluate BODY in the context of the headers buffer connected to
+this view."
+  `(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 in the headers buffer
+connected with this message view. 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 in the headers buffer
+connected with this message view. 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 (when 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 in the headers
+buffer connected with this message view. 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 in the headers
+buffer connected with this message view. If this succeeds, return
+the new docid. Otherwise, return nil."
+  (interactive)
+  (mu4e~view-prev-or-next-unread nil))
+
+\f
+;;; Interactive functions
+
+(defun mu4e-view-toggle-hide-cited ()
+  "Toggle hiding of cited lines in the message body."
+  (interactive)
+  (if mu4e~view-cited-hidden
+      (mu4e-view-refresh)
+    (mu4e~view-hide-cited)))
+
+(defun mu4e-view-toggle-html ()
+  "Toggle html-display of the message body (if any)."
+  (interactive)
+  (setq mu4e~view-html-text
+        (if mu4e~message-body-html 'text 'html))
+  (mu4e-view-refresh))
+
+(defun mu4e-view-refresh ()
+  "Redisplay the current message."
+  (interactive)
+  (mu4e-view mu4e~view-message)
+  (setq mu4e~view-cited-hidden nil))
+
+(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 ()
+  "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)
+  "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)
+  "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
+   (call-interactively 'mu4e-headers-search-narrow)))
+
+(defun mu4e-view-search-edit ()
+  "Run `mu4e-headers-search-edit' in the headers buffer."
+  (interactive)
+  (mu4e~view-in-headers-context (mu4e-headers-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)
+        (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 'face 'mu4e-region-code))
+          (setq beg nil end nil))))))
+
+;;; Wash functions
+
+(defun mu4e-view-fill-long-lines ()
+  "Fill lines that are wider than the window width or `fill-column'."
+  (interactive)
+  (with-current-buffer (mu4e-get-view-buffer)
+    (save-excursion
+      (let ((inhibit-read-only t)
+            (width (window-width (get-buffer-window (current-buffer)))))
+        (save-restriction
+          (message-goto-body)
+          (while (not (eobp))
+            (end-of-line)
+            (when (>= (current-column) (min fill-column width))
+              (narrow-to-region (min (1+ (point)) (point-max))
+                                (point-at-bol))
+              (let ((goback (point-marker)))
+                (fill-paragraph nil)
+                (goto-char (marker-position goback)))
+              (widen))
+            (forward-line 1)))))))
+
+;;; Attachment handling
+
+(defun mu4e~view-get-attach-num (prompt _msg &optional multi)
+  "Ask the user with PROMPT for an attachment number for MSG, and
+ensure it is valid. The number is [1..n] for attachments
+\[0..(n-1)] in the message. If MULTI is nil, return the number for
+the attachment; otherwise (MULTI is non-nil), accept ranges of
+attachment numbers, as per `mu4e-split-ranges-to-numbers', and
+return the corresponding string."
+  (let* ((count (hash-table-count mu4e~view-attach-map)) (def))
+    (when (zerop count) (mu4e-warn "No attachments 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-get-attach (msg attnum)
+  "Return the attachment plist in MSG corresponding to attachment
+number ATTNUM."
+  (let* ((partid (gethash attnum mu4e~view-attach-map))
+         (attach
+          (cl-find-if
+           (lambda (part)
+             (eq (mu4e-message-part-field part :index) partid))
+           (mu4e-message-field msg :parts))))
+    (or attach (mu4e-error "Not a valid attachment"))))
+
+(defun mu4e~view-request-attachment-path (fname path)
+  "Ask the user where to save FNAME (default is PATH/FNAME)."
+  (let ((fpath (expand-file-name
+                (read-file-name
+                 (mu4e-format "Save as ")
+                 path nil nil fname) path)))
+    (if (file-directory-p fpath)
+        (expand-file-name fname fpath)
+      fpath)))
+
+(defun mu4e~view-request-attachments-dir (path)
+  "Ask the user where to save multiple attachments (default is PATH)."
+  (let ((fpath (expand-file-name
+                (read-directory-name
+                 (mu4e-format "Save in directory ")
+                 path nil nil nil) path)))
+    (if (file-directory-p fpath)
+        fpath)))
+
+(defun mu4e-view-save-attachment-single (&optional msg attnum)
+  "Save attachment number ATTNUM from MSG.
+If MSG is nil use the message returned by `message-at-point'.
+If ATTNUM is nil ask for the attachment number."
+  (interactive)
+  (let* ((msg (or msg (mu4e-message-at-point)))
+         (attnum (or attnum
+                     (mu4e~view-get-attach-num "Attachment to save" msg)))
+         (att (mu4e~view-get-attach msg attnum))
+         (fname  (plist-get att :name))
+         (mtype  (plist-get att :mime-type))
+         (path (concat
+                (mu4e~get-attachment-dir fname mtype) "/"))
+         (index (plist-get att :index))
+         (retry t) (fpath))
+    (while retry
+      (setq fpath (mu4e~view-request-attachment-path fname path))
+      (setq retry
+            (and (file-exists-p fpath)
+                 (not (y-or-n-p (mu4e-format "Overwrite '%s'?" fpath))))))
+    (mu4e~proc-extract
+     'save (mu4e-message-field msg :docid)
+     index mu4e-decryption-policy fpath)))
+
+(defun mu4e-view-save-attachment-multi (&optional msg)
+  "Offer to save multiple email attachments from the current message.
+Default is to save all messages, [1..n], where n is the number of
+attachments.  You can type multiple values separated by space, e.g.
+  1 3-6 8
+will save attachments 1,3,4,5,6 and 8.
+
+Furthermore, there is a shortcut \"a\" which so means all
+attachments, but as this is the default, you may not need it."
+  (interactive)
+  (let* ((msg (or msg (mu4e-message-at-point)))
+         (attachstr (mu4e~view-get-attach-num
+                     "Attachment number range (or 'a' for 'all')" msg t))
+         (count (hash-table-count mu4e~view-attach-map))
+         (attachnums (mu4e-split-ranges-to-numbers attachstr count)))
+    (if mu4e-save-multiple-attachments-without-asking
+        (let* ((path (concat (mu4e~get-attachment-dir) "/"))
+               (attachdir (mu4e~view-request-attachments-dir path)))
+          (dolist (num attachnums)
+            (let* ((att (mu4e~view-get-attach msg num))
+                   (fname  (plist-get att :name))
+                   (index (plist-get att :index))
+                   (retry t)
+                   fpath)
+              (while retry
+                (setq fpath (expand-file-name (concat attachdir fname) path))
+                (setq retry
+                      (and (file-exists-p fpath)
+                           (not (y-or-n-p
+                                 (mu4e-format "Overwrite '%s'?" fpath))))))
+              (mu4e~proc-extract
+               'save (mu4e-message-field msg :docid)
+               index mu4e-decryption-policy fpath))))
+      (dolist (num attachnums)
+        (mu4e-view-save-attachment-single msg num)))))
+
+(defalias #'mu4e-view-save-attachment #'mu4e-view-save-attachment-multi)
+
+(defun mu4e-view-open-attachment (&optional msg attnum)
+  "Open attachment number ATTNUM from MSG.
+If MSG is nil use the message returned by `message-at-point'.  If
+ATTNUM is nil ask for the attachment number."
+  (interactive)
+  (let* ((msg (or msg (mu4e-message-at-point)))
+         (attnum (or attnum
+                     (progn
+                       (unless mu4e~view-attach-map
+                         (mu4e~view-construct-attachments-header msg))
+                       (mu4e~view-get-attach-num "Attachment to open" msg))))
+         (att (or (mu4e~view-get-attach msg attnum)))
+         (index (plist-get att :index))
+         (docid (mu4e-message-field msg :docid))
+         (mimetype (plist-get att :mime-type)))
+    (if (and mimetype (string= mimetype "message/rfc822"))
+        ;; special handling for message-attachments; we open them in mu4e. we also
+        ;; send the docid as parameter (4th arg); we'll get this back from the
+        ;; server, and use it to determine the parent message (ie., the current
+        ;; message) when showing the embedded message/rfc822, and return to the
+        ;; current message when quitting that one.
+        (mu4e~view-temp-action docid index 'mu4e (format "%s" docid))
+      ;; otherwise, open with the default program (handled in mu-server
+      (mu4e~proc-extract 'open docid index mu4e-decryption-policy))))
+
+(defun mu4e~view-temp-action (docid index what &optional param)
+  "Open attachment INDEX for message with DOCID, and invoke ACTION."
+  (interactive)
+  (mu4e~proc-extract 'temp docid index mu4e-decryption-policy nil what param ))
+
+(defvar mu4e~view-open-with-hist nil "History list for the open-with argument.")
+
+(defun mu4e-view-open-attachment-with (msg attachnum &optional cmd)
+  "Open MSG's attachment ATTACHNUM with CMD.
+If CMD is nil, ask user for it."
+  (let* ((att (mu4e~view-get-attach msg attachnum))
+         (ext (file-name-extension (plist-get att :name)))
+         (cmd (or cmd
+                  (read-string
+                   (mu4e-format "Shell command to open it with: ")
+                   (assoc-default ext mu4e-view-attachment-assoc)
+                   'mu4e~view-open-with-hist)))
+         (index (plist-get att :index)))
+    (mu4e~view-temp-action
+     (mu4e-message-field msg :docid) index 'open-with cmd)))
+
+(defvar mu4e~view-pipe-hist nil
+  "History list for the pipe argument.")
+
+(defun mu4e-view-pipe-attachment (msg attachnum &optional pipecmd)
+  "Feed MSG's attachment ATTACHNUM through pipe PIPECMD.
+If PIPECMD is nil, ask user for it."
+  (let* ((att (mu4e~view-get-attach msg attachnum))
+         (pipecmd (or pipecmd
+                      (read-string
+                       (mu4e-format "Pipe: ")
+                       nil
+                       'mu4e~view-pipe-hist)))
+         (index (plist-get att :index)))
+    (mu4e~view-temp-action
+     (mu4e-message-field msg :docid) index 'pipe pipecmd)))
+
+(defun mu4e-view-open-attachment-emacs (msg attachnum)
+  "Open MSG's attachment ATTACHNUM in the current emacs instance."
+  (let* ((att (mu4e~view-get-attach msg attachnum))
+         (index (plist-get att :index)))
+    (mu4e~view-temp-action (mu4e-message-field msg :docid) index 'emacs)))
+
+(defun mu4e-view-import-attachment-diary (msg attachnum)
+  "Open MSG's attachment ATTACHNUM in the current emacs instance."
+  (interactive)
+  (let* ((att (mu4e~view-get-attach msg attachnum))
+         (index (plist-get att :index)))
+    (mu4e~view-temp-action (mu4e-message-field msg :docid) index 'diary)))
+
+(defun mu4e-view-import-public-key (msg attachnum)
+  "Import MSG's attachment ATTACHNUM into the gpg-keyring."
+  (interactive)
+  (let* ((att (mu4e~view-get-attach msg attachnum))
+         (index (plist-get att :index))
+         (mime-type (plist-get att :mime-type)))
+    (if (string= "application/pgp-keys" mime-type)
+        (mu4e~view-temp-action (mu4e-message-field msg :docid) index 'gpg)
+      (mu4e-error "Invalid mime-type for a pgp-key: `%s'" mime-type))))
+
+(defun mu4e-view-attachment-action (&optional msg)
+  "Ask user what to do with attachments in MSG
+If MSG is nil use the message returned by `message-at-point'.
+The actions are specified in `mu4e-view-attachment-actions'."
+  (interactive)
+  (let* ((msg (or msg (mu4e-message-at-point)))
+         (actionfunc (mu4e-read-option
+                      "Action on attachment: "
+                      mu4e-view-attachment-actions))
+         (multi (eq actionfunc 'mu4e-view-save-attachment-multi))
+         (attnum (unless multi
+                   (mu4e~view-get-attach-num "Which attachment" msg multi))))
+    (cond ((and actionfunc attnum)
+           (funcall actionfunc msg attnum))
+          ((and actionfunc multi)
+           (funcall actionfunc msg)))))
+
+;; handler-function to handle the response we get from the server when we
+;; want to do something with one of the attachments.
+(defun mu4e~view-temp-handler (path what docid param)
+  "Handler function for doing things with temp files (ie.,
+attachments) in response to a (mu4e~proc-extract 'temp ... )."
+  (cond
+   ((string= what "open-with")
+    ;; 'param' will be the program to open-with
+    (start-process "*mu4e-open-with-proc*" "*mu4e-open-with*" param path))
+   ((string= what "pipe")
+    ;; 'param' will be the pipe command, path the infile for this
+    (mu4e-process-file-through-pipe path param))
+   ;; if it's mu4e, it's some embedded message; 'param' may contain the docid
+   ;; of the parent message.
+   ((string= what "mu4e")
+    ;; remember the mapping path->docid, which maps the path of the embedded
+    ;; message to the docid of its parent
+    (puthash path docid mu4e~path-parent-docid-map)
+    (mu4e~proc-view-path path mu4e-view-show-images mu4e-decryption-policy))
+   ((string= what "emacs")
+    (find-file path)
+    ;; make the buffer read-only since it usually does not make
+    ;; sense to edit the temp buffer; use C-x C-q if you insist...
+    (setq buffer-read-only t))
+   ((string= what "diary")
+    (icalendar-import-file path diary-file))
+   ((string= what "gpg")
+    (epa-import-keys path))
+   (t (mu4e-error "Unsupported action %S" what))))
+
+;;; 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 can't scroll-up
+anymore, 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
+
+(defun mu4e~view-get-urls-num (prompt &optional multi)
+  "Ask the user with PROMPT for an URL number for MSG, and ensure
+it is valid. 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 to url(s). If MULTI (prefix-argument) is nil, go to
+a single one, otherwise, 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(s) 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(s). 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)
+  "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 url NUM in the current message, prompting the
+user with PROMPT."
+  (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.
+
+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))
+
+;;; Various commands
+
+(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-field-at-point :path))
+        (buf (get-buffer-create mu4e~view-raw-buffer-name)))
+    (unless (and path (file-readable-p path))
+      (mu4e-error "Not a readable file: %S" path))
+    (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-field (mu4e-message-at-point) :path)))
+    (mu4e-process-file-through-pipe path cmd)))
+
+(defconst mu4e~verify-buffer-name " *mu4e-verify*")
+
+(defun mu4e-view-verify-msg-popup (&optional msg)
+  "Pop-up a signature verification window for MSG.
+If MSG is nil, use the message at point."
+  (interactive)
+  (let* ((msg (or msg (mu4e-message-at-point)))
+         (path (mu4e-message-field msg :path))
+         (cmd (format "%s verify --verbose %s %s"
+                      mu4e-mu-binary
+                      (shell-quote-argument path)
+                      (if mu4e-decryption-policy
+                          "--decrypt --use-agent"
+                        "")))
+         (output (shell-command-to-string cmd))
+         ;; create a new one
+         (buf (get-buffer-create mu4e~verify-buffer-name))
+         (win (or (get-buffer-window buf)
+                  (split-window-vertically (- (window-height) 6)))))
+    (with-selected-window win
+      (let ((inhibit-read-only t))
+        ;; (set-window-dedicated-p win t)
+        (switch-to-buffer buf)
+        (erase-buffer)
+        (insert output)
+        (goto-char (point-min))
+        (local-set-key "q" 'kill-buffer-and-window))
+      (setq buffer-read-only t))
+    (select-window win)))
+
+(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))))))))
+
+;;
+;; Loading messages
+;;
+
+(defvar mu4e-loading-mode-map nil  "Keymap for *mu4e-loading* buffers.")
+(unless mu4e-loading-mode-map
+  (setq mu4e-loading-mode-map
+        (let ((map (make-sparse-keymap)))
+          (define-key map "n" 'ignore)
+          (define-key map "p" 'ignore)
+          (define-key map "q"
+            (lambda()(interactive)
+              (if (eq mu4e-split-view 'single-window)
+                  'kill-buffer
+                'kill-buffer-and-window)))
+          map)))
+(fset 'mu4e-loading-mode-map mu4e-loading-mode-map)
+
+(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))))
+
+;;;
+(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..edeb267
--- /dev/null
@@ -0,0 +1,67 @@
+;;; mu4e.el --- part of mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2019 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: email
+;; Version: 0.0
+
+;; This file is not part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'mu4e-vars)
+(require 'mu4e-headers)  ;; headers view
+(require 'mu4e-view)     ;; message view
+(require 'mu4e-main)     ;; main screen
+(require 'mu4e-compose)  ;; message composition / sending
+(require 'mu4e-proc)     ;; communication with backend
+(require 'mu4e-utils)    ;; utility functions
+(require 'mu4e-context)  ;; support for contexts
+
+(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.
+(require 'desktop)
+(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~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)))
+
+;;; _
+(provide 'mu4e)
+;;; mu4e.el ends here
diff --git a/mu4e/mu4e.texi b/mu4e/mu4e.texi
new file mode 100644 (file)
index 0000000..52eaa60
--- /dev/null
@@ -0,0 +1,4667 @@
+\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-2020 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
+24.4 or higher, 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 frequenly 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 / 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
+mererly 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} (@inforef{Top,,smtpmail}), which is part of
+Emacs. In addition, @t{mu4e} piggybacks on Gnus' message editor;
+@inforef{Top,,message}.
+@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
+* 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 24 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 the Guile 2.2 (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++ compilers (both @command{gcc} and
+@command{clang} should work), GNU Autotools 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 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
+
+# get emacs 25 or higher if you don't have it yet
+$ sudo apt-get install emacs
+
+# optional
+$ sudo apt-get install guile-2.2-dev html2text xdg-utils
+
+# optional: only needed for msg2pdf and mug (toy gtk+ frontend)
+$ sudo apt-get install libwebkitgtk-3.0-dev
+@end example
+
+@subsection Dependencies for Fedora
+
+@example
+$ sudo yum install gmime30-devel xapian-core-devel
+
+# get emacs 25 or higher if you don't have it yet
+$ sudo yum install emacs
+
+# optional
+$ sudo yum install html2text xdg-utils guile22-devel
+
+# optional: only needed for msg2pdf and mug (toy gtk+ frontend)
+$ sudo yum install webkitgtk3-devel
+@end example
+
+@subsection Building on Msys2
+@example
+# 1) install makepkg tool
+
+pacman -S base-devel msys-devel gcc git
+
+# 2) clone packages repo
+
+cd ~
+git clone https://github.com/msys2-unofficial/MSYS2-packages.git --depth=1
+
+# make and install dependencies
+
+cd ~/MSYS2-packages/xapian-core
+makepkg -s
+pacman -U xapian-core-1.4.15-1-x86_64.pkg.tar.xz
+
+cd ~/MSYS2-packages/gmime3
+makepkg -s
+pacman -U gmime3-devel-3.2.6-1-x86_64.pkg.tar.xz
+
+# 4) make and install mu from git (changes versions as needed)
+
+cd ~/MSYS2-packages/mu
+makepkg -sfp PKGBUILD-git
+pacman -U mu-git-2020-03-01-r4854.17f38dc4-1-x86_64.pkg.tar.xz
+@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>.tar.gz  # 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}
+
+Alternatively, if you build from the git repository or use a tarball
+like the ones that @t{github} produces, the instructions are slightly
+different, and require you to 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
+
+The maildir must be on a single file-system; and symbolic links are
+not 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.
+
+Assuming that your maildir is at @file{~/Maildir}, we issue the
+following command:
+@example
+  $ mu init --maildir=~/Maildir
+@end example
+
+Optionally, 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.
+
+If you want to see the current values, 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
+
+Note, the folder 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. Note that you can of
+course occasionally run a thorough indexing round.
+
+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} (@inforef{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}
+(@inforef{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 @value{VERSION}
+
+  Basics
+
+       * [j]ump to some maildir
+       * enter a [s]earch query
+       * [C]ompose a new message
+
+  Bookmarks
+
+       * [bu] Unread messages      (26119/26119)
+       * [bt] Today's messages     (1/7)
+       * [bw] Last 7 days          (30/126)
+       * [bp] Messages with images (268/2309)
+
+  Misc
+
+       * [;]Switch context
+       * [U]pdate email & database
+       * toggle [m]ail sending mode (currently direct)
+
+       * [N]ews
+       * [A]bout mu4e
+       * [H]elp
+       * [q]uit
+
+  Info
+
+        * database-path       : /home/user/.cache/mu/xapian
+        * maildir             : /home/user/Maildir
+        * in store            : 78825 messages
+@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
+your own shortcuts. You can use @code{mu4e-mailing-list-patterns} to
+to specify generic shortcuts, e.g. to shorten list names which contain
+dots (@t{mu4e} defaults to shortening up to the first dot):
+@lisp
+(setq mu4e-mailing-list-patterns '(``\\([-_a-z0-9.]+\\)\.lists\.company\.com'')))
+@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
+----
+;            switch context
+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
+
+
+@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, the messages are
+@emph{threaded}, i.e., shown in the context of a discussion thread; this also
+affects the sort order.
+
+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 field 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}.
+
+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
+
+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}.
+
+Note, the current message view is to be replaced by a new one, based on
+Gnus' article-mode. It is available now as a 'tech preview', which you
+can try by setting @code{mu4e-view-use-gnus} to @code{t} before starting
+@code{mu4e}.
+
+@menu
+* Overview: MSGV Overview. What is the Message View
+* Keybindings: MSGV Keybindings. Do things with your keyboard
+* Attachments:: Opening and saving them
+* Viewing images inline::Images display inside emacs
+* Displaying rich-text messages::Dealing with HTML mail
+* Verifying signatures and decryption: MSGV Crypto. Support for cryptography
+* Custom headers: MSGV Custom headers. Your 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: Mon 19 Jan 2004 09:39:42 AM EET
+    Maildir: /inbox
+    Attachments(2): [1]DSCN4961.JPG(1.3M), [2]DSCN4962.JPG(1.4M)
+
+    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 You can set the date format with the variable
+@code{mu4e-date-format-long}.
+@item By default, only the names of contacts in address fields are visible
+(see @code{mu4e-view-show-addresses} to change this). You can view the e-mail
+addresses by clicking on the name, or pressing @key{M-RET}.
+@item You can compose a message for the contact at point by either clicking
+@key{[mouse-2]} or pressing @key{C}.
+@item The body text can be line-wrapped using @t{visual-line-mode}. @t{mu4e}
+defines @key{w} to toggle between the wrapped and unwrapped state. If you want
+to do this automatically when viewing a message, invoke @code{visual-line-mode}
+in your @code{mu4e-view-mode-hook}.
+@item For messages that support it, you can toggle between html and text versions using
+@code{mu4e-view-toggle-html}, bound to @key{h};
+@item You can hide cited parts
+in messages (the parts starting with ``@t{>}'') using
+@code{mu4e-view-hide-cited}, bound to @key{#}. If you want to do this
+automatically for every message, invoke the function in your
+@code{mu4e-view-mode-hook}.
+@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
+e            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
+-------
+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)
+o            open attachment (asks for number)
+(or: <mouse-1> or M-RET with point on attachment)
+
+a            execute some custom action on the message
+A            execute some custom action on an attachment
+
+misc
+----
+;            switch context
+c            copy address at point (with C-u copy long version)
+
+h            toggle between html/text (if available)
+w            toggle line wrapping
+#            toggle show/hide cited parts
+
+v            show details about the cryptographic signature
+
+.            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
+
+For the marking commands, please refer to @ref{Marking messages}.
+
+@node Attachments
+@section Attachments
+
+By default, @t{mu4e} uses the @t{xdg-open}-program
+@footnote{@url{https://www.freedesktop.org/wiki/Software/xdg-utils/}} or (on
+OS X) the @t{open} program for opening attachments. If you want to use another
+program, you do so by setting the @t{MU_PLAY_PROGRAM} environment variable to
+the program to be used.
+
+The default directory for attaching and extracting (saving)
+attachmentsis your home directory (@file{~/}); you can change this
+using the variable @code{mu4e-attachment-dir}, for example:
+
+@lisp
+(setq mu4e-attachment-dir "~/Downloads")
+@end lisp
+
+For more flexibility, @code{mu4e-attachment-dir} can also be a user-provided
+function. This function receives two parameters: the file-name and the
+mime-type as found in the e-mail message@footnote{sadly, often
+@t{application/octet-stream} is used for the mime-type, even if a better type
+is available} of the attachment, either or both of which can be @t{nil}. For
+example:
+
+@lisp
+(setq mu4e-attachment-dir
+  (lambda (fname mtype)
+     (cond
+        ;; docfiles go to ~/Desktop
+       ((and fname (string-match "\\.doc$" fname))  "~/Desktop")
+       ;; ... other cases  ...
+       (t "~/Downloads")))) ;; everything else
+@end lisp
+
+You can extract multiple attachments at once by prefixing the extracting
+command by @key{C-u}; so @kbd{C-u e} asks you for a range of attachments to
+extract (for example, @kbd{1 3-6 8}). The range "@samp{a}" is a shortcut for
+@emph{all} attachments.
+
+@node Viewing images inline
+@section Viewing images inline
+
+It is possible to show images inline in the message view buffer if you run
+Emacs in GUI-mode. You can enable this by setting the variable
+@code{mu4e-view-show-images} to @t{t}. Since Emacs does not always
+handle images correctly, this is not enabled by default. If you are using
+Emacs 24 with
+@emph{ImageMagick}@footnote{@url{http://www.imagemagick.org/}} support, make
+sure you call @code{imagemagick-register-types} in your configuration, so it
+is used for images.
+
+@lisp
+;; enable inline images
+(setq mu4e-view-show-images t)
+;; use imagemagick, if available
+(when (fboundp 'imagemagick-register-types)
+  (imagemagick-register-types))
+@end lisp
+
+@node Displaying rich-text messages
+@section Displaying rich-text messages
+
+@t{mu4e} normally prefers the plain-text version for messages that
+consist of both a plain-text and html (rich-text) versions of the
+body-text. You can change this by setting @code{mu4e-view-prefer-html}
+to @t{t}. And you can toggle this value (globally) using @kbd{h} in the
+message view; this also refreshes the message with the new setting.
+
+Note, when using html-based rendering, you don't get the hyperlink
+shortcuts the text-version provides.
+
+If there is only an html-version, or if the plain-text version is too
+short in comparison with the html part@footnote{this is e.g. for the
+case where the text-part is only a short blurb telling you to use the
+html-version; see @code{mu4e-view-html-plaintext-ratio-heuristic}},
+@t{mu4e} tries to convert the html into plain-text for display.
+
+With emacs 24.4 or newer, this defaults to @code{mu4e-shr2text}, which
+uses the built-in @t{shr} renderer. For older emacs versions, this
+defaults to the built-in @code{html2text} function. In practice, the
+latter gives much better results.
+
+If you use @code{mu4e-shr2text}, it might be useful to emulate some of
+the @t{shr} key bindings, with something like:
+@lisp
+(add-hook 'mu4e-view-mode-hook
+  (lambda()
+    ;; try to emulate some of the eww key-bindings
+    (local-set-key (kbd "<tab>") 'shr-next-link)
+    (local-set-key (kbd "<backtab>") 'shr-previous-link)))
+@end lisp
+
+If you're using a dark 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
+
+If your emacs does not have @t{shr} yet, it can be useful to use a
+custom method. For that, you can set the variable
+@code{mu4e-html2text-command} to either a shell command or a function
+instead.
+
+@subsection Html2text commands
+
+If @code{mu4e-html2text-command} is a shell command, it is expected to
+take html from standard input and write plain text in @t{UTF-8} encoding
+on standard output.
+
+An example of such a program is the program that is actually
+@emph{called}
+@t{html2text}@footnote{@url{http://www.mbayer.de/html2text/}}. After
+installation, you can set it up with something like the following:
+
+@lisp
+(setq mu4e-html2text-command "html2text -utf8 -width 72")
+@end lisp
+
+An alternative to this is the Python @t{python-html2text} package; after
+installing that, you can tell @t{mu4e} to use it with something like:
+
+@lisp
+(setq mu4e-html2text-command
+  "html2markdown | grep -v '&nbsp_place_holder;'")
+@end lisp
+
+On OS X, there is a program called @t{textutil} as yet another
+alternative:
+
+@lisp
+(setq mu4e-html2text-command
+  "textutil -stdin -format html -convert txt -stdout")
+@end lisp
+
+@subsection Html2text functions
+@anchor{Html2text functions}
+
+If @code{mu4e-html2text-command} refers to an elisp function, the
+function is expected to take a message plist as its input, and returns
+the transformed data.
+
+
+You can easily create your own function, for instance:
+
+@lisp
+(defun my-mu4e-html2text (msg)
+  "My html2text function; shows short message inline, show
+long messages in some external browser (see `browse-url-generic-program')."
+  (let ((html (or (mu4e-message-field msg :body-html) "")))
+    (if (> (length html) 20000)
+      (progn
+       (mu4e-action-view-in-browser msg)
+       "[Viewing message in external browser]")
+      (mu4e-shr2text msg))))
+
+(setq mu4e-html2text-command 'my-mu4e-html2text)
+@end lisp
+
+@subsection Privacy aspects
+@anchor{Privacy aspects}
+
+When opening your messages in a graphical browser, it may expose you
+doing so to the sender, due to the presence of specially crafted image
+URLs, or Javascript.
+
+If that is an issue, it is recommended to use a browser (or browser
+profile) that does not load images. The same applies to Javascript.
+
+
+@node MSGV Crypto
+@section Crypto
+
+The @t{mu4e} message view supports decryption of encrypted messages,
+as well as verification of signatures. For signing/encrypting messages
+your outgoing messages, see @ref{Signing and encrypting}.
+
+For all of this to work, @command{gpg-agent} must be running, and it
+must set the environment variable @t{GPG_AGENT_INFO}. You can check from
+Emacs with @key{M-x getenv GPG_AGENT_INFO}.
+
+In many mainstream Linux/Unix desktop environments, everything works
+out-of-the-box, but if your environment does not automatically start
+@command{gpg-agent}, you can do so by hand:
+@verbatim
+$ eval $(gpg-agent --daemon)
+@end verbatim
+
+@noindent
+This starts the daemon, and sets the environment variable.
+
+Some users have reported problems with certain S/MIME-signed messages
+where mu checks if the certificate has been revoked. This can be
+avoided by adding @t{disable-crl-checks} to @t{~/.gnupg/gpgsm.conf};
+alternatively, you could use the gnus-based viewer.
+
+@subsection Decryption
+@anchor{Decryption}
+
+If you receive messages that are encrypted (using PGP/MIME), @t{mu4e}
+can try to decrypt them, base on the setting of
+@code{mu4e-decryption-policy}. If you set it to @t{t}, @t{mu4e} attempts
+to decrypt messages automatically; this is the default. If you set it to
+@t{nil}, @t{mu4e} @emph{won't} attempt to decrypt anything. Finally, if
+you set it to @t{'ask}, it asks you what to do, each time an encrypted
+message is encountered.
+
+When opening an encrypted message, @t{mu} consults @t{gpg-agent} to see
+if it already has unlocked the key needed to decrypt the message; if
+not, it prompts you for a password (typically with a separate top-level
+window). This is only needed once per session.
+
+@subsection Verifying signatures
+@anchor{Verifying signatures}
+
+Some e-mail messages are cryptographically signed, and @t{mu4e} can
+check the validity of these signatures. If a message has one or more
+signatures, the message view shows an extra header @t{Signature:}
+(assuming it is part of your @code{mu4e-view-fields}), and one or more
+`verdicts' of the signatures found; either @t{verified}, @t{unverified}
+or @t{error}. For instance:
+
+@verbatim
+Signature: unverified (Details)
+@end verbatim
+or
+@verbatim
+Signature: verified Darrow Andromedus <darrow@rising.com> (Details)
+@end verbatim
+
+You can see the details of the signature verification by activating the
+@t{Details} or pressing @key{v}. This pops up a little window with the
+details of the signatures found and whether they could be verified or
+not.
+
+Note that @t{mu4e} does not check whether the signer is the same as the
+sender of the message, since this would cause too many false negatives
+for senders that use an address that is not part of their certificate.
+Also, the From: address can easily be forged.
+
+For more information, see the @command{mu-verify} manual page.
+
+@node MSGV 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.
+
+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 Attachment actions
+Similarly, there is @code{mu4e-view-attachment-action} (@key{A}) for actions
+on attachments, which you can specify with
+@code{mu4e-view-attachment-actions}.
+
+@t{mu4e} predefines a number of attachment-actions:
+@itemize
+@item @t{open-with} (@key{w}): open the attachment with some arbitrary
+program. For example, suppose you have received a message with a picture
+attachment; then, @kbd{A w 1 RET gimp RET} opens that attachment in @emph{The
+Gimp}
+@item @t{pipe} (@key{|}: process the attachment with some Unix shell-pipe and
+see the results. Suppose you receive a patch file, and would like to get an
+overview of the changes, using the @t{diffstat} program. You can use something
+like: @kbd{A | 1 RET diffstat -b RET}.
+@item Emacs (@key{e}): open the attachment in your running Emacs. For
+example, if you receive some text file you'd like to open in Emacs:
+@kbd{A e 1 RET}.
+@end itemize
+
+These actions all work on a @emph{temporary copy} of the attachment.
+
+@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)
+
+(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-ignore-address-regexp} --- a regular expression to
+filter out other `junk' e-mail addresses; defaults to ``@t{no-?reply}''.
+@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}
+(@inforef{Composing,,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}.
+
+The support for encryption and signing is @emph{independent} of the support
+for their counterparts, decrypting and signature verification (as discussed in
+@ref{MSGV Crypto}). Even if your @t{mu4e} does not have support for the latter
+two, you can still sign/encrypt messages.
+
+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}. Let's
+look at some examples here; you can consult the @code{mu-query} man page
+for the details.
+
+@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}).
+
+@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~proc-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" )
+                  ( 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" )
+                  ( 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 me go to /archive
+      ;; also `mu4e-user-mail-address-p' can be used
+      ((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
+      ((find-if
+        (lambda (addr)
+          (mu4e-message-contact-field-matches msg :from addr))
+        (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
+* Attachment actions::Doing things with 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-attachment-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:" (cdar (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{cdar}, remember that the @t{From:}-field is a
+list of @code{(NAME . EMAIL)} cells; thus, @code{cdar} gets us the e-mail
+address of the first in the list. @t{From:}-fields rarely contain multiple
+cells.
+
+@node Attachment actions
+@section Attachment actions
+
+Finally, let's define an attachment action. As mentioned, attachment-action
+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-attachment-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, or listening to a message's body-text using text-to-speech.
+
+@node Extending mu4e
+@chapter Extending mu4e
+
+@t{mu4e} is designed to be easily extendible --- 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 headers in the message-view and
+headers-view --- see @ref{HV Custom headers}, @ref{MSGV 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 an attachment --- see @ref{Attachment 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~}. 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} that start with
+@code{mu4e~}; don't touch them.  Let me repeat that:
+@verbatim
+Do not use 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
+
+@subsection Rewriting the message body
+
+Message body rewriting allows you to modify the message text that is
+presented in the message view. This can be useful if the message needs
+special processing, for instance for special filling or cleaning up
+encoding artifacts (this is what @t{mu4e} uses this for internally).
+
+To enable this, you can append your rewrite-function to
+@code{mu4e-message-body-rewrite-functions}; your function is expected to
+take two parameters @code{MSG} and @code{TXT}, which are the
+message-plist and the body-text, which could be the result of earlier
+transformations, including html->text conversion as per
+@code{mu4e-html2-text-command}. The function is expected to return the
+transformed text.
+
+As a fairly useless example, suppose we insist on reading @t{mu4e} as
+@t{MU4E}:
+@lisp
+(defun mu4e-to-MU4E-rewrite (msg txt)
+  (replace-regexp-in-string "mu4e" "MU4E" txt))
+
+(add-to-list 'mu4e-message-body-rewrite-functions 'mu4e-to-MU4E-rewrite t)
+@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}@footnote{@code{user-error} only appears in Emacs
+24.2 and later; in older versions it falls back to @code{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
+* 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
+* 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
+
+@node Org-mode links
+@section Org-mode links
+
+It can be useful to include links to e-mail messages or even 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 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{org-mu4e} 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: 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 %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") 'org-mu4e-store-and-capture)
+  (define-key mu4e-view-mode-map    (kbd "C-c c") 'org-mu4e-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 Sauron
+@section Sauron
+
+The Emacs package @t{sauron}@footnote{Sauron can be found at
+@url{https://github.com/djcb/sauron}, or in the Marmalade package repository
+at @url{https://marmalade-repo.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`; 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}
+(@inforef{Dired,,emacs}), using the following steps (based on a post on the
+@t{mu-discuss} mailing list by @emph{Stephen Eglen}).
+
+To prepare for this, you need a special version of the
+@code{gnus-dired-mail-buffers} function so it understands @t{mu4e} buffers as
+well; so put in your configuration:
+
+@lisp
+(require 'gnus-dired)
+;; make the `gnus-dired-mail-buffers' function also work on
+;; message-mode derived modes, such as mu4e-compose-mode
+(defun gnus-dired-mail-buffers ()
+  "Return a list of active message buffers."
+  (let (buffers)
+    (save-current-buffer
+      (dolist (buffer (buffer-list t))
+       (set-buffer buffer)
+       (when (and (derived-mode-p 'message-mode)
+               (null message-sent-message-via))
+         (push (buffer-name buffer) buffers))))
+    (nreverse buffers)))
+
+(setq gnus-dired-mail-mode 'mu4e-user-agent)
+(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; and easy way to achieve this, is by using anm 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; processed: 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; processed: 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 @command{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}.
+
+@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. Otherwise, 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, during which
+you have to wait for messages to open. 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.
+@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 view attached images in my message view buffers? See
+@ref{Viewing images inline}.
+@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 Does @t{mu4e} support crypto (i.e., decrypting messages and verifying signatures)?
+Yes --- it is possible to do both (note, only PGP/MIME is
+supported). In the @ref{Main view} the support is indicated by a big
+letter @t{C} on the right hand side of the @t{mu4e} version. See
+@ref{Decryption} and @ref{Verifying signatures}. For encryption and
+signing messages, see @ref{Writing messages}.
+@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 How can I use the @t{eww} browser to view rich-text messages?
+With a new enough emacs, this happens automatically. See
+@ref{Html2text functions} for some details.
+@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 webbrowser. You can influence the browser with
+@code{browse-url-generic-program}; and see @ref{Privacy aspects}.
+@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. @inforef{Insertion
+Variables,,message}.
+
+@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 Addres 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}.
+
+@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} sensd 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 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
+(if (equal window-system 'x)
+    (progn
+      (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 save 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
+
+A typical message s-expression looks something like the following:
+
+@lisp
+(:docid 32461
+ :from (("Nikola Tesla" . "niko@@example.com"))
+ :to (("Thomas Edison" . "tom@@example.com"))
+ :cc (("Rupert The Monkey" . "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)
+ :parts ( (:index 1 :mime-type "text/plain" :size 12345 :attachment nil)
+          (:index 2 :name "photo.jpg" :mime-type "image/jpeg"
+           :size 147331 :attachment t)
+          (:index 3 :name "book.pdf" :mime-type "application/pdf"
+           :size 192220 :attachment t))
+ :references  ("C8384574032D81EE81AF0114E4E74@@123213.mail.example.com"
+ "38203498230942D81EE81AF0114E4E74@@123213.mail.example.com")
+ :in-reply-to "38203498230942D81EE81AF0114E4E74@@123213.mail.example.com"
+ :body-txt "Hi Tom,
+ ....
+")
+@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 pairs @code{(name . 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.}
+@item Attachments are a list of elements with fields @t{:index} (the number of
+the MIME-part), @t{:name} (the file name, if any), @t{:mime-type} (the
+MIME-type, if any) and @t{:size} (the size in bytes, if any).
+@item Messages in the @ref{Headers view} come from the database and do not have
+@t{:attachments}. @t{:body-txt} or @t{:body-html} fields. Message in the
+@ref{Message view} use the actual message file, and do include these fields.
+@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-proc-ping}, and registers a (lambda) function for
+@t{mu4e-proc-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-proc.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/org-mu4e.el b/mu4e/org-mu4e.el
new file mode 100644 (file)
index 0000000..6b62de7
--- /dev/null
@@ -0,0 +1,230 @@
+;;; 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.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; 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-utils")
+(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
+                  (lambda ()
+                    (mu4e-error "Switch to mu4e-compose-mode (M-m) before saving"))
+                  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-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/toys/Makefile.am b/toys/Makefile.am
new file mode 100644 (file)
index 0000000..5767986
--- /dev/null
@@ -0,0 +1,24 @@
+## 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
+
+SUBDIRS=
+
+# for mug2 and msg2pdf, we need webkit
+if BUILD_GUI
+SUBDIRS += mug # msg2pdf
+endif
diff --git a/toys/msg2pdf/Makefile.am b/toys/msg2pdf/Makefile.am
new file mode 100644 (file)
index 0000000..279c22c
--- /dev/null
@@ -0,0 +1,53 @@
+## 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
+
+# enforce compiling this dir first before descending into tests/
+SUBDIRS= .
+
+AM_CPPFLAGS=-I${top_srcdir}/lib $(GTK_CFLAGS) $(WEBKIT_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) -Wall -Wextra -Wno-unused-parameter -Wdeclaration-after-statement
+
+noinst_PROGRAMS=                                       \
+       msg2pdf
+
+# note, mug.cc is '.cc' only because libmu must explicitly
+# be linked as c++, not c.
+msg2pdf_SOURCES=                                       \
+       msg2pdf.c                                       \
+       dummy.cc
+
+# we need to use dummy.cc to enforce c++ linking...
+BUILT_SOURCES=                                         \
+       dummy.cc
+
+dummy.cc:
+       touch dummy.cc
+
+DISTCLEANFILES=                                                \
+       $(BUILT_SOURCES)
+
+
+msg2pdf_LDADD=                                         \
+       $(ASAN_LDFLAGS)                                 \
+       ${top_builddir}/lib/libmu.la                    \
+       $(GTK_LIBS)                                     \
+       ${WEBKIT_LIBS}
diff --git a/toys/msg2pdf/msg2pdf.c b/toys/msg2pdf/msg2pdf.c
new file mode 100644 (file)
index 0000000..1faf2fe
--- /dev/null
@@ -0,0 +1,315 @@
+/*
+** 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.
+**
+*/
+
+#include <mu-msg.h>
+#include <utils/mu-date.h>
+#include <mu-msg-part.h>
+
+#include <gtk/gtk.h>
+#include <webkit2/webkit2.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+typedef enum { UNKNOWN, OK, FAILED } Result;
+
+static void
+on_failed (WebKitPrintOperation *print_operation,
+           GError               *error,
+           Result               *result)
+{
+        if (error)
+                g_warning ("%s", error->message);
+
+        *result = FAILED;
+}
+
+
+static void
+on_finished (WebKitPrintOperation *print_operation,
+             Result               *result)
+{
+        if (*result == UNKNOWN)
+                *result = OK;
+}
+
+
+static gboolean
+print_to_pdf (GtkWidget *webview, GError **err)
+{
+        GtkWidget            *win;
+       WebKitPrintOperation *op;
+        GtkPrintSettings     *settings;
+       char                 *path, *uri;
+        Result                res;
+        time_t                started;
+       const int             max_time = 3; /* max 3 seconds to download stuff */
+
+
+       path = g_strdup_printf ("%s%c%x.pdf", mu_util_cache_dir(),
+                               G_DIR_SEPARATOR, (unsigned)random());
+       if (!mu_util_create_dir_maybe (mu_util_cache_dir(), 0700, FALSE)) {
+               g_warning ("Couldn't create tempdir");
+                g_free (path);
+               return FALSE;
+       }
+        uri = g_filename_to_uri (path, NULL, NULL);
+        g_print ("%s\n", path);
+        g_free(path);
+        if (!uri) {
+                g_warning ("failed to create uri");
+                return FALSE;
+        }
+
+        win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+        gtk_container_add(GTK_CONTAINER(win), webview);
+        gtk_widget_show_all(win);
+
+
+       op       = webkit_print_operation_new (WEBKIT_WEB_VIEW(webview));
+        settings = gtk_print_settings_new();
+        gtk_print_settings_set(settings,
+                               GTK_PRINT_SETTINGS_OUTPUT_URI, uri);
+        gtk_print_settings_set(settings,
+                               GTK_PRINT_SETTINGS_OUTPUT_FILE_FORMAT, "PDF");
+        g_free(uri);
+
+        webkit_print_operation_set_print_settings(op, settings);
+
+
+        webkit_print_operation_run_dialog(op, NULL);
+
+
+        res = UNKNOWN;
+        g_signal_connect(op, "failed", G_CALLBACK(on_failed), &res);
+        g_signal_connect(op, "finished", G_CALLBACK(on_finished), &res);
+
+        webkit_print_operation_print(op);
+        started = time (NULL);
+        do {
+                gtk_main_iteration_do (FALSE);
+       } while (res == UNKNOWN /*&& (time(NULL) - started) <= max_time*/);
+
+        g_object_unref (op);
+
+        return res == OK;
+}
+
+
+static char*
+save_file_for_cid (MuMsg *msg, const char* cid)
+{
+       gint idx;
+       gchar *filepath;
+       GError *err;
+
+       g_return_val_if_fail (msg, NULL);
+       g_return_val_if_fail (cid, NULL);
+
+       idx = mu_msg_find_index_for_cid (msg, MU_MSG_OPTION_NONE, cid);
+       if (idx < 0) {
+               g_warning ("%s: cannot find %s", __func__, cid);
+               return NULL;
+       }
+
+       err = NULL;
+       filepath = mu_msg_part_get_cache_path (msg, MU_MSG_OPTION_NONE, idx, NULL);
+       if (!filepath)
+               goto errexit;
+
+       if (!mu_msg_part_save (msg, MU_MSG_OPTION_USE_EXISTING, filepath, idx,
+                              &err))
+               goto errexit;
+
+       return filepath;
+
+errexit:
+       g_warning ("%s: failed to save %s: %s", __func__,
+                  filepath,
+                  err&&err->message?err->message:"error");
+       g_clear_error (&err);
+       g_free (filepath);
+
+       return NULL;
+}
+
+static void
+on_resource_load_started (WebKitWebView     *self,
+                          WebKitWebResource *resource,
+                          WebKitURIRequest  *request,
+                          MuMsg             *msg)
+{
+       const char* uri;
+
+       uri = webkit_uri_request_get_uri (request);
+
+       if (g_ascii_strncasecmp (uri, "cid:", 4) == 0) {
+               gchar *filepath;
+               filepath = save_file_for_cid (msg, uri);
+               if (filepath) {
+                       gchar *fileuri;
+                       fileuri = g_strdup_printf ("file://%s", filepath);
+                       webkit_uri_request_set_uri (request, fileuri);
+                        g_debug("printing %s", fileuri);
+                       g_free (fileuri);
+                       g_free (filepath);
+               }
+       }
+}
+
+
+/* return the path to the output file, or NULL in case of error */
+static gboolean
+generate_pdf (MuMsg *msg, const char *str, GError **err)
+{
+       GtkWidget      *view;
+       WebKitSettings *settings;
+       time_t          started;
+        gboolean        loading;
+       const int       max_time = 3; /* max 3 seconds to download stuff */
+
+       settings = webkit_settings_new ();
+       g_object_set (G_OBJECT(settings),
+                     "enable-javascript", FALSE,
+                     "auto-load-images", TRUE,
+                     "enable-plugins", FALSE,
+                      NULL);
+
+       view = webkit_web_view_new ();
+
+       /* to support cid: */
+       g_signal_connect (G_OBJECT(view), "resource-load-started",
+                         G_CALLBACK (on_resource_load_started), msg);
+
+       webkit_web_view_set_settings (WEBKIT_WEB_VIEW(view), settings);
+       webkit_web_view_load_html (WEBKIT_WEB_VIEW(view), str, NULL);
+       g_object_unref (settings);
+
+       started = time (NULL);
+       do {
+               loading = webkit_web_view_is_loading (WEBKIT_WEB_VIEW(view));
+               gtk_main_iteration_do (FALSE);
+       } while (loading && (time(NULL) - started) <= max_time);
+
+       return print_to_pdf (view, err);
+}
+
+static void
+add_header (GString *gstr, const char* header, const char *val)
+{
+       char *esc;
+
+       if (!val)
+               return;
+
+       esc = g_markup_escape_text (val, -1);
+       g_string_append_printf (gstr, "<b>%s</b>: %s<br>", header, esc);
+       g_free (esc);
+}
+
+ /* return the path to the output file, or NULL in case of error */
+static gboolean
+convert_to_pdf (MuMsg *msg, GError **err)
+{
+       GString    *gstr;
+       const char *body;
+       gchar      *data;
+       gboolean    rv;
+
+       gstr = g_string_sized_new (4096);
+
+       add_header (gstr, "From", mu_msg_get_from (msg));
+       add_header (gstr, "To", mu_msg_get_to (msg));
+       add_header (gstr, "Cc", mu_msg_get_cc (msg));
+       add_header (gstr, "Subject", mu_msg_get_subject (msg));
+       add_header (gstr, "Date", mu_date_str_s
+                   ("%c", mu_msg_get_date(msg)));
+
+       gstr =  g_string_append (gstr, "<hr>\n");
+
+       body = mu_msg_get_body_html (msg, MU_MSG_OPTION_NONE);
+       if (body)
+               g_string_append_printf (gstr, "%s", body);
+       else {
+               body = mu_msg_get_body_text (msg, MU_MSG_OPTION_NONE);
+               if (body) {
+                       gchar *esc;
+                       esc = g_markup_escape_text (body, -1);
+                       g_string_append_printf (gstr, "<pre>\n%s\n</pre>",
+                                               esc);
+                       g_free (esc);
+               } else
+                       gstr = g_string_append
+                               (gstr, "<i>No body</i>\n");
+       }
+
+       data = g_string_free (gstr, FALSE);
+       rv = generate_pdf (msg, data, err);
+       g_free (data);
+
+       return rv;
+}
+
+
+int
+main(int argc, char *argv[])
+{
+       MuMsg  *msg;
+       GError *err;
+
+       if (argc != 2) {
+               g_print ("msg2pdf: generate pdf files from e-mail messages\n"
+                "usage: msg2pdf <msgfile>\n");
+               return 1;
+       }
+
+       gtk_init (&argc, &argv);
+
+       if (access (argv[1], R_OK) != 0) {
+               g_printerr ("%s is not a readable file\n", argv[1]);
+               return 1;
+       }
+
+       err = NULL;
+       msg = mu_msg_new_from_file (argv[1], NULL, &err);
+       if (!msg) {
+               g_printerr ("failed to create msg for %s\n", argv[1]);
+               goto err;
+       }
+
+       if (!convert_to_pdf (msg, &err)) {
+               g_printerr ("failed to create pdf from %s\n", argv[1]);
+               goto err;
+       }
+
+       /* it worked! */
+       mu_msg_unref (msg);
+       return 0;
+
+err:
+       /* some error occurred */
+       mu_msg_unref (msg);
+
+       if (err)
+               g_printerr ("error: %s", err->message);
+
+       g_clear_error (&err);
+       return 1;
+
+}
diff --git a/toys/mug/Makefile.am b/toys/mug/Makefile.am
new file mode 100644 (file)
index 0000000..f9b3491
--- /dev/null
@@ -0,0 +1,98 @@
+## Copyright (C) 2008-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
+## t   he 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
+
+# enforce compiling this dir first before descending into tests/
+SUBDIRS= .
+
+AM_CPPFLAGS=-I${top_srcdir} -I${top_srcdir}/lib $(GTK_CFLAGS) $(WEBKIT_CFLAGS) \
+       -DICONDIR='"$(icondir)"' -DMUGDIR='"$(abs_srcdir)"'                     \
+       -DGSEAL_ENABLE
+
+# remove -DGTK_DISABLE_DEPRECATED for now, since it breaks 3.10+ compilation
+
+# 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=                                                                     \
+       $(WARN_CFLAGS)                                                          \
+       $(ASAN_CFLAGS)                                                          \
+       -Wno-redundant-decls                                                    \
+       -Wno-deprecated-declarations                                            \
+       -Wno-switch-enum
+
+AM_CXXFLAGS=$(WARN_CXXFLAGS)
+
+#
+# Distributors: this is a _toy_, not for distribution. the "noinst_" says enough
+#
+noinst_PROGRAMS=                                                               \
+       mug
+
+# note, mug.cc is '.cc' only because libmu must explicitly
+# be linked as c++, not c.
+mug_SOURCES=                                                                   \
+       mug.c                                                                   \
+       mug-msg-list-view.c                                                     \
+       mug-msg-list-view.h                                                     \
+       mug-msg-view.h                                                          \
+       mug-msg-view.c                                                          \
+       mug-query-bar.h                                                         \
+       mug-query-bar.c                                                         \
+       mug-shortcuts.c                                                         \
+       mug-shortcuts.h                                                         \
+       dummy.cc
+
+# we need to use dummy.cc to enforce c++ linking...
+BUILT_SOURCES=                                                                 \
+       dummy.cc
+
+dummy.cc:
+       touch dummy.cc
+
+DISTCLEANFILES=                                                                        \
+       $(BUILT_SOURCES)
+
+mug_LDADD=                                                                     \
+       $(ASAN_LDFLAGS)                                                         \
+       ${top_builddir}/lib/libmu.la                                            \
+       libmuwidgets.la                                                         \
+       ${GTK_LIBS}
+
+noinst_LTLIBRARIES=                                                            \
+       libmuwidgets.la
+
+libmuwidgets_la_SOURCES=                                                       \
+       mu-widget-util.h                                                        \
+       mu-widget-util.c                                                        \
+       mu-msg-attach-view.c                                                    \
+       mu-msg-attach-view.h                                                    \
+       mu-msg-body-view.c                                                      \
+       mu-msg-body-view.h                                                      \
+       mu-msg-header-view.c                                                    \
+       mu-msg-header-view.h                                                    \
+       mu-msg-view.h                                                           \
+       mu-msg-view.c
+
+libmuwidgets_la_LIBADD=                                                                \
+       ${top_builddir}/lib/libmu.la                                            \
+       ${GTK_LIBS}                                                             \
+       ${WEBKIT_LIBS}                                                          \
+       ${GIO_LIBS}
+
+EXTRA_DIST=                                                                    \
+       mug.svg
diff --git a/toys/mug/mu-msg-attach-view.c b/toys/mug/mu-msg-attach-view.c
new file mode 100644 (file)
index 0000000..630f077
--- /dev/null
@@ -0,0 +1,296 @@
+/*
+** 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.
+**
+*/
+
+#include "mu-msg-attach-view.h"
+#include "mu-widget-util.h"
+#include <mu-msg-part.h>
+
+enum {
+       ICON_COL,
+       NAME_COL,
+       PARTNUM_COL,
+
+       NUM_COL
+};
+
+
+/* 'private'/'protected' functions */
+static void mu_msg_attach_view_class_init (MuMsgAttachViewClass *klass);
+static void mu_msg_attach_view_init       (MuMsgAttachView *obj);
+static void mu_msg_attach_view_finalize   (GObject *obj);
+
+/* list my signals  */
+enum {
+       ATTACH_ACTIVATED,
+       /* MY_SIGNAL_2, */
+       LAST_SIGNAL
+};
+
+struct _MuMsgAttachViewPrivate {
+       MuMsg *_msg;
+};
+#define MU_MSG_ATTACH_VIEW_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
+                                                MU_TYPE_MSG_ATTACH_VIEW, \
+                                                MuMsgAttachViewPrivate))
+/* globals */
+static GtkIconViewClass *parent_class = NULL;
+
+static guint signals[LAST_SIGNAL] = {0};
+
+G_DEFINE_TYPE (MuMsgAttachView, mu_msg_attach_view, GTK_TYPE_ICON_VIEW);
+
+
+static void
+set_message (MuMsgAttachView *self, MuMsg *msg)
+{
+       if (self->_priv->_msg == msg)
+               return; /* nothing to todo */
+
+       if (self->_priv->_msg)  {
+               mu_msg_unref (self->_priv->_msg);
+               self->_priv->_msg = NULL;
+       }
+
+       if (msg)
+               self->_priv->_msg = mu_msg_ref (msg);
+}
+
+
+static void
+mu_msg_attach_view_class_init (MuMsgAttachViewClass *klass)
+{
+       GObjectClass *gobject_class;
+       gobject_class = (GObjectClass*) klass;
+
+       parent_class            = g_type_class_peek_parent (klass);
+       gobject_class->finalize = mu_msg_attach_view_finalize;
+
+       g_type_class_add_private (gobject_class, sizeof(MuMsgAttachViewPrivate));
+
+       signals[ATTACH_ACTIVATED] =
+               g_signal_new ("attach-activated",
+                             G_TYPE_FROM_CLASS (gobject_class),
+                             G_SIGNAL_RUN_FIRST,
+                             G_STRUCT_OFFSET (MuMsgAttachViewClass,
+                                              attach_activated),
+                             NULL, NULL,
+                             g_cclosure_marshal_VOID__UINT_POINTER,
+                             G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_POINTER);
+}
+
+static void
+item_activated (MuMsgAttachView *self, GtkTreePath *tpath)
+{
+       GtkTreeModel *model;
+       GtkTreeIter iter;
+       guint partnum;
+
+       model = gtk_icon_view_get_model (GTK_ICON_VIEW(self));
+       if (!gtk_tree_model_get_iter (model, &iter, tpath)) {
+               g_warning ("could not find path");
+       }
+
+       gtk_tree_model_get (model, &iter,
+                           PARTNUM_COL, &partnum,
+                           -1);
+
+       g_signal_emit (G_OBJECT (self),
+                      signals[ATTACH_ACTIVATED], 0,
+                      partnum, self->_priv->_msg);
+}
+
+static void
+accumulate_parts (MuMsgAttachView *self, GtkTreePath *path, GSList **lst)
+{
+       GtkTreeIter iter;
+       GtkTreeModel *model;
+
+       /* don't unref */
+       model = gtk_icon_view_get_model (GTK_ICON_VIEW(self));
+
+       if (gtk_tree_model_get_iter (model, &iter, path)) {
+               gchar *filepath;
+               gint idx;
+               gtk_tree_model_get (model, &iter, PARTNUM_COL, &idx, -1);
+               filepath = mu_msg_part_get_cache_path (self->_priv->_msg,
+                                                      MU_MSG_OPTION_NONE,
+                                                      idx, NULL);
+               if (filepath) {
+                       if (mu_msg_part_save (self->_priv->_msg,
+                                             MU_MSG_OPTION_USE_EXISTING,
+                                             filepath,
+                                             idx, NULL)) {
+                               GFile *file;
+                               file = g_file_new_for_path (filepath);
+                               *lst = g_slist_prepend (*lst, g_file_get_uri(file));
+                               g_object_unref (file);
+                       } else
+                               g_warning ("error saving msg part");
+                       g_free (filepath);
+               }
+       }
+}
+
+
+
+static void
+on_drag_data_get (MuMsgAttachView *self, GdkDragContext *drag_context,
+                 GtkSelectionData *data, guint info, guint time, gpointer user_data)
+{
+       GSList *lst, *cur;
+       char **uris;
+       int i;
+
+       lst = NULL;
+       gtk_icon_view_selected_foreach (GTK_ICON_VIEW(self),
+                                       (GtkIconViewForeachFunc)accumulate_parts,
+                                       &lst);
+
+       uris = g_new(char*, g_slist_length(lst) + 1);
+       for (cur = lst, i = 0; cur; cur = g_slist_next(cur))
+               uris[i++] = (gchar*)cur->data;
+
+       uris[i] = NULL;
+       gtk_selection_data_set_uris (data, uris);
+
+       g_free (uris);
+       g_slist_foreach (lst, (GFunc)g_free, NULL);
+       g_slist_free (lst);
+}
+
+static void
+mu_msg_attach_view_init (MuMsgAttachView *obj)
+{
+       GtkListStore *store;
+
+       obj->_priv = MU_MSG_ATTACH_VIEW_GET_PRIVATE(obj);
+       obj->_priv->_msg = NULL;
+
+       store = gtk_list_store_new (NUM_COL,GDK_TYPE_PIXBUF,
+                                   G_TYPE_STRING, G_TYPE_UINT);
+       gtk_icon_view_set_model (GTK_ICON_VIEW(obj), GTK_TREE_MODEL(store));
+       g_object_unref (store);
+
+       gtk_icon_view_set_pixbuf_column (GTK_ICON_VIEW(obj), ICON_COL);
+       gtk_icon_view_set_text_column (GTK_ICON_VIEW(obj), NAME_COL);
+
+       gtk_icon_view_set_margin (GTK_ICON_VIEW(obj), 0);
+       gtk_icon_view_set_spacing (GTK_ICON_VIEW(obj), 0);
+       gtk_icon_view_set_item_padding (GTK_ICON_VIEW(obj), 0);
+
+       /* note: only since GTK+ 2.22 */
+       /* gtk_icon_view_set_item_orientation (GTK_ICON_VIEW(obj), */
+       /*                                  GTK_ORIENTATION_HORIZONTAL); */
+
+       g_signal_connect (G_OBJECT(obj), "item-activated",
+                         G_CALLBACK(item_activated), NULL);
+
+       gtk_icon_view_set_selection_mode (GTK_ICON_VIEW(obj),
+                                         GTK_SELECTION_MULTIPLE);
+       /* drag & drop */
+       gtk_icon_view_enable_model_drag_source (GTK_ICON_VIEW(obj), 0, NULL, 0,
+                                               GDK_ACTION_COPY);
+       gtk_drag_source_add_uri_targets(GTK_WIDGET(obj));
+       g_signal_connect (obj, "drag-data-get", G_CALLBACK(on_drag_data_get), NULL);
+}
+
+
+static void
+mu_msg_attach_view_finalize (GObject *obj)
+{
+       set_message (MU_MSG_ATTACH_VIEW(obj), NULL);
+
+       G_OBJECT_CLASS(parent_class)->finalize (obj);
+}
+
+GtkWidget*
+mu_msg_attach_view_new (void)
+{
+       return GTK_WIDGET(g_object_new(MU_TYPE_MSG_ATTACH_VIEW, NULL));
+}
+
+struct _CBData {
+       GtkListStore *store;
+       guint count;
+};
+typedef struct _CBData CBData;
+
+
+
+static void
+each_part (MuMsg *msg, MuMsgPart *part, CBData *cbdata)
+{
+       GtkTreeIter treeiter;
+       GdkPixbuf *pixbuf;
+       char ctype[128];
+
+       if (!mu_msg_part_maybe_attachment(part))
+               return;
+
+       if (!part->type || !part->subtype)
+               snprintf (ctype, sizeof(ctype), "%s",
+                         "application/octet-stream");
+       else
+               snprintf (ctype, sizeof(ctype), "%s/%s",
+                         part->type, part->subtype);
+
+       pixbuf = mu_widget_util_get_icon_pixbuf_for_content_type (ctype, 16);
+       if (!pixbuf) {
+               g_debug ("%s: could not get icon pixbuf for '%s'",
+                        __func__, ctype);
+               pixbuf = mu_widget_util_get_icon_pixbuf_for_content_type
+                       ("application/octet-stream", 16);
+       }
+
+       gtk_list_store_append (cbdata->store, &treeiter);
+       gtk_list_store_set (cbdata->store, &treeiter,
+                           NAME_COL, mu_msg_part_get_filename (part, TRUE),
+                           ICON_COL, pixbuf,
+                           PARTNUM_COL, part->index,
+                           -1);
+       if (pixbuf)
+               g_object_unref (pixbuf);
+
+       ++cbdata->count;
+}
+
+gint
+mu_msg_attach_view_set_message (MuMsgAttachView *self, MuMsg *msg)
+{
+       GtkListStore *store;
+       CBData cbdata;
+
+       g_return_val_if_fail (MU_IS_MSG_ATTACH_VIEW(self), -1);
+
+       store = GTK_LIST_STORE (gtk_icon_view_get_model (GTK_ICON_VIEW(self)));
+       gtk_list_store_clear (store);
+
+       set_message (self, msg);
+
+       if (!msg)
+               return 0;
+
+
+       cbdata.store = store;
+       cbdata.count = 0;
+       mu_msg_part_foreach (msg, MU_MSG_OPTION_NONE,
+                            (MuMsgPartForeachFunc)each_part, &cbdata);
+
+       return cbdata.count;
+}
diff --git a/toys/mug/mu-msg-attach-view.h b/toys/mug/mu-msg-attach-view.h
new file mode 100644 (file)
index 0000000..2da3806
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+** 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.
+**
+*/
+
+
+#ifndef __MU_MSG_ATTACH_VIEW_H__
+#define __MU_MSG_ATTACH_VIEW_H__
+
+#include <gtk/gtk.h>
+#include <mu-msg.h>
+
+G_BEGIN_DECLS
+
+/* convenience macros */
+#define MU_TYPE_MSG_ATTACH_VIEW             (mu_msg_attach_view_get_type())
+#define MU_MSG_ATTACH_VIEW(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj),MU_TYPE_MSG_ATTACH_VIEW,MuMsgAttachView))
+#define MU_MSG_ATTACH_VIEW_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass),MU_TYPE_MSG_ATTACH_VIEW,MuMsgAttachViewClass))
+#define MU_IS_MSG_ATTACH_VIEW(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj),MU_TYPE_MSG_ATTACH_VIEW))
+#define MU_IS_MSG_ATTACH_VIEW_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass),MU_TYPE_MSG_ATTACH_VIEW))
+#define MU_MSG_ATTACH_VIEW_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj),MU_TYPE_MSG_ATTACH_VIEW,MuMsgAttachViewClass))
+
+typedef struct _MuMsgAttachView      MuMsgAttachView;
+typedef struct _MuMsgAttachViewClass MuMsgAttachViewClass;
+typedef struct _MuMsgAttachViewPrivate         MuMsgAttachViewPrivate;
+
+struct _MuMsgAttachView {
+        GtkIconView parent;
+       /* insert public members, if any */
+
+       /* private */
+       MuMsgAttachViewPrivate *_priv;
+};
+
+struct _MuMsgAttachViewClass {
+       GtkIconViewClass parent_class;
+       void (* attach_activated) (MuMsgAttachView* obj, guint partnum,
+                                  MuMsg *msg);
+};
+
+/* member functions */
+GType        mu_msg_attach_view_get_type    (void) G_GNUC_CONST;
+
+/* parameter-less _new function (constructor) */
+/* if this is a kind of GtkWidget, it should probably return at GtkWidget* */
+GtkWidget*   mu_msg_attach_view_new         (void);
+
+/* returns # of attachments */
+int  mu_msg_attach_view_set_message (MuMsgAttachView *self, MuMsg *msg);
+
+G_END_DECLS
+
+#endif /* __MU_MSG_ATTACH_VIEW_H__ */
diff --git a/toys/mug/mu-msg-body-view.c b/toys/mug/mu-msg-body-view.c
new file mode 100644 (file)
index 0000000..28b37e1
--- /dev/null
@@ -0,0 +1,379 @@
+/*
+** Copyright (C) 2011-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, 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-msg-body-view.h"
+#include <mu-msg-part.h>
+
+enum _ViewMode {
+       VIEW_MODE_MSG,
+       VIEW_MODE_SOURCE,
+       VIEW_MODE_NOTE,
+
+       VIEW_MODE_NONE
+};
+typedef enum _ViewMode ViewMode;
+
+/* 'private'/'protected' functions */
+static void mu_msg_body_view_class_init (MuMsgBodyViewClass *klass);
+static void mu_msg_body_view_init       (MuMsgBodyView *obj);
+static void mu_msg_body_view_finalize   (GObject *obj);
+
+/* list my signals  */
+enum {
+       ACTION_REQUESTED,
+       /* MY_SIGNAL_2, */
+       LAST_SIGNAL
+};
+
+
+struct _MuMsgBodyViewPrivate {
+       WebKitSettings *_settings;
+       MuMsg             *_msg;
+       ViewMode          _view_mode;
+};
+
+#define MU_MSG_BODY_VIEW_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
+                                             MU_TYPE_MSG_BODY_VIEW, \
+                                             MuMsgBodyViewPrivate))
+/* globals */
+static WebKitWebViewClass *parent_class = NULL;
+
+static guint signals[LAST_SIGNAL] = {0};
+
+G_DEFINE_TYPE (MuMsgBodyView, mu_msg_body_view, WEBKIT_TYPE_WEB_VIEW);
+
+static void
+set_message (MuMsgBodyView *self, MuMsg *msg)
+{
+       if (self->_priv->_msg == msg)
+               return; /* nothing to todo */
+
+       if (self->_priv->_msg)  {
+               mu_msg_unref (self->_priv->_msg);
+               self->_priv->_msg = NULL;
+       }
+
+       if (msg)
+               self->_priv->_msg = mu_msg_ref (msg);
+}
+
+static void
+mu_msg_body_view_class_init (MuMsgBodyViewClass *klass)
+{
+       GObjectClass *gobject_class;
+       gobject_class = (GObjectClass*) klass;
+
+       parent_class            = g_type_class_peek_parent (klass);
+       gobject_class->finalize = mu_msg_body_view_finalize;
+
+       g_type_class_add_private (gobject_class, sizeof(MuMsgBodyViewPrivate));
+
+       signals[ACTION_REQUESTED] =
+               g_signal_new ("action-requested",
+                             G_TYPE_FROM_CLASS (gobject_class),
+                             G_SIGNAL_RUN_FIRST,
+                             G_STRUCT_OFFSET (MuMsgBodyViewClass,
+                                              action_requested),
+                             NULL, NULL,
+                             g_cclosure_marshal_VOID__STRING,
+                             G_TYPE_NONE, 1, G_TYPE_STRING);
+}
+
+static char*
+save_file_for_cid (MuMsg *msg, const char* cid)
+{
+       gint idx;
+       gchar *filepath;
+       gboolean rv;
+       GError *err;
+
+       g_return_val_if_fail (msg, NULL);
+       g_return_val_if_fail (cid, NULL);
+
+       idx = mu_msg_find_index_for_cid (msg, MU_MSG_OPTION_NONE, cid);
+       if (idx < 0) {
+               g_warning ("%s: cannot find %s", __func__, cid);
+               return NULL;
+       }
+
+       filepath = mu_msg_part_get_cache_path (msg, MU_MSG_OPTION_NONE, idx, NULL);
+       if (!filepath) {
+               g_warning ("%s: cannot create filepath", filepath);
+               return NULL;
+       }
+
+       err = NULL;
+       rv = mu_msg_part_save (msg, MU_MSG_OPTION_USE_EXISTING,
+                              filepath, idx, &err);
+       if (!rv) {
+               g_warning ("%s: failed to save %s: %s", __func__, filepath,
+                          err&&err->message?err->message:"error");
+               g_clear_error (&err);
+               g_free (filepath);
+               filepath = NULL;
+       }
+
+       return filepath;
+}
+
+
+static gboolean
+on_navigation_policy_decision_requested (MuMsgBodyView *self,
+                                        WebKitPolicyDecision *decision,
+                                         WebKitPolicyDecisionType decision_type,
+                                        gpointer data)
+{
+       /* const char* uri; */
+
+       /* uri    = webkit_network_request_get_uri (request); */
+
+       /* XXX if it wasn't a user click, don't navigate */
+       if (decision_type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
+                webkit_policy_decision_ignore (decision);
+               return TRUE;
+       }
+
+
+       /* /\* if there are 'cmd:<action>" links in the body text of */
+       /*  * mu-internal messages (ie., notification from mu, not real */
+       /*  * e-mail messages), we emit the 'action requested' */
+       /*  * signal. this allows e.g triggering a database refresh from */
+       /*  * a <a href="cmd:refresh">Refresh</a> link */
+       /*  *\/ */
+       /* if (g_ascii_strncasecmp (uri, "cmd:", 4) == 0)  { */
+       /*      if (self->_priv->_view_mode == VIEW_MODE_NOTE) { */
+       /*              g_signal_emit (G_OBJECT(self), */
+       /*                             signals[ACTION_REQUESTED], 0, */
+       /*                             uri + 4); */
+       /*      } */
+       /*      return TRUE; */
+       /* } */
+
+       /* /\* don't try to play files on our local file system, this is not something */
+       /*  * external content should do.*\/ */
+       /* if (!mu_util_is_local_file(uri)) */
+       /*      mu_util_play (uri, FALSE, TRUE, NULL); */
+
+       return TRUE;
+}
+
+static void
+on_resource_load_started (MuMsgBodyView *self, WebKitWebResource *resource,
+                           WebKitURIRequest *request, gpointer data)
+{
+       const char*  uri;
+       MuMsg       *msg;
+
+       msg = self->_priv->_msg;
+       uri = webkit_uri_request_get_uri (request);
+
+       /* g_warning ("%s: %s", __func__, uri); */
+
+       if (g_ascii_strncasecmp (uri, "cid:", 4) == 0) {
+               gchar *filepath;
+               filepath = save_file_for_cid (msg, uri);
+               if (filepath) {
+                       gchar *fileuri;
+                       fileuri = g_strdup_printf ("file://%s", filepath);
+                       webkit_uri_request_set_uri (request, fileuri);
+                       g_free (fileuri);
+                       g_free (filepath);
+               }
+       }
+}
+
+
+static void
+on_menu_item_activate (GtkMenuItem *item, MuMsgBodyView *self)
+{
+       g_signal_emit (G_OBJECT(self),
+                      signals[ACTION_REQUESTED], 0,
+                      g_object_get_data (G_OBJECT(item), "action"));
+}
+
+static void
+popup_menu (MuMsgBodyView *self, guint button, guint32 activate_time)
+{
+       GtkWidget *menu;
+       int i;
+       struct {
+               const char* title;
+               const char* action;
+               ViewMode mode;
+       } actions[] = {
+               { "View source...", "view-source", VIEW_MODE_MSG },
+               { "View message...", "view-message", VIEW_MODE_SOURCE },
+       };
+
+       menu = gtk_menu_new ();
+
+       for (i = 0; i != G_N_ELEMENTS(actions); ++i) {
+               GtkWidget *item;
+
+               if (self->_priv->_view_mode != actions[i].mode)
+                       continue;
+
+               item = gtk_menu_item_new_with_label(actions[i].title);
+               g_object_set_data (G_OBJECT(item), "action", (gpointer)actions[i].action);
+               g_signal_connect (item, "activate", G_CALLBACK(on_menu_item_activate),
+                                 self);
+               gtk_menu_attach (GTK_MENU(menu), item, 0, 1, i, i+1);
+               gtk_widget_show (item);
+       }
+       gtk_menu_popup (GTK_MENU(menu), NULL, NULL, NULL, NULL, 0, 0);
+}
+
+
+static gboolean
+on_button_press_event (MuMsgBodyView *self, GdkEventButton *event, gpointer data)
+{
+    /* ignore all but the first (typically, left) mouse button */
+       switch (event->button) {
+       case 1: return FALSE; /* propagate, let widget handle it */
+       case 3:
+               /* no popup menus for notes */
+               if (self->_priv->_view_mode != VIEW_MODE_NOTE)
+                       popup_menu (self, event->button, event->time);
+               break;
+       default: return TRUE; /* ignore */
+       }
+
+       return (event->button > 1) ? TRUE : FALSE;
+}
+
+
+static void
+mu_msg_body_view_init (MuMsgBodyView *obj)
+{
+       obj->_priv = MU_MSG_BODY_VIEW_GET_PRIVATE(obj);
+
+       obj->_priv->_msg = NULL;
+       obj->_priv->_view_mode = VIEW_MODE_NONE;
+
+       obj->_priv->_settings = webkit_settings_new ();
+       g_object_set (G_OBJECT(obj->_priv->_settings),
+                     "enable-javascript", FALSE,
+                     "auto-load-images", TRUE,
+                     "enable-plugins", FALSE,
+                     NULL);
+
+       webkit_web_view_set_settings (WEBKIT_WEB_VIEW(obj), obj->_priv->_settings);
+
+       /* to support cid: */
+       g_signal_connect (obj, "resource-load-started",
+                         G_CALLBACK (on_resource_load_started), NULL);
+       g_signal_connect (obj, "button-press-event",
+                         G_CALLBACK(on_button_press_event), NULL);
+}
+
+static void
+mu_msg_body_view_finalize (GObject *obj)
+{
+       MuMsgBodyViewPrivate *priv;
+
+       priv = MU_MSG_BODY_VIEW_GET_PRIVATE(obj);
+       if (priv && priv->_settings)
+               g_object_unref (priv->_settings);
+
+       set_message (MU_MSG_BODY_VIEW(obj), NULL);
+
+       G_OBJECT_CLASS(parent_class)->finalize (obj);
+}
+
+GtkWidget*
+mu_msg_body_view_new (void)
+{
+       return GTK_WIDGET(g_object_new(MU_TYPE_MSG_BODY_VIEW, NULL));
+}
+
+
+static void
+set_html (MuMsgBodyView *self, const char* html)
+{
+       g_return_if_fail (MU_IS_MSG_BODY_VIEW(self));
+
+       webkit_web_view_load_html (WEBKIT_WEB_VIEW(self),
+                                   html ? html : "",
+                                   NULL);
+}
+
+static void
+set_text (MuMsgBodyView *self, const char* txt)
+{
+       g_return_if_fail (MU_IS_MSG_BODY_VIEW(self));
+
+       webkit_web_view_load_plain_text (WEBKIT_WEB_VIEW(self), txt ? txt : "");
+}
+
+void
+mu_msg_body_view_set_message (MuMsgBodyView *self, MuMsg *msg)
+{
+       const char* data;
+
+       g_return_if_fail (self);
+
+       set_message (self, msg);
+
+       data = msg ? mu_msg_get_body_html (msg, MU_MSG_OPTION_NONE) : "";
+       if (data)
+               set_html (self, data);
+       else
+               set_text (self,
+                         mu_msg_get_body_text (msg, MU_MSG_OPTION_NONE));
+
+       self->_priv->_view_mode = VIEW_MODE_MSG;
+}
+
+
+void
+mu_msg_body_view_set_message_source (MuMsgBodyView *self, MuMsg *msg)
+{
+       const gchar *path;
+       gchar *data;
+
+       g_return_if_fail (MU_IS_MSG_BODY_VIEW(self));
+       g_return_if_fail (msg);
+
+       set_message (self, NULL);
+
+       path = msg ? mu_msg_get_path (msg) : NULL;
+
+       if (path && g_file_get_contents (path, &data, NULL, NULL)) {
+               set_text (self, data);
+               g_free (data);
+       } else
+               set_text (self, "");
+
+       self->_priv->_view_mode = VIEW_MODE_SOURCE;
+}
+
+
+
+void
+mu_msg_body_view_set_note (MuMsgBodyView *self, const gchar *html)
+{
+       g_return_if_fail (self);
+       g_return_if_fail (html);
+
+       set_message (self, NULL);
+
+       set_html (self, html);
+
+       self->_priv->_view_mode = VIEW_MODE_NOTE;
+}
diff --git a/toys/mug/mu-msg-body-view.h b/toys/mug/mu-msg-body-view.h
new file mode 100644 (file)
index 0000000..f8659f8
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_MSG_BODY_VIEW_H__
+#define __MU_MSG_BODY_VIEW_H__
+
+#include <webkit2/webkit2.h>
+#include <mu-msg.h>
+
+G_BEGIN_DECLS
+
+/* convenience macros */
+#define MU_TYPE_MSG_BODY_VIEW             (mu_msg_body_view_get_type())
+#define MU_MSG_BODY_VIEW(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj),MU_TYPE_MSG_BODY_VIEW,MuMsgBodyView))
+#define MU_MSG_BODY_VIEW_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass),MU_TYPE_MSG_BODY_VIEW,MuMsgBodyViewClass))
+#define MU_IS_MSG_BODY_VIEW(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj),MU_TYPE_MSG_BODY_VIEW))
+#define MU_IS_MSG_BODY_VIEW_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass),MU_TYPE_MSG_BODY_VIEW))
+#define MU_MSG_BODY_VIEW_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj),MU_TYPE_MSG_BODY_VIEW,MuMsgBodyViewClass))
+
+typedef struct _MuMsgBodyView      MuMsgBodyView;
+typedef struct _MuMsgBodyViewClass MuMsgBodyViewClass;
+typedef struct _MuMsgBodyViewPrivate         MuMsgBodyViewPrivate;
+
+
+struct _MuMsgBodyView {
+        WebKitWebView parent;
+       /* insert public members, if any */
+
+       /* private */
+       MuMsgBodyViewPrivate *_priv;
+};
+
+struct _MuMsgBodyViewClass {
+       WebKitWebViewClass parent_class;
+
+       /* supported actions: "reindex", "view-source" */
+       void (* action_requested) (MuMsgBodyView* self, const char* action);
+};
+
+/* member functions */
+GType        mu_msg_body_view_get_type    (void) G_GNUC_CONST;
+
+/* parameter-less _new function (constructor) */
+/* if this is a kind of GtkWidget, it should probably return at GtkWidget* */
+GtkWidget*    mu_msg_body_view_new         (void);
+
+void mu_msg_body_view_set_message (MuMsgBodyView *self, MuMsg *msg);
+void mu_msg_body_view_set_note (MuMsgBodyView *self, const gchar *html);
+void mu_msg_body_view_set_message_source (MuMsgBodyView *self, MuMsg *msg);
+
+G_END_DECLS
+
+#endif /* __MU_MSG_BODY_VIEW_H__ */
diff --git a/toys/mug/mu-msg-header-view.c b/toys/mug/mu-msg-header-view.c
new file mode 100644 (file)
index 0000000..f1da5ac
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+** 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.
+**
+*/
+
+#include "mu-msg-header-view.h"
+
+#include <utils/mu-str.h>
+#include <utils/mu-date.h>
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_CONFIG_H*/
+
+/* 'private'/'protected' functions */
+static void mu_msg_header_view_class_init (MuMsgHeaderViewClass *klass);
+static void mu_msg_header_view_init       (MuMsgHeaderView *obj);
+static void mu_msg_header_view_finalize   (GObject *obj);
+
+/* list my signals  */
+enum {
+       /* MY_SIGNAL_1, */
+       /* MY_SIGNAL_2, */
+       LAST_SIGNAL
+};
+
+struct _MuMsgHeaderViewPrivate {
+       GtkWidget *_grid;
+};
+#define MU_MSG_HEADER_VIEW_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
+                                               MU_TYPE_MSG_HEADER_VIEW, \
+                                               MuMsgHeaderViewPrivate))
+/* globals */
+static GtkBoxClass *parent_class = NULL;
+
+/* uncomment the following if you have defined any signals */
+/* static guint signals[LAST_SIGNAL] = {0}; */
+
+G_DEFINE_TYPE (MuMsgHeaderView, mu_msg_header_view, GTK_TYPE_BOX);
+
+
+static void
+mu_msg_header_view_class_init (MuMsgHeaderViewClass *klass)
+{
+       GObjectClass *gobject_class;
+       gobject_class = (GObjectClass*) klass;
+
+       parent_class            = g_type_class_peek_parent (klass);
+       gobject_class->finalize = mu_msg_header_view_finalize;
+
+       g_type_class_add_private (gobject_class, sizeof(MuMsgHeaderViewPrivate));
+
+       /* signal definitions go here, e.g.: */
+/*     signals[MY_SIGNAL_1] = */
+/*             g_signal_new ("my_signal_1",....); */
+/*     signals[MY_SIGNAL_2] = */
+/*             g_signal_new ("my_signal_2",....); */
+/*     etc. */
+}
+
+static void
+mu_msg_header_view_init (MuMsgHeaderView *obj)
+{
+
+/*     static GtkBoxClass *parent_class = NULL; */
+       obj->_priv = MU_MSG_HEADER_VIEW_GET_PRIVATE(obj);
+       obj->_priv->_grid = NULL;
+}
+
+static void
+mu_msg_header_view_finalize (GObject *obj)
+{
+       G_OBJECT_CLASS(parent_class)->finalize (obj);
+}
+
+GtkWidget*
+mu_msg_header_view_new (void)
+{
+       return GTK_WIDGET(g_object_new(MU_TYPE_MSG_HEADER_VIEW, NULL));
+}
+
+
+static GtkWidget*
+get_label (const gchar *txt, gboolean istitle)
+{
+       GtkWidget *label;
+
+       label = gtk_label_new (NULL);
+       if (istitle) {
+               char* markup;
+               markup = g_strdup_printf ("<b>%s</b>: ", txt);
+               gtk_label_set_markup (GTK_LABEL(label), markup);
+               gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_RIGHT);
+               g_free (markup);
+       } else {
+               gtk_label_set_selectable (GTK_LABEL (label), TRUE);
+               gtk_label_set_text (GTK_LABEL(label), txt ? txt : "");
+               gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_LEFT);
+       }
+
+       return label;
+}
+
+static gboolean
+add_row (GtkWidget *grid, guint row, const char* fieldname, const char *value,
+        gboolean showempty)
+{
+       GtkWidget *label, *al;
+
+       if (!value && !showempty)
+               return FALSE;
+
+       gtk_grid_insert_row (GTK_GRID(grid), row);
+
+       label = get_label (fieldname, TRUE);
+       al = gtk_alignment_new (0.0, 0.0, 0.0, 0.0);
+       gtk_container_add (GTK_CONTAINER (al), label);
+
+       gtk_grid_attach (GTK_GRID(grid), al, 0, row, 1, 1);
+
+       al = gtk_alignment_new (0.0, 1.0, 0.0, 0.0);
+
+       label = get_label (value, FALSE);
+       gtk_container_add (GTK_CONTAINER (al), label);
+       gtk_grid_attach (GTK_GRID(grid), al, 1, row, 1, 1);
+
+       return TRUE;
+}
+
+
+static GtkWidget*
+get_grid (MuMsg *msg)
+{
+       GtkWidget *grid;
+       int row;
+
+       row = 0;
+       grid = gtk_grid_new (); /* 5 2 */
+
+       gtk_grid_insert_column (GTK_GRID(grid), 0);
+       gtk_grid_insert_column (GTK_GRID(grid), 1);
+
+       if (add_row (grid, row, "From", mu_msg_get_from (msg), TRUE))
+               ++row;
+       if (add_row (grid, row, "To", mu_msg_get_to (msg), FALSE))
+               ++row;
+       if (add_row (grid, row, "Cc", mu_msg_get_cc (msg), FALSE))
+               ++row;
+       if (add_row (grid, row, "Subject", mu_msg_get_subject (msg), TRUE))
+               ++row;
+       if (add_row (grid, row, "Date", mu_date_str_s
+                         ("%c", mu_msg_get_date (msg)),TRUE))
+               ++row;
+
+       return grid;
+}
+
+void
+mu_msg_header_view_set_message (MuMsgHeaderView *self, MuMsg *msg)
+{
+       g_return_if_fail (MU_IS_MSG_HEADER_VIEW(self));
+
+       if (self->_priv->_grid) {
+               gtk_container_remove (GTK_CONTAINER(self), self->_priv->_grid);
+               self->_priv->_grid = NULL;
+       }
+
+       if (msg) {
+               self->_priv->_grid = get_grid (msg);
+               gtk_box_pack_start (GTK_BOX(self), self->_priv->_grid,
+                                   TRUE, TRUE, 0);
+               gtk_widget_show_all (self->_priv->_grid);
+       }
+}
diff --git a/toys/mug/mu-msg-header-view.h b/toys/mug/mu-msg-header-view.h
new file mode 100644 (file)
index 0000000..b2ff72d
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_MSG_HEADER_VIEW_H__
+#define __MU_MSG_HEADER_VIEW_H__
+
+#include <gtk/gtk.h>
+#include <mu-msg.h>
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_CONFIG_H*/
+
+G_BEGIN_DECLS
+
+/* convenience macros */
+#define MU_TYPE_MSG_HEADER_VIEW             (mu_msg_header_view_get_type())
+#define MU_MSG_HEADER_VIEW(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj),MU_TYPE_MSG_HEADER_VIEW,MuMsgHeaderView))
+#define MU_MSG_HEADER_VIEW_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass),MU_TYPE_MSG_HEADER_VIEW,MuMsgHeaderViewClass))
+#define MU_IS_MSG_HEADER_VIEW(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj),MU_TYPE_MSG_HEADER_VIEW))
+#define MU_IS_MSG_HEADER_VIEW_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass),MU_TYPE_MSG_HEADER_VIEW))
+#define MU_MSG_HEADER_VIEW_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj),MU_TYPE_MSG_HEADER_VIEW,MuMsgHeaderViewClass))
+
+typedef struct _MuMsgHeaderView      MuMsgHeaderView;
+typedef struct _MuMsgHeaderViewClass MuMsgHeaderViewClass;
+typedef struct _MuMsgHeaderViewPrivate         MuMsgHeaderViewPrivate;
+
+struct _MuMsgHeaderView {
+       GtkBox parent;
+       /* insert public members, if any */
+       /* private */
+       MuMsgHeaderViewPrivate *_priv;
+};
+
+struct _MuMsgHeaderViewClass {
+       GtkBoxClass parent_class;
+       /* insert signal callback declarations, e.g. */
+       /* void (* my_event) (MuMsgHeaderView* obj); */
+};
+
+/* member functions */
+GType        mu_msg_header_view_get_type    (void) G_GNUC_CONST;
+
+/* parameter-less _new function (constructor) */
+/* if this is a kind of GtkWidget, it should probably return at GtkWidget* */
+GtkWidget*   mu_msg_header_view_new         (void);
+
+void mu_msg_header_view_set_message (MuMsgHeaderView *self, MuMsg *msg);
+
+
+G_END_DECLS
+
+#endif /* __MU_MSG_HEADER_VIEW_H__ */
diff --git a/toys/mug/mu-msg-view.c b/toys/mug/mu-msg-view.c
new file mode 100644 (file)
index 0000000..b2b19ce
--- /dev/null
@@ -0,0 +1,242 @@
+/*
+** 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.
+**
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_CONFIG_H*/
+
+#include "mu-msg-view.h"
+#include "mu-msg-body-view.h"
+#include "mu-msg-header-view.h"
+#include "mu-msg-attach-view.h"
+#include <mu-msg-part.h>
+
+
+/* 'private'/'protected' functions */
+static void mu_msg_view_class_init (MuMsgViewClass *klass);
+static void mu_msg_view_init       (MuMsgView *obj);
+static void mu_msg_view_finalize   (GObject *obj);
+
+/* list my signals  */
+enum {
+       /* MY_SIGNAL_1, */
+       /* MY_SIGNAL_2, */
+       LAST_SIGNAL
+};
+
+struct _MuMsgViewPrivate {
+       GtkWidget *_headers, *_attach, *_attachexpander, *_body;
+       MuMsg *_msg;
+};
+#define MU_MSG_VIEW_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
+                                         MU_TYPE_MSG_VIEW, \
+                                         MuMsgViewPrivate))
+/* globals */
+static GtkBoxClass *parent_class = NULL;
+
+/* uncomment the following if you have defined any signals */
+/* static guint signals[LAST_SIGNAL] = {0}; */
+
+G_DEFINE_TYPE (MuMsgView, mu_msg_view, GTK_TYPE_BOX);
+
+static void
+set_message (MuMsgView *self, MuMsg *msg)
+{
+       if (self->_priv->_msg == msg)
+               return; /* nothing to todo */
+
+       if (self->_priv->_msg)  {
+               mu_msg_unref (self->_priv->_msg);
+               self->_priv->_msg = NULL;
+       }
+
+       if (msg)
+               self->_priv->_msg = mu_msg_ref (msg);
+}
+
+
+static void
+mu_msg_view_class_init (MuMsgViewClass *klass)
+{
+       GObjectClass *gobject_class;
+       gobject_class = (GObjectClass*) klass;
+
+       parent_class            = g_type_class_peek_parent (klass);
+       gobject_class->finalize = mu_msg_view_finalize;
+
+       g_type_class_add_private (gobject_class, sizeof(MuMsgViewPrivate));
+
+       /* signal definitions go here, e.g.: */
+/*     signals[MY_SIGNAL_1] = */
+/*             g_signal_new ("my_signal_1",....); */
+/*     signals[MY_SIGNAL_2] = */
+/*             g_signal_new ("my_signal_2",....); */
+/*     etc. */
+}
+
+
+static void
+on_body_action_requested (GtkWidget *w, const char* action,
+                         MuMsgView *self)
+{
+       if (g_strcmp0 (action, "view-source") == 0) {
+               if (self->_priv->_msg)
+                       mu_msg_view_set_message_source (self, self->_priv->_msg);
+
+       } else if (g_strcmp0 (action, "view-message") == 0) {
+               if (self->_priv->_msg)
+                       mu_msg_view_set_message (self, self->_priv->_msg);
+
+       } else if (g_strcmp0 (action, "reindex") == 0)
+               g_warning ("reindex");
+       else
+               g_warning ("unknown action '%s'", action);
+}
+
+static void
+on_attach_activated (GtkWidget *w, guint partnum, MuMsg *msg)
+{
+       gchar *filepath;
+       GError *err;
+
+       err = NULL;
+       filepath = mu_msg_part_get_cache_path (msg, MU_MSG_OPTION_NONE, partnum,
+                                              &err);
+       if (!filepath) {
+               g_warning ("failed to get cache path: %s",
+                          err&&err->message?err->message:"error");
+               g_clear_error (&err);
+               return;
+       }
+
+       if (!mu_msg_part_save (msg, MU_MSG_OPTION_USE_EXISTING,
+                              filepath, partnum, &err)) {
+               g_warning ("failed to save %s: %s", filepath,
+                          err&&err->message?err->message:"error");
+               g_clear_error (&err);
+               return;
+
+       } else
+               mu_util_play (filepath, TRUE, FALSE, NULL);
+
+       g_free (filepath);
+}
+
+
+static void
+mu_msg_view_init (MuMsgView *self)
+{
+       gtk_orientable_set_orientation (GTK_ORIENTABLE(self),
+                                       GTK_ORIENTATION_VERTICAL);
+
+       self->_priv = MU_MSG_VIEW_GET_PRIVATE(self);
+
+        self->_priv->_msg            = NULL;
+       self->_priv->_headers        = mu_msg_header_view_new ();
+       self->_priv->_attach         = mu_msg_attach_view_new ();
+       self->_priv->_attachexpander = gtk_expander_new_with_mnemonic
+               ("_Attachments");
+
+        gtk_container_add (GTK_CONTAINER(self->_priv->_attachexpander),
+                          self->_priv->_attach);
+       g_signal_connect (self->_priv->_attach, "attach-activated",
+                         G_CALLBACK(on_attach_activated),
+                         self);
+
+       self->_priv->_body = mu_msg_body_view_new ();
+       g_signal_connect (self->_priv->_body,
+                         "action-requested",
+                         G_CALLBACK(on_body_action_requested),
+                         self);
+
+       gtk_box_pack_start (GTK_BOX(self), self->_priv->_headers,
+                           FALSE, FALSE, 2);
+       gtk_box_pack_start (GTK_BOX(self), self->_priv->_attachexpander,
+                           FALSE, FALSE, 2);
+       gtk_box_pack_start (GTK_BOX(self), self->_priv->_body,
+                           TRUE, TRUE, 2);
+}
+
+static void
+mu_msg_view_finalize (GObject *obj)
+{
+       set_message (MU_MSG_VIEW (obj), NULL);
+
+       G_OBJECT_CLASS(parent_class)->finalize (obj);
+}
+
+GtkWidget*
+mu_msg_view_new (void)
+{
+       return GTK_WIDGET(g_object_new(MU_TYPE_MSG_VIEW, NULL));
+}
+
+void
+mu_msg_view_set_message (MuMsgView *self, MuMsg *msg)
+{
+       gint attachnum;
+
+       g_return_if_fail (MU_IS_MSG_VIEW(self));
+
+       set_message (self, msg);
+
+       mu_msg_header_view_set_message (MU_MSG_HEADER_VIEW(self->_priv->_headers),
+                                       msg);
+       attachnum = mu_msg_attach_view_set_message (MU_MSG_ATTACH_VIEW(self->_priv->_attach),
+                                                   msg);
+
+       mu_msg_body_view_set_message (MU_MSG_BODY_VIEW(self->_priv->_body),
+                                     msg);
+
+       gtk_widget_set_visible  (self->_priv->_headers, TRUE);
+       gtk_widget_set_visible  (self->_priv->_attachexpander, attachnum > 0);
+       gtk_widget_set_visible  (self->_priv->_body, TRUE);
+}
+
+
+
+void
+mu_msg_view_set_message_source (MuMsgView *self, MuMsg *msg)
+{
+       g_return_if_fail (MU_IS_MSG_VIEW(self));
+
+       set_message (self, msg);
+
+       mu_msg_body_view_set_message_source (MU_MSG_BODY_VIEW(self->_priv->_body),
+                                            msg);
+
+       gtk_widget_set_visible  (self->_priv->_headers, FALSE);
+       gtk_widget_set_visible  (self->_priv->_attachexpander, FALSE);
+       gtk_widget_set_visible  (self->_priv->_body, TRUE);
+}
+
+
+
+void
+mu_msg_view_set_note (MuMsgView *self, const gchar* html)
+{
+       g_return_if_fail (MU_IS_MSG_VIEW(self));
+
+       gtk_widget_set_visible  (self->_priv->_headers, FALSE);
+       gtk_widget_set_visible  (self->_priv->_attachexpander, FALSE);
+       gtk_widget_set_visible  (self->_priv->_body, TRUE);
+
+       mu_msg_body_view_set_note (MU_MSG_BODY_VIEW(self->_priv->_body),
+                                  html);
+}
diff --git a/toys/mug/mu-msg-view.h b/toys/mug/mu-msg-view.h
new file mode 100644 (file)
index 0000000..b399646
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_MSG_VIEW_H__
+#define __MU_MSG_VIEW_H__
+
+#include <gtk/gtk.h>
+#include <mu-msg.h>
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_CONFIG_H*/
+
+G_BEGIN_DECLS
+
+/* convenience macros */
+#define MU_TYPE_MSG_VIEW             (mu_msg_view_get_type())
+#define MU_MSG_VIEW(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj),MU_TYPE_MSG_VIEW,MuMsgView))
+#define MU_MSG_VIEW_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass),MU_TYPE_MSG_VIEW,MuMsgViewClass))
+#define MU_IS_MSG_VIEW(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj),MU_TYPE_MSG_VIEW))
+#define MU_IS_MSG_VIEW_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass),MU_TYPE_MSG_VIEW))
+#define MU_MSG_VIEW_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj),MU_TYPE_MSG_VIEW,MuMsgViewClass))
+
+typedef struct _MuMsgView      MuMsgView;
+typedef struct _MuMsgViewClass MuMsgViewClass;
+typedef struct _MuMsgViewPrivate         MuMsgViewPrivate;
+
+struct _MuMsgView {
+       GtkBox parent;
+       /* private */
+       MuMsgViewPrivate *_priv;
+};
+
+struct _MuMsgViewClass {
+       GtkBoxClass parent_class;
+       /* void (* my_event) (MuMsgView* obj); */
+};
+
+/* member functions */
+GType        mu_msg_view_get_type    (void) G_GNUC_CONST;
+
+/* parameter-less _new function (constructor) */
+/* if this is a kind of GtkWidget, it should probably return at GtkWidget* */
+GtkWidget*   mu_msg_view_new         (void);
+
+void mu_msg_view_set_message (MuMsgView *self, MuMsg *msg);
+void mu_msg_view_set_note (MuMsgView *self, const gchar* html);
+void mu_msg_view_set_message_source (MuMsgView *self, MuMsg *msg);
+
+G_END_DECLS
+
+#endif /* __MU_MSG_VIEW_H__ */
diff --git a/toys/mug/mu-widget-util.c b/toys/mug/mu-widget-util.c
new file mode 100644 (file)
index 0000000..777a728
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+** 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.
+**
+*/
+
+
+#include <gtk/gtk.h>
+#include <gio/gio.h>
+#include "mu-widget-util.h"
+
+static const char*
+second_guess_content_type (const char *ctype)
+{
+       int i;
+       struct {
+               const char *orig, *subst;
+       } substtable [] = {
+               {"text", "text/plain"},
+               {"image/pjpeg", "image/jpeg" }
+       };
+
+       for (i = 0; i != G_N_ELEMENTS(substtable); ++i)
+               if (g_str_has_prefix (ctype, substtable[i].orig))
+                       return substtable[i].subst;
+
+       return "application/octet-stream";
+}
+
+static GdkPixbuf*
+get_icon_pixbuf_for_content_type (const char *ctype, size_t size)
+{
+       GIcon *icon;
+       GdkPixbuf *pixbuf;
+       GError *err;
+
+       icon = g_content_type_get_icon (ctype);
+       pixbuf = NULL;
+       err = NULL;
+
+       /* based on a snippet from http://www.gtkforums.com/about4721.html */
+       if (G_IS_THEMED_ICON(icon)) {
+               gchar const * const *names;
+               names = g_themed_icon_get_names (G_THEMED_ICON(icon));
+               pixbuf = gtk_icon_theme_load_icon (gtk_icon_theme_get_default(),
+                                                  *names, size, 0, &err);
+       } else if (G_IS_FILE_ICON(icon)) {
+               GFile *icon_file;
+               gchar *path;
+               icon_file = g_file_icon_get_file (G_FILE_ICON(icon));
+               path = g_file_get_path (icon_file);
+               pixbuf = gdk_pixbuf_new_from_file_at_size (path, size, size, &err);
+               g_free (path);
+               g_object_unref(icon_file);
+       }
+       g_object_unref(icon);
+
+       if (err) {
+               g_warning ("error: %s\n", err->message);
+               g_clear_error (&err);
+       }
+
+       return pixbuf;
+}
+
+
+GdkPixbuf*
+mu_widget_util_get_icon_pixbuf_for_content_type (const char *ctype, size_t size)
+{
+       GdkPixbuf *pixbuf;
+
+       g_return_val_if_fail (ctype, NULL);
+       g_return_val_if_fail (size > 0, NULL);
+
+       pixbuf = get_icon_pixbuf_for_content_type (ctype, size);
+       if (!pixbuf)
+               pixbuf = get_icon_pixbuf_for_content_type
+                       (second_guess_content_type (ctype), size);
+
+       return pixbuf;
+}
diff --git a/toys/mug/mu-widget-util.h b/toys/mug/mu-widget-util.h
new file mode 100644 (file)
index 0000000..c01b7eb
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MU_WIDGET_UTIL_H__
+#define __MU_WIDGET_UTIL_H__
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+
+/** 
+ * get a pixbuf (icon) for a certain content-type (ie., 'image/jpeg')
+ * 
+ * @param ctype the content-type (MIME-type)
+ * @param size the size of the icon
+ * 
+ * @return a new GdkPixbuf, or NULL in case of error. Use
+ * g_object_unref when the pixbuf is no longer needed.
+ */
+GdkPixbuf* mu_widget_util_get_icon_pixbuf_for_content_type (const char *ctype,
+                                                           size_t size)
+      G_GNUC_WARN_UNUSED_RESULT;
+
+
+#endif /*__MU_WIDGET_UTIL_H__*/
diff --git a/toys/mug/mug-msg-list-view.c b/toys/mug/mug-msg-list-view.c
new file mode 100644 (file)
index 0000000..c265a15
--- /dev/null
@@ -0,0 +1,473 @@
+/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
+/*
+** Copyright (C) 2008-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.
+**
+*/
+
+#include "mug-msg-list-view.h"
+#include "mu-query.h"
+#include "utils/mu-str.h"
+#include "utils/mu-date.h"
+#include "mu-threader.h"
+
+/* 'private'/'protected' functions */
+static void mug_msg_list_view_class_init (MugMsgListViewClass * klass);
+static void mug_msg_list_view_init (MugMsgListView * obj);
+static void mug_msg_list_view_finalize (GObject * obj);
+
+/* list my signals  */
+enum {
+       MUG_MSG_SELECTED,
+       MUG_ERROR_OCCURED,
+       LAST_SIGNAL
+};
+
+enum {
+       MUG_COL_DATESTR,
+       MUG_COL_MAILDIR,
+       MUG_COL_FLAGSSTR,
+       MUG_COL_FROM,
+       MUG_COL_TO,
+       MUG_COL_SUBJECT,
+       MUG_COL_PATH,
+       MUG_COL_PRIO,
+       MUG_COL_FLAGS,
+       MUG_COL_TIME,
+       MUG_N_COLS
+};
+
+typedef struct _MugMsgListViewPrivate MugMsgListViewPrivate;
+struct _MugMsgListViewPrivate {
+       GtkTreeStore *_store;
+       char *_xpath;
+       char *_query;
+};
+#define MUG_MSG_LIST_VIEW_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
+                                          MUG_TYPE_MSG_LIST_VIEW, \
+                                          MugMsgListViewPrivate))
+/* globals */
+static GtkTreeViewClass *parent_class = NULL;
+
+/* uncomment the following if you have defined any signals */
+static guint signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE (MugMsgListView, mug_msg_list_view, GTK_TYPE_TREE_VIEW);
+
+static void
+mug_msg_list_view_class_init (MugMsgListViewClass * klass)
+{
+       GObjectClass *gobject_class;
+       gobject_class = (GObjectClass *) klass;
+
+       parent_class = g_type_class_peek_parent (klass);
+       gobject_class->finalize = mug_msg_list_view_finalize;
+
+       g_type_class_add_private (gobject_class,
+                                 sizeof (MugMsgListViewPrivate));
+
+       signals[MUG_MSG_SELECTED] =
+           g_signal_new ("msg-selected",
+                         G_TYPE_FROM_CLASS (gobject_class),
+                         G_SIGNAL_RUN_FIRST,
+                         G_STRUCT_OFFSET (MugMsgListViewClass,
+                                          msg_selected),
+                         NULL, NULL,
+                         g_cclosure_marshal_VOID__STRING,
+                         G_TYPE_NONE, 1, G_TYPE_STRING);
+       signals[MUG_ERROR_OCCURED] =
+           g_signal_new ("error-occured",
+                         G_TYPE_FROM_CLASS (gobject_class),
+                         G_SIGNAL_RUN_FIRST,
+                         G_STRUCT_OFFSET (MugMsgListViewClass,
+                                          error_occured),
+                         NULL, NULL,
+                         g_cclosure_marshal_VOID__UINT,
+                         G_TYPE_NONE, 1, G_TYPE_UINT);
+}
+
+static void
+on_cursor_changed (GtkTreeView * tview, MugMsgListView * lst)
+{
+       GtkTreeSelection *sel;
+       GtkTreeIter iter;
+       MugMsgListViewPrivate *priv;
+
+       priv = MUG_MSG_LIST_VIEW_GET_PRIVATE (tview);
+
+       sel = gtk_tree_view_get_selection (tview);
+       if (!sel)
+               return;         /* hmmm */
+       if (gtk_tree_selection_get_selected (sel, NULL, &iter)) {
+               char *path;
+               gtk_tree_model_get (GTK_TREE_MODEL (priv->_store), &iter,
+                                   MUG_COL_PATH, &path, -1);
+               g_signal_emit (G_OBJECT (lst),
+                              signals[MUG_MSG_SELECTED], 0, path);
+               g_free (path);
+       }
+}
+
+static void
+treecell_func (GtkTreeViewColumn * tree_column, GtkCellRenderer * renderer,
+              GtkTreeModel * tree_model, GtkTreeIter * iter, gpointer data)
+{
+       MuFlags flags;
+       MuMsgPrio prio;
+
+       gtk_tree_model_get (tree_model, iter,
+                           MUG_COL_FLAGS, &flags, MUG_COL_PRIO, &prio, -1);
+
+       g_object_set (G_OBJECT (renderer),
+                     "weight", (flags & MU_FLAG_NEW) ? 800 : 400,
+                     "weight", (flags & MU_FLAG_SEEN) ? 400 : 800,
+                     "foreground", prio == MU_MSG_PRIO_HIGH ? "red" : NULL,
+                     NULL);
+}
+
+/* sortcolidx == -1 means 'sortcolidx = colidx' */
+static void
+append_col (GtkTreeView * treeview, const char *label, int colidx,
+           int sortcolidx, gint maxwidth)
+{
+       GtkTreeViewColumn *col;
+       GtkCellRenderer *renderer;
+
+       renderer = gtk_cell_renderer_text_new ();
+       g_object_set (G_OBJECT (renderer), "ellipsize", PANGO_ELLIPSIZE_END,
+                     NULL);
+
+       col = gtk_tree_view_column_new_with_attributes (label, renderer, "text",
+                                                       colidx, NULL);
+       g_object_set (G_OBJECT (col), "resizable", TRUE, NULL);
+
+       gtk_tree_view_column_set_sort_indicator (col, TRUE);
+
+       if (sortcolidx == -1)
+               sortcolidx = colidx;
+       gtk_tree_view_column_set_sort_column_id (col, sortcolidx);
+       gtk_tree_view_column_set_sizing (col, GTK_TREE_VIEW_COLUMN_FIXED);
+
+       if (maxwidth) {
+               gtk_tree_view_column_set_fixed_width (col, maxwidth);
+               gtk_tree_view_column_set_expand (col, FALSE);
+       } else
+               gtk_tree_view_column_set_expand (col, TRUE);
+
+       gtk_tree_view_column_set_cell_data_func (col, renderer,
+                                                (GtkTreeCellDataFunc)
+                                                treecell_func, NULL, NULL);
+
+       gtk_tree_view_append_column (treeview, col);
+
+       gtk_tree_view_columns_autosize (treeview);
+       gtk_tree_view_set_fixed_height_mode (treeview, TRUE);
+}
+
+static void
+mug_msg_list_view_init (MugMsgListView * obj)
+{
+       MugMsgListViewPrivate *priv;
+       GtkTreeView *tview;
+
+       priv = MUG_MSG_LIST_VIEW_GET_PRIVATE (obj);
+
+       priv->_xpath = priv->_query = NULL;
+       priv->_store = gtk_tree_store_new (MUG_N_COLS, G_TYPE_STRING,   /* date */
+                                          G_TYPE_STRING,/* folder */
+                                          G_TYPE_STRING,/* flagstr */
+                                          G_TYPE_STRING, /* from */
+                                          G_TYPE_STRING,/* to */
+                                          G_TYPE_STRING,/* subject */
+                                          G_TYPE_STRING, /* path */
+                                          G_TYPE_UINT, /* prio */
+                                          G_TYPE_UINT, /* flags */
+                                          G_TYPE_INT); /* timeval */
+
+       tview = GTK_TREE_VIEW (obj);
+       gtk_tree_view_set_model (tview, GTK_TREE_MODEL (priv->_store));
+       gtk_tree_view_set_headers_clickable (GTK_TREE_VIEW (obj), TRUE);
+       gtk_tree_view_set_grid_lines (GTK_TREE_VIEW (obj),
+                                     GTK_TREE_VIEW_GRID_LINES_VERTICAL);
+       gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (obj), TRUE);
+
+       append_col (tview, "Date", MUG_COL_DATESTR, MUG_COL_TIME, 80);
+       append_col (tview, "Folder", MUG_COL_MAILDIR, -1, 60);
+       append_col (tview, "F", MUG_COL_FLAGSSTR, -1, 25);
+       append_col (tview, "From", MUG_COL_FROM, -1, 0);
+       append_col (tview, "To", MUG_COL_TO, -1, 0);
+       append_col (tview, "Subject", MUG_COL_SUBJECT, -1, 0);
+
+       g_signal_connect (G_OBJECT (obj), "cursor-changed",
+                         G_CALLBACK (on_cursor_changed), obj);
+}
+
+static void
+mug_msg_list_view_finalize (GObject * obj)
+{
+       MugMsgListViewPrivate *priv;
+       priv = MUG_MSG_LIST_VIEW_GET_PRIVATE (obj);
+
+       if (priv->_store)
+               g_object_unref (priv->_store);
+
+       g_free (priv->_xpath);
+       g_free (priv->_query);
+
+       G_OBJECT_CLASS (parent_class)->finalize (obj);
+}
+
+void
+mug_msg_list_view_move_first (MugMsgListView * self)
+{
+       GtkTreePath *path;
+
+       g_return_if_fail (MUG_IS_MSG_LIST_VIEW (self));
+
+       path = gtk_tree_path_new_first ();
+       gtk_tree_view_set_cursor (GTK_TREE_VIEW (self), path, NULL, FALSE);
+
+       gtk_tree_path_free (path);
+}
+
+static gboolean
+msg_list_view_move (MugMsgListView * self, gboolean next)
+{
+       GtkTreePath *path;
+
+       gtk_tree_view_get_cursor (GTK_TREE_VIEW (self), &path, NULL);
+       if (!path)
+               return FALSE;
+
+       if (next)
+               gtk_tree_path_next (path);
+       else
+               gtk_tree_path_prev (path);
+
+       gtk_tree_view_set_cursor (GTK_TREE_VIEW (self), path, NULL, FALSE);
+       gtk_tree_path_free (path);
+
+       return TRUE;
+}
+
+gboolean
+mug_msg_list_view_move_next (MugMsgListView * self)
+{
+       g_return_val_if_fail (MUG_IS_MSG_LIST_VIEW (self), FALSE);
+
+       return msg_list_view_move (self, TRUE);
+}
+
+gboolean
+mug_msg_list_view_move_prev (MugMsgListView * self)
+{
+       g_return_val_if_fail (MUG_IS_MSG_LIST_VIEW (self), FALSE);
+
+       return msg_list_view_move (self, FALSE);
+}
+
+GtkWidget *
+mug_msg_list_view_new (const char *xpath)
+{
+       GtkWidget *w;
+       MugMsgListViewPrivate *priv;
+
+       g_return_val_if_fail (xpath, NULL);
+
+       w = GTK_WIDGET (g_object_new (MUG_TYPE_MSG_LIST_VIEW, NULL));
+
+       priv = MUG_MSG_LIST_VIEW_GET_PRIVATE (w);
+       priv->_xpath = g_strdup (xpath);
+
+       return w;
+}
+
+static gchar *
+empty_or_display_contact (const gchar * str)
+{
+       if (!str || *str == '\0')
+               return g_strdup ("-");
+       else
+               return mu_str_display_contact (str);
+
+}
+
+static MugError
+mu_result_to_mug_error (MuError r)
+{
+       switch (r) {
+       /* case MU_ERROR_XAPIAN_DIR_NOT_ACCESSIBLE: */
+       /*      return MUG_ERROR_XAPIAN_DIR; */
+       /* case MU_ERROR_XAPIAN_VERSION_MISMATCH: */
+       /*      return MUG_ERROR_XAPIAN_NOT_UPTODATE; */
+       case MU_ERROR_XAPIAN_QUERY:
+               return MUG_ERROR_QUERY;
+       default:
+               return MUG_ERROR_OTHER;
+       }
+}
+
+static MuMsgIter *
+run_query (const char *xpath, const char *query, MugMsgListView * self)
+{
+       GError *err;
+       MuQuery *xapian;
+       MuMsgIter *iter;
+       MuStore *store;
+       MuQueryFlags qflags;
+
+       err = NULL;
+       if (! (store = mu_store_new_readable (xpath, &err)) ||
+           ! (xapian = mu_query_new (store, &err))) {
+               if (store)
+                       mu_store_unref (store);
+               g_warning ("Error: %s", err->message);
+               g_signal_emit (G_OBJECT (self),
+                              signals[MUG_ERROR_OCCURED], 0,
+                              mu_result_to_mug_error (err->code));
+               g_error_free (err);
+               return NULL;
+       }
+       mu_store_unref (store);
+
+       qflags =
+               MU_QUERY_FLAG_DESCENDING         |
+               MU_QUERY_FLAG_SKIP_UNREADABLE    |
+               MU_QUERY_FLAG_SKIP_DUPS          |
+               MU_QUERY_FLAG_THREADS;
+
+       iter = mu_query_run (xapian, query, MU_MSG_FIELD_ID_DATE,
+                            -1, qflags, &err);
+       mu_query_destroy (xapian);
+       if (!iter) {
+               g_warning ("Error: %s", err->message);
+               g_signal_emit (G_OBJECT (self),
+                              signals[MUG_ERROR_OCCURED], 0,
+                              mu_result_to_mug_error (err->code));
+               g_error_free (err);
+               return NULL;
+       }
+
+       return iter;
+}
+
+static void
+add_row (GtkTreeStore * store, MuMsg *msg, GtkTreeIter *treeiter)
+{
+       const gchar *datestr, *flagstr;
+       gchar *from, *to;
+       time_t timeval;
+
+       timeval = mu_msg_get_date (msg);
+       datestr = timeval == 0 ? "-" : mu_date_display_s (timeval);
+       from = empty_or_display_contact (mu_msg_get_from (msg));
+       to = empty_or_display_contact (mu_msg_get_to (msg));
+       flagstr = mu_flags_to_str_s (mu_msg_get_flags (msg), MU_FLAG_TYPE_ANY);
+
+       /* if (0) { */
+       /*      GtkTreeIter myiter; */
+       /*      if (!gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL(store), */
+       /*                                                &myiter, path)) */
+       /*              g_warning ("%s: cannot get iter for %s",
+        *              __func__, path); */
+       /* } */
+
+       gtk_tree_store_set (store, treeiter,
+                           MUG_COL_DATESTR, datestr,
+                           MUG_COL_MAILDIR, mu_msg_get_maildir (msg),
+                           MUG_COL_FLAGSSTR, flagstr,
+                           MUG_COL_FROM, from,
+                           MUG_COL_TO, to,
+                           MUG_COL_SUBJECT, mu_msg_get_subject (msg),
+                           MUG_COL_PATH, mu_msg_get_path (msg),
+                           MUG_COL_PRIO, mu_msg_get_prio (msg),
+                           MUG_COL_FLAGS, mu_msg_get_flags (msg),
+                           MUG_COL_TIME, timeval, -1);
+       g_free (from);
+       g_free (to);
+}
+
+static int
+update_model (GtkTreeStore *store, const char *xpath, const char *query,
+             MugMsgListView *self)
+{
+       MuMsgIter *iter;
+       int count;
+       const MuMsgIterThreadInfo *prev_ti = NULL;
+
+       iter = run_query (xpath, query, self);
+       if (!iter) {
+               g_warning ("error: running query failed\n");
+               return -1;
+       }
+
+       for (count = 0; !mu_msg_iter_is_done (iter);
+            mu_msg_iter_next (iter), ++count) {
+
+               GtkTreeIter treeiter, prev_treeiter;
+               const MuMsgIterThreadInfo *ti;
+
+               ti = mu_msg_iter_get_thread_info (iter);
+
+               if (!prev_ti || !g_str_has_prefix (ti->threadpath,
+                                                  prev_ti->threadpath))
+                       gtk_tree_store_append (store, &treeiter, NULL);
+               else
+                       gtk_tree_store_append (store, &treeiter, &prev_treeiter);
+
+               /* don't unref msg */
+               add_row (store, mu_msg_iter_get_msg_floating (iter), &treeiter);
+
+               prev_ti = ti;
+               prev_treeiter = treeiter;
+       }
+
+       mu_msg_iter_destroy (iter);
+
+       return count;
+}
+
+int
+mug_msg_list_view_query (MugMsgListView * self, const char *query)
+{
+       MugMsgListViewPrivate *priv;
+       gboolean rv;
+
+       g_return_val_if_fail (MUG_IS_MSG_LIST_VIEW (self), FALSE);
+
+       priv = MUG_MSG_LIST_VIEW_GET_PRIVATE (self);
+       gtk_tree_store_clear (priv->_store);
+
+       g_free (priv->_query);
+       priv->_query = query ? g_strdup (query) : NULL;
+
+       if (!query)
+               return TRUE;
+
+       rv = update_model (priv->_store, priv->_xpath, query, self);
+
+       gtk_tree_view_expand_all (GTK_TREE_VIEW(self));
+
+       return rv;
+}
+
+const gchar *
+mug_msg_list_view_get_query (MugMsgListView * self)
+{
+       g_return_val_if_fail (MUG_IS_MSG_LIST_VIEW (self), NULL);
+
+       return MUG_MSG_LIST_VIEW_GET_PRIVATE (self)->_query;
+}
diff --git a/toys/mug/mug-msg-list-view.h b/toys/mug/mug-msg-list-view.h
new file mode 100644 (file)
index 0000000..71b4260
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+** 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.
+**
+*/
+
+#ifndef __MUG_MSG_LIST_VIEW_H__
+#define __MUG_MSG_LIST_VIEW_H__
+
+#include <gtk/gtk.h>
+#include <utils/mu-util.h>
+
+G_BEGIN_DECLS
+/* convenience macros */
+#define MUG_TYPE_MSG_LIST_VIEW             (mug_msg_list_view_get_type())
+#define MUG_MSG_LIST_VIEW(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj),MUG_TYPE_MSG_LIST_VIEW,MugMsgListView))
+#define MUG_MSG_LIST_VIEW_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass),MUG_TYPE_MSG_LIST_VIEW,MugMsgListViewClass))
+#define MUG_IS_MSG_LIST_VIEW(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj),MUG_TYPE_MSG_LIST_VIEW))
+#define MUG_IS_MSG_LIST_VIEW_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass),MUG_TYPE_MSG_LIST_VIEW))
+#define MUG_MSG_LIST_VIEW_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj),MUG_TYPE_MSG_LIST_VIEW,MugMsgListViewClass))
+typedef struct _MugMsgListView MugMsgListView;
+typedef struct _MugMsgListViewClass MugMsgListViewClass;
+
+struct _MugMsgListView {
+       GtkTreeView parent;
+       /* insert public members, if any */
+};
+
+enum _MugError {
+       MUG_ERROR_XAPIAN_NOT_UPTODATE,
+       MUG_ERROR_XAPIAN_DIR,
+       MUG_ERROR_QUERY,
+       MUG_ERROR_OTHER
+};
+typedef enum _MugError MugError;
+
+struct _MugMsgListViewClass {
+       GtkTreeViewClass parent_class;
+       /* insert signal callback declarations, e.g. */
+       void (*msg_selected) (MugMsgListView * obj, const char *msgpath);
+       void (*error_occured) (MugMsgListView * obj, MugError err);
+};
+
+/* member functions */
+GType
+mug_msg_list_view_get_type (void)
+    G_GNUC_CONST;
+
+/* parameter-less _new function (constructor) */
+/* if this is a kind of GtkWidget, it should probably return at GtkWidget* */
+GtkWidget *
+mug_msg_list_view_new (const char *xpath);
+
+int
+mug_msg_list_view_query (MugMsgListView * self, const char *query);
+
+void
+mug_msg_list_view_move_first (MugMsgListView * self);
+
+gboolean
+mug_msg_list_view_move_prev (MugMsgListView * self);
+gboolean
+mug_msg_list_view_move_next (MugMsgListView * self);
+
+const gchar *
+mug_msg_list_view_get_query (MugMsgListView * self);
+
+G_END_DECLS
+#endif                         /* __MUG_MSG_LIST_VIEW_H__ */
diff --git a/toys/mug/mug-msg-view.c b/toys/mug/mug-msg-view.c
new file mode 100644 (file)
index 0000000..c91cfcc
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+** 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.
+**
+*/
+#ifdef HAVE_CONFIG
+#include "config.h"
+#endif /*HAVE_CONFIG*/
+
+#include <unistd.h>
+#include "mu-msg-view.h"
+#include "mug-msg-view.h"
+#include "mu-msg.h"
+#include "utils/mu-str.h"
+
+/* 'private'/'protected' functions */
+static void mug_msg_view_class_init (MugMsgViewClass * klass);
+static void mug_msg_view_init (MugMsgView * obj);
+static void mug_msg_view_finalize (GObject * obj);
+
+/* list my signals  */
+enum {
+       /* MY_SIGNAL_1, */
+       /* MY_SIGNAL_2, */
+       LAST_SIGNAL
+};
+
+
+typedef struct _MugMsgViewPrivate MugMsgViewPrivate;
+struct _MugMsgViewPrivate {
+       GtkWidget *_view;
+};
+#define MUG_MSG_VIEW_GET_PRIVATE(o)(G_TYPE_INSTANCE_GET_PRIVATE((o),MUG_TYPE_MSG_VIEW, MugMsgViewPrivate))
+/* globals */
+
+static GtkBoxClass *parent_class = NULL;
+G_DEFINE_TYPE (MugMsgView, mug_msg_view, GTK_TYPE_BOX);
+
+/* uncomment the following if you have defined any signals */
+/* static guint signals[LAST_SIGNAL] = {0}; */
+
+static void
+mug_msg_view_class_init (MugMsgViewClass * klass)
+{
+       GObjectClass *gobject_class;
+       gobject_class = (GObjectClass *) klass;
+
+       parent_class = g_type_class_peek_parent (klass);
+       gobject_class->finalize = mug_msg_view_finalize;
+
+       g_type_class_add_private (gobject_class, sizeof (MugMsgViewPrivate));
+
+       /* signal definitions go here, e.g.: */
+/*     signals[MY_SIGNAL_1] = */
+/*             g_signal_new ("my_signal_1",....); */
+/*     signals[MY_SIGNAL_2] = */
+/*             g_signal_new ("my_signal_2",....); */
+/*     etc. */
+}
+
+static void
+mug_msg_view_init (MugMsgView * obj)
+{
+       MugMsgViewPrivate *priv;
+       GtkWidget *scrolled;
+
+       priv = MUG_MSG_VIEW_GET_PRIVATE (obj);
+
+       priv->_view = mu_msg_view_new ();
+
+       scrolled = gtk_scrolled_window_new (NULL, NULL);
+       gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
+                                       GTK_POLICY_AUTOMATIC,
+                                       GTK_POLICY_AUTOMATIC);
+       gtk_container_add (GTK_CONTAINER (scrolled), priv->_view);
+
+       gtk_box_pack_start (GTK_BOX (obj), scrolled, TRUE, TRUE, 0);
+}
+
+static void
+mug_msg_view_finalize (GObject * obj)
+{
+/*     free/unref instance resources here */
+       G_OBJECT_CLASS (parent_class)->finalize (obj);
+}
+
+GtkWidget *
+mug_msg_view_new (void)
+{
+       return GTK_WIDGET (g_object_new (MUG_TYPE_MSG_VIEW, NULL));
+}
+
+
+
+gboolean
+mug_msg_view_set_msg (MugMsgView * self, const char *msgpath)
+{
+       MugMsgViewPrivate *priv;
+       g_return_val_if_fail (MUG_IS_MSG_VIEW (self), FALSE);
+
+       priv = MUG_MSG_VIEW_GET_PRIVATE (self);
+
+       if (!msgpath)
+               mu_msg_view_set_message (MU_MSG_VIEW(priv->_view), NULL);
+       else {
+               MuMsg *msg;
+
+               if (access (msgpath, R_OK) == 0) {
+                       msg = mu_msg_new_from_file (msgpath, NULL, NULL);
+                       mu_msg_view_set_message (MU_MSG_VIEW(priv->_view), msg);
+                       if (msg)
+                               mu_msg_unref (msg);
+               } else {
+                       gchar *note;
+                       note =  g_strdup_printf (
+                               "<h1>Note</h1><hr>"
+                               "<p>Message <tt>%s</tt> does not seem to be present "
+                               "on the file system."
+                               "<p>Maybe you need to run <tt>mu index</tt>?",
+                               msgpath);
+                       mu_msg_view_set_note (MU_MSG_VIEW (priv->_view), note);
+                       g_free (note);
+               }
+       }
+
+       return TRUE;
+}
+
+
+void
+mug_msg_view_set_note (MugMsgView * self, const char* html)
+{
+       MugMsgViewPrivate *priv;
+       g_return_if_fail (MUG_IS_MSG_VIEW (self));
+
+       priv = MUG_MSG_VIEW_GET_PRIVATE (self);
+
+       mu_msg_view_set_note (MU_MSG_VIEW (priv->_view), html);
+}
diff --git a/toys/mug/mug-msg-view.h b/toys/mug/mug-msg-view.h
new file mode 100644 (file)
index 0000000..96f9da7
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+** Copyright (C) 2010-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.
+**
+*/
+
+#ifndef __MUG_MSG_VIEW_H__
+#define __MUG_MSG_VIEW_H__
+
+#include <gtk/gtk.h>
+/* other include files */
+
+G_BEGIN_DECLS
+/* convenience macros */
+#define MUG_TYPE_MSG_VIEW             (mug_msg_view_get_type())
+#define MUG_MSG_VIEW(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj),MUG_TYPE_MSG_VIEW,MugMsgView))
+#define MUG_MSG_VIEW_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass),MUG_TYPE_MSG_VIEW,MugMsgViewClass))
+#define MUG_IS_MSG_VIEW(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj),MUG_TYPE_MSG_VIEW))
+#define MUG_IS_MSG_VIEW_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass),MUG_TYPE_MSG_VIEW))
+#define MUG_MSG_VIEW_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj),MUG_TYPE_MSG_VIEW,MugMsgViewClass))
+typedef struct _MugMsgView MugMsgView;
+typedef struct _MugMsgViewClass MugMsgViewClass;
+
+struct _MugMsgView {
+       GtkBox parent;
+};
+
+struct _MugMsgViewClass {
+       GtkBoxClass parent_class;
+       /* insert signal callback declarations, e.g. */
+       /* void (* my_event) (MugMsg* obj); */
+};
+
+/* member functions */
+GType mug_msg_view_get_type (void) G_GNUC_CONST;
+
+/* parameter-less _new function (constructor) */
+/* if this is a kind of GtkWidget, it should probably return at GtkWidget* */
+GtkWidget* mug_msg_view_new (void);
+gboolean mug_msg_view_set_msg (MugMsgView * self, const char *msgpath);
+void mug_msg_view_set_note (MugMsgView * self, const char* html);
+
+G_END_DECLS
+
+#endif /* __MUG_MSG_VIEW_H__ */
diff --git a/toys/mug/mug-query-bar.c b/toys/mug/mug-query-bar.c
new file mode 100644 (file)
index 0000000..6e48843
--- /dev/null
@@ -0,0 +1,112 @@
+/* mug-query-bar.c */
+
+/* insert (c)/licensing information) */
+
+#include "mug-query-bar.h"
+/* include other impl specific header files */
+
+/* 'private'/'protected' functions */
+static void mug_query_bar_class_init (MugQueryBarClass * klass);
+static void mug_query_bar_init (MugQueryBar * obj);
+static void mug_query_bar_finalize (GObject * obj);
+
+/* list my signals  */
+enum {
+       MUG_QUERY_CHANGED,
+       LAST_SIGNAL
+};
+
+typedef struct _MugQueryBarPrivate MugQueryBarPrivate;
+struct _MugQueryBarPrivate {
+       GtkWidget *_entry;
+};
+#define MUG_QUERY_BAR_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
+                                           MUG_TYPE_QUERY_BAR, \
+                                           MugQueryBarPrivate))
+/* globals */
+static GtkContainerClass *parent_class = NULL;
+
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE (MugQueryBar, mug_query_bar, GTK_TYPE_BOX);
+
+static void
+mug_query_bar_class_init (MugQueryBarClass * klass)
+{
+       GObjectClass *gobject_class;
+       gobject_class = (GObjectClass *) klass;
+
+       parent_class = g_type_class_peek_parent (klass);
+       gobject_class->finalize = mug_query_bar_finalize;
+
+       g_type_class_add_private (gobject_class, sizeof (MugQueryBarPrivate));
+
+       /* signal definitions go here, e.g.: */
+       signals[MUG_QUERY_CHANGED] =
+           g_signal_new ("query_changed",
+                         G_TYPE_FROM_CLASS (gobject_class),
+                         G_SIGNAL_RUN_FIRST,
+                         G_STRUCT_OFFSET (MugQueryBarClass, query_changed),
+                         NULL, NULL,
+                         g_cclosure_marshal_VOID__STRING,
+                         G_TYPE_NONE, 1, G_TYPE_STRING);
+}
+
+static void
+on_entry_activated (GtkWidget * w, MugQueryBar * bar)
+{
+       g_signal_emit (G_OBJECT (bar), signals[MUG_QUERY_CHANGED], 0,
+                      gtk_entry_get_text (GTK_ENTRY (w)));
+}
+
+static void
+mug_query_bar_init (MugQueryBar * obj)
+{
+       MugQueryBarPrivate *priv;
+
+       priv = MUG_QUERY_BAR_GET_PRIVATE (obj);
+
+       priv->_entry = gtk_entry_new ();
+
+       g_signal_connect (priv->_entry, "activate",
+                         G_CALLBACK (on_entry_activated), obj);
+
+       gtk_box_pack_start (GTK_BOX (obj), priv->_entry, TRUE, TRUE, 0);
+}
+
+static void
+mug_query_bar_finalize (GObject * obj)
+{
+/*     free/unref instance resources here */
+       G_OBJECT_CLASS (parent_class)->finalize (obj);
+}
+
+GtkWidget *
+mug_query_bar_new (void)
+{
+       return GTK_WIDGET (g_object_new (MUG_TYPE_QUERY_BAR, NULL));
+}
+
+void
+mug_query_bar_set_query (MugQueryBar * self, const char *query, gboolean run)
+{
+       MugQueryBarPrivate *priv;
+
+       g_return_if_fail (MUG_IS_QUERY_BAR (self));
+       priv = MUG_QUERY_BAR_GET_PRIVATE (self);
+
+       gtk_entry_set_text (GTK_ENTRY (priv->_entry), query ? query : "");
+
+       if (run)
+               on_entry_activated (priv->_entry, self);
+}
+
+void
+mug_query_bar_grab_focus (MugQueryBar * self)
+{
+       g_return_if_fail (MUG_IS_QUERY_BAR (self));
+
+       gtk_widget_grab_focus
+           (GTK_WIDGET (MUG_QUERY_BAR_GET_PRIVATE (self)->_entry));
+}
diff --git a/toys/mug/mug-query-bar.h b/toys/mug/mug-query-bar.h
new file mode 100644 (file)
index 0000000..bb011a9
--- /dev/null
@@ -0,0 +1,52 @@
+/* mug-query-bar.h */
+/* insert (c)/licensing information) */
+
+#ifndef __MUG_QUERY_BAR_H__
+#define __MUG_QUERY_BAR_H__
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG*/
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+/* convenience macros */
+#define MUG_TYPE_QUERY_BAR             (mug_query_bar_get_type())
+#define MUG_QUERY_BAR(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj),MUG_TYPE_QUERY_BAR,MugQueryBar))
+#define MUG_QUERY_BAR_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass),MUG_TYPE_QUERY_BAR,MugQueryBarClass))
+#define MUG_IS_QUERY_BAR(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj),MUG_TYPE_QUERY_BAR))
+#define MUG_IS_QUERY_BAR_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass),MUG_TYPE_QUERY_BAR))
+#define MUG_QUERY_BAR_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj),MUG_TYPE_QUERY_BAR,MugQueryBarClass))
+typedef struct _MugQueryBar MugQueryBar;
+typedef struct _MugQueryBarClass MugQueryBarClass;
+
+struct _MugQueryBar {
+       GtkBox parent;
+};
+
+struct _MugQueryBarClass {
+       GtkBox parent;
+       GtkBoxClass parent_class;
+       /* insert signal callback declarations, e.g. */
+       void (*query_changed) (MugQueryBar * obj, const char *query);
+};
+
+/* member functions */
+GType
+mug_query_bar_get_type (void)
+    G_GNUC_CONST;
+
+/* parameter-less _new function (constructor) */
+/* if this is a kind of GtkWidget, it should probably return at GtkWidget* */
+GtkWidget *
+mug_query_bar_new (void);
+
+void
+mug_query_bar_grab_focus (MugQueryBar * self);
+
+void
+mug_query_bar_set_query (MugQueryBar * self, const char *query, gboolean run);
+
+G_END_DECLS
+#endif                         /* __MUG_QUERY_BAR_H__ */
diff --git a/toys/mug/mug-shortcuts.c b/toys/mug/mug-shortcuts.c
new file mode 100644 (file)
index 0000000..316bd09
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+** Copyright (C) 2010 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 "mug-shortcuts.h"
+#include "mu-bookmarks.h"
+
+/* include other impl specific header files */
+
+/* 'private'/'protected' functions */
+static void mug_shortcuts_class_init (MugShortcutsClass * klass);
+static void mug_shortcuts_init (MugShortcuts * obj);
+static void mug_shortcuts_finalize (GObject * obj);
+
+#define MUG_SHORTCUT_BOOKMARK "bookmark"
+
+/* list my signals  */
+enum {
+       SHORTCUT_CLICKED,
+       /* MY_SIGNAL_1, */
+       /* MY_SIGNAL_2, */
+       LAST_SIGNAL
+};
+
+struct _MugShortcutsPrivate {
+       GtkWidget *_bbox;
+
+};
+#define MUG_SHORTCUTS_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
+                                           MUG_TYPE_SHORTCUTS, \
+                                           MugShortcutsPrivate))
+/* globals */
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+
+static GtkBoxClass *parent_class = NULL;
+G_DEFINE_TYPE (MugShortcuts, mug_shortcuts, GTK_TYPE_BOX);
+
+static void
+mug_shortcuts_class_init (MugShortcutsClass * klass)
+{
+       GObjectClass *gobject_class;
+       gobject_class = (GObjectClass *) klass;
+
+       parent_class = g_type_class_peek_parent (klass);
+       gobject_class->finalize = mug_shortcuts_finalize;
+
+       g_type_class_add_private (gobject_class, sizeof (MugShortcutsPrivate));
+
+       /* signal definitions go here, e.g.: */
+       signals[SHORTCUT_CLICKED] =
+           g_signal_new ("clicked",
+                         G_TYPE_FROM_CLASS (gobject_class),
+                         G_SIGNAL_RUN_FIRST,
+                         G_STRUCT_OFFSET (MugShortcutsClass, clicked),
+                         NULL, NULL,
+                         g_cclosure_marshal_VOID__STRING,
+                         G_TYPE_NONE, 1, G_TYPE_STRING);
+
+/*     signals[MY_SIGNAL_2] = */
+/*             g_signal_new ("my_signal_2",....); */
+/*     etc. */
+}
+
+static void
+mug_shortcuts_init (MugShortcuts * obj)
+{
+       obj->_priv = MUG_SHORTCUTS_GET_PRIVATE (obj);
+       obj->_priv->_bbox = gtk_button_box_new (GTK_ORIENTATION_VERTICAL);
+
+       gtk_button_box_set_layout (GTK_BUTTON_BOX (obj->_priv->_bbox),
+                                  GTK_BUTTONBOX_START);
+       gtk_box_pack_start (GTK_BOX (obj), obj->_priv->_bbox, TRUE, TRUE, 0);
+}
+
+static void
+mug_shortcuts_finalize (GObject * obj)
+{
+/*     free/unref instance resources here */
+       G_OBJECT_CLASS (parent_class)->finalize (obj);
+}
+
+static void
+on_button_clicked (GtkWidget * button, MugShortcuts * self)
+{
+       g_signal_emit (G_OBJECT (self),
+                      signals[SHORTCUT_CLICKED], 0,
+                      (const gchar *)g_object_get_data (G_OBJECT (button),
+                                                        MUG_SHORTCUT_BOOKMARK));
+}
+
+static void
+each_bookmark (const char *key, const char *val, MugShortcuts * self)
+{
+       GtkWidget *button;
+
+       button = gtk_button_new_with_label (key);
+       g_object_set_data_full (G_OBJECT (button), MUG_SHORTCUT_BOOKMARK,
+                               g_strdup (val), g_free);
+       g_signal_connect (G_OBJECT (button), "clicked",
+                         G_CALLBACK (on_button_clicked), self);
+
+       gtk_container_add (GTK_CONTAINER (self->_priv->_bbox), button);
+}
+
+static gboolean
+init_shortcuts (MugShortcuts * self, const char *bmpath)
+{
+       MuBookmarks *bookmarks;
+
+       bookmarks = mu_bookmarks_new (bmpath);
+       if (!bookmarks)
+               return TRUE;
+
+       mu_bookmarks_foreach (bookmarks, (MuBookmarksForeachFunc) each_bookmark,
+                             self);
+
+       mu_bookmarks_destroy (bookmarks);
+       return TRUE;
+}
+
+GtkWidget *
+mug_shortcuts_new (const char *bmpath)
+{
+       MugShortcuts *self;
+
+       self = MUG_SHORTCUTS (g_object_new (MUG_TYPE_SHORTCUTS, NULL));
+       if (!init_shortcuts (self, bmpath)) {
+               g_object_unref (self);
+               return NULL;
+       }
+
+       return GTK_WIDGET (self);
+}
+
+/* following: other function implementations */
+/* such as mug_shortcuts_do_something, or mug_shortcuts_has_foo */
diff --git a/toys/mug/mug-shortcuts.h b/toys/mug/mug-shortcuts.h
new file mode 100644 (file)
index 0000000..f8df47f
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+** Copyright (C) 2010 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 __MUG_SHORTCUTS_H__
+#define __MUG_SHORTCUTS_H__
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif /*HAVE_CONFIG_H*/
+
+
+#include <gtk/gtk.h>
+/* other include files */
+
+G_BEGIN_DECLS
+/* convenience macros */
+#define MUG_TYPE_SHORTCUTS             (mug_shortcuts_get_type())
+#define MUG_SHORTCUTS(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj),MUG_TYPE_SHORTCUTS,MugShortcuts))
+#define MUG_SHORTCUTS_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass),MUG_TYPE_SHORTCUTS,MugShortcutsClass))
+#define MUG_IS_SHORTCUTS(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj),MUG_TYPE_SHORTCUTS))
+#define MUG_IS_SHORTCUTS_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass),MUG_TYPE_SHORTCUTS))
+#define MUG_SHORTCUTS_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj),MUG_TYPE_SHORTCUTS,MugShortcutsClass))
+typedef struct _MugShortcuts MugShortcuts;
+typedef struct _MugShortcutsClass MugShortcutsClass;
+typedef struct _MugShortcutsPrivate MugShortcutsPrivate;
+
+struct _MugShortcuts {
+       GtkBox parent;
+       /* private */
+       MugShortcutsPrivate *_priv;
+};
+
+struct _MugShortcutsClass {
+       GtkBoxClass parent_class;
+       void (*clicked) (MugShortcuts * obj, const char *query);
+};
+
+/* member functions */
+GType mug_shortcuts_get_type (void) G_GNUC_CONST;
+
+/* parameter-less _new function (constructor) */
+/* if this is a kind of GtkWidget, it should probably return at GtkWidget* */
+GtkWidget *
+mug_shortcuts_new (const char *bmpath);
+
+/* fill in other public functions, e.g.: */
+/*     void       mug_shortcuts_do_something (MugShortcuts *self, const gchar* param); */
+/*     gboolean   mug_shortcuts_has_foo      (MugShortcuts *self, gint value); */
+
+G_END_DECLS
+
+#endif /* __MUG_SHORTCUTS_H__ */
diff --git a/toys/mug/mug.c b/toys/mug/mug.c
new file mode 100644 (file)
index 0000000..a95a87c
--- /dev/null
@@ -0,0 +1,422 @@
+/*
+** Copyright (C) 2010-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, 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*/
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#include <string.h>            /* for memset */
+
+#include <utils/mu-util.h>
+#include <mu-store.hh>
+#include <mu-runtime.h>
+#include <mu-index.h>
+
+#include "mug-msg-list-view.h"
+#include "mug-query-bar.h"
+#include "mug-msg-view.h"
+#include "mug-shortcuts.h"
+
+struct _MugData {
+       GtkWidget *win;
+       GtkWidget *statusbar;
+       GtkWidget *mlist;
+       GtkWidget *toolbar;
+       GtkWidget *msgview;
+       GtkWidget *querybar;
+       GtkWidget *shortcuts;
+       gchar *muhome;
+};
+typedef struct _MugData MugData;
+
+
+static void
+about_mug (MugData * mugdata)
+{
+       GtkWidget *about;
+       about = gtk_message_dialog_new
+           (GTK_WINDOW (mugdata->win), GTK_DIALOG_MODAL,
+            GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
+            "Mug version %s\n"
+            "A graphical frontend to the 'mu' e-mail search engine\n\n"
+            "(c) 2010-2013 Dirk-Jan C. Binnema\n"
+            "Released under the terms of the GPLv3+", VERSION);
+
+       gtk_dialog_run (GTK_DIALOG (about));
+       gtk_widget_destroy (about);
+}
+
+enum _ToolAction {
+       ACTION_PREV_MSG = 1,
+       ACTION_NEXT_MSG,
+       ACTION_REINDEX,
+       ACTION_DO_QUIT,
+       ACTION_ABOUT,
+       ACTION_SEPARATOR        /* pseudo action */
+};
+typedef enum _ToolAction ToolAction;
+
+static void
+on_tool_button_clicked (GtkToolButton * btn, MugData * mugdata)
+{
+       ToolAction action;
+       action = (ToolAction)
+           GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (btn), "action"));
+       switch (action) {
+
+       case ACTION_DO_QUIT:
+               gtk_main_quit ();
+               break;
+       case ACTION_NEXT_MSG:
+               mug_msg_list_view_move_next (MUG_MSG_LIST_VIEW
+                                            (mugdata->mlist));
+               break;
+       case ACTION_PREV_MSG:
+               mug_msg_list_view_move_prev (MUG_MSG_LIST_VIEW
+                                            (mugdata->mlist));
+               break;
+       case ACTION_ABOUT:
+               about_mug (mugdata);
+               break;
+       default:
+               g_print ("%u\n", action);
+       }
+}
+
+
+
+static GtkToolItem*
+tool_button (const char *name)
+{
+       GtkWidget *icon;
+
+       icon = gtk_image_new_from_icon_name
+               (name, GTK_ICON_SIZE_SMALL_TOOLBAR);
+
+       return gtk_menu_tool_button_new (icon, NULL);
+}
+
+
+static GtkToolItem*
+get_connected_tool_button (const char* stock_id, ToolAction action,
+                          MugData *mugdata)
+{
+       GtkToolItem *btn;
+
+       btn = tool_button (stock_id);
+       g_object_set_data (G_OBJECT (btn), "action",
+                          GUINT_TO_POINTER (action));
+       g_signal_connect (G_OBJECT (btn), "clicked",
+                         G_CALLBACK (on_tool_button_clicked),
+                         mugdata);
+       return btn;
+}
+
+static GtkWidget *
+mug_toolbar (MugData * mugdata)
+{
+       GtkWidget *toolbar;
+       int i;
+       struct {
+               const char *stock_id;
+               ToolAction action;
+       } tools[] = {
+               {"go-up", ACTION_PREV_MSG},
+               {"go-down", ACTION_NEXT_MSG},
+               {NULL, ACTION_SEPARATOR},
+               {"view-refresh", ACTION_REINDEX},
+               {NULL, ACTION_SEPARATOR},
+               {"help-about", ACTION_ABOUT},
+               {NULL, ACTION_SEPARATOR},
+               {"application-exit", ACTION_DO_QUIT}};
+
+       toolbar = gtk_toolbar_new ();
+       for (i = 0; i != G_N_ELEMENTS (tools); ++i) {
+               if (tools[i].action == ACTION_SEPARATOR) { /* separator? */
+                       gtk_toolbar_insert (GTK_TOOLBAR (toolbar),
+                                           gtk_separator_tool_item_new (), i);
+                       continue;
+               } else /* nope: a real item */
+                       gtk_toolbar_insert (GTK_TOOLBAR (toolbar),
+                                           get_connected_tool_button
+                                           (tools[i].stock_id, tools[i].action,
+                                            mugdata), i);
+       }
+
+       return toolbar;
+}
+
+static void
+on_shortcut_clicked (GtkWidget * w, const gchar * query, MugData * mdata)
+{
+       mug_query_bar_set_query (MUG_QUERY_BAR (mdata->querybar), query, TRUE);
+}
+
+static GtkWidget *
+mug_shortcuts_bar (MugData * data)
+{
+       data->shortcuts = mug_shortcuts_new
+               (mu_runtime_path(MU_RUNTIME_PATH_BOOKMARKS));
+
+       g_signal_connect (G_OBJECT (data->shortcuts), "clicked",
+                         G_CALLBACK (on_shortcut_clicked), data);
+
+       return data->shortcuts;
+}
+
+static GtkWidget *
+mug_statusbar (void)
+{
+       GtkWidget *statusbar;
+
+       statusbar = gtk_statusbar_new ();
+
+       return statusbar;
+}
+
+static void
+on_query_changed (MugQueryBar * bar, const char *query, MugData * mugdata)
+{
+       int count;
+
+       /* clear the old message */
+       mug_msg_view_set_msg (MUG_MSG_VIEW (mugdata->msgview), NULL);
+
+       count = mug_msg_list_view_query (MUG_MSG_LIST_VIEW (mugdata->mlist),
+                                        query);
+       if (count >= 0) {
+               gchar *msg =
+                   g_strdup_printf ("%d message%s found matching '%s'",
+                                    count,
+                                    count > 1 ? "s" : "",
+                                    mug_msg_list_view_get_query
+                                    (MUG_MSG_LIST_VIEW (mugdata->mlist)));
+               gtk_statusbar_push (GTK_STATUSBAR (mugdata->statusbar), 0, msg);
+               g_free (msg);
+
+               mug_msg_list_view_move_first (MUG_MSG_LIST_VIEW
+                                             (mugdata->mlist));
+               gtk_widget_grab_focus (GTK_WIDGET (mugdata->mlist));
+       }
+
+       if (count == 0)         /* nothing found */
+               mug_query_bar_grab_focus (MUG_QUERY_BAR (bar));
+}
+
+static void
+on_msg_selected (MugMsgListView * mlist, const char *mpath, MugData * mugdata)
+{
+       mug_msg_view_set_msg (MUG_MSG_VIEW (mugdata->msgview), mpath);
+}
+
+static void
+on_list_view_error (MugMsgListView * mlist, MugError err, MugData * mugdata)
+{
+       GtkWidget *errdialog;
+       const char *msg;
+
+       switch (err) {
+       case MUG_ERROR_XAPIAN_NOT_UPTODATE:
+               msg = "The Xapian Database has the wrong version\n"
+                   "Please run 'mu index --rebuild'";
+               break;
+       case MUG_ERROR_XAPIAN_DIR:
+               msg = "Cannot find the Xapian database dir\n"
+                   "Please restart mug with --muhome=... pointing\n"
+                   "to your mu home directory";
+               break;
+       case MUG_ERROR_QUERY:
+               msg = "Error in query";
+               break;
+       default:
+               msg = "Some error occurred";
+               break;
+       }
+
+       errdialog = gtk_message_dialog_new
+           (GTK_WINDOW (mugdata->win), GTK_DIALOG_MODAL,
+            GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", msg);
+
+       gtk_dialog_run (GTK_DIALOG (errdialog));
+       gtk_widget_destroy (errdialog);
+
+       if (err == MUG_ERROR_QUERY)
+               mug_query_bar_grab_focus (MUG_QUERY_BAR (mugdata->querybar));
+}
+
+static GtkWidget *
+mug_querybar (void)
+{
+       GtkWidget *querybar;
+
+       querybar = mug_query_bar_new ();
+
+       return querybar;
+}
+
+static GtkWidget *
+mug_query_area (MugData * mugdata)
+{
+       GtkWidget *queryarea, *paned, *scrolled;
+
+       queryarea = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
+       paned = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
+
+       mugdata->mlist = mug_msg_list_view_new
+               (mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB));
+       scrolled = gtk_scrolled_window_new (NULL, NULL);
+       gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
+                                       GTK_POLICY_AUTOMATIC,
+                                       GTK_POLICY_AUTOMATIC);
+
+       gtk_container_add (GTK_CONTAINER (scrolled), mugdata->mlist);
+       gtk_paned_add1 (GTK_PANED (paned), scrolled);
+
+       mugdata->msgview = mug_msg_view_new ();
+       mug_msg_view_set_note (MUG_MSG_VIEW(mugdata->msgview),
+                              "<h1>Welcome to <i>mug</i>!</h1><hr>"
+                              "<tt>mug</tt> is an experimental UI for <tt>mu</tt>, which will "
+                              "slowly evolve into something useful.<br><br>Enjoy the ride.");
+       g_signal_connect (G_OBJECT (mugdata->mlist), "msg-selected",
+                         G_CALLBACK (on_msg_selected), mugdata);
+       g_signal_connect (G_OBJECT (mugdata->mlist), "error-occured",
+                         G_CALLBACK (on_list_view_error), mugdata);
+       gtk_paned_add2 (GTK_PANED (paned), mugdata->msgview);
+
+       mugdata->querybar = mug_querybar ();
+       g_signal_connect (G_OBJECT (mugdata->querybar), "query-changed",
+                         G_CALLBACK (on_query_changed), mugdata);
+
+       gtk_box_pack_start (GTK_BOX (queryarea),
+                           mugdata->querybar, FALSE, FALSE, 2);
+       gtk_box_pack_start (GTK_BOX (queryarea), paned, TRUE, TRUE, 2);
+
+       gtk_widget_show_all (queryarea);
+       return queryarea;
+}
+
+static GtkWidget *
+mug_main_area (MugData * mugdata)
+{
+       GtkWidget *mainarea, *w;
+
+       mainarea = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 5);
+
+       w = mug_shortcuts_bar (mugdata);
+       gtk_box_pack_start (GTK_BOX (mainarea), w, FALSE, FALSE, 0);
+       gtk_widget_show (w);
+
+       w = mug_query_area (mugdata);
+       gtk_box_pack_start (GTK_BOX (mainarea), w, TRUE, TRUE, 0);
+       gtk_widget_show (w);
+
+       return mainarea;
+}
+
+static GtkWidget*
+mug_shell (MugData *mugdata)
+{
+       GtkWidget *vbox;
+
+       mugdata->win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+       gtk_window_set_title (GTK_WINDOW (mugdata->win), "Mug Mail Search");
+
+       vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
+
+       mugdata->toolbar = mug_toolbar (mugdata);
+       gtk_box_pack_start (GTK_BOX (vbox), mugdata->toolbar, FALSE, FALSE, 2);
+       gtk_box_pack_start (GTK_BOX (vbox), mug_main_area (mugdata), TRUE,
+                           TRUE, 2);
+
+       mugdata->statusbar = mug_statusbar ();
+       gtk_box_pack_start (GTK_BOX (vbox), mugdata->statusbar, FALSE, FALSE,
+                           2);
+
+       gtk_container_add (GTK_CONTAINER (mugdata->win), vbox);
+       gtk_widget_show_all (vbox);
+
+       gtk_window_set_default_size (GTK_WINDOW (mugdata->win), 700, 500);
+       gtk_window_set_resizable (GTK_WINDOW (mugdata->win), TRUE);
+
+       {
+               gchar *icon;
+               icon = g_strdup_printf ("%s%cmug.svg",
+                                       MUGDIR, G_DIR_SEPARATOR);
+               gtk_window_set_icon_from_file (GTK_WINDOW (mugdata->win), icon, NULL);
+               g_free (icon);
+       }
+
+       return mugdata->win;
+}
+
+static gint
+on_focus_query_bar (GtkWidget* ignored, GdkEventKey *event, MugData* mugdata)
+{
+       if (event->type==GDK_KEY_RELEASE && event->keyval==GDK_KEY_Escape) {
+               mug_query_bar_grab_focus (MUG_QUERY_BAR (mugdata->querybar));
+               return 1;
+       }
+       return 0;
+}
+
+int
+main (int argc, char *argv[])
+{
+       MugData mugdata;
+       GtkWidget *mugshell;
+       GOptionContext *octx;
+       GOptionEntry entries[] = {
+               {"muhome", 0, 0, G_OPTION_ARG_FILENAME, &mugdata.muhome,
+                "specify an alternative mu directory", NULL},
+               {NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL}       /* sentinel */
+       };
+
+       gtk_init (&argc, &argv);
+
+       octx = g_option_context_new ("- mug options");
+       g_option_context_add_main_entries (octx, entries, "Mug");
+
+       memset (&mugdata, 0, sizeof (MugData));
+       if (!g_option_context_parse (octx, &argc, &argv, NULL)) {
+               g_option_context_free (octx);
+               g_printerr ("mug: error in options\n");
+               return 1;
+       }
+
+       g_option_context_free (octx);
+       mu_runtime_init (mugdata.muhome, "mug");
+
+       mugshell = mug_shell (&mugdata);
+       g_signal_connect (G_OBJECT (mugshell), "destroy",
+                         G_CALLBACK (gtk_main_quit), NULL);
+       g_signal_connect (G_OBJECT (mugshell), "key_release_event",
+                         G_CALLBACK ( on_focus_query_bar ), (gpointer)&mugdata );
+
+       gtk_widget_show (mugshell);
+       mug_query_bar_grab_focus (MUG_QUERY_BAR (mugdata.querybar));
+
+       gtk_main ();
+       g_free (mugdata.muhome);
+
+       mu_runtime_uninit ();
+
+       return 0;
+}
diff --git a/toys/mug/mug.svg b/toys/mug/mug.svg
new file mode 100644 (file)
index 0000000..b2ddb5c
--- /dev/null
@@ -0,0 +1,443 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="64px"
+   height="64px"
+   id="svg3060"
+   version="1.1"
+   inkscape:version="0.48.0 r9654"
+   sodipodi:docname="mug.svg">
+  <defs
+     id="defs3062">
+    <radialGradient
+       cx="488.89267"
+       cy="588.70575"
+       r="219.20311"
+       fx="488.89267"
+       fy="588.70575"
+       id="radialGradient4633"
+       xlink:href="#linearGradient4931"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-0.00800352,1.2096377,-1.5872404,-0.01050209,1429.2231,7.1996709)" />
+    <linearGradient
+       id="linearGradient4931">
+      <stop
+         id="stop4933"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop4935"
+         style="stop-color:#cccccc;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       cx="459.32651"
+       cy="604.53241"
+       r="218.49857"
+       fx="459.32651"
+       fy="604.53241"
+       id="radialGradient4635"
+       xlink:href="#linearGradient4909"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-2.4717924e-8,0.4792097,-0.761379,-2.2833566e-8,965.49485,426.4337)" />
+    <linearGradient
+       id="linearGradient4909">
+      <stop
+         id="stop4911"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop4913"
+         style="stop-color:#cccccc;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       x1="507.53687"
+       y1="695.01477"
+       x2="505.21655"
+       y2="435.03162"
+       id="linearGradient4637"
+       xlink:href="#linearGradient4893"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,-1,0,1171.1728)" />
+    <linearGradient
+       id="linearGradient4893">
+      <stop
+         id="stop4895"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop4897"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       cx="518.93054"
+       cy="498.96671"
+       r="218.49857"
+       fx="518.93054"
+       fy="498.96671"
+       id="radialGradient4639"
+       xlink:href="#linearGradient4917"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.00681824,0.7964116,-0.908528,0.00777809,968.71756,124.33984)" />
+    <linearGradient
+       id="linearGradient4917">
+      <stop
+         id="stop4919"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop4921"
+         style="stop-color:#f2f2f2;stop-opacity:1"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       x1="507.53687"
+       y1="695.01477"
+       x2="505.21655"
+       y2="435.03162"
+       id="linearGradient4641"
+       xlink:href="#linearGradient4893"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(0,6)" />
+    <linearGradient
+       id="linearGradient3095">
+      <stop
+         id="stop3097"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3099"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       x1="507.53687"
+       y1="695.01477"
+       x2="505.21655"
+       y2="435.03162"
+       id="linearGradient4643"
+       xlink:href="#linearGradient4893"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,-1,0,1175.1728)" />
+    <linearGradient
+       id="linearGradient3102">
+      <stop
+         id="stop3104"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3106"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       x1="507.53687"
+       y1="695.01477"
+       x2="505.21655"
+       y2="435.03162"
+       id="linearGradient4645"
+       xlink:href="#linearGradient4893"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(0,4)" />
+    <linearGradient
+       id="linearGradient3109">
+      <stop
+         id="stop3111"
+         style="stop-color:#000000;stop-opacity:1"
+         offset="0" />
+      <stop
+         id="stop3113"
+         style="stop-color:#000000;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient4145"
+       id="linearGradient4216"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-15.844294,-42.822415)"
+       x1="427.79593"
+       y1="418.59042"
+       x2="308.88391"
+       y2="339.36896" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient4145">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop4147" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop4149" />
+    </linearGradient>
+    <filter
+       color-interpolation-filters="sRGB"
+       inkscape:collect="always"
+       id="filter4141">
+      <feGaussianBlur
+         inkscape:collect="always"
+         stdDeviation="1.0889941"
+         id="feGaussianBlur4143" />
+    </filter>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient1439"
+       id="radialGradient4218"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.1767241,-0.06570592,0.06229194,1.0081481,242.27164,193.29801)"
+       cx="61.56411"
+       cy="105.93911"
+       fx="69.98687"
+       fy="90.41909"
+       r="27.959114" />
+    <linearGradient
+       id="linearGradient1439">
+      <stop
+         id="stop1440"
+         offset="0"
+         style="stop-color:#ffffff;stop-opacity:0.5704698;" />
+      <stop
+         id="stop1441"
+         offset="1"
+         style="stop-color:#96b0c6;stop-opacity:0.89932889;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient1045"
+       id="linearGradient4220"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.1481635,-0.03408387,0.03066403,0.9334827,252.4323,191.06145)"
+       x1="57.667629"
+       y1="84.017433"
+       x2="60.490723"
+       y2="111.23763" />
+    <linearGradient
+       id="linearGradient1045">
+      <stop
+         id="stop1046"
+         offset="0.00000000"
+         style="stop-color:#ffffff;stop-opacity:0.74901962;" />
+      <stop
+         id="stop1047"
+         offset="1.0000000"
+         style="stop-color:#ffffff;stop-opacity:0.00000000;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient1045"
+       id="linearGradient4222"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.1879329,-0.03526444,0.02518942,0.7668232,265.95849,191.03645)"
+       x1="42.497837"
+       y1="103.57257"
+       x2="54.32185"
+       y2="167.94406" />
+    <linearGradient
+       id="linearGradient3225">
+      <stop
+         id="stop3227"
+         offset="0.00000000"
+         style="stop-color:#ffffff;stop-opacity:0.74901962;" />
+      <stop
+         id="stop3229"
+         offset="1.0000000"
+         style="stop-color:#ffffff;stop-opacity:0.00000000;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient1045"
+       id="linearGradient4224"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.0181639,-0.03179448,0.0306403,0.9812034,248.50031,191.28893)"
+       x1="110.05048"
+       y1="138.23856"
+       x2="103.6906"
+       y2="145.33636" />
+    <linearGradient
+       id="linearGradient3232">
+      <stop
+         id="stop3234"
+         offset="0.00000000"
+         style="stop-color:#ffffff;stop-opacity:0.74901962;" />
+      <stop
+         id="stop3236"
+         offset="1.0000000"
+         style="stop-color:#ffffff;stop-opacity:0.00000000;" />
+    </linearGradient>
+    <linearGradient
+       y2="327.80692"
+       x2="358.85184"
+       y1="336.3714"
+       x1="350.71558"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient3164"
+       xlink:href="#linearGradient4191"
+       inkscape:collect="always" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient4191">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop4193" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="1"
+         id="stop4195" />
+    </linearGradient>
+    <linearGradient
+       y2="327.80692"
+       x2="358.85184"
+       y1="336.3714"
+       x1="350.71558"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient3251"
+       xlink:href="#linearGradient4191"
+       inkscape:collect="always" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.3797387"
+     inkscape:cx="23.090914"
+     inkscape:cy="36.54545"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:document-units="px"
+     inkscape:grid-bbox="true"
+     inkscape:window-width="1426"
+     inkscape:window-height="943"
+     inkscape:window-x="0"
+     inkscape:window-y="26"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata3065">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer">
+    <g
+       transform="matrix(0.13627872,0,0,0.13834282,-32.522534,-32.527783)"
+       id="layer1-0">
+      <g
+         transform="matrix(0.9262457,-0.1633221,0.1633221,0.9262457,-94.266784,-20.740432)"
+         id="g4939">
+        <rect
+           width="438.40622"
+           height="252.53813"
+           rx="12"
+           ry="12"
+           x="291.87317"
+           y="468.43307"
+           id="rect4929"
+           style="fill:url(#radialGradient4633);fill-opacity:1;stroke:none" />
+        <rect
+           width="438.40622"
+           height="252.53813"
+           rx="12"
+           ry="12"
+           x="285.87317"
+           y="462.43307"
+           id="rect4886"
+           style="fill:#f4eed7;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+        <path
+           inkscape:connector-curvature="0"
+           d="m 287.46875,708.86033 c -1.4331,-5.96063 0.14236,-12.48611 4.8125,-17.15625 L 492.3125,491.67283 c 7.14709,-7.14709 18.66539,-7.14711 25.8125,0 l 200,200.03125 c 4.67014,4.67014 6.27685,11.19562 4.84375,17.15625 l -435.5,0 z"
+           id="path4901"
+           style="fill:url(#radialGradient4635);fill-opacity:1;stroke:url(#linearGradient4637);stroke-width:0.47990575;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+        <path
+           inkscape:connector-curvature="0"
+           d="m 287.46875,468.3125 c -1.4331,5.96063 0.14236,12.48611 4.8125,17.15625 L 492.3125,685.5 c 7.14709,7.14709 18.66539,7.14711 25.8125,0 l 200,-200.03125 c 4.67014,-4.67014 6.27685,-11.19562 4.84375,-17.15625 l -435.5,0 z"
+           id="rect4888"
+           style="fill:url(#radialGradient4639);fill-opacity:1;stroke:url(#linearGradient4641);stroke-width:0.47990575;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+        <path
+           inkscape:connector-curvature="0"
+           d="m 287.46875,712.86033 c -1.4331,-5.96063 0.14236,-12.48611 4.8125,-17.15625 L 492.3125,495.67283 c 7.14709,-7.14709 18.66539,-7.14711 25.8125,0 l 200,200.03125 c 4.67014,4.67014 6.27685,11.19562 4.84375,17.15625 l -435.5,0 z"
+           id="path4905"
+           style="fill:#f4eed7;fill-opacity:1;stroke:url(#linearGradient4643);stroke-width:0.47990575;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+        <path
+           inkscape:connector-curvature="0"
+           d="m 287.46875,466.3125 c -1.4331,5.96063 0.14236,12.48611 4.8125,17.15625 L 492.3125,683.5 c 7.14709,7.14709 18.66539,7.14711 25.8125,0 l 200,-200.03125 c 4.67014,-4.67014 6.27685,-11.19562 4.84375,-17.15625 l -435.5,0 z"
+           id="path4925"
+           style="fill:#f4eed7;fill-opacity:1;stroke:url(#linearGradient4645);stroke-width:0.47990575;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" />
+      </g>
+    </g>
+    <g
+       id="g4206"
+       transform="matrix(0.66491655,0,0,0.77814599,-189.90991,-208.191)">
+      <path
+         sodipodi:nodetypes="cssscccccsccssssc"
+         id="path4039"
+         d="m 316.07229,281.98202 c -0.43848,0.0486 -0.86293,0.0963 -1.30329,0.16735 -14.09135,2.2736 -23.67275,15.31462 -21.39425,29.11832 2.2785,13.8037 15.55266,23.18673 29.64399,20.91314 5.32396,-0.85901 9.9974,-3.24509 13.63696,-6.64407 l 29.69403,21.53613 c 3.12107,-0.43375 5.32521,-2.54648 6.63691,-6.36414 l -31.28016,-21.37742 -0.60163,0.70667 c 2.99888,-4.94941 4.31388,-10.8985 3.30565,-17.00664 -2.20729,-13.37234 -14.74564,-22.55444 -28.33821,-21.04934 z m 0.11619,1.0566 c 13.01836,-1.44152 25.05638,7.3638 27.17043,20.17124 2.18223,13.22057 -8.30725,25.74182 -21.80333,27.91939 -13.49606,2.17754 -24.91335,-6.79082 -27.0956,-20.0114 -2.18223,-13.22055 6.99145,-25.73934 20.48752,-27.91689 0.42175,-0.0681 0.82103,-0.11586 1.24098,-0.16234 z"
+         style="fill:url(#linearGradient4216);fill-opacity:1;fill-rule:evenodd;stroke-width:0.5;filter:url(#filter4141)"
+         inkscape:connector-curvature="0" />
+      <path
+         style="fill:#807d74;fill-opacity:1;fill-rule:evenodd;stroke-width:0.5"
+         d="m 344.17328,310.53057 29.61718,27.04843 c -1.89242,3.87599 -4.50058,5.8091 -7.81004,5.83855 l -28.02027,-27.09828 6.21313,-5.7887 z"
+         id="path852"
+         sodipodi:nodetypes="ccccc"
+         inkscape:connector-curvature="0" />
+      <path
+         sodipodi:nodetypes="ccc"
+         id="path4204"
+         d="m 344.98664,308.53682 c -25.57774,26.69357 -58.35719,-3.9253 -41.04008,-28.54056 -22.47432,26.70691 17.05217,59.23993 41.04008,28.54056 z"
+         style="fill:#273f57;fill-opacity:0.99607843;fill-rule:nonzero;stroke:none"
+         inkscape:connector-curvature="0" />
+      <path
+         style="fill:url(#radialGradient4218);fill-opacity:1;fill-rule:evenodd;stroke:#314e6c;stroke-width:1.13486826;stroke-miterlimit:4;stroke-opacity:0.99523806;stroke-dasharray:none"
+         d="m 349.29738,294.36408 c 0.43574,13.26512 -10.53563,24.36721 -24.48967,24.78145 -13.95405,0.41424 -25.63272,-10.01548 -26.06847,-23.2806 -0.43574,-13.26512 10.53563,-24.3672 24.48968,-24.78143 13.95404,-0.41424 25.6327,10.01546 26.06846,23.28058 z"
+         id="path853"
+         inkscape:connector-curvature="0" />
+      <path
+         style="fill:url(#linearGradient4220);fill-opacity:0.09090905;fill-rule:evenodd;stroke-width:1pt"
+         d="m 334.58464,277.67286 c 3.57111,4.2824 -0.18987,12.77638 -8.39505,18.9598 -8.2052,6.18342 -17.76278,7.72625 -21.33391,3.44384 -3.57111,-4.28239 0.18987,-12.77639 8.39506,-18.95979 8.20519,-6.18343 17.76278,-7.72627 21.3339,-3.44385 z"
+         id="path863"
+         inkscape:connector-curvature="0" />
+      <path
+         style="fill:url(#linearGradient4222);fill-opacity:0.55208333;fill-rule:evenodd;stroke-width:1pt"
+         d="m 310.11078,312.32383 c 14.49429,4.30904 31.85616,-4.46732 36.42449,-18.21714 4.41474,12.57528 -23.28046,39.53212 -36.42449,18.21714 z"
+         id="path885"
+         sodipodi:nodetypes="ccc"
+         inkscape:connector-curvature="0" />
+      <path
+         sodipodi:nodetypes="ccccc"
+         id="path1766"
+         d="m 339.59269,316.18967 4.6224,-4.38252 27.97152,25.69252 c 1.23309,3.32848 -0.44562,5.68159 -6.10065,4.88028 l -26.49327,-26.19028 z"
+         style="fill:url(#linearGradient4224);fill-opacity:0.75;fill-rule:evenodd;stroke:none"
+         inkscape:connector-curvature="0" />
+      <path
+         sodipodi:nodetypes="ccccc"
+         id="path4187"
+         d="m 338.93805,316.8243 4.6224,-4.38252 27.97152,25.69252 c 1.23309,3.32848 -0.44562,5.68159 -6.10065,4.88028 L 338.93805,316.8243 z"
+         style="fill:url(#linearGradient3251);fill-opacity:1;fill-rule:evenodd;stroke:none"
+         inkscape:connector-curvature="0" />
+    </g>
+  </g>
+</svg>
diff --git a/www/cheatsheet.md b/www/cheatsheet.md
new file mode 100644 (file)
index 0000000..3ff6553
--- /dev/null
@@ -0,0 +1,148 @@
+---
+layout: default
+permalink: code/mu/cheatsheet.html
+---
+
+# Mu Cheatsheet
+
+  Here are some tips for using `mu`. If you want to know more, please
+  refer to the `mu` man pages. For a quick warm-up, there's also the
+  `mu-easy` man-page.
+## Indexing your mail
+``` $ mu index```
+
+If `mu` did not guess the right Maildir, you can set it explicitly:
+
+``` $ mu index --maildir=~/MyMaildir```
+
+### Excluding directories from indexing
+
+If you want to exclude certain directories from being indexed (for example,
+directories with spam-messages), put a file called `.noindex` in the directory
+to exclude, and it will be ignored when indexing (including its children)
+
+## Finding messages
+
+After you have indexed your messages, you can search them. Here are some
+examples. Also note the `--threads` argument to get a threaded display of
+the messages, and `--color` to get colors (both since 0.9.7).
+
+### messages about Helsinki (in message body, subject, sender, ...)
+``` $ mu find Helsinki```
+
+### messages to Jack with subject jellyfish containing the word tumbleweed
+``` $ mu find to:Jack subject:jellyfish tumbleweed```
+
+### messages between 2 kilobytes and a 2Mb, written in December 2009 with an attachment from Bill
+``` $ mu find size:2k..2m date:20091201..20093112 flag:attach from:bill```
+
+### signed messages about apples *OR* oranges
+```  $ mu find flag:signed apples OR oranges```
+
+### messages about yoghurt in the Sent Items folder (note the quoting):
+```  $ mu find maildir:'/Sent Items' yoghurt```
+
+
+### unread messages about things starting with 'soc' (soccer, society, socrates, ...)
+```  $ mu find 'subject:soc*' flag:unread```
+
+Note, the '*' only works at the /end/ of a search term, and you need to
+quote it or the shell will interpret it before `mu` sees it.
+(searching using the '*' wildcard is available since mu 0.9.6)
+
+### finding messages with images as attachment
+```  $ mu find 'mime:image/*' ```
+       (since mu version 0.9.8)
+
+### finding messages with 'milk' in one of its text parts (such as text-based attachments):
+```  $ mu find embed:milk ```
+       (since mu version 0.9.8)
+
+### finding /all/ your messages
+```  $ mu find ""```
+       (since mu version 0.9.7)
+
+## Finding contacts
+
+Contacts (names + email addresses) are cached separately, and can be
+searched with `mu cfind` (after your messages have been indexed):
+
+### all contacts with 'john' in either name or e-mail address
+``` $ mu cfind john```
+
+    `mu cfind` takes a regular expression for matching.
+
+You can export the contact information to a number of formats for use
+in e-mail clients. For example:
+
+### export /all/ your contacts to the `mutt` addressbook format
+``` $ mu cfind --format=mutt-alias```
+
+Other formats are: `plain`, `mutt-ab`, `wl` (Wanderlust), `org-contact`,
+`bbdb` and `csv` (comma-separated values).
+
+## Retrieving attachments from messages
+
+You can retrieve attachments from messages using `mu extract`, which takes a
+message file as an argument. Without any other arguments, it displays the
+MIME-parts of the message. You can then get specific attachments:
+
+``` $ mu extract --parts=3,4 my-msg-file```
+
+will get you parts 3 and 4. You can also extract files based on their name:
+
+``` $ mu extract my-msg-file '.*\.jpg'```
+
+The second argument is a case-insensitive regular expression, and the command
+will extract any files matching the pattern -- in the example, all
+`.jpg`-files.
+
+Do not confuse the '.*' /regular expression/ in `mu extract` (and `mu
+cfind`) with the '*' /wildcard/ in `mu find`.
+
+## Getting more colorful output
+
+Some of the `mu` commands, such as `mu find`, `mu cfind` and `mu view`
+support colorized output. By default this is turned off, but you can enable
+it with `--color`, or setting the `MU_COLORS` environment variable to
+non-empty.
+
+``` $ mu find --color capibara```
+
+   (since `mu` version 0.9.6)
+
+## Integration with mail clients
+
+The `mu-find` man page contains examples for `mutt` and `wanderlust`. And
+since version 0.9.8, `mu` includes its own e-mail client for `emacs`, `mu4e`.
+
+## Viewing specific messages
+
+You can view message contents with `mu view`; it does not use the database
+and simply takes a message file as it's argument:
+
+``` $ mu view ~/Maildir/inbox/cur/message24```
+
+You can also use `--color` to get colorized output, and `--summary` to get a
+summary of the message contents instead of the whole thing.
+
+## Further processing of matched messages
+
+If you need to process the results of your queries with some other program,
+you can return the results as a list of absolute paths to the messages found:
+
+For example, to get the number of lines in all your messages mentioning
+/banana/, you could use something like:
+
+``` $ mu find --exec='wc -l'```
+
+Note that we use 'l', so the returned message paths will be quoted. This is
+useful if you have maildirs with spaces in their names.
+
+For further processing, also the ~--format`(xml|sexp)~ can be useful. For
+example,
+
+``` $ mu find --format=xml pancake```
+
+will give you a list of pancake-related messages in XML-format.
diff --git a/www/graph01.png b/www/graph01.png
new file mode 100644 (file)
index 0000000..327e079
Binary files /dev/null and b/www/graph01.png differ
diff --git a/www/index.md b/www/index.md
new file mode 100644 (file)
index 0000000..3bcceb2
--- /dev/null
@@ -0,0 +1,179 @@
+---
+layout: default
+permalink: /code/mu/
+---
+
+# Welcome to mu!
+
+<img src="mu.jpg" align="right" margin="10px"/> With the *enormous* amounts of e-mail many people
+gather and the importance of e-mail messages in our daily work-flow, it is very important to be able
+to quickly deal with all that - in particular, to instantly find that one important e-mail you need
+right now.
+
+For that, *mu* was created. *mu* is a tool for dealing with e-mail messages stored in the
+[Maildir](http://en.wikipedia.org/wiki/Maildir)-format, on Unix-like systems. *mu*'s main purpose is
+to help you to find the messages you need, quickly; in addition, it allows you to view messages,
+extract attachments, create new maildirs, ... See the [mu cheatsheet](cheatsheet.html) for some
+examples. Mu's source code is available [in github](https://github.com/djcb/mu), and there is the
+[mu-discuss](http://groups.google.com/group/mu-discuss) mailing list.
+
+*mu* includes an emacs-based e-mail client (`mu4e`), a simple GUI (`mug`) and bindings for the
+Guile/Scheme programming language.
+
+## Features
+
+- fast indexing for [Maildir](http://en.wikipedia.org/wiki/Maildir), Maildir+ and Maildir-on-VFAT
+- search for messages based on the sender, receiver, subject, date-range,
+size, priority, words in message, flags (signed, encrypted, new, replied,
+has-attachment,...), message-id, maildir, tags, attachment (name,
+mime-type, text) and more
+- support for encrypted and signed messages
+- command-line tools for indexing, searching, viewing, adding/removing
+messages, extracting attachments, exporting/searching address lists,
+creating maildirs, ...
+- accent/case normalization - so *angstrom* matches *Ångström*
+- can be integrated with other e-mail clients such as
+[mutt](http://www.mutt.org/) and
+[Wanderlust](http://www.emacswiki.org/emacs/WanderLust).
+- [mu4e](mu4e.html), an emacs-based e-mail client based on `mu` (see screenshot).
+- [mu-guile](mu-guile.html):
+[guile 2.0](http://www.gnu.org/software/guile/) bindings that
+allow for scripting, advanced processing of your data, and doing
+all kinds of statistics
+- fully documented (man pages, info pages)
+
+## News
+
+### 2019-04-07: mu/mu4e 1.2 is available
+
+A new release is available; see the [release notes](https://github.com/djcb/mu/releases/tag/1.2) and
+grab the [tarball](https://github.com/djcb/mu/releases/download/1.2/mu-1.2.0.tar.xz).
+
+
+### 2018-02-03: mu/mu4e 1.0 is available
+
+After a decade of development, mu 1.0 is available. Read
+[NEWS](https://github.com/djcb/mu/blob/v1.0/NEWS.org) with all the details.
+
+### 2016-12-05: mu/mu4e 0.9.18 is available
+
+mu 0.9.18 offers a number of improvements across the board. For
+example, people with huge maildirs can use a special "lazy-checking"
+mode to speed up indexing; it's now possible to view rich-text message
+in an embedded webkit-view, and the release adds support for org-mode
+9.x. There also many small fixes and tweaks in mu4e, all based on
+user-feedback.
+
+For all the details,
+see: [NEWS.org](https://github.com/djcb/mu/blob/0.9.18/NEWS.org).
+
+Get it from the [Release page](https://github.com/djcb/mu/releases).
+
+### 2016-01-21: mu/mu4e 0.9.16 is here, and it is our latest stable release!
+
+#### Better behaviour and context handling
+- Context Handling just got smart:  new ‘mu4e-context’ defines and switches between various contexts, which are groups of settings. This may be used for instance to easily configure and switch between multiple accounts.
+- Improved behaviour in html and messages marks: ability to toggle between html and text display of messages & better management of messages marked as read or unread. 
+
+#### User Interface improvements
+- Numerous improvements in threads view and mailing lists management
+- Fancy characters can now be properly used as well as special customizations for message views
+
+#### Faster Indexing and message management
+- Indexing & caching optimizations
+
+You can grab the tarball directly
+[from Github](https://github.com/djcb/mu-releases) or wait a bit to
+get it through your distribution channels (details may vary from one
+distribution to another).
+
+None of this would be possible without a team of dedicated
+individuals: 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, 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.)
+
+We hope you will enjoy this release as much as we do. Happy Hacking!
+
+-- The mu/mu4e Team
+
+## Old News
+
+- 2015-09-24: After almost 6 months, a new release of mu/mu4e. We are
+happy to announce mu and mu4e 0.9.9.13! have just been
+released. The following key features and improvements have been
+added:
+
+* Change the way the headers are displayed and sorted
+* Fancy characters now enabled distinctively both for marks and
+headers
+* Composing a message is now possible in a separate frame
+* Ability to display the subject of a thread only on top of it for
+enhanced clarity
+* Lots of bugs squashed, updates to the documentation (BDDB), as
+well as embedding the News file inside mu4e itself.
+
+
+- 2013-03-30: released [mu-0.9.9.5](http://code.google.com/p/mu0/downloads/detail?name%3Dmu-0.9.9.5.tar.gz); full with new features and bug
+fixes – see the download link for some of the details. Many
+thanks to all who contributed!
+- 2012-10-14: released [mu-0.9.9](http://code.google.com/p/mu0/downloads/detail?name%3Dmu-0.9.9.tar.gz); a new barrage of fixes and
+improvements – check the link and [NEWS](https://github.com/djcb/mu/blob/master/NEWS). Also, note the
+[mu4e-manual](http://code.google.com/p/mu0/downloads/detail?name%3Dmu4e-manual-0.9.9.pdf) (PDF).
+- 2012-07-01: released [mu-0.9.8.5](http://code.google.com/p/mu0/downloads/detail?name%3Dmu-0.9.8.5.tar.gz); more fixes, improvements (see
+the link).
+- 2012-05-08: released
+[mu-0.9.8.4](http://code.google.com/p/mu0/downloads/detail?name%3Dmu-0.9.8.4.tar.gz)
+with even more improvements (the link has all the details)
+- 2012-04-06: released
+[mu-0.9.8.3](http://code.google.com/p/mu0/downloads/detail?name%3Dmu-0.9.8.3.tar.gz),
+with many improvements, fixes. See the link for details. *NOTE*:
+existing `mu` and `mu4e` users are recommended to execute `mu
+index --rebuild` after installation.
+- 2012-03-11: released
+[mu-0.9.8.2](http://code.google.com/p/mu0/downloads/detail?name=mu-0.9.8.2.tar.gz),
+with a number of fixes and improvements, see the link for the
+details.
+- 2012-02-17: released
+[mu-0.9.8.1](http://code.google.com/p/mu0/downloads/detail?name%3Dmu-0.9.8.1.tar.gz),
+which has a number of improvements to the 0.9.8 release: add mark
+as read/unread, colorize cited message parts, better handling of
+text-based message parts, documentation fixes, documentation
+updates and a few fixes here and there
+- 2012-02-09: moved the mu source code repository
+[to Github](https://github.com/djcb/mu).
+- 2012-01-31: finally,
+[mu-0.9.8](http://mu0.googlecode.com/files/mu-0.9.8.tar.gz) is
+available. It comes with an emacs-based e-mail client,
+[mu4e](file:mu4e.html), and much improved
+[guile bindings](file:mu-guile.html). Furthermore, It adds
+search for attachment mime type and search inside any text part
+of a message, more tests, improvements in many parts of the code.
+- 2011-09-03: mu 0.9.7 is now available; compared to the -pre
+version there are a few small changes; the most important one is
+a fix specifically for running mu on MacOS.
+
+- [Old news](file:old-news.org)
+
+## Development & download
+
+<a href="mu4e-splitview.png" border="0"><img src="mu4e-splitview-small.png" align="right" margin="10px"/></a>
+
+Some Linux-distributions already provide pre-built mu packages; if
+there's no packagage for your distribution, or if you want the
+latest release, you can [download mu source packages](http://code.google.com/p/mu0/downloads/list) from Google
+Code. In case you find a bug, or have a feature requests, please
+use the [issue tracker](https://github.com/djcb/mu/issues).
+
+If you'd like to work with the mu source code, you can find it [in Github](https://github.com/djcb/mu);
+also, see the notes on [HACKING](https://github.com/djcb/mu/blob/master/HACKING) the mu source code.
+
+There's also a [mailing list](http://groups.google.com/group/mu-discuss).
+
+## License & Copyright
+
+*mu* was designed and implemented by Dirk-Jan C. Binnema, and is Free
+Software, licensed under the GNU GPLv3
diff --git a/www/mu-guile.md b/www/mu-guile.md
new file mode 100644 (file)
index 0000000..168afc3
--- /dev/null
@@ -0,0 +1,164 @@
+---
+layout: default
+permalink: /code/mu/mu-guile.html
+---
+
+# mu-guile
+
+Starting from version 0.9.7,
+[GNU/Guile](http://www.djcbsoftware.nl/code/mu][mu]] had experimental
+bindings for the
+[[http://www.gnu.org/software/guile/) programming language, which is a version of the [Scheme](http://en.wikipedia.org/wiki/Scheme_(programming_language))
+programming language, specifically designed for extending existing
+programs.
+
+`mu` version 0.9.8 has much improved bindings, and they are
+[documented](file:mu-guile/index.html), with many examples. You can
+find more examples in the `guile/examples` directory of the `mu`
+source package.
+
+It must be said that Scheme (and in general, languages from the Lisp-family)
+initially may look a bit 'strange' -- all these parentheses etc.; so please
+bear with us -- you will get used to it.
+
+## Some examples
+
+Here are some examples; we don't provide too much explanation /how/ they do
+what they do, but the [manual](file:mu-guile/index.html) takes you through that, step-by-step.
+
+*NOTE (1)*: if you get errors like `ERROR: no code for module (mu)`,
+`guile` cannot find the `mu` modules. To solve this, you need to set
+the `GUILE_LOAD_PATH` to the directory with the installed `mu.scm`,
+e.g.
+
+``` sh
+   export GUILE_LOAD_PATH="/usr/local/share/guile/site/2.0"
+```
+
+(you need to adapt this if you installed `mu` in some non-standard place; but
+it's always the directory with the installed `mu.scm`).
+
+*NOTE (2)*: for the graphs (below) to work, you will need to have the `gnuplot`
+program installed.
+
+*NOTE (3)*: the examples below assume that you have your messages indexed
+already using `mu`; see the man pages, or the [mu cheat sheet](http://www.djcbsoftware.nl/code/mu/cheatsheet.html).
+
+### Messages per weekday
+
+    #!/bin/sh
+    exec guile -s $0 $@
+    !#
+     
+    (use-modules (mu) (mu message) (mu stats) (mu plot))
+    (mu:initialize)
+     
+    ;; create a list like (("Mon" . 13) ("Tue" . 23) ...)
+    (define weekday-table
+      (mu:weekday-numbers->names
+        (sort
+          (mu:tabulate-messages
+       (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)
+
+Which outputs something like:
+
+
+    Sun: 2278
+    Mon: 2991
+    Tue: 3077
+    Wed: 2734
+    Thu: 2796
+    Fri: 2343
+    Sat: 1856
+
+The numbers may be a bit different though... In my case, Saturday
+seems a particularly slow day for e-mail.
+
+### Drawing graphs
+
+We can also draw graphs from this, by adding the following to the script:
+
+    ;; plain-text graph
+    (mu:plot (weekday-table) "Messages per weekday" "Day" "Messages" #t)
+     
+    ;; GUI graph
+    (mu:plot (weekday-table) "Messages per weekday" "Day" "Messages")
+
+
+This gives us the following:
+
+### plain text graph
+     
+                                   Messages per weekday
+     Messages
+       3200 ++---+--------+---------+--------+---------+---------+--------+---++
+            |    +        +     "/tmp/filel8NGRf" using 2:xticlabels(1) ****** |
+       3000 ++                  *       *                                     ++
+            |         ***********       *                                      |
+            |         *        **       *                                      |
+       2800 ++        *        **       *          *********                  ++
+            |         *        **       ************       *                   |
+       2600 ++        *        **       **        **       *                  ++
+            |         *        **       **        **       *                   |
+            |         *        **       **        **       *                   |
+       2400 ++        *        **       **        **       ***********        ++
+            ***********        **       **        **       **        *         |
+       2200 *+       **        **       **        **       **        *        ++
+            *        **        **       **        **       **        *         |
+            *        **        **       **        **       **        *         |
+       2000 *+       **        **       **        **       **        *        ++
+            *    +   **   +    **   +   **   +    **   +   **    +   ***********
+       1800 ********************************************************************
+                Sun      Mon       Tue      Wed       Thu       Fri      Sat
+                                            Day
+#### GUI graph
+
+<img src="graph01.png">
+
+### Export contacts to `mutt`
+
+`mu` provides `mu cfind` to get contact information from the database;
+it's fast, since it uses cached contact data. But sometimes, we may
+want to get a bit more advanced. For examples, suppose I want a list
+of names and e-mail addresses of people that were seen at least 20
+times since 2010, in the `mutt` address book format.
+
+We could get such a list with something like the following:
+
+       !/bin/sh
+       exec guile -s $0 $@
+       !#
+        
+       (use-modules (mu) (mu message) (mu contact))
+       (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))
+
+
+## License & Copyright
+
+*mu-guile* was designed and implemented by Dirk-Jan C. Binnema, and is
+Free Software, licensed under the GNU GPLv3
diff --git a/www/mu-guile.org b/www/mu-guile.org
new file mode 100644 (file)
index 0000000..de6ad7d
--- /dev/null
@@ -0,0 +1,177 @@
+#+title: mu-guile: guile-bindings for mu
+#+style: <link rel="stylesheet" type="text/css" href="mu.css">
+#+options: skip t
+
+  Starting from version 0.9.7, [[http://www.djcbsoftware.nl/code/mu][mu]] had experimental bindings for the [[http://www.gnu.org/software/guile/][GNU/Guile]]
+  programming language, which is a version of the [[http://en.wikipedia.org/wiki/Scheme_(programming_language)][Scheme]] programming language,
+  specifically designed for extending existing programs.
+
+  =mu= version 0.9.8 has much improved bindings, and they are [[file:mu-guile/index.html][documented]], with
+  many examples. You can find more examples in the =guile/examples= directory of
+  the =mu= source package.
+
+  It must be said that Scheme (and in general, languages from the Lisp-family)
+  initially may look a bit 'strange' -- all these parentheses etc.; so please
+  bear with us -- you will get used to it.
+
+** Some examples
+
+   Here are some examples; we don't provide too much explanation /how/ they do
+   what they do, but the [[file:mu-guile/index.html][manual]] takes you through that, step-by-step.
+
+   *NOTE (1)*: if you get errors like =ERROR: no code for module (mu)=, ~guile~
+   cannot find the ~mu~ modules. To solve this, you need to set the
+   ~GUILE_LOAD_PATH~ to the directory with the installed ~mu.scm~, e.g.
+
+#+begin_src sh
+   export GUILE_LOAD_PATH="/usr/local/share/guile/site/2.0"
+#+end_src
+
+   (you need to adapt this if you installed =mu= in some non-standard place; but
+   it's always the directory with the installed ~mu.scm~).
+
+   *NOTE (2)*: for the graphs (below) to work, you will need to have the =gnuplot=
+    program installed.
+    
+   *NOTE (3)*: the examples below assume that you have your messages indexed
+   already using =mu=; see the man pages, or the [[http://www.djcbsoftware.nl/code/mu/cheatsheet.html][mu cheat sheet]].
+
+*** Messages per weekday
+
+#+begin_src scheme
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu) (mu message) (mu stats) (mu plot))
+(mu:initialize)
+
+;; create a list like (("Mon" . 13) ("Tue" . 23) ...)
+(define weekday-table
+  (mu:weekday-numbers->names
+    (sort
+      (mu:tabulate-messages
+       (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_src
+
+    Which outputs something like:
+
+#+begin_example
+Sun: 2278
+Mon: 2991
+Tue: 3077
+Wed: 2734
+Thu: 2796
+Fri: 2343
+Sat: 1856
+#+end_example
+
+    The numbers may be a bit different though... In my case, Saturday seems a
+    particularly slow day for e-mail.
+
+*** Drawing graphs
+
+    We can also draw graphs from this, by adding the following to the script:
+
+#+begin_src scheme
+;; plain-text graph
+(mu:plot (weekday-table) "Messages per weekday" "Day" "Messages" #t)
+
+;; GUI graph
+(mu:plot (weekday-table) "Messages per weekday" "Day" "Messages")
+#+end_src scheme
+
+    This gives us the following:
+
+**** plain text graph
+#+begin_example
+                               Messages per weekday
+ Messages
+   3200 ++---+--------+---------+--------+---------+---------+--------+---++
+        |    +        +     "/tmp/filel8NGRf" using 2:xticlabels(1) ****** |
+   3000 ++                  *       *                                     ++
+        |         ***********       *                                      |
+        |         *        **       *                                      |
+   2800 ++        *        **       *          *********                  ++
+        |         *        **       ************       *                   |
+   2600 ++        *        **       **        **       *                  ++
+        |         *        **       **        **       *                   |
+        |         *        **       **        **       *                   |
+   2400 ++        *        **       **        **       ***********        ++
+        ***********        **       **        **       **        *         |
+   2200 *+       **        **       **        **       **        *        ++
+        *        **        **       **        **       **        *         |
+        *        **        **       **        **       **        *         |
+   2000 *+       **        **       **        **       **        *        ++
+        *    +   **   +    **   +   **   +    **   +   **    +   ***********
+   1800 ********************************************************************
+            Sun      Mon       Tue      Wed       Thu       Fri      Sat
+                                        Day
+#+end_example
+
+**** GUI graph
+
+     [[file:graph01.png]]
+
+
+
+*** Export contacts to =mutt=
+
+    =mu= provides =mu cfind= to get contact information from the database; it's
+    fast, since it uses cached contact data. But sometimes, we may want to get a
+    bit more advanced. For examples, suppose I want a list of names and e-mail
+    addresses of people that were seen at least 20 times since 2010, in the
+    =mutt= address book format.
+
+    We could get such a list with something like the following:
+
+#+begin_src scheme
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu) (mu message) (mu contact))
+(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_src
+
+** License & Copyright
+
+   *mu-guile* was designed and implemented by Dirk-Jan C. Binnema, and is Free
+   Software, licensed under the GNU GPLv3
+
+#+html:<hr/><div align="center">&copy; 2011-2012 Dirk-Jan C. Binnema</div>
+#+begin_html
+<script type="text/javascript">
+var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
+document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+</script>
+<script type="text/javascript">
+var pageTracker = _gat._getTracker("UA-578531-1");
+pageTracker._trackPageview();
+</script>
+#+end_html
diff --git a/www/mu-small.png b/www/mu-small.png
new file mode 100644 (file)
index 0000000..da133a6
Binary files /dev/null and b/www/mu-small.png differ
diff --git a/www/mu.css b/www/mu.css
new file mode 100644 (file)
index 0000000..ecbaca0
--- /dev/null
@@ -0,0 +1,116 @@
+/* stylesheet for mu website */
+
+body {
+        background:#ffffff;
+        margin: 50px;
+        padding: 10px;
+       font-family: arial, Helvetica, 'Bitstream Vera Sans', 'Luxi Sans', Verdana,
+                     Sans-Serif;
+       font-size: 12px;
+}
+
+.content {
+   margin:30px;
+   background: #5fb6de;
+}
+
+
+a.menu         {font-weight: bold}
+a.menu:link    {color: #ffffff; text-decoration: none;  }
+a.menu:active  {color: #ff0000; text-decoration: none; }
+a.menu:visited {color: #ffffff; text-decoration: none; }
+a.menu:hover   {color: #ff0000; text-decoration: underline; }
+
+.mine          {color: #ffffff; font-weight: bold}
+
+
+
+
+/* emacs-code -----------------------*/
+
+/* zenburnesque code blocks in for html-exported org mode */
+
+
+pre.src {
+   background: #dddddd;
+   color: #555555;
+}
+
+.org-preprocessor {
+   color: #8cd0d3;
+}
+
+.org-variable-name {
+   color: #0084C8;
+   font-weight: bold;
+}
+
+.org-string {
+   color: #4E9A06;
+}
+
+.org-type {
+   color: #2F8B58;
+   font-weight: bold;
+}
+
+.org-function-name {
+   color: #00578E;
+   font-weight: bold
+}
+
+.org-keyword {
+   color: #A52A2A;
+   font-weight: bold;
+}
+
+.org-comment {
+   color: #204A87;
+}
+
+.org-doc {
+   color: #afd8af;
+}
+
+.org-comment-delimiter {
+   color: #708070;
+}
+
+.org-constant {
+   color: #F5666D;
+}
+
+.org-builtin {
+   color: #A020F0;
+}
+
+
+.warning,  .org-warning {
+       color: yellow;
+       font-weight: bold
+}
+
+
+/* emacs other stuff --------------------- */
+      .org-date, .org-org-date {
+        /* org-date */
+        color: #00ffff;
+        text-decoration: underline;
+      }
+      .org-hide, .org-org-hide {
+        /* org-hide */
+        color: #000000;
+      }
+      .org-level-1, .org-org-level-1  {
+        /* org-level-1 */
+        color: #356da0;
+      }
+      .org-level-2,.org-org-level-2 {
+        /* org-level-2 */
+        color: #7685de;
+      }
+      .org-todo,.org-org-todo {
+        /* org-todo */
+        color: #ffc0cb;
+        font-weight: bold;
+      }
diff --git a/www/mu.jpg b/www/mu.jpg
new file mode 100644 (file)
index 0000000..14d0302
Binary files /dev/null and b/www/mu.jpg differ
diff --git a/www/mu.png b/www/mu.png
new file mode 100644 (file)
index 0000000..dd9e91f
Binary files /dev/null and b/www/mu.png differ
diff --git a/www/mu4e-1.png b/www/mu4e-1.png
new file mode 100644 (file)
index 0000000..55988fe
Binary files /dev/null and b/www/mu4e-1.png differ
diff --git a/www/mu4e-2.png b/www/mu4e-2.png
new file mode 100644 (file)
index 0000000..eade6d7
Binary files /dev/null and b/www/mu4e-2.png differ
diff --git a/www/mu4e-3.png b/www/mu4e-3.png
new file mode 100644 (file)
index 0000000..23a57bf
Binary files /dev/null and b/www/mu4e-3.png differ
diff --git a/www/mu4e-splitview-small.png b/www/mu4e-splitview-small.png
new file mode 100644 (file)
index 0000000..ad05988
Binary files /dev/null and b/www/mu4e-splitview-small.png differ
diff --git a/www/mu4e-splitview.png b/www/mu4e-splitview.png
new file mode 100644 (file)
index 0000000..7e50549
Binary files /dev/null and b/www/mu4e-splitview.png differ
diff --git a/www/mu4e.md b/www/mu4e.md
new file mode 100644 (file)
index 0000000..cd361fc
--- /dev/null
@@ -0,0 +1,59 @@
+---
+layout: default
+permalink: /code/mu/mu4e.html
+---
+
+Starting with version 0.9.8, [mu](http://www.djcbsoftware.nl/code/mu)
+provides an emacs-based e-mail client which uses `mu` as its back-end:
+*mu4e*.
+
+Through `mu`, `mu4e` sits on top of your Maildir (which you update
+with e.g. [`offlineimap`](http://offlineimap.org/),
+[`mbsync`](http://isync.sourceforge.net) or
+[`fetchmail`](http://www.fetchmail.info/)). `mu4e` is designed to
+enable super-efficient handling of e-mail; searching, reading,
+replying, moving, deleting. The overall 'feel' is a bit of a mix of
+[`dired`](http://www.gnu.org/software/emacs/manual/html_node/emacs/Dired.html)
+and [Wanderlust](http://www.gohome.org/wl/).
+
+Features include:
+
+   - Fully search-based: there are no folders, only queries
+   - UI optimized for speed with quick key strokes for common actions
+   - Fully documented, with example configurations
+   - Asynchronous: heavy actions never block emacs
+   - Write rich-text e-mails using /org-mode/ (experimental)
+   - Address auto-completion based on your messages -- no need for
+     managing address books
+   - Extendable in many places using custom actions
+
+For all the details, please see the [manual](mu4e/), or
+check the screenshots below. `mu4e` is part of the normal
+[mu source package](http://code.google.com/p/mu0/downloads/list) and
+also [available on Github](https://github.com/djcb/mu).
+
+# Screenshots
+
+## The main view
+<img src="mu4e-1.png">
+
+## The headers view
+<img src="mu4e-2.png">
+
+## The message view
+<img src="mu4e-3.png">
+
+## The message/headers split view (0.9.8.4)
+
+<img src="mu4e-splitview.png">
+
+The message/headers split view, and speedbar support.
+
+## View message as pdf (0.9.8.4)
+
+<img src="mu4egraph.png">
+
+## License & Copyright
+
+*mu4e* was designed and implemented by Dirk-Jan C. Binnema, and is
+Free Software, licensed under the GNU GPLv3
diff --git a/www/mu4egraph.png b/www/mu4egraph.png
new file mode 100644 (file)
index 0000000..089a744
Binary files /dev/null and b/www/mu4egraph.png differ
diff --git a/www/mug-full.png b/www/mug-full.png
new file mode 100644 (file)
index 0000000..8aff20f
Binary files /dev/null and b/www/mug-full.png differ
diff --git a/www/mug-thumb.png b/www/mug-thumb.png
new file mode 100644 (file)
index 0000000..012c8ab
Binary files /dev/null and b/www/mug-thumb.png differ
diff --git a/www/mug.org b/www/mug.org
new file mode 100644 (file)
index 0000000..3737e83
--- /dev/null
@@ -0,0 +1,39 @@
+#+title: Mug
+#+html:<a href="index.html"><img src="mu-small.png" border="0" align="right"/></a>
+#+style: <link rel="stylesheet" type="text/css" href="mu.css">
+
+
+* Mug
+/Mug/ is a toy/demo user-interface for =mu=. It is not installable, you'll need
+to run it from its source directory.
+
+Mug comes in two flavors:
+       - =mug= (in toys/mug), old simple UI, only adding dependency to GTK+
+       - =mug2= (in toys/mug2), the new UI, which requires GTK+, Webkit and a
+          recent GLib.
+
+The plan for =mug= is to be a testing ground for the widget-code which will
+slowly evolve into a full-featured UI.
+
+#+html:<a href="mug-full.png"><img src="mug-thumb.png" border="0" align="center"/></a>
+
+=mug2= supports:
+  - HTML email
+  - attachments (including in-place opening, drag & drop to desktop)
+  - bookmarks (see the =mu-bookmarks= man page, the UI will load these in the
+    left pane)
+  - view source
+#+html:<hr/><div align="center">&copy; 2011 Dirk-Jan C. Binnema</div>
+#+begin_html
+<script type="text/javascript">
+var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
+document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+</script>
+<script type="text/javascript">
+var pageTracker = _gat._getTracker("UA-578531-1");
+pageTracker._trackPageview();
+</script>
+#+end_html
+
+
diff --git a/www/old-news.md b/www/old-news.md
new file mode 100644 (file)
index 0000000..04178e4
--- /dev/null
@@ -0,0 +1,90 @@
+---
+layout: default
+permalink: code/mu/old-news.html
+---
+
+# Old news
+
+- 2011-07-31: mu *0.9.7-pre* is now available with a number of interesting
+new features and fixes, many based on user suggestions. `mu` now supports
+/mail threading/ based on the [JWZ-algorithm](http://www.jwz.org/doc/threading.html); output is now automatically
+converted to the user-locale; `mu view` can output separators between
+messages for easier processing, support for X-Label-tags, and last but not
+least, `mu` now has bindings for the [Guile](http://www.gnu.org/s/guile/) (Scheme) programming language -
+there is a new toy (`toys/muile`) that allows you to inspect messages and
+do all kinds of statistics - see the [README](https://gitorious.org/mu/mu/blobs/master/toys/muile/README) for more information.
+
+- 2011-06-02: after quite a bit of testing, *0.9.6* has been promoted to be
+the next release -- forget about the 'bèta'. Development continues for
+the next release.
+
+- 2011-05-28: *mu-0.9.6* (bèta). A lot of internal changes, but also quite
+some new features, for example:
+- wild-card searching for most fields: mu find 'car*'
+- search for message with certain attachments with 'attach:/a:': mu find
+'attach:resume*'
+- color for `mu find`, `mu cfind`, `mu extract` and `mu view`
+Everything is documented in the man-pages, and there are examples in the [[file:cheatsheet.org][mu
+cheatsheet]].
+
+- 2011-04-25: *mu-0.9.5* a small, but important, bugfix in maildir-detection,
+some small optimizations.
+
+- 2011-04-12: *mu 0.9.4* released - adds the `cfind` command, to find
+contacts (name + e-mail); add `flag:unread` which is a synonym for
+`flag:new OR NOT flag:seen`. Updates to the documentation and some internal
+updates. This is a *bèta-version*.
+
+- 2011-02-13: *mu 0.9.3*; fixes a bunch of minor issues in 0.9.2; updated the
+web page with pages for [mu cheatsheet](file:mug.org][mug]] (the experimental UI) and the [[file:cheatsheet.org).
+
+- 2011-02-02: *mu 0.9.2* released, which adds support for matching on message
+size, and various new output format. See [NEWS](http://gitorious.org/mu/mu/blobs/master/NEWS) for all the user-visible
+changes, also from older releases.
+
+
+- [2010-12-05] *mu version 0.9.1* released; fixes a couple of issues users
+found with a missing icon, the unit-tests.
+- [2010-12-04] *mu version 0.9* released. Compared to the bèta-release, there
+were a number of improvements to the documentation and the unit
+tests. Pre-processing queries is a little bit smarter now, making matching
+e-mail address more eager. Experimental support for Fedora-14.
+- [2010-11-27] *mu version 0.9-beta* released. New features: searching is now
+accent-insensitive; you can now search for message priority (`prio:`),
+time-interval (`date:`) and message flags (`flag:`). Also, you can now store
+('bookmark') often-used queries. To top it off, there is a simple graphical
+UI now, called `mug`. Documentation has been update, and all known bugs have
+been fixed.
+- [2010-10-30] *mu version 0.8* released, with only some small cosmetic
+updates compared to 0.8-beta. Hurray!
+- [2010-10-23] *mu version 0.8-beta* released. The new version brings `mu
+extract` for getting the attachments from messages, improved searching
+(matching is a bit more 'greedy'), updated and extended documentation,
+including the `mu-easy` manpage with simple examples. All known
+non-enhancement bugs were fixed.
+- [2010-02-27] *mu version 0.7* released. Compared to the beta version, there
+are few changes. The maildir-matching syntax now contains a starting `/`, so
+`~/Maildir/foo/bar/(cur|new)/msg` can be matched with `m:/foo/bar`. The
+top-level maildir can be matched with `m:/`. Apart from that, there are so
+small cosmetic fixes and documentation updates.
+- [2010-02-11] *mu version 0.7-beta* released. A lot of changes:
+- Automatic database scheme version check, notifies users when an
+upgrade is needed
+- Adds the `mu view` command, to view mail message files
+- Removes the 10K match limit
+- Support for unattended upgrades - that is, the database can
+automatically be 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|new)/msg` with `m:foo/bar`. This replaces the
+search for path/p in 0.6
+- Fixes for reported issues #17 and #18
+- A test suite with a growing number of unit tests
+- Updated documentation
+- Many internal refactoring and other changes
+This version has been
+tagged as `v0.7-beta` in repository, and must be considered a code-complete
+preview of the upcoming release 0.7. Please report any problems you encounter
+with it.