From: David Bremner Date: Sun, 8 Jan 2017 12:42:26 +0000 (-0400) Subject: Merge branch 'release' X-Git-Tag: 0.24_rc0~52 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=b15b96f846dcfabbd0c7f43e741573cfd14ab3c8;hp=44520bb6add5e39e12de689a168007d3d2c7c734 Merge branch 'release' These are the (tentative) changes for 0.23.5 --- diff --git a/Makefile b/Makefile index 4c0e8c62..0ef57fa9 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,8 @@ ifeq ($(configure_options),) endif $(srcdir)/configure $(configure_options) +# runtime variable definitions available in all subdirs +include $(srcdir)/Makefile.global # Finally, include all of the Makefile.local fragments where all the # real work is done. diff --git a/Makefile.global b/Makefile.global new file mode 100644 index 00000000..d8f335af --- /dev/null +++ b/Makefile.global @@ -0,0 +1,65 @@ +# Here's the (hopefully simple) versioning scheme. +# +# Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We +# increment the second digit for each release and increment the first +# digit when we reach particularly major milestones of usability. +# +# Between releases, (such as when compiling notmuch from the git +# repository), we let git append identification of the actual commit. +PACKAGE=notmuch + +IS_GIT:=$(if $(wildcard ${srcdir}/.git),yes,no) + +ifeq ($(IS_GIT),yes) +DATE:=$(shell git --git-dir=${srcdir}/.git log --date=short -1 --pretty=format:%cd) +else +DATE:=$(shell date +%F) +endif + +VERSION:=$(shell cat ${srcdir}/version) +ELPA_VERSION:=$(subst ~,_,$(VERSION)) +ifeq ($(filter release release-message pre-release update-versions,$(MAKECMDGOALS)),) +ifeq ($(IS_GIT),yes) +VERSION:=$(shell git --git-dir=${srcdir}/.git describe --abbrev=7 --match '[0-9.]*'|sed -e s/_/~/ -e s/-/+/ -e s/-/~/) +# drop the ~g$sha1 part +ELPA_VERSION:=$(word 1,$(subst ~, ,$(VERSION))) +# convert git version to package.el friendly form +ELPA_VERSION:=$(subst +,snapshot,$(ELPA_VERSION)) + +# Write the file 'version.stamp' in case its contents differ from $(VERSION) +FILE_VERSION:=$(shell test -f version.stamp && read vs < version.stamp || vs=; echo $$vs) +ifneq ($(FILE_VERSION),$(VERSION)) + $(shell echo "$(VERSION)" > version.stamp) +endif +endif +endif + +UPSTREAM_TAG=$(subst ~,_,$(VERSION)) +DEB_TAG=debian/$(UPSTREAM_TAG)-1 + +RELEASE_HOST=notmuchmail.org +RELEASE_DIR=/srv/notmuchmail.org/www/releases +RELEASE_URL=https://notmuchmail.org/releases +TAR_FILE=$(PACKAGE)-$(VERSION).tar.gz +ELPA_FILE:=$(PACKAGE)-emacs-$(ELPA_VERSION).tar +DEB_TAR_FILE=$(PACKAGE)_$(VERSION).orig.tar.gz +SHA1_FILE=$(TAR_FILE).sha1 +GPG_FILE=$(SHA1_FILE).asc + +PV_FILE=bindings/python/notmuch/version.py + +# Smash together user's values with our extra values +STD_CFLAGS := -std=gnu99 +FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(STD_CFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS) +FINAL_CXXFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CONFIGURE_CXXFLAGS) +FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Lutil -lutil -Llib -lnotmuch +ifeq ($(LIBDIR_IN_LDCONFIG),0) +FINAL_NOTMUCH_LDFLAGS += $(RPATH_LDFLAGS) +endif +FINAL_NOTMUCH_LDFLAGS += $(AS_NEEDED_LDFLAGS) $(GMIME_LDFLAGS) $(TALLOC_LDFLAGS) $(ZLIB_LDFLAGS) +FINAL_NOTMUCH_LINKER = CC +ifneq ($(LINKER_RESOLVES_LIBRARY_DEPENDENCIES),1) +FINAL_NOTMUCH_LDFLAGS += $(CONFIGURE_LDFLAGS) +FINAL_NOTMUCH_LINKER = CXX +endif +FINAL_LIBNOTMUCH_LDFLAGS = $(LDFLAGS) $(AS_NEEDED_LDFLAGS) $(CONFIGURE_LDFLAGS) diff --git a/Makefile.local b/Makefile.local index 0a122ab0..3548ed96 100644 --- a/Makefile.local +++ b/Makefile.local @@ -1,67 +1,5 @@ # -*- makefile -*- -# Here's the (hopefully simple) versioning scheme. -# -# Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We -# increment the second digit for each release and increment the first -# digit when we reach particularly major milestones of usability. -# -# Between releases, (such as when compiling notmuch from the git -# repository), we let git append identification of the actual commit. -PACKAGE=notmuch - -IS_GIT:=$(if $(wildcard ${srcdir}/.git),yes,no) - -ifeq ($(IS_GIT),yes) -DATE:=$(shell git --git-dir=${srcdir}/.git log --date=short -1 --pretty=format:%cd) -else -DATE:=$(shell date +%F) -endif - -VERSION:=$(shell cat ${srcdir}/version) -ELPA_VERSION:=$(subst ~,_,$(VERSION)) -ifeq ($(filter release release-message pre-release update-versions,$(MAKECMDGOALS)),) -ifeq ($(IS_GIT),yes) -VERSION:=$(shell git --git-dir=${srcdir}/.git describe --abbrev=7 --match '[0-9.]*'|sed -e s/_/~/ -e s/-/+/ -e s/-/~/) -# drop the ~g$sha1 part -ELPA_VERSION:=$(word 1,$(subst ~, ,$(VERSION))) -# Write the file 'version.stamp' in case its contents differ from $(VERSION) -FILE_VERSION:=$(shell test -f version.stamp && read vs < version.stamp || vs=; echo $$vs) -ifneq ($(FILE_VERSION),$(VERSION)) - $(shell echo "$(VERSION)" > version.stamp) -endif -endif -endif - -UPSTREAM_TAG=$(subst ~,_,$(VERSION)) -DEB_TAG=debian/$(UPSTREAM_TAG)-1 - -RELEASE_HOST=notmuchmail.org -RELEASE_DIR=/srv/notmuchmail.org/www/releases -RELEASE_URL=https://notmuchmail.org/releases -TAR_FILE=$(PACKAGE)-$(VERSION).tar.gz -DEB_TAR_FILE=$(PACKAGE)_$(VERSION).orig.tar.gz -SHA1_FILE=$(TAR_FILE).sha1 -GPG_FILE=$(SHA1_FILE).asc - -PV_FILE=bindings/python/notmuch/version.py - -# Smash together user's values with our extra values -STD_CFLAGS := -std=gnu99 -FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CPPFLAGS) $(STD_CFLAGS) $(CFLAGS) $(WARN_CFLAGS) $(extra_cflags) $(CONFIGURE_CFLAGS) -FINAL_CXXFLAGS = $(CPPFLAGS) $(CXXFLAGS) $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CONFIGURE_CXXFLAGS) -FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Lutil -lutil -Llib -lnotmuch -ifeq ($(LIBDIR_IN_LDCONFIG),0) -FINAL_NOTMUCH_LDFLAGS += $(RPATH_LDFLAGS) -endif -FINAL_NOTMUCH_LDFLAGS += $(AS_NEEDED_LDFLAGS) $(GMIME_LDFLAGS) $(TALLOC_LDFLAGS) $(ZLIB_LDFLAGS) -FINAL_NOTMUCH_LINKER = CC -ifneq ($(LINKER_RESOLVES_LIBRARY_DEPENDENCIES),1) -FINAL_NOTMUCH_LDFLAGS += $(CONFIGURE_LDFLAGS) -FINAL_NOTMUCH_LINKER = CXX -endif -FINAL_LIBNOTMUCH_LDFLAGS = $(LDFLAGS) $(AS_NEEDED_LDFLAGS) $(CONFIGURE_LDFLAGS) - .PHONY: all all: notmuch notmuch-shared build-man ruby-bindings ifeq ($(MAKECMDGOALS),) @@ -336,11 +274,6 @@ ifeq ($(WITH_EMACS), 1) endif endif -.PHONY: install-desktop -install-desktop: - mkdir -p "$(DESTDIR)$(desktop_dir)" - desktop-file-install --mode 0644 --dir "$(DESTDIR)$(desktop_dir)" notmuch.desktop - SRCS := $(SRCS) $(notmuch_client_srcs) CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules) CLEAN := $(CLEAN) version.stamp notmuch-*.tar.gz.tmp diff --git a/completion/notmuch-completion.bash b/completion/notmuch-completion.bash index 78047b5f..d44b2a28 100644 --- a/completion/notmuch-completion.bash +++ b/completion/notmuch-completion.bash @@ -58,6 +58,34 @@ _notmuch_email() sed 's/[^<]*<\([^>]*\)>/\1/' | tr "[:upper:]" "[:lower:]" | sort -u } +_notmuch_mimetype() +{ + # use mime types from mime-support package if available, and fall + # back to a handful of common ones otherwise + if [ -r "/etc/mime.types" ]; then + sed -n '/^[[:alpha:]]/{s/[[:space:]].*//;p;}' /etc/mime.types + else + cat < /dev/null; then + printf "Yes.\n" + else + printf "No (so will not install .desktop file).\n" + WITH_DESKTOP=0 + fi +fi + libdir_in_ldconfig=0 printf "Checking which platform we are on... " @@ -1123,6 +1143,9 @@ VALGRIND_CFLAGS = ${valgrind_cflags} # Support for emacs WITH_EMACS = ${WITH_EMACS} +# Support for desktop file +WITH_DESKTOP = ${WITH_DESKTOP} + # Support for bash completion WITH_BASH = ${WITH_BASH} @@ -1142,6 +1165,7 @@ COMMON_CONFIGURE_CFLAGS = \\ -DSTD_GETPWUID=\$(STD_GETPWUID) \\ -DSTD_ASCTIME=\$(STD_ASCTIME) \\ -DHAVE_XAPIAN_COMPACT=\$(HAVE_XAPIAN_COMPACT) \\ + -DSILENCE_XAPIAN_DEPRECATION_WARNINGS \\ -DHAVE_XAPIAN_FIELD_PROCESSOR=\$(HAVE_XAPIAN_FIELD_PROCESSOR) \\ -DHAVE_XAPIAN_DB_RETRY_LOCK=\$(HAVE_XAPIAN_DB_RETRY_LOCK) \\ -DUTIL_BYTE_ORDER=\$(UTIL_BYTE_ORDER) diff --git a/devel/emacs-keybindings.org b/devel/emacs-keybindings.org new file mode 100644 index 00000000..464b9467 --- /dev/null +++ b/devel/emacs-keybindings.org @@ -0,0 +1,58 @@ +|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------| +| Key | Search Mode | Show Mode | Tree Mode | +|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------| +| a | notmuch-search-archive-thread | notmuch-show-archive-message-then-next-or-next-thread | notmuch-tree-archive-message-then-next | +| b | notmuch-search-scroll-down | notmuch-show-resend-message | notmuch-show-resend-message | +| c | notmuch-search-stash-map | notmuch-show-stash-map | notmuch-show-stash-map | +| d | | | | +| e | | | (notmuch-tree-button-activate) | +| f | | notmuch-show-forward-message | notmuch-show-forward-message | +| g | | | | +| h | | notmuch-show-toggle-visibility-headers | | +| i | | | | +| j | notmuch-jump-search | notmuch-jump-search | notmuch-jump-search | +| k | notmuch-tag-jump | notmuch-tag-jump | notmuch-tag-jump | +| l | notmuch-search-filter | notmuch-show-filter-thread | | +| m | notmuch-mua-new-mail | notmuch-mua-new-mail | notmuch-mua-new-mail | +| n | notmuch-search-next-thread | notmuch-show-next-open-message | notmuch-tree-next-matching-message | +| o | notmuch-search-toggle-order | | | +| p | notmuch-search-previous-thread | notmuch-show-previous-open-message | notmuch-tree-prev-matching-message | +| q | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | notmuch-bury-or-kill-this-buffer | +| r | notmuch-search-reply-to-thread-sender | notmuch-show-reply-sender | notmuch-show-reply-sender | +| s | notmuch-search | notmuch-search | notmuch-search | +| t | notmuch-search-filter-by-tag | toggle-truncate-lines | | +| u | | | | +| v | | | notmuch-show-view-all-mime-parts | +| w | | notmuch-show-save-attachments | notmuch-show-save-attachments | +| x | notmuch-bury-or-kill-this-buffer | notmuch-show-archive-message-then-next-or-exit | notmuch-tree-quit | +| y | | | | +| z | notmuch-tree | notmuch-tree | notmuch-tree-to-tree | +| A | | notmuch-show-archive-thread-then-next | notmuch-tree-archive-thread | +| F | | notmuch-show-forward-open-messages | | +| G | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer | notmuch-poll-and-refresh-this-buffer | +| N | | notmuch-show-next-message | notmuch-tree-next-message | +| O | | | | +| P | | notmuch-show-previous-message | notmuch-tree-prev-message | +| R | notmuch-search-reply-to-thread | notmuch-show-reply | notmuch-show-reply | +| S | | | notmuch-search-from-tree-current-query | +| V | | notmuch-show-view-raw-message | notmuch-show-view-raw-message | +| X | | notmuch-show-archive-thread-then-exit | | +| Z | notmuch-tree-from-search-current-query | notmuch-tree-from-show-current-query | | +| =!= | | notmuch-show-toggle-elide-non-matching | | +| =#= | | notmuch-show-print-message | | +| =$= | | notmuch-show-toggle-process-crypto | | +| =*= | notmuch-search-tag-all | notmuch-show-tag-all | notmuch-tree-tag-thread | +| + | notmuch-search-add-tag | notmuch-show-add-tag | notmuch-tree-add-tag | +| - | notmuch-search-remove-tag | notmuch-show-remove-tag | notmuch-tree-remove-tag | +| . | | notmuch-show-part-map | | +| < | notmuch-search-first-thread | notmuch-show-toggle-thread-indentation | | +| | notmuch-search-scroll-down | notmuch-show-rewind | notmuch-tree-scroll-message-window-back | +| | notmuch-search-show-thread | notmuch-show-toggle-message | notmuch-tree-show-message | +| | notmuch-search-scroll-up | notmuch-show-advance | notmuch-tree-scroll-or-next | +| | | notmuch-show-next-button | notmuch-show-next-button | +| | | notmuch-show-previous-button | notmuch-show-previous-button | +| = | notmuch-refresh-this-buffer | notmuch-refresh-this-buffer | notmuch-tree-refresh-view | +| > | notmuch-search-last-thread | | | +| ? | notmuch-help | notmuch-help | notmuch-help | +| \vert | | notmuch-show-pipe-message | notmuch-show-pipe-message | +|-----------+----------------------------------------+-------------------------------------------------------+-----------------------------------------| diff --git a/devel/nmbug/nmbug b/devel/nmbug/nmbug index 1dd5f14f..6febf16f 100755 --- a/devel/nmbug/nmbug +++ b/devel/nmbug/nmbug @@ -475,7 +475,7 @@ def log(args=()): 'nmbug log HEAD..@{upstream}'. """ # we don't want output trapping here, because we want the pager. - args = ['log', '--name-status'] + list(args) + args = ['log', '--name-status', '--no-renames'] + list(args) with _git(args=args, expect=(0, 1, -13)) as p: p.wait() diff --git a/doc/.gitignore b/doc/.gitignore index d0da78e5..9fa35d08 100644 --- a/doc/.gitignore +++ b/doc/.gitignore @@ -1,4 +1,3 @@ *.pyc -docdeps.mk _build config.dox diff --git a/doc/Makefile.local b/doc/Makefile.local index 8633cfcd..c6f05ca8 100644 --- a/doc/Makefile.local +++ b/doc/Makefile.local @@ -7,13 +7,23 @@ SPHINXOPTS := -q SPHINXBUILD = sphinx-build DOCBUILDDIR := $(dir)/_build -mkdocdeps := $(PYTHON) $(srcdir)/$(dir)/mkdocdeps.py - # Internal variables. ALLSPHINXOPTS := -d $(DOCBUILDDIR)/doctrees $(SPHINXOPTS) $(srcdir)/$(dir) APIMAN := $(DOCBUILDDIR)/man/man3/notmuch.3 DOXYFILE := $(srcdir)/$(dir)/doxygen.cfg +MAN1_RST := $(wildcard $(srcdir)/doc/man1/*.rst) +MAN5_RST := $(wildcard $(srcdir)/doc/man5/*.rst) +MAN7_RST := $(wildcard $(srcdir)/doc/man7/*.rst) +MAN_RST_FILES := $(MAN1_RST) $(MAN5_RST) $(MAN7_RST) + +MAN1_ROFF := $(patsubst $(srcdir)/doc/%,$(DOCBUILDDIR)/man/%,$(MAN1_RST:.rst=.1)) +MAN5_ROFF := $(patsubst $(srcdir)/doc/%,$(DOCBUILDDIR)/man/%,$(MAN5_RST:.rst=.5)) +MAN7_ROFF := $(patsubst $(srcdir)/doc/%,$(DOCBUILDDIR)/man/%,$(MAN7_RST:.rst=.7)) +MAN_ROFF_FILES := $(MAN1_ROFF) $(MAN5_ROFF) $(MAN7_ROFF) + +MAN_GZIP_FILES := $(addsuffix .gz,${MAN_ROFF_FILES}) + .PHONY: sphinx-html sphinx-texinfo sphinx-info .PHONY: install-man build-man apidocs install-apidocs @@ -30,10 +40,6 @@ sphinx-texinfo: sphinx-info: sphinx-texinfo make -C $(DOCBUILDDIR)/texinfo info --include $(dir)/docdeps.mk - -MAN_GZIP_FILES := $(addsuffix .gz,${MAN_ROFF_FILES}) - # Use the man page converter that is available. We should never depend # on MAN_ROFF_FILES if a converter is not available. ${MAN_ROFF_FILES}: $(DOCBUILDDIR)/.roff.stamp @@ -53,7 +59,7 @@ else @echo "Fatal: build dependency fail." @false endif - touch ${MAN_ROFF_FILES} $@ + touch $@ install-man: install-apidocs @@ -62,7 +68,7 @@ MAN_GZIP_FILES += ${APIMAN}.gz apidocs: $(APIMAN) install-apidocs: ${APIMAN}.gz mkdir -p "$(DESTDIR)$(mandir)/man3" - install -m0644 $(DOCBUILDDIR)/man/man3/*.3.gz $(DESTDIR)/$(mandir)/man3 + install -m0644 $(filter %.3.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man3 $(APIMAN): $(dir)/config.dox $(srcdir)/$(dir)/doxygen.cfg $(srcdir)/lib/notmuch.h mkdir -p $(DOCBUILDDIR)/man/man3 @@ -86,9 +92,9 @@ install-man: ${MAN_GZIP_FILES} mkdir -p "$(DESTDIR)$(mandir)/man1" mkdir -p "$(DESTDIR)$(mandir)/man5" mkdir -p "$(DESTDIR)$(mandir)/man7" - install -m0644 $(DOCBUILDDIR)/man/man1/*.1.gz $(DESTDIR)/$(mandir)/man1 - install -m0644 $(DOCBUILDDIR)/man/man5/*.5.gz $(DESTDIR)/$(mandir)/man5 - install -m0644 $(DOCBUILDDIR)/man/man7/*.7.gz $(DESTDIR)/$(mandir)/man7 + install -m0644 $(filter %.1.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man1 + install -m0644 $(filter %.5.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man5 + install -m0644 $(filter %.7.gz,$(MAN_GZIP_FILES)) $(DESTDIR)/$(mandir)/man7 cd $(DESTDIR)/$(mandir)/man1 && ln -sf notmuch.1.gz notmuch-setup.1.gz endif @@ -96,8 +102,5 @@ $(dir)/config.dox: version.stamp echo "PROJECT_NAME = \"Notmuch $(VERSION)\"" > $@ echo "INPUT=${srcdir}/lib/notmuch.h" >> $@ -$(dir)/docdeps.mk: $(dir)/conf.py $(dir)/mkdocdeps.py - $(mkdocdeps) $(srcdir)/doc $(DOCBUILDDIR) $@ - -CLEAN := $(CLEAN) $(DOCBUILDDIR) $(dir)/docdeps.mk $(DOCBUILDDIR)/.roff.stamp +CLEAN := $(CLEAN) $(DOCBUILDDIR) $(DOCBUILDDIR)/.roff.stamp CLEAN := $(CLEAN) $(MAN_GZIP_FILES) $(MAN_ROFF_FILES) $(dir)/conf.pyc $(dir)/config.dox diff --git a/doc/conf.py b/doc/conf.py index 356a2b2b..a3d82696 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -52,74 +52,74 @@ htmlhelp_basename = 'notmuchdoc' # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - -('man1/notmuch','notmuch', - u'thread-based email index, search, and tagging', - [u'Carl Worth and many others'], 1), - -('man1/notmuch-address','notmuch-address', - u'output addresses from matching messages', - [u'Carl Worth and many others'], 1), +notmuch_authors = u'Carl Worth and many others' -('man1/notmuch-compact','notmuch-compact', - u'compact the notmuch database', - [u'Carl Worth and many others'], 1), +man_pages = [ + ('man1/notmuch', 'notmuch', + u'thread-based email index, search, and tagging', + [notmuch_authors], 1), -('man1/notmuch-config','notmuch-config', - u'access notmuch configuration file', - [u'Carl Worth and many others'], 1), + ('man1/notmuch-address', 'notmuch-address', + u'output addresses from matching messages', + [notmuch_authors], 1), -('man1/notmuch-count','notmuch-count', - u'count messages matching the given search terms', - [u'Carl Worth and many others'], 1), + ('man1/notmuch-compact', 'notmuch-compact', + u'compact the notmuch database', + [notmuch_authors], 1), -('man1/notmuch-dump','notmuch-dump', - u'creates a plain-text dump of the tags of each message', - [u'Carl Worth and many others'], 1), + ('man1/notmuch-config', 'notmuch-config', + u'access notmuch configuration file', + [notmuch_authors], 1), -('man1/notmuch-emacs-mua','notmuch-emacs-mua', - u'send mail with notmuch and emacs', - [u'Carl Worth and many others'], 1), + ('man1/notmuch-count', 'notmuch-count', + u'count messages matching the given search terms', + [notmuch_authors], 1), -('man5/notmuch-hooks','notmuch-hooks', - u'hooks for notmuch', - [u'Carl Worth and many others'], 5), + ('man1/notmuch-dump', 'notmuch-dump', + u'creates a plain-text dump of the tags of each message', + [notmuch_authors], 1), -('man1/notmuch-insert','notmuch-insert', - u'add a message to the maildir and notmuch database', - [u'Carl Worth and many others'], 1), + ('man1/notmuch-emacs-mua', 'notmuch-emacs-mua', + u'send mail with notmuch and emacs', + [notmuch_authors], 1), -('man1/notmuch-new','notmuch-new', - u'incorporate new mail into the notmuch database', - [u'Carl Worth and many others'], 1), + ('man5/notmuch-hooks', 'notmuch-hooks', + u'hooks for notmuch', + [notmuch_authors], 5), -('man1/notmuch-reply','notmuch-reply', - u'constructs a reply template for a set of messages', - [u'Carl Worth and many others'], 1), + ('man1/notmuch-insert', 'notmuch-insert', + u'add a message to the maildir and notmuch database', + [notmuch_authors], 1), -('man1/notmuch-restore','notmuch-restore', - u'restores the tags from the given file (see notmuch dump)', - [u'Carl Worth and many others'], 1), + ('man1/notmuch-new', 'notmuch-new', + u'incorporate new mail into the notmuch database', + [notmuch_authors], 1), -('man1/notmuch-search','notmuch-search', - u'search for messages matching the given search terms', - [u'Carl Worth and many others'], 1), + ('man1/notmuch-reply', 'notmuch-reply', + u'constructs a reply template for a set of messages', + [notmuch_authors], 1), -('man7/notmuch-search-terms','notmuch-search-terms', - u'syntax for notmuch queries', - [u'Carl Worth and many others'], 7), + ('man1/notmuch-restore', 'notmuch-restore', + u'restores the tags from the given file (see notmuch dump)', + [notmuch_authors], 1), -('man1/notmuch-show','notmuch-show', - u'show messages matching the given search terms', - [u'Carl Worth and many others'], 1), + ('man1/notmuch-search', 'notmuch-search', + u'search for messages matching the given search terms', + [notmuch_authors], 1), -('man1/notmuch-tag','notmuch-tag', - u'add/remove tags for all messages matching the search terms', - [u'Carl Worth and many others'], 1), + ('man7/notmuch-search-terms', 'notmuch-search-terms', + u'syntax for notmuch queries', + [notmuch_authors], 7), + ('man1/notmuch-show', 'notmuch-show', + u'show messages matching the given search terms', + [notmuch_authors], 1), + ('man1/notmuch-tag', 'notmuch-tag', + u'add/remove tags for all messages matching the search terms', + [notmuch_authors], 1), ] + # If true, show URL addresses after external links. #man_show_urls = False @@ -132,52 +132,19 @@ man_pages = [ texinfo_no_detailmenu = True texinfo_documents = [ - ('notmuch-emacs', 'notmuch-emacs', u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-emacs', - 'emacs based front-end for notmuch', 'Miscellaneous'), -('man1/notmuch','notmuch',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch', - 'thread-based email index, search, and tagging','Miscellaneous'), -('man1/notmuch-address','notmuch-address',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-address', - 'output addresses from matching messages','Miscellaneous'), -('man1/notmuch-compact','notmuch-compact',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-compact', - 'compact the notmuch database','Miscellaneous'), -('man1/notmuch-config','notmuch-config',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-config', - 'access notmuch configuration file','Miscellaneous'), -('man1/notmuch-count','notmuch-count',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-count', - 'count messages matching the given search terms','Miscellaneous'), -('man1/notmuch-dump','notmuch-dump',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-dump', - 'creates a plain-text dump of the tags of each message','Miscellaneous'), -('man5/notmuch-hooks','notmuch-hooks',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-hooks', - 'hooks for notmuch','Miscellaneous'), -('man1/notmuch-insert','notmuch-insert',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-insert', - 'add a message to the maildir and notmuch database','Miscellaneous'), -('man1/notmuch-new','notmuch-new',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-new', - 'incorporate new mail into the notmuch database','Miscellaneous'), -('man1/notmuch-reply','notmuch-reply',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-reply', - 'constructs a reply template for a set of messages','Miscellaneous'), -('man1/notmuch-restore','notmuch-restore',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-restore', - 'restores the tags from the given file (see notmuch dump)','Miscellaneous'), -('man1/notmuch-search','notmuch-search',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-search', - 'search for messages matching the given search terms','Miscellaneous'), -('man7/notmuch-search-terms','notmuch-search-terms',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-search-terms', - 'syntax for notmuch queries','Miscellaneous'), -('man1/notmuch-show','notmuch-show',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-show', - 'show messages matching the given search terms','Miscellaneous'), -('man1/notmuch-tag','notmuch-tag',u'notmuch Documentation', - u'Carl Worth and many others', 'notmuch-tag', - 'add/remove tags for all messages matching the search terms','Miscellaneous'), + ('notmuch-emacs', 'notmuch-emacs', u'notmuch-emacs documentation', + notmuch_authors, 'notmuch-emacs', + 'emacs based front-end for notmuch', 'Miscellaneous'), ] + +# generate texinfo list from man page list +texinfo_documents += [ + ( + x[0], # source start file + x[1], # target name + x[1] + u' documentation', # title + x[3][0], # author + x[1], # dir menu entry + x[2], # description + 'Miscellaneous' # category + ) for x in man_pages] diff --git a/doc/man1/notmuch-address.rst b/doc/man1/notmuch-address.rst index 7f7214b3..446cefbd 100644 --- a/doc/man1/notmuch-address.rst +++ b/doc/man1/notmuch-address.rst @@ -64,11 +64,11 @@ Supported options for **address** include messages. This is not applicable with --output=count. **mailbox** - Deduplicate addresses based on the full, case sensitive - name and email address, or mailbox. This is effectively - the same as piping the --deduplicate=no output to **sort | - uniq**, except for the order of results. This is the - default. + Deduplicate addresses based on the full, case sensitive + name and email address, or mailbox. This is effectively + the same as piping the --deduplicate=no output to **sort | + uniq**, except for the order of results. This is the + default. **address** Deduplicate addresses based on the case insensitive diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst index 5a517ebd..7483b75f 100644 --- a/doc/man1/notmuch-config.rst +++ b/doc/man1/notmuch-config.rst @@ -129,14 +129,14 @@ The available configuration items are described below. Name (or full path) of gpg binary to use in verification and decryption of PGP/MIME messages. - + Default: ``gpg``. **built_with.** - Compile time feature . Current possibilities include - "compact" (see **notmuch-compact(1)**) - and "field_processor" (see **notmuch-search-terms(7)**). + Compile time feature . Current possibilities include + "compact" (see **notmuch-compact(1)**) + and "field_processor" (see **notmuch-search-terms(7)**). **query.** diff --git a/doc/man1/notmuch-count.rst b/doc/man1/notmuch-count.rst index 99de13a9..90d852ae 100644 --- a/doc/man1/notmuch-count.rst +++ b/doc/man1/notmuch-count.rst @@ -48,9 +48,9 @@ Supported options for **count** include compatible with specifying search terms on the command line. ``--lastmod`` - Append lastmod (counter for number of database updates) and UUID - to the output. lastmod values are only comparable between databases - with the same UUID. + Append lastmod (counter for number of database updates) and UUID + to the output. lastmod values are only comparable between databases + with the same UUID. ``--input=``\ Read input from given file, instead of from stdin. Implies diff --git a/doc/man1/notmuch-dump.rst b/doc/man1/notmuch-dump.rst index 58570272..ca048aeb 100644 --- a/doc/man1/notmuch-dump.rst +++ b/doc/man1/notmuch-dump.rst @@ -77,21 +77,21 @@ Supported options for **dump** include **config** - Output configuration data stored in the database. Each line - starts with "#@ ", followed by a space separated key-value - pair. Both key and value are hex encoded if needed. + Output configuration data stored in the database. Each line + starts with "#@ ", followed by a space separated key-value + pair. Both key and value are hex encoded if needed. **properties** - Output per-message (key,value) metadata. Each line starts - with "#= ", followed by a message id, and a space separated - list of key=value pairs. pair. Ids, keys and values are hex - encoded if needed. + Output per-message (key,value) metadata. Each line starts + with "#= ", followed by a message id, and a space separated + list of key=value pairs. pair. Ids, keys and values are hex + encoded if needed. **tags** - Output per-message boolean metadata, namely tags. See *format* above - for description of the output. + Output per-message boolean metadata, namely tags. See *format* above + for description of the output. The default is to include all available types of data. The option can be specified multiple times to select some subset. As diff --git a/doc/man1/notmuch-emacs-mua.rst b/doc/man1/notmuch-emacs-mua.rst index 7c572904..87787e20 100644 --- a/doc/man1/notmuch-emacs-mua.rst +++ b/doc/man1/notmuch-emacs-mua.rst @@ -5,15 +5,15 @@ notmuch-emacs-mua SYNOPSIS ======== -**notmuch-emacs-mua** [options ...] [ ...] +**notmuch** **emacs-mua** [options ...] [ ... | ] DESCRIPTION =========== Start composing an email in the Notmuch Emacs UI with the specified -subject, recipients, and message body. +subject, recipients, and message body, or mailto: URL. -Supported options for **notmuch-emacs-mua** include +Supported options for **emacs-mua** include ``-h, --help`` Display help. @@ -33,6 +33,10 @@ Supported options for **notmuch-emacs-mua** include ``-i, --body=``\ Specify a file to include into the body of the message. + ``--hello`` + Go to the Notmuch hello screen instead of the message composition + window if no message composition parameters are given. + ``--no-window-system`` Even if a window system is available, use the current terminal. @@ -56,7 +60,9 @@ Supported options for **notmuch-emacs-mua** include Output the resulting elisp to stdout instead of evaluating it. The supported positional parameters and short options are a compatible -subset of the **mutt** MUA command-line options. +subset of the **mutt** MUA command-line options. The options and +positional parameters modifying the message can't be combined with the +mailto: URL. Options may be specified multiple times. diff --git a/doc/man1/notmuch-restore.rst b/doc/man1/notmuch-restore.rst index c681fa2d..cb68bc8a 100644 --- a/doc/man1/notmuch-restore.rst +++ b/doc/man1/notmuch-restore.rst @@ -54,23 +54,23 @@ Supported options for **restore** include Control what kind of metadata is restored. - **config** + **config** - Restore configuration data to the database. Each configuration line starts - with "#@ ", followed by a space separated key-value pair. - Both key and value are hex encoded if needed. + Restore configuration data to the database. Each configuration line starts + with "#@ ", followed by a space separated key-value pair. + Both key and value are hex encoded if needed. - **properties** + **properties** - Output per-message (key,value) metadata. Each line starts - with "#= ", followed by a message id, and a space separated - list of key=value pairs. pair. Ids, keys and values are - hex encoded if needed. + Output per-message (key,value) metadata. Each line starts + with "#= ", followed by a message id, and a space separated + list of key=value pairs. pair. Ids, keys and values are + hex encoded if needed. - **tags** + **tags** - Output per-message metadata, namely tags. See *format* above - for more details. + Output per-message metadata, namely tags. See *format* above + for more details. The default is to restore all available types of data. The option can be specified multiple times to select some subset. diff --git a/doc/mkdocdeps.py b/doc/mkdocdeps.py deleted file mode 100644 index b87fe3e8..00000000 --- a/doc/mkdocdeps.py +++ /dev/null @@ -1,18 +0,0 @@ -import sys - -srcdir = sys.argv[1] -builddir = sys.argv[2] -outfile = sys.argv[3] - -sys.path.insert(0, srcdir) -import conf - -roff_files = [] -rst_files = [] -for page in conf.man_pages: - rst_files = rst_files + ["{0:s}/{1:s}.rst".format(srcdir,page[0])] - roff_files = roff_files + ["{0:s}/man/{1:s}.{2:d}".format(builddir,page[0],page[4])] - -with open(outfile, 'w') as out: - out.write('MAN_ROFF_FILES := ' + ' \\\n\t'.join(roff_files) + '\n') - out.write('MAN_RST_FILES := ' + ' \\\n\t'.join(rst_files) + '\n') diff --git a/doc/notmuch-emacs.rst b/doc/notmuch-emacs.rst index d68542d3..5e25996f 100644 --- a/doc/notmuch-emacs.rst +++ b/doc/notmuch-emacs.rst @@ -189,10 +189,12 @@ following key bindings: ``j`` Jump to saved searches using :ref:`notmuch-jump`. +.. _notmuch-jump: + notmuch-jump ------------ -Saved searches configured through :ref:`notmuch-saved-searches` can +Saved searches configured through :ref:`saved-searches` can include a "shortcut key" that's accessible through notmuch-jump. Pressing ``j`` anywhere in notmuch followed by the configured shortcut key of a saved search will immediately jump to that saved search. For diff --git a/emacs/Makefile.local b/emacs/Makefile.local index dfa7c1f1..040e64d4 100644 --- a/emacs/Makefile.local +++ b/emacs/Makefile.local @@ -21,7 +21,10 @@ emacs_sources := \ $(dir)/notmuch-print.el \ $(dir)/notmuch-version.el \ $(dir)/notmuch-jump.el \ - $(dir)/notmuch-company.el + $(dir)/notmuch-company.el \ + $(dir)/notmuch-draft.el + +elpa_sources := ${emacs_sources} $(dir)/notmuch-pkg.el $(dir)/notmuch-version.el: $(dir)/Makefile.local version.stamp $(dir)/notmuch-version.el: $(srcdir)/$(dir)/notmuch-version.el.tmpl @@ -35,6 +38,9 @@ $(dir)/notmuch-pkg.el: $(srcdir)/$(dir)/notmuch-pkg.el.tmpl all: $(dir)/notmuch-pkg.el install-emacs: $(dir)/notmuch-pkg.el +emacs_mua := $(dir)/notmuch-emacs-mua +emacs_mua_desktop := $(dir)/notmuch-emacs-mua.desktop + emacs_images := \ $(srcdir)/$(dir)/notmuch-logo.png @@ -72,6 +78,14 @@ ifeq ($(HAVE_EMACS),1) $(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $< endif +elpa: $(ELPA_FILE) + +notmuch-emacs-%.tar: ${elpa_sources} + mkdir -p .elpa-build/notmuch-${ELPA_VERSION} + cp ${elpa_sources} .elpa-build/notmuch-${ELPA_VERSION} + tar -C .elpa-build -cf $@ notmuch-${ELPA_VERSION} + rm -r .elpa-build + ifeq ($(WITH_EMACS),1) ifeq ($(HAVE_EMACS),1) all: $(emacs_bytecode) @@ -90,5 +104,12 @@ ifeq ($(HAVE_EMACS),1) endif mkdir -p "$(DESTDIR)$(emacsetcdir)" install -m0644 $(emacs_images) "$(DESTDIR)$(emacsetcdir)" + mkdir -p "$(DESTDIR)$(prefix)/bin/" + install $(emacs_mua) "$(DESTDIR)$(prefix)/bin" +ifeq ($(WITH_DESKTOP),1) + mkdir -p "$(DESTDIR)$(desktop_dir)" + desktop-file-install --mode 0644 --dir "$(DESTDIR)$(desktop_dir)" $(emacs_mua_desktop) + -update-desktop-database "$(DESTDIR)$(desktop_dir)" +endif CLEAN := $(CLEAN) $(emacs_bytecode) $(dir)/notmuch-version.el $(dir)/notmuch-pkg.el diff --git a/emacs/notmuch-address.el b/emacs/notmuch-address.el index 34793dbe..b3c56cfa 100644 --- a/emacs/notmuch-address.el +++ b/emacs/notmuch-address.el @@ -98,6 +98,17 @@ to know how address selection is made by default." :group 'notmuch-send :group 'notmuch-external) +(defcustom notmuch-address-post-completion-functions nil + "Functions called after completing address. + +The completed address is passed as an argument to each function. +Note that this hook will be invoked for completion in headers +matching `notmuch-address-completion-headers-regexp'. +" + :type 'hook + :group 'notmuch-address + :group 'notmuch-hooks) + (defun notmuch-address-selection-function (prompt collection initial-input) "Call (`completing-read' PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)" @@ -194,12 +205,20 @@ external commands." (t (funcall notmuch-address-selection-function (format "Address (%s matches): " num-options) - (cdr options) (car options)))))) + ;; We put the first match as the initial + ;; input; we put all the matches as + ;; possible completions, moving the + ;; first match to the end of the list + ;; makes cursor up/down in the list work + ;; better. + (append (cdr options) (list (car options))) + (car options)))))) (if chosen (progn (push chosen notmuch-address-history) (delete-region beg end) - (insert chosen)) + (insert chosen) + (run-hook-with-args 'notmuch-address-post-completion-functions chosen)) (message "No matches.") (ding)))) (t nil))) diff --git a/emacs/notmuch-company.el b/emacs/notmuch-company.el index 5d75c145..ebe2c084 100644 --- a/emacs/notmuch-company.el +++ b/emacs/notmuch-company.el @@ -87,6 +87,7 @@ (match (if (string-match notmuch-company-last-prefix arg) (match-end 0) 0)) + (post-completion (run-hook-with-args 'notmuch-address-post-completion-functions arg)) (no-cache t)))) diff --git a/emacs/notmuch-crypto.el b/emacs/notmuch-crypto.el index e376aa80..68a7e9f3 100644 --- a/emacs/notmuch-crypto.el +++ b/emacs/notmuch-crypto.el @@ -42,7 +42,12 @@ mode." :group 'notmuch-crypto) (defface notmuch-crypto-part-header - '((t (:foreground "blue"))) + '((((class color) + (background dark)) + (:foreground "LightBlue1")) + (((class color) + (background light)) + (:foreground "blue"))) "Face used for crypto parts headers." :group 'notmuch-crypto :group 'notmuch-faces) diff --git a/emacs/notmuch-draft.el b/emacs/notmuch-draft.el new file mode 100644 index 00000000..fb7f4f55 --- /dev/null +++ b/emacs/notmuch-draft.el @@ -0,0 +1,267 @@ +;;; notmuch-draft.el --- functions for postponing and editing drafts +;; +;; Copyright © Mark Walters +;; Copyright © David Bremner +;; +;; This file is part of Notmuch. +;; +;; Notmuch is free software: you can redistribute 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. +;; +;; Notmuch is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. +;; +;; You should have received a copy of the GNU General Public License +;; along with Notmuch. If not, see . +;; +;; Authors: Mark Walters +;; David Bremner + +;;; Code: + +(require 'notmuch-maildir-fcc) +(require 'notmuch-tag) + +(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare)) +(declare-function notmuch-message-mode "notmuch-mua") + +(defgroup notmuch-draft nil + "Saving and editing drafts in Notmuch." + :group 'notmuch) + +(defcustom notmuch-draft-tags '("+draft") + "List of tags changes to apply to a draft message when it is saved in the database. + +Tags starting with \"+\" (or not starting with either \"+\" or +\"-\") in the list will be added, and tags starting with \"-\" +will be removed from the message being stored. + +For example, if you wanted to give the message a \"draft\" tag +but not the (normally added by default) \"inbox\" tag, you would +set: + (\"+draft\" \"-inbox\")" + :type '(repeat string) + :group 'notmuch-draft) + +(defcustom notmuch-draft-folder "drafts" + "Folder to save draft messages in. + +This should be specified relative to the root of the notmuch +database. It will be created if necessary." + :type 'string + :group 'notmuch-draft) + +(defcustom notmuch-draft-quoted-tags '() + "Mml tags to quote. + +This should be a list of mml tags to quote before saving. You do +not need to include \"secure\" as that is handled separately. + +If you include \"part\" then attachments will not be saved with +the draft -- if not then they will be saved with the draft. The +former means the attachments may not still exist when you resume +the message, the latter means that the attachments as they were +when you postponed will be sent with the resumed message. + +Note you may get strange results if you change this between +postponing and resuming a message." + :type '(repeat string) + :group 'notmuch-send) + +(defcustom notmuch-draft-save-plaintext 'ask + "Should notmuch save/postpone in plaintext messages that seem + like they are intended to be sent encrypted +(i.e with an mml encryption tag in it)." + :type '(radio + (const :tag "Never" nil) + (const :tag "Ask every time" ask) + (const :tag "Always" t)) + :group 'notmuch-draft + :group 'notmuch-crypto) + +(defvar notmuch-draft-encryption-tag-regex + "<#\\(part encrypt\\|secure.*mode=.*encrypt>\\)" + "Regular expression matching mml tags indicating encryption of part or message") + +(defvar notmuch-draft-id nil + "Message-id of the most recent saved draft of this message") +(make-variable-buffer-local 'notmuch-draft-id) + +(defun notmuch-draft--mark-deleted () + "Tag the last saved draft deleted. + +Used when a new version is saved, or the message is sent." + (when notmuch-draft-id + (notmuch-tag notmuch-draft-id '("+deleted")))) + +(defun notmuch-draft-quote-some-mml () + "Quote the mml tags in `notmuch-draft-quoted-tags`." + (save-excursion + ;; First we deal with any secure tag separately. + (message-goto-body) + (when (looking-at "<#secure[^\n]*>\n") + (let ((secure-tag (match-string 0))) + (delete-region (match-beginning 0) (match-end 0)) + (message-add-header (concat "X-Notmuch-Emacs-Secure: " secure-tag)))) + ;; This is copied from mml-quote-region but only quotes the + ;; specified tags. + (when notmuch-draft-quoted-tags + (let ((re (concat "<#!*/?\\(" + (mapconcat 'regexp-quote notmuch-draft-quoted-tags "\\|") + "\\)"))) + (message-goto-body) + (while (re-search-forward re nil t) + ;; Insert ! after the #. + (goto-char (+ (match-beginning 0) 2)) + (insert "!")))))) + +(defun notmuch-draft-unquote-some-mml () + "Unquote the mml tags in `notmuch-draft-quoted-tags`." + (save-excursion + (when notmuch-draft-quoted-tags + (let ((re (concat "<#!+/?\\(" + (mapconcat 'regexp-quote notmuch-draft-quoted-tags "\\|") + "\\)"))) + (message-goto-body) + (while (re-search-forward re nil t) + ;; Remove one ! from after the #. + (goto-char (+ (match-beginning 0) 2)) + (delete-char 1)))) + (let (secure-tag) + (save-restriction + (message-narrow-to-headers) + (setq secure-tag (message-fetch-field "X-Notmuch-Emacs-Secure" 't)) + (message-remove-header "X-Notmuch-Emacs-Secure")) + (message-goto-body) + (when secure-tag + (insert secure-tag "\n"))))) + +(defun notmuch-draft--has-encryption-tag () + "Returns t if there is an mml secure tag." + (save-excursion + (message-goto-body) + (re-search-forward notmuch-draft-encryption-tag-regex nil 't))) + +(defun notmuch-draft--query-encryption () + "Checks if we should save a message that should be encrypted. + +`notmuch-draft-save-plaintext' controls the behaviour." + (case notmuch-draft-save-plaintext + ((ask) + (unless (yes-or-no-p "(Customize `notmuch-draft-save-plaintext' to avoid this warning) +This message contains mml tags that suggest it is intended to be encrypted. +Really save and index an unencrypted copy? ") + (error "Save aborted"))) + ((nil) + (error "Refusing to save draft with encryption tags (see `notmuch-draft-save-plaintext')")) + ((t) + (ignore)))) + +(defun notmuch-draft--make-message-id () + ;; message-make-message-id gives the id inside a "<" ">" pair, + ;; but notmuch doesn't want that form, so remove them. + (concat "draft-" (substring (message-make-message-id) 1 -1))) + +(defun notmuch-draft-save () + "Save the current draft message in the notmuch database. + +This saves the current message in the database with tags +`notmuch-draft-tags` (in addition to any default tags +applied to newly inserted messages)." + (interactive) + (when (notmuch-draft--has-encryption-tag) + (notmuch-draft--query-encryption)) + (let ((id (notmuch-draft--make-message-id))) + (with-temporary-notmuch-message-buffer + ;; We insert a Date header and a Message-ID header, the former + ;; so that it is easier to search for the message, and the + ;; latter so we have a way of accessing the saved message (for + ;; example to delete it at a later time). We check that the + ;; user has these in `message-deletable-headers` (the default) + ;; as otherwise they are doing something strange and we + ;; shouldn't interfere. Note, since we are doing this in a new + ;; buffer we don't change the version in the compose buffer. + (cond + ((member 'Message-ID message-deletable-headers) + (message-remove-header "Message-ID") + (message-add-header (concat "Message-ID: <" id ">"))) + (t + (message "You have customized emacs so Message-ID is not a deletable header, so not changing it") + (setq id nil))) + (cond + ((member 'Date message-deletable-headers) + (message-remove-header "Date") + (message-add-header (concat "Date: " (message-make-date)))) + (t + (message "You have customized emacs so Date is not a deletable header, so not changing it"))) + (message-add-header "X-Notmuch-Emacs-Draft: True") + (notmuch-draft-quote-some-mml) + (notmuch-maildir-setup-message-for-saving) + (notmuch-maildir-notmuch-insert-current-buffer + notmuch-draft-folder 't notmuch-draft-tags)) + ;; We are now back in the original compose buffer. Note the + ;; function notmuch-call-notmuch-process (called by + ;; notmuch-maildir-notmuch-insert-current-buffer) signals an error + ;; on failure, so to get to this point it must have + ;; succeeded. Also, notmuch-draft-id is still the id of the + ;; previous draft, so it is safe to mark it deleted. + (notmuch-draft--mark-deleted) + (setq notmuch-draft-id (concat "id:" id)) + (set-buffer-modified-p nil))) + +(defun notmuch-draft-postpone () + "Save the draft message in the notmuch database and exit buffer." + (interactive) + (notmuch-draft-save) + (kill-buffer)) + +(defun notmuch-draft-resume (id) + "Resume editing of message with id ID." + (let* ((tags (process-lines notmuch-command "search" "--output=tags" + "--exclude=false" id)) + (draft (equal tags (notmuch-update-tags tags notmuch-draft-tags)))) + (when (or draft + (yes-or-no-p "Message does not appear to be a draft: really resume? ")) + (switch-to-buffer (get-buffer-create (concat "*notmuch-draft-" id "*"))) + (setq buffer-read-only nil) + (erase-buffer) + (let ((coding-system-for-read 'no-conversion)) + (call-process notmuch-command nil t nil "show" "--format=raw" id)) + (mime-to-mml) + (goto-char (point-min)) + (when (re-search-forward "^$" nil t) + (replace-match mail-header-separator t t)) + ;; Remove the Date and Message-ID headers (unless the user has + ;; explicitly customized emacs to tell us not to) as they will + ;; be replaced when the message is sent. + (save-restriction + (message-narrow-to-headers) + (when (member 'Message-ID message-deletable-headers) + (message-remove-header "Message-ID")) + (when (member 'Date message-deletable-headers) + (message-remove-header "Date")) + ;; The X-Notmuch-Emacs-Draft header is a more reliable + ;; indication of whether the message really is a draft. + (setq draft (> (message-remove-header "X-Notmuch-Emacs-Draft") 0))) + ;; If the message is not a draft we should not unquote any mml. + (when draft + (notmuch-draft-unquote-some-mml)) + (notmuch-message-mode) + (message-goto-body) + (set-buffer-modified-p nil) + ;; If the resumed message was a draft then set the draft + ;; message-id so that we can delete the current saved draft if the + ;; message is resaved or sent. + (setq notmuch-draft-id (when draft id))))) + + +(add-hook 'message-send-hook 'notmuch-draft--mark-deleted) + + +(provide 'notmuch-draft) + +;;; notmuch-draft.el ends here diff --git a/emacs/notmuch-emacs-mua b/emacs/notmuch-emacs-mua new file mode 100755 index 00000000..a5214977 --- /dev/null +++ b/emacs/notmuch-emacs-mua @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# +# notmuch-emacs-mua - start composing a mail on the command line +# +# Copyright © 2014 Jani Nikula +# +# This program is free software: you can redistribute 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 https://www.gnu.org/licenses/ . +# +# Authors: Jani Nikula +# + +set -eu + +# escape: "expand" '\' as '\\' and '"' as '\"' +# calling convention: escape -v var "$arg" (like in bash printf). +escape () +{ + local __escape_arg__=${3//\\/\\\\} + printf -v $2 '%s' "${__escape_arg__//\"/\\\"}" +} + +EMACS=${EMACS:-emacs} +EMACSCLIENT=${EMACSCLIENT:-emacsclient} + +PRINT_ONLY= +NO_WINDOW= +USE_EMACSCLIENT= +AUTO_DAEMON= +CREATE_FRAME= +ELISP= +MAILTO= +HELLO= + +# Short options compatible with mutt(1). +while getopts :s:c:b:i:h opt; do + # Handle errors and long options. + case "${opt}" in + :) + echo "$0: short option -${OPTARG} requires an argument." >&2 + exit 1 + ;; + \?) + opt=$1 + if [ "${OPTARG}" != "-" ]; then + echo "$0: unknown short option -${OPTARG}." >&2 + exit 1 + fi + + case "${opt}" in + # Long options with arguments. + --subject=*|--to=*|--cc=*|--bcc=*|--body=*) + OPTARG=${opt#--*=} + opt=${opt%%=*} + ;; + # Long options without arguments. + --help|--print|--no-window-system|--client|--auto-daemon|--create-frame|--hello) + ;; + *) + echo "$0: unknown long option ${opt}, or argument mismatch." >&2 + exit 1 + ;; + esac + # getopts does not do this for what it considers errors. + OPTIND=$((OPTIND + 1)) + ;; + esac + + escape -v OPTARG "${OPTARG-none}" + + case "${opt}" in + --help|h) + exec man notmuch-emacs-mua + ;; + --subject|s) + ELISP="${ELISP} (message-goto-subject) (insert \"${OPTARG}\")" + ;; + --to) + ELISP="${ELISP} (message-goto-to) (insert \"${OPTARG}, \")" + ;; + --cc|c) + ELISP="${ELISP} (message-goto-cc) (insert \"${OPTARG}, \")" + ;; + --bcc|b) + ELISP="${ELISP} (message-goto-bcc) (insert \"${OPTARG}, \")" + ;; + --body|i) + ELISP="${ELISP} (message-goto-body) (insert-file \"${OPTARG}\")" + ;; + --print) + PRINT_ONLY=1 + ;; + --no-window-system) + NO_WINDOW="-nw" + ;; + --client) + USE_EMACSCLIENT="yes" + ;; + --auto-daemon) + AUTO_DAEMON="--alternate-editor=" + CREATE_FRAME="-c" + ;; + --create-frame) + CREATE_FRAME="-c" + ;; + --hello) + HELLO=1 + ;; + *) + # We should never end up here. + echo "$0: internal error (option ${opt})." >&2 + exit 1 + ;; + esac + + shift $((OPTIND - 1)) + OPTIND=1 +done + +# Positional parameters. +for arg; do + escape -v arg "${arg}" + case $arg in + mailto:*) + if [ -n "${MAILTO}" ]; then + echo "$0: more than one mailto: argument." >&2 + exit 1 + fi + MAILTO="${arg}" + ;; + *) + ELISP="${ELISP} (message-goto-to) (insert \"${arg}, \")" + ;; + esac +done + +if [ -n "${MAILTO}" ]; then + if [ -n "${ELISP}" ]; then + echo "$0: mailto: is not compatible with other message parameters." >&2 + exit 1 + fi + ELISP="(browse-url-mail \"${MAILTO}\")" +elif [ -z "${ELISP}" -a -n "${HELLO}" ]; then + ELISP="(notmuch)" +else + ELISP="(notmuch-mua-new-mail) ${ELISP}" +fi + +# Kill the terminal/frame if we're creating one. +if [ -z "$USE_EMACSCLIENT" -o -n "$CREATE_FRAME" -o -n "$NO_WINDOW" ]; then + ELISP="${ELISP} (message-add-action #'save-buffers-kill-terminal 'exit)" +fi + +escape -v pwd "$PWD" + +# The crux of it all: construct an elisp progn and eval it. +ELISP="(prog1 'done (require 'notmuch) (cd \"$pwd\") ${ELISP})" + +if [ -n "$PRINT_ONLY" ]; then + echo ${ELISP} + exit 0 +fi + +if [ -n "$USE_EMACSCLIENT" ]; then + # Evaluate the progn. + exec ${EMACSCLIENT} ${NO_WINDOW} ${CREATE_FRAME} ${AUTO_DAEMON} --eval "${ELISP}" +else + exec ${EMACS} ${NO_WINDOW} --eval "${ELISP}" +fi diff --git a/emacs/notmuch-emacs-mua.desktop b/emacs/notmuch-emacs-mua.desktop new file mode 100644 index 00000000..f1600473 --- /dev/null +++ b/emacs/notmuch-emacs-mua.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=Notmuch (emacs interface) +Exec=emacs -f notmuch +Icon=emblem-mail +Terminal=false +Type=Application +Categories=Network;Email; diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index d582bff7..c858a20b 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -604,10 +604,11 @@ with `notmuch-hello-query-counts'." (defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png"))) -(defun notmuch-hello-update (&optional no-display) - "Update the current notmuch view." +(defun notmuch-hello-update () + "Update the notmuch-hello buffer." ;; Lazy - rebuild everything. - (notmuch-hello no-display)) + (interactive) + (notmuch-hello t)) (defun notmuch-hello-window-configuration-change () "Hook function to update the hello buffer when it is switched to." diff --git a/emacs/notmuch-jump.el b/emacs/notmuch-jump.el index 963253c9..3e20b8c7 100644 --- a/emacs/notmuch-jump.el +++ b/emacs/notmuch-jump.el @@ -104,7 +104,7 @@ not appear in the pop-up buffer. (copy-sequence minibuffer-prompt-properties) 'face)) ;; Build the keymap with our bindings - (minibuffer-map (notmuch-jump--make-keymap action-map)) + (minibuffer-map (notmuch-jump--make-keymap action-map prompt)) ;; The bindings save the the action in notmuch-jump--action (notmuch-jump--action nil)) ;; Read the action @@ -161,18 +161,47 @@ buffer." (set-keymap-parent map minibuffer-local-map) ;; Make this like a special-mode keymap, with no self-insert-command (suppress-keymap map) + (define-key map (kbd "DEL") 'exit-minibuffer) map) "Base keymap for notmuch-jump's minibuffer keymap.") -(defun notmuch-jump--make-keymap (action-map) +(defun notmuch-jump--make-keymap (action-map prompt) "Translate ACTION-MAP into a minibuffer keymap." (let ((map (make-sparse-keymap))) (set-keymap-parent map notmuch-jump-minibuffer-map) (dolist (action action-map) - (define-key map (first action) - `(lambda () (interactive) - (setq notmuch-jump--action ',(third action)) - (exit-minibuffer)))) + (if (= (length (first action)) 1) + (define-key map (first action) + `(lambda () (interactive) + (setq notmuch-jump--action ',(third action)) + (exit-minibuffer))))) + ;; By doing this in two passes (and checking if we already have a + ;; binding) we avoid problems if the user specifies a binding which + ;; is a prefix of another binding. + (dolist (action action-map) + (if (> (length (first action)) 1) + (let* ((key (elt (first action) 0)) + (keystr (string key)) + (new-prompt (concat prompt (format-kbd-macro keystr) " ")) + (action-submap nil)) + (unless (lookup-key map keystr) + (dolist (act action-map) + (when (= key (elt (first act) 0)) + (push (list (substring (first act) 1) + (second act) + (third act)) + action-submap))) + ;; We deal with backspace specially + (push (list (kbd "DEL") + "Backup" + (apply-partially #'notmuch-jump action-map prompt)) + action-submap) + (setq action-submap (nreverse action-submap)) + (define-key map keystr + `(lambda () (interactive) + (setq notmuch-jump--action + ',(apply-partially #'notmuch-jump action-submap new-prompt)) + (exit-minibuffer))))))) map)) ;; diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index 23bd81c1..5dc67970 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -58,6 +58,10 @@ (custom-add-to-group 'notmuch-send 'message 'custom-group) +(defgroup notmuch-tag nil + "Tags and tagging in Notmuch." + :group 'notmuch) + (defgroup notmuch-crypto nil "Processing and display of cryptographic MIME parts." :group 'notmuch) @@ -148,6 +152,7 @@ For example, if you wanted to remove an \"inbox\" tag and add an (define-key map "z" 'notmuch-tree) (define-key map "m" 'notmuch-mua-new-mail) (define-key map "=" 'notmuch-refresh-this-buffer) + (define-key map (kbd "M-=") 'notmuch-refresh-all-buffers) (define-key map "G" 'notmuch-poll-and-refresh-this-buffer) (define-key map "j" 'notmuch-jump-search) map) @@ -414,10 +419,8 @@ of its command symbol." "Refresh the current buffer." (interactive) (when notmuch-buffer-refresh-function - (if (commandp notmuch-buffer-refresh-function) - ;; Pass prefix argument, etc. - (call-interactively notmuch-buffer-refresh-function) - (funcall notmuch-buffer-refresh-function)))) + ;; Pass prefix argument, etc. + (call-interactively notmuch-buffer-refresh-function))) (defun notmuch-poll-and-refresh-this-buffer () "Invoke `notmuch-poll' to import mail, then refresh the current buffer." @@ -425,6 +428,21 @@ of its command symbol." (notmuch-poll) (notmuch-refresh-this-buffer)) +(defun notmuch-refresh-all-buffers () + "Invoke `notmuch-refresh-this-buffer' on all notmuch major-mode buffers. + +The buffers are silently refreshed, i.e. they are not forced to +be displayed." + (interactive) + (dolist (buffer (buffer-list)) + (let ((buffer-mode (buffer-local-value 'major-mode buffer))) + (when (memq buffer-mode '(notmuch-show-mode + notmuch-tree-mode + notmuch-search-mode + notmuch-hello-mode)) + (with-current-buffer buffer + (notmuch-refresh-this-buffer)))))) + (defun notmuch-prettify-subject (subject) ;; This function is used by `notmuch-search-process-filter' which ;; requires that we not disrupt its' matching state. diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el index 55bc2672..93747b1c 100644 --- a/emacs/notmuch-mua.el +++ b/emacs/notmuch-mua.el @@ -27,12 +27,15 @@ (require 'notmuch-lib) (require 'notmuch-address) +(require 'notmuch-draft) (eval-when-compile (require 'cl)) (declare-function notmuch-show-insert-body "notmuch-show" (msg body depth)) (declare-function notmuch-fcc-header-setup "notmuch-maildir-fcc" ()) (declare-function notmuch-maildir-message-do-fcc "notmuch-maildir-fcc" ()) +(declare-function notmuch-draft-postpone "notmuch-draft" ()) +(declare-function notmuch-draft-save "notmuch-draft" ()) ;; @@ -251,6 +254,10 @@ mutiple parts get a header." (notmuch-show-max-text-part-size 0) ;; Insert headers for parts as appropriate for replying. (notmuch-show-insert-header-p-function notmuch-mua-reply-insert-header-p-function) + ;; Ensure that any encrypted parts are + ;; decrypted during the generation of the reply + ;; text. + (notmuch-show-process-crypto process-crypto) ;; Don't indent multipart sub-parts. (notmuch-show-indent-multipart nil)) ;; We don't want sigstatus buttons (an information leak and usually wrong anyway). @@ -285,6 +292,8 @@ mutiple parts get a header." (define-key notmuch-message-mode-map (kbd "C-c C-c") #'notmuch-mua-send-and-exit) (define-key notmuch-message-mode-map (kbd "C-c C-s") #'notmuch-mua-send) +(define-key notmuch-message-mode-map (kbd "C-c C-p") #'notmuch-draft-postpone) +(define-key notmuch-message-mode-map (kbd "C-x C-s") #'notmuch-draft-save) (defun notmuch-mua-pop-to-buffer (name switch-function) "Pop to buffer NAME, and warn if it already exists and is @@ -490,15 +499,64 @@ will be addressed to all recipients of the source message." (notmuch-mua-reply query-string sender reply-all) (deactivate-mark))) +(defun notmuch-mua-check-no-misplaced-secure-tag () + "Query user if there is a misplaced secure mml tag. + +Emacs message-send will (probably) ignore a secure mml tag unless +it is at the start of the body. Returns t if there is no such +tag, or the user confirms they mean it." + (save-excursion + (let ((body-start (progn (message-goto-body) (point)))) + (goto-char (point-max)) + (or + ;; We are always fine if there is no secure tag. + (not (search-backward "<#secure" nil 't)) + ;; There is a secure tag, so it must be at the start of the + ;; body, with no secure tag earlier (i.e., in the headers). + (and (= (point) body-start) + (not (search-backward "<#secure" nil 't))) + ;; The user confirms they means it. + (yes-or-no-p "\ +There is a <#secure> tag not at the start of the body. It is +likely that the message will be sent unsigned and unencrypted. +Really send? "))))) + +(defun notmuch-mua-check-secure-tag-has-newline () + "Query if the secure mml tag has a newline following it. + +Emacs message-send will (probably) ignore a correctly placed +secure mml tag unless it is followed by a newline. Returns t if +any secure tag is followed by a newline, or the user confirms +they mean it." + (save-excursion + (message-goto-body) + (or + ;; There is no (correctly placed) secure tag. + (not (looking-at "<#secure")) + ;; The secure tag is followed by a newline. + (looking-at "<#secure[^\n>]*>\n") + ;; The user confirms they means it. + (yes-or-no-p "\ +The <#secure> tag at the start of the body is not followed by a +newline. It is likely that the message will be sent unsigned and +unencrypted. Really send? ")))) + +(defun notmuch-mua-send-common (arg &optional exit) + (interactive "P") + (when (and (notmuch-mua-check-no-misplaced-secure-tag) + (notmuch-mua-check-secure-tag-has-newline)) + (letf (((symbol-function 'message-do-fcc) #'notmuch-maildir-message-do-fcc)) + (if exit + (message-send-and-exit arg) + (message-send arg))))) + (defun notmuch-mua-send-and-exit (&optional arg) (interactive "P") - (letf (((symbol-function 'message-do-fcc) #'notmuch-maildir-message-do-fcc)) - (message-send-and-exit arg))) + (notmuch-mua-send-common arg 't)) (defun notmuch-mua-send (&optional arg) (interactive "P") - (letf (((symbol-function 'message-do-fcc) #'notmuch-maildir-message-do-fcc)) - (message-send arg))) + (notmuch-mua-send-common arg)) (defun notmuch-mua-kill-buffer () (interactive) diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index e7d16f81..364004b8 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -38,6 +38,7 @@ (require 'notmuch-mua) (require 'notmuch-crypto) (require 'notmuch-print) +(require 'notmuch-draft) (declare-function notmuch-call-notmuch-process "notmuch" (&rest args)) (declare-function notmuch-search-next-thread "notmuch" nil) @@ -50,6 +51,7 @@ (&optional query query-context target buffer-name open-target)) (declare-function notmuch-tree-get-message-properties "notmuch-tree" nil) (declare-function notmuch-read-query "notmuch" (prompt)) +(declare-function notmuch-draft-resume "notmuch-draft" (id)) (defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date") "Headers that should be shown in a message, in this order. @@ -1263,6 +1265,18 @@ matched." (message "No messages matched the query!") nil)))) +(defun notmuch-show--build-queries (thread context) + "Return a list of queries to try for this search. + +THREAD and CONTEXT are both strings, though CONTEXT may be nil. +When CONTEXT is not nil, the first query is the conjunction of it +and THREAD. The next query is THREAD alone, and serves as a +fallback if the prior matches no messages." + (let (queries) + (push (list thread) queries) + (if context (push (list thread "and (" context ")") queries)) + queries)) + (defun notmuch-show--build-buffer (&optional state) "Display messages matching the current buffer context. @@ -1270,25 +1284,20 @@ Apply the previously saved STATE if supplied, otherwise show the first relevant message. If no messages match the query return NIL." - (let* ((basic-args (list notmuch-show-thread-id)) - (args (if notmuch-show-query-context - (append (list "\'") basic-args - (list "and (" notmuch-show-query-context ")\'")) - (append (list "\'") basic-args (list "\'")))) - (cli-args (cons "--exclude=false" + (let* ((cli-args (cons "--exclude=false" (when notmuch-show-elide-non-matching-messages (list "--entire-thread=false")))) - - (forest (or (notmuch-query-get-threads (append cli-args args)) - ;; If a query context reduced the number of - ;; results to zero, try again without it. - (and notmuch-show-query-context - (notmuch-query-get-threads (append cli-args basic-args))))) - + (queries (notmuch-show--build-queries + notmuch-show-thread-id notmuch-show-query-context)) + (forest nil) ;; Must be reset every time we are going to start inserting ;; messages into the buffer. (notmuch-show-previous-subject "")) - + ;; Use results from the first query that returns some. + (while (and (not forest) queries) + (setq forest (notmuch-query-get-threads + (append cli-args (list "'") (car queries) (list "'")))) + (setq queries (cdr queries))) (when forest (notmuch-show-insert-forest forest) @@ -1319,8 +1328,13 @@ If no messages match the query return NIL." This includes: - the list of open messages, - - the current message." - (list (notmuch-show-get-message-id) (notmuch-show-get-message-ids-for-open-messages))) + - the combination of current message id with/for each visible window." + (let* ((win-list (get-buffer-window-list (current-buffer) nil t)) + (win-id-combo (mapcar (lambda (win) + (with-selected-window win + (list win (notmuch-show-get-message-id)))) + win-list))) + (list win-id-combo (notmuch-show-get-message-ids-for-open-messages)))) (defun notmuch-show-get-query () "Return the current query in this show buffer" @@ -1347,8 +1361,8 @@ This includes: This includes: - opening the messages previously opened, - closing all other messages, - - moving to the correct current message." - (let ((current (car state)) + - moving to the correct current message in every displayed window." + (let ((win-msg-alist (car state)) (open (cadr state))) ;; Open those that were open. @@ -1357,8 +1371,10 @@ This includes: (member (notmuch-show-get-message-id) open)) until (not (notmuch-show-goto-message-next))) - ;; Go to the previously open message. - (notmuch-show-goto-message current))) + (dolist (win-msg-pair win-msg-alist) + (with-selected-window (car win-msg-pair) + ;; Go to the previously open message in this window + (notmuch-show-goto-message (cadr win-msg-pair)))))) (defun notmuch-show-refresh-view (&optional reset-state) "Refresh the current view. @@ -1431,8 +1447,10 @@ reset based on the original query." (define-key map "|" 'notmuch-show-pipe-message) (define-key map "w" 'notmuch-show-save-attachments) (define-key map "V" 'notmuch-show-view-raw-message) + (define-key map "e" 'notmuch-show-resume-message) (define-key map "c" 'notmuch-show-stash-map) (define-key map "h" 'notmuch-show-toggle-visibility-headers) + (define-key map "k" 'notmuch-tag-jump) (define-key map "*" 'notmuch-show-tag-all) (define-key map "-" 'notmuch-show-remove-tag) (define-key map "+" 'notmuch-show-add-tag) @@ -1967,6 +1985,11 @@ to show, nil otherwise." (setq buffer-read-only t) (view-buffer buf 'kill-buffer-if-not-modified))) +(defun notmuch-show-resume-message () + "Resume EDITING the current draft message." + (interactive) + (notmuch-draft-resume (notmuch-show-get-message-id))) + (put 'notmuch-show-pipe-message 'notmuch-doc "Pipe the contents of the current message to a command.") (put 'notmuch-show-pipe-message 'notmuch-prefix-doc diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el index 6c8b6a75..09d182df 100644 --- a/emacs/notmuch-tag.el +++ b/emacs/notmuch-tag.el @@ -28,6 +28,57 @@ (require 'crm) (require 'notmuch-lib) +(declare-function notmuch-search-tag "notmuch" tag-changes) +(declare-function notmuch-show-tag "notmuch-show" tag-changes) +(declare-function notmuch-tree-tag "notmuch-tree" tag-changes) + +(autoload 'notmuch-jump "notmuch-jump") + +(define-widget 'notmuch-tag-key-type 'list + "A single key tagging binding." + :format "%v" + :args '((list :inline t + :format "%v" + (key-sequence :tag "Key") + (radio :tag "Tag operations" (repeat :tag "Tag list" (string :format "%v" :tag "change")) + (variable :tag "Tag variable")) + (string :tag "Name")))) + +(defcustom notmuch-tagging-keys + `((,(kbd "a") notmuch-archive-tags "Archive") + (,(kbd "u") notmuch-show-mark-read-tags "Mark read") + (,(kbd "f") ("+flagged") "Flag") + (,(kbd "s") ("+spam" "-inbox") "Mark as spam") + (,(kbd "d") ("+deleted" "-inbox") "Delete")) + "A list of keys and corresponding tagging operations. + +For each key (or key sequence) you can specify a sequence of +tagging operations to apply, or a variable which contains a list +of tagging operations such as `notmuch-archive-tags'. The final +element is a name for this tagging operation. If the name is +omitted or empty then the list of tag changes, or the variable +name is used as the name. + +The key `notmuch-tag-jump-reverse-key' (k by default) should not +be used (either as a key, or as the start of a key sequence) as +it is already bound: it switches the menu to a menu of the +reverse tagging operations. The reverse of a tagging operation is +the same list of individual tag-ops but with `+tag` replaced by +`-tag` and vice versa. + +If setting this variable outside of customize then it should be a +list of triples (lists of three elements). Each triple should be +of the form (key-binding tagging-operations name). KEY-BINDING +can be a single character or a key sequence; TAGGING-OPERATIONS +should either be a list of individual tag operations each of the +form `+tag` or `-tag`, or the variable name of a variable that is +a list of tagging operations; NAME should be a name for the +tagging operation, if omitted or empty than then name is taken +from TAGGING-OPERATIONS." + :tag "List of tagging bindings" + :type '(repeat notmuch-tag-key-type) + :group 'notmuch-tag) + (define-widget 'notmuch-tag-format-type 'lazy "Customize widget for notmuch-tag-format and friends" :type '(alist :key-type (regexp :tag "Tag") @@ -64,7 +115,12 @@ Used in the default value of `notmuch-tag-formats`." :group 'notmuch-faces) (defface notmuch-tag-flagged - '((t :foreground "blue")) + '((((class color) + (background dark)) + (:foreground "LightBlue1")) + (((class color) + (background light)) + (:foreground "blue"))) "Face used for the flagged tag. Used in the default value of `notmuch-tag-formats`." @@ -437,6 +493,55 @@ begin with a \"+\" or a \"-\". If REVERSE is non-nil, replace all s))) tags)) +(defvar notmuch-tag-jump-reverse-key "k" + "The key in tag-jump to switch to the reverse tag changes.") + +(defun notmuch-tag-jump (reverse) + "Create a jump menu for tagging operations. + +Creates and displays a jump menu for the tagging operations +specified in `notmuch-tagging-keys'. If REVERSE is set then it +offers a menu of the reverses of the operations specified in +`notmuch-tagging-keys'; i.e. each `+tag` is replaced by `-tag` +and vice versa." + ;; In principle this function is simple, but it has to deal with + ;; lots of cases: different modes (search/show/tree), whether a name + ;; is specified, whether the tagging operations is a list of + ;; tag-ops, or a symbol that evaluates to such a list, and whether + ;; REVERSE is specified. + (interactive "P") + (let (action-map) + (dolist (binding notmuch-tagging-keys) + (let* ((tag-function (case major-mode + (notmuch-search-mode #'notmuch-search-tag) + (notmuch-show-mode #'notmuch-show-tag) + (notmuch-tree-mode #'notmuch-tree-tag))) + (key (first binding)) + (forward-tag-change (if (symbolp (second binding)) + (symbol-value (second binding)) + (second binding))) + (tag-change (if reverse + (notmuch-tag-change-list forward-tag-change 't) + forward-tag-change)) + (name (or (and (not (string= (third binding) "")) + (third binding)) + (and (symbolp (second binding)) + (symbol-name (second binding))))) + (name-string (if name + (if reverse (concat "Reverse " name) + name) + (mapconcat #'identity tag-change " ")))) + (push (list key name-string + `(lambda () (,tag-function ',tag-change))) + action-map))) + (push (list notmuch-tag-jump-reverse-key + (if reverse + "Forward tag changes " + "Reverse tag changes") + (apply-partially 'notmuch-tag-jump (not reverse))) + action-map) + (setq action-map (nreverse action-map)) + (notmuch-jump action-map "Tag: "))) ;; diff --git a/emacs/notmuch-tree.el b/emacs/notmuch-tree.el index 658c4f90..7bebdbab 100644 --- a/emacs/notmuch-tree.el +++ b/emacs/notmuch-tree.el @@ -209,6 +209,13 @@ open (if the message pane is closed it does nothing)." (with-selected-window notmuch-tree-message-window (call-interactively #',func))))) +(defun notmuch-tree-inherit-from-message-pane (sym) + "Return value of SYM in message-pane if open, or tree-pane if not" + (if (window-live-p notmuch-tree-message-window) + (with-selected-window notmuch-tree-message-window + (symbol-value sym)) + (symbol-value sym))) + (defun notmuch-tree-button-activate (&optional button) "Activate BUTTON or button at point @@ -226,8 +233,10 @@ FUNC." `(lambda () ,(concat "(Close message pane and) " (documentation func t)) (interactive) - (notmuch-tree-close-message-window) - (call-interactively #',func))) + (let ((notmuch-show-process-crypto + (notmuch-tree-inherit-from-message-pane 'notmuch-show-process-crypto))) + (notmuch-tree-close-message-window) + (call-interactively #',func)))) (defvar notmuch-tree-mode-map (let ((map (make-sparse-keymap))) @@ -257,7 +266,7 @@ FUNC." (define-key map (kbd "M-TAB") (notmuch-tree-to-message-pane #'notmuch-show-previous-button)) (define-key map (kbd "") (notmuch-tree-to-message-pane #'notmuch-show-previous-button)) (define-key map (kbd "TAB") (notmuch-tree-to-message-pane #'notmuch-show-next-button)) - (define-key map "e" (notmuch-tree-to-message-pane #'notmuch-tree-button-activate)) + (define-key map "$" (notmuch-tree-to-message-pane #'notmuch-show-toggle-process-crypto)) ;; bindings from show (or elsewhere) but we close the message pane first. (define-key map "f" (notmuch-tree-close-message-pane-and #'notmuch-show-forward-message)) @@ -271,7 +280,6 @@ FUNC." (define-key map "x" 'notmuch-tree-quit) (define-key map "A" 'notmuch-tree-archive-thread) (define-key map "a" 'notmuch-tree-archive-message-then-next) - (define-key map "=" 'notmuch-tree-refresh-view) (define-key map "z" 'notmuch-tree-to-tree) (define-key map "n" 'notmuch-tree-next-matching-message) (define-key map "p" 'notmuch-tree-prev-matching-message) @@ -279,11 +287,13 @@ FUNC." (define-key map "P" 'notmuch-tree-prev-message) (define-key map (kbd "M-p") 'notmuch-tree-prev-thread) (define-key map (kbd "M-n") 'notmuch-tree-next-thread) + (define-key map "k" 'notmuch-tag-jump) (define-key map "-" 'notmuch-tree-remove-tag) (define-key map "+" 'notmuch-tree-add-tag) (define-key map "*" 'notmuch-tree-tag-thread) (define-key map " " 'notmuch-tree-scroll-or-next) (define-key map (kbd "DEL") 'notmuch-tree-scroll-message-window-back) + (define-key map "e" 'notmuch-tree-resume-message) map)) (fset 'notmuch-tree-mode-map notmuch-tree-mode-map) @@ -364,12 +374,18 @@ updated." (defun notmuch-tree-tag-update-display (&optional tag-changes) "Update display for TAG-CHANGES to current message. -Does NOT change the database." +Updates the message in the message pane if appropriate, but does +NOT change the database." (let* ((current-tags (notmuch-tree-get-tags)) - (new-tags (notmuch-update-tags current-tags tag-changes))) + (new-tags (notmuch-update-tags current-tags tag-changes)) + (tree-msg-id (notmuch-tree-get-message-id))) (unless (equal current-tags new-tags) (notmuch-tree-set-tags new-tags) - (notmuch-tree-refresh-result)))) + (notmuch-tree-refresh-result) + (when (window-live-p notmuch-tree-message-window) + (with-selected-window notmuch-tree-message-window + (when (string= tree-msg-id (notmuch-show-get-message-id)) + (notmuch-show-update-tags new-tags))))))) (defun notmuch-tree-tag (tag-changes) "Change tags for the current message" @@ -390,6 +406,15 @@ Does NOT change the database." (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message" "-"))) (notmuch-tree-tag tag-changes)) +(defun notmuch-tree-resume-message () + "Resume EDITING the current draft message." + (interactive) + (notmuch-tree-close-message-window) + (let ((id (notmuch-tree-get-message-id))) + (if id + (notmuch-draft-resume id) + (message "No message to resume!")))) + ;; The next two functions close the message window before calling ;; notmuch-search or notmuch-tree but they do so after the user has ;; entered the query (in case the user was basing the query on diff --git a/emacs/notmuch.el b/emacs/notmuch.el index 46f14fea..d8d3afeb 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -169,6 +169,7 @@ there will be called at other points of notmuch execution." (define-key map "t" 'notmuch-search-filter-by-tag) (define-key map "l" 'notmuch-search-filter) (define-key map [mouse-1] 'notmuch-search-show-thread) + (define-key map "k" 'notmuch-tag-jump) (define-key map "*" 'notmuch-search-tag-all) (define-key map "a" 'notmuch-search-archive-thread) (define-key map "-" 'notmuch-search-remove-tag) @@ -312,7 +313,11 @@ there will be called at other points of notmuch execution." :group 'notmuch-faces) (defface notmuch-search-flagged-face - '((t + '((((class color) + (background dark)) + (:foreground "LightBlue1")) + (((class color) + (background light)) (:foreground "blue"))) "Face used in search mode face for flagged threads. @@ -561,12 +566,15 @@ Returns (TAG-CHANGES REGION-BEGIN REGION-END)." See `notmuch-tag' for information on the format of TAG-CHANGES. When called interactively, this uses the region if the region is active. When called directly, BEG and END provide the region. -If these are nil or not provided, this applies to the thread at -point. +If these are nil or not provided, then, if the region is active +this applied to all threads meeting the region, and if the region +is inactive this applies to the thread at point. If ONLY-MATCHED is non-nil, only tag matched messages." (interactive (notmuch-search-interactive-tag-changes)) - (unless (and beg end) (setq beg (point) end (point))) + (unless (and beg end) + (setq beg (car (notmuch-search-interactive-region)) + end (cadr (notmuch-search-interactive-region)))) (let ((search-string (notmuch-search-find-stable-query-region beg end only-matched))) (notmuch-tag search-string tag-changes) @@ -900,9 +908,10 @@ PROMPT is the string to prompt with." (process-lines notmuch-command "search" "--output=tags" "*"))) (completions (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:" - "subject:" "attachment:" "mimetype:") + "subject:" "attachment:") (mapcar (lambda (tag) (concat "tag:" tag)) all-tags) - (mapcar (lambda (tag) (concat "is:" tag)) all-tags)))) + (mapcar (lambda (tag) (concat "is:" tag)) all-tags) + (mapcar (lambda (mimetype) (concat "mimetype:" mimetype)) (mailcap-mime-types))))) (let ((keymap (copy-keymap minibuffer-local-map)) (current-query (case major-mode (notmuch-search-mode (notmuch-search-get-query)) @@ -933,7 +942,7 @@ PROMPT is the string to prompt with." (put 'notmuch-search 'notmuch-doc "Search for messages.") ;;;###autoload -(defun notmuch-search (&optional query oldest-first target-thread target-line) +(defun notmuch-search (&optional query oldest-first target-thread target-line no-display) "Display threads matching QUERY in a notmuch-search buffer. If QUERY is nil, it is read interactively from the minibuffer. @@ -944,6 +953,9 @@ Other optional parameters are used as follows: current if it appears in the search results. TARGET-LINE: The line number to move to if the target thread does not appear in the search results. + NO-DISPLAY: Do not try to foreground the search results buffer. If it is + already foregrounded i.e. displayed in a window, this has no + effect, meaning the buffer will remain visible. When called interactively, this will prompt for a query and use the configured default sort order." @@ -957,7 +969,9 @@ the configured default sort order." (let* ((query (or query (notmuch-read-query "Notmuch search: "))) (buffer (get-buffer-create (notmuch-search-buffer-title query)))) - (switch-to-buffer buffer) + (if no-display + (set-buffer buffer) + (switch-to-buffer buffer)) (notmuch-search-mode) ;; Don't track undo information for this buffer (set 'buffer-undo-list t) @@ -993,17 +1007,18 @@ the configured default sort order." (defun notmuch-search-refresh-view () "Refresh the current view. -Kills the current buffer and runs a new search with the same +Erases the current buffer and runs a new search with the same query string as the current search. If the current thread is in the new search results, then point will be placed on the same thread. Otherwise, point will be moved to attempt to be in the same relative position within the new buffer." + (interactive) (let ((target-line (line-number-at-pos)) (oldest-first notmuch-search-oldest-first) (target-thread (notmuch-search-find-thread-id 'bare)) (query notmuch-search-query-string)) - (notmuch-bury-or-kill-this-buffer) - (notmuch-search query oldest-first target-thread target-line) + ;; notmuch-search erases the current buffer. + (notmuch-search query oldest-first target-thread target-line t) (goto-char (point-min)))) (defun notmuch-search-toggle-order () diff --git a/lib/database-private.h b/lib/database-private.h index ca71a92f..ccc1e9a1 100644 --- a/lib/database-private.h +++ b/lib/database-private.h @@ -32,6 +32,10 @@ #include "notmuch-private.h" +#ifdef SILENCE_XAPIAN_DEPRECATION_WARNINGS +#define XAPIAN_DEPRECATED(D) D +#endif + #include #pragma GCC visibility push(hidden) diff --git a/lib/database.cc b/lib/database.cc index f0bfe566..2d19f20c 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -1711,7 +1711,7 @@ notmuch_database_end_atomic (notmuch_database_t *notmuch) * However, we rely on flushing to test atomicity. */ const char *thresh = getenv ("XAPIAN_FLUSH_THRESHOLD"); if (thresh && atoi (thresh) == 1) - db->flush (); + db->commit (); } catch (const Xapian::Error &error) { _notmuch_database_log (notmuch, "A Xapian exception occurred committing transaction: %s.\n", error.get_msg().c_str()); diff --git a/lib/notmuch.h b/lib/notmuch.h index 18678c0a..59cd0f56 100644 --- a/lib/notmuch.h +++ b/lib/notmuch.h @@ -126,15 +126,15 @@ typedef enum _notmuch_status { NOTMUCH_STATUS_READ_ONLY_DATABASE, /** * A Xapian exception occurred. + * + * @todo We don't really want to expose this lame XAPIAN_EXCEPTION + * value. Instead we should map to things like DATABASE_LOCKED or + * whatever. */ NOTMUCH_STATUS_XAPIAN_EXCEPTION, /** * An error occurred trying to read or write to a file (this could * be file not found, permission denied, etc.) - * - * @todo We don't really want to expose this lame XAPIAN_EXCEPTION - * value. Instead we should map to things like DATABASE_LOCKED or - * whatever. */ NOTMUCH_STATUS_FILE_ERROR, /** diff --git a/notmuch-config.c b/notmuch-config.c index e5d42a0c..b202bb1e 100644 --- a/notmuch-config.c +++ b/notmuch-config.c @@ -202,6 +202,85 @@ get_username_from_passwd_file (void *ctx) return name; } +static notmuch_bool_t +get_config_from_file (notmuch_config_t *config, notmuch_bool_t create_new) +{ + #define BUF_SIZE 4096 + char *config_str = NULL; + int config_len = 0; + int config_bufsize = BUF_SIZE; + size_t len; + GError *error = NULL; + notmuch_bool_t ret = FALSE; + + FILE *fp = fopen(config->filename, "r"); + if (fp == NULL) { + /* If create_new is true, then the caller is prepared for a + * default configuration file in the case of FILE NOT FOUND. + */ + if (create_new) { + config->is_new = TRUE; + ret = TRUE; + goto out; + } else if (errno == ENOENT) { + fprintf (stderr, "Configuration file %s not found.\n" + "Try running 'notmuch setup' to create a configuration.\n", + config->filename); + goto out; + } else { + fprintf (stderr, "Error opening config file '%s': %s\n" + "Try running 'notmuch setup' to create a configuration.\n", + config->filename, strerror(errno)); + goto out; + } + } + + config_str = talloc_zero_array (config, char, config_bufsize); + if (config_str == NULL) { + fprintf (stderr, "Error reading '%s': Out of memory\n", config->filename); + goto out; + } + + while ((len = fread (config_str + config_len, 1, + config_bufsize - config_len, fp)) > 0) { + config_len += len; + if (config_len == config_bufsize) { + config_bufsize += BUF_SIZE; + config_str = talloc_realloc (config, config_str, char, config_bufsize); + if (config_str == NULL) { + fprintf (stderr, "Error reading '%s': Failed to reallocate memory\n", + config->filename); + goto out; + } + } + } + + if (ferror (fp)) { + fprintf (stderr, "Error reading '%s': I/O error\n", config->filename); + goto out; + } + + if (g_key_file_load_from_data (config->key_file, config_str, config_len, + G_KEY_FILE_KEEP_COMMENTS, &error)) { + ret = TRUE; + goto out; + } + + fprintf (stderr, "Error parsing config file '%s': %s\n", + config->filename, error->message); + + g_error_free (error); + +out: + if (fp) + fclose(fp); + + if (config_str) + talloc_free(config_str); + + return ret; +} + /* Open the named notmuch configuration file. If the filename is NULL, * the value of the environment variable $NOTMUCH_CONFIG will be used. * If $NOTMUCH_CONFIG is unset, the default configuration file @@ -255,7 +334,7 @@ notmuch_config_open (void *ctx, int file_had_search_group; int file_had_crypto_group; - notmuch_config_t *config = talloc (ctx, notmuch_config_t); + notmuch_config_t *config = talloc_zero (ctx, notmuch_config_t); if (config == NULL) { fprintf (stderr, "Out of memory.\n"); return NULL; @@ -263,6 +342,9 @@ notmuch_config_open (void *ctx, talloc_set_destructor (config, notmuch_config_destructor); + /* non-zero defaults */ + config->maildir_synchronize_flags = TRUE; + if (filename) { config->filename = talloc_strdup (config, filename); } else if ((notmuch_config_env = getenv ("NOTMUCH_CONFIG"))) { @@ -274,51 +356,9 @@ notmuch_config_open (void *ctx, config->key_file = g_key_file_new (); - config->is_new = FALSE; - config->database_path = NULL; - config->user_name = NULL; - config->user_primary_email = NULL; - config->user_other_email = NULL; - config->user_other_email_length = 0; - config->new_tags = NULL; - config->new_tags_length = 0; - config->new_ignore = NULL; - config->new_ignore_length = 0; - config->maildir_synchronize_flags = TRUE; - config->search_exclude_tags = NULL; - config->search_exclude_tags_length = 0; - config->crypto_gpg_path = NULL; - - if (! g_key_file_load_from_file (config->key_file, - config->filename, - G_KEY_FILE_KEEP_COMMENTS, - &error)) - { - if (error->domain == G_FILE_ERROR && error->code == G_FILE_ERROR_NOENT) { - /* If create_new is true, then the caller is prepared for a - * default configuration file in the case of FILE NOT - * FOUND. - */ - if (create_new) { - g_error_free (error); - config->is_new = TRUE; - } else { - fprintf (stderr, "Configuration file %s not found.\n" - "Try running 'notmuch setup' to create a configuration.\n", - config->filename); - talloc_free (config); - g_error_free (error); - return NULL; - } - } - else - { - fprintf (stderr, "Error reading configuration file %s: %s\n", - config->filename, error->message); - talloc_free (config); - g_error_free (error); - return NULL; - } + if (! get_config_from_file (config, create_new)) { + talloc_free (config); + return NULL; } /* Whenever we know of configuration sections that don't appear in @@ -591,11 +631,11 @@ _config_get_list (notmuch_config_t *config, static void _config_set_list (notmuch_config_t *config, - const char *group, const char *name, + const char *group, const char *key, const char *list[], size_t length, const char ***config_var ) { - g_key_file_set_string_list (config->key_file, group, name, list, length); + g_key_file_set_string_list (config->key_file, group, key, list, length); /* drop the cached value */ talloc_free (*config_var); diff --git a/notmuch-emacs-mua b/notmuch-emacs-mua deleted file mode 100755 index f9d83713..00000000 --- a/notmuch-emacs-mua +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env bash -# -# notmuch-emacs-mua - start composing a mail on the command line -# -# Copyright © 2014 Jani Nikula -# -# This program is free software: you can redistribute 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 https://www.gnu.org/licenses/ . -# -# Authors: Jani Nikula -# - -set -eu - -# escape: "expand" '\' as '\\' and '"' as '\"' -# calling convention: escape -v var "$arg" (like in bash printf). -escape () -{ - local __escape_arg__=${3//\\/\\\\} - printf -v $2 '%s' "${__escape_arg__//\"/\\\"}" -} - -EMACS=${EMACS:-emacs} -EMACSCLIENT=${EMACSCLIENT:-emacsclient} - -PRINT_ONLY= -NO_WINDOW= -USE_EMACSCLIENT= -AUTO_DAEMON= -CREATE_FRAME= - -escape -v pwd "$PWD" - -# The crux of it all: construct an elisp progn and eval it. -ELISP="(prog1 'done (require 'notmuch) (cd \"$pwd\") (notmuch-mua-new-mail)" - -# Short options compatible with mutt(1). -while getopts :s:c:b:i:h opt; do - # Handle errors and long options. - case "${opt}" in - :) - echo "$0: short option -${OPTARG} requires an argument." >&2 - exit 1 - ;; - \?) - opt=$1 - if [ "${OPTARG}" != "-" ]; then - echo "$0: unknown short option -${OPTARG}." >&2 - exit 1 - fi - - case "${opt}" in - # Long options with arguments. - --subject=*|--to=*|--cc=*|--bcc=*|--body=*) - OPTARG=${opt#--*=} - opt=${opt%%=*} - ;; - # Long options without arguments. - --help|--print|--no-window-system|--client|--auto-daemon|--create-frame) - ;; - *) - echo "$0: unknown long option ${opt}, or argument mismatch." >&2 - exit 1 - ;; - esac - # getopts does not do this for what it considers errors. - OPTIND=$((OPTIND + 1)) - ;; - esac - - escape -v OPTARG "${OPTARG-none}" - - case "${opt}" in - --help|h) - exec man notmuch-emacs-mua - ;; - --subject|s) - ELISP="${ELISP} (message-goto-subject) (insert \"${OPTARG}\")" - ;; - --to) - ELISP="${ELISP} (message-goto-to) (insert \"${OPTARG}, \")" - ;; - --cc|c) - ELISP="${ELISP} (message-goto-cc) (insert \"${OPTARG}, \")" - ;; - --bcc|b) - ELISP="${ELISP} (message-goto-bcc) (insert \"${OPTARG}, \")" - ;; - --body|i) - ELISP="${ELISP} (message-goto-body) (insert-file \"${OPTARG}\")" - ;; - --print) - PRINT_ONLY=1 - ;; - --no-window-system) - NO_WINDOW="-nw" - ;; - --client) - USE_EMACSCLIENT="yes" - ;; - --auto-daemon) - AUTO_DAEMON="--alternate-editor=" - CREATE_FRAME="-c" - ;; - --create-frame) - CREATE_FRAME="-c" - ;; - *) - # We should never end up here. - echo "$0: internal error (option ${opt})." >&2 - exit 1 - ;; - esac - - shift $((OPTIND - 1)) - OPTIND=1 -done - -# Positional parameters. -for arg; do - escape -v arg "${arg}" - ELISP="${ELISP} (message-goto-to) (insert \"${arg}, \")" -done - -# Kill the terminal/frame if we're creating one. -if [ -z "$USE_EMACSCLIENT" -o -n "$CREATE_FRAME" -o -n "$NO_WINDOW" ]; then - ELISP="${ELISP} (message-add-action #'save-buffers-kill-terminal 'exit)" -fi - -# End progn. -ELISP="${ELISP})" - -if [ -n "$PRINT_ONLY" ]; then - echo ${ELISP} - exit 0 -fi - -if [ -n "$USE_EMACSCLIENT" ]; then - # Evaluate the progn. - exec ${EMACSCLIENT} ${NO_WINDOW} ${CREATE_FRAME} ${AUTO_DAEMON} --eval "${ELISP}" -else - exec ${EMACS} ${NO_WINDOW} --eval "${ELISP}" -fi diff --git a/notmuch.c b/notmuch.c index 38b73c1d..b9c32032 100644 --- a/notmuch.c +++ b/notmuch.c @@ -363,6 +363,39 @@ notmuch_command (notmuch_config_t *config, return EXIT_SUCCESS; } +/* + * Try to run subcommand in argv[0] as notmuch- prefixed external + * command. argv must be NULL terminated (argv passed to main always + * is). + * + * Does not return if the external command is found and + * executed. Return TRUE if external command is not found. Return + * FALSE on errors. + */ +static notmuch_bool_t try_external_command(char *argv[]) +{ + char *old_argv0 = argv[0]; + notmuch_bool_t ret = TRUE; + + argv[0] = talloc_asprintf (NULL, "notmuch-%s", old_argv0); + + /* + * This will only return on errors. Not finding an external + * command (ENOENT) is not an error from our perspective. + */ + execvp (argv[0], argv); + if (errno != ENOENT) { + fprintf (stderr, "Error: Running external command '%s' failed: %s\n", + argv[0], strerror(errno)); + ret = FALSE; + } + + talloc_free (argv[0]); + argv[0] = old_argv0; + + return ret; +} + int main (int argc, char *argv[]) { @@ -406,8 +439,10 @@ main (int argc, char *argv[]) command = find_command (command_name); if (!command) { - fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n", - command_name); + /* This won't return if the external command is found. */ + if (try_external_command(argv + opt_index)) + fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n", + command_name); ret = EXIT_FAILURE; goto DONE; } diff --git a/notmuch.desktop b/notmuch.desktop deleted file mode 100644 index f1600473..00000000 --- a/notmuch.desktop +++ /dev/null @@ -1,7 +0,0 @@ -[Desktop Entry] -Name=Notmuch (emacs interface) -Exec=emacs -f notmuch -Icon=emblem-mail -Terminal=false -Type=Application -Categories=Network;Email; diff --git a/test/Makefile.local b/test/Makefile.local index 91b36936..f8cf90d0 100644 --- a/test/Makefile.local +++ b/test/Makefile.local @@ -77,4 +77,4 @@ check: test SRCS := $(SRCS) $(test_srcs) CLEAN += $(TEST_BINARIES) $(addsuffix .o,$(TEST_BINARIES)) \ $(dir)/database-test.o \ - $(dir)/corpus.mail $(dir)/test-results $(dir)/tmp.* + $(dir)/corpora.mail $(dir)/test-results $(dir)/tmp.* diff --git a/test/T000-basic.sh b/test/T000-basic.sh index d6811bd1..0a8d6cdf 100755 --- a/test/T000-basic.sh +++ b/test/T000-basic.sh @@ -92,7 +92,7 @@ test_expect_equal \ "$(echo $PATH|cut -f1 -d: | sed -e 's,/test/valgrind/bin$,,')" test_begin_subtest 'notmuch is compiled with debugging symbols' -readelf --sections $(which notmuch) | grep \.debug +readelf --sections $(command -v notmuch) | grep \.debug test_expect_equal 0 $? test_done diff --git a/test/T050-new.sh b/test/T050-new.sh index beeb574a..2bc799d2 100755 --- a/test/T050-new.sh +++ b/test/T050-new.sh @@ -298,4 +298,38 @@ output=$(NOTMUCH_NEW --debug 2>&1 | sed 's/: .*$//' ) chmod u+w ${MAIL_DIR}/.notmuch/xapian/*.${db_ending} test_expect_equal "$output" "A Xapian exception occurred opening database" + +test_begin_subtest "Handle files vanishing between scandir and add_file" + +# A file for scandir to find. It won't get indexed, so can be empty. +touch ${MAIL_DIR}/vanish + +# Breakpoint to remove the file before indexing +cat < notmuch-new-vanish.gdb +set breakpoint pending on +set logging file notmuch-new-vanish-gdb.log +set logging on +break add_file +commands +shell rm -f ${MAIL_DIR}/vanish +continue +end +run +EOF + +gdb --batch-silent --return-child-result -x notmuch-new-vanish.gdb \ + --args notmuch new 2>OUTPUT 1>/dev/null +echo "exit status: $?" >> OUTPUT + +# Clean up the file in case gdb isn't available. +rm -f ${MAIL_DIR}/vanish + +cat < EXPECTED +Unexpected error with file ${MAIL_DIR}/vanish +add_file: Something went wrong trying to read or write a file +Error opening ${MAIL_DIR}/vanish: No such file or directory +exit status: 75 +EOF +test_expect_equal_file EXPECTED OUTPUT + test_done diff --git a/test/T060-count.sh b/test/T060-count.sh index d6933a76..69ab591f 100755 --- a/test/T060-count.sh +++ b/test/T060-count.sh @@ -126,4 +126,32 @@ sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean test_expect_equal_file EXPECTED OUTPUT.clean restore_database +test_begin_subtest "count library function is non-destructive" +test_subtest_known_broken +cat< EXPECTED +1: 52 messages +2: 52 messages +Exclude 'spam' +3: 52 messages +4: 52 messages +EOF +test_python < OUTPUT.clean +cat <EXPECTED +From: Notmuch Test Suite +To: +Subject: draft-test-0003 +Fcc: MAIL_DIR/sent +--text follows this line-- +<#secure method=pgpmime mode=sign> +EOF +test_expect_equal_file EXPECTED OUTPUT.clean +test_done diff --git a/test/test-lib.sh b/test/test-lib.sh index 86e792a8..f55d2c67 100644 --- a/test/test-lib.sh +++ b/test/test-lib.sh @@ -624,15 +624,15 @@ test_expect_equal_file () error "bug in the test script: not 2 or 3 parameters to test_expect_equal" file1="$1" - basename1=`basename "$file1"` file2="$2" - basename2=`basename "$file2"` if ! test_skip "$test_subtest_name" then if diff -q "$file1" "$file2" >/dev/null ; then test_ok_ else testname=$this_test.$test_count + basename1=`basename "$file1"` + basename2=`basename "$file2"` cp "$file1" "$testname.$basename1" cp "$file2" "$testname.$basename2" test_failure_ "$(diff -u "$testname.$basename1" "$testname.$basename2")"