NOTMUCH_STATUS_UPGRADE_REQUIRED,
NOTMUCH_STATUS_PATH_ERROR,
NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
+ NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL,
+ NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION,
+ NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL,
+ NOTMUCH_STATUS_NO_CONFIG,
+ NOTMUCH_STATUS_DATABASE_EXISTS,
NOTMUCH_STATUS_LAST_STATUS
} notmuch_status_t;
typedef enum {
PathError,
capi.lib.NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
IllegalArgumentError,
+ capi.lib.NOTMUCH_STATUS_NO_CONFIG:
+ NoConfigError,
+ capi.lib.NOTMUCH_STATUS_DATABASE_EXISTS:
+ DatabaseExistsError,
}
return types[status]
class UpgradeRequiredError(NotmuchError): pass
class PathError(NotmuchError): pass
class IllegalArgumentError(NotmuchError): pass
-
+class NoConfigError(NotmuchError): pass
+class DatabaseExistsError(NotmuchError): pass
class ObjectDestroyedError(NotmuchError):
"""The object has already been destroyed and it's memory freed.
db.create(tmppath)
def test_create_existing(self, tmppath, db):
- with pytest.raises(errors.FileError):
+ with pytest.raises(errors.DatabaseExistsError):
dbmod.Database.create(path=tmppath)
def test_close(self, db):
-#!/usr/bin/env python
+#!/usr/bin/env python3
"""
This file is part of notmuch.
border-bottom-right-radius: {border_radius};
}}
tbody:nth-child(4n+1) tr td {{
+ color: #000;
background-color: #ffd96e;
}}
tbody:nth-child(4n+3) tr td {{
+ color: #000;
background-color: #bce;
}}
hr {{
SEPARATE_MEMBER_PAGES = NO
TAB_SIZE = 8
ALIASES =
-TCL_SUBST =
OPTIMIZE_OUTPUT_FOR_C = YES
OPTIMIZE_OUTPUT_JAVA = NO
OPTIMIZE_FOR_FORTRAN = NO
The **config** command can be used to get or set settings in the notmuch
configuration file and corresponding database.
-Items marked **[STORED IN DATABASE]** are only in the database. They
-should not be placed in the configuration file, and should be accessed
-programmatically as described in the SYNOPSIS above.
-
**get**
The value of the specified configuration item is printed to
stdout. If the item has multiple values (it is a list), each value
Default: ``$MAILDIR`` variable if set, otherwise ``$HOME/mail``.
+**database.hook_dir**
+
+ Directory containing hooks run by notmuch commands. See
+ **notmuch-hooks(5)**.
+
**user.name**
Your full name.
Default: ``true``.
-**index.decrypt** **[STORED IN DATABASE]**
+**index.decrypt**
Policy for decrypting encrypted messages during indexing. Must be
one of: ``false``, ``auto``, ``nostash``, or ``true``.
Default: ``auto``.
-**index.header.<prefix>** **[STORED IN DATABASE]**
+**index.header.<prefix>**
Define the query prefix <prefix>, based on a mail header. For
example ``index.header.List=List-Id`` will add a probabilistic
prefix ``List:`` that searches the ``List-Id`` field. User
(since notmuch 0.30, "compact" and "field_processor" are
always included.)
-**query.<name>** **[STORED IN DATABASE]**
+**query.<name>**
Expansion for named query called <name>. See
**notmuch-search-terms(7)** for more information about named
queries.
of notmuch.
**NOTMUCH\_CONFIG**
- Specifies the location of the notmuch configuration file. Notmuch
- will use ${HOME}/.notmuch-config if this variable is not set.
+ Specifies the location of the notmuch configuration file.
+
+**NOTMUCH_PROFILE**
+ Selects among notmuch configurations.
+
+FILES
+=====
+
+CONFIGURATION
+-------------
+
+If ``NOTMUCH_CONFIG`` is unset, notmuch tries (in order)
+
+- ``$XDG_CONFIG_HOME/notmuch/<profile>/config`` where ``<profile>`` is
+ defined by ``$NOTMUCH_PROFILE`` or "default"
+- ``${HOME}/.notmuch-config<profile>`` where ``<profile>`` is
+ ``.$NOTMUCH_PROFILE`` or ""
+
+Hooks
+-----
+
+If ``database.hook_dir`` is unset, notmuch tries (in order)
+
+- ``$XDG_CONFIG_HOME/notmuch/<profile>/hooks`` where ``<profile>`` is
+ defined by ``$NOTMUCH_PROFILE`` or "default"
+- ``<database.path>/.notmuch/hooks``
SEE ALSO
========
SYNOPSIS
========
-$DATABASEDIR/.notmuch/hooks/*
+<hook_dir>/{pre-new, post-new, post-insert}
DESCRIPTION
===========
Hooks are scripts (or arbitrary executables or symlinks to such) that
notmuch invokes before and after certain actions. These scripts reside
-in the .notmuch/hooks directory within the database directory and must
-have executable permissions.
+in a directory defined as described in **notmuch-config(1)**. They
+must have executable permissions.
The currently available hooks are described below.
-;;; coolj.el --- automatically wrap long lines -*- coding:utf-8 -*-
+;;; coolj.el --- automatically wrap long lines -*- lexical-binding: t; coding: utf-8 -*-
;; Copyright (C) 2000, 2001, 2004-2009 Free Software Foundation, Inc.
;;; Commentary:
-;;; This is a simple derivative of some functionality from
-;;; `longlines.el'. The key difference is that this version will
-;;; insert a prefix at the head of each wrapped line. The prefix is
-;;; calculated from the originating long line.
+;; This is a simple derivative of some functionality from
+;; `longlines.el'. The key difference is that this version will
+;; insert a prefix at the head of each wrapped line. The prefix is
+;; calculated from the originating long line.
-;;; No minor-mode is provided, the caller is expected to call
-;;; `coolj-wrap-region' to wrap the region of interest.
+;; No minor-mode is provided, the caller is expected to call
+;; `coolj-wrap-region' to wrap the region of interest.
;;; Code:
:group 'coolj
:type 'regexp)
-(defvar coolj-wrap-point nil)
-
-(make-variable-buffer-local 'coolj-wrap-point)
+(defvar-local coolj-wrap-point nil)
(defun coolj-determine-prefix ()
"Determine the prefix for the current line."
-;;; make-deps.el --- compute make dependencies for Elisp sources
+;;; make-deps.el --- compute make dependencies for Elisp sources -*- lexical-binding: t -*-
;;
;; Copyright © Austin Clements
;;
-;;; notmuch-address.el --- address completion with notmuch
+;;; notmuch-address.el --- address completion with notmuch -*- lexical-binding: t -*-
;;
;; Copyright © David Edmondson
;;
(require 'notmuch-parser)
(require 'notmuch-lib)
(require 'notmuch-company)
-;;
+
(declare-function company-manual-begin "company")
+;;; Cache internals
+
(defvar notmuch-address-last-harvest 0
"Time of last address harvest.")
This variable is set by calling `notmuch-address-harvest'.")
(defvar notmuch-address-full-harvest-finished nil
- "t indicates that full completion address harvesting has been finished.
-Use notmuch-address--harvest-ready to access as that will load a
-saved hash if necessary (and available).")
+ "Whether full completion address harvesting has finished.
+Use `notmuch-address--harvest-ready' to access as that will load
+a saved hash if necessary (and available).")
(defun notmuch-address--harvest-ready ()
"Return t if there is a full address hash available.
(or notmuch-address-full-harvest-finished
(notmuch-address--load-address-hash)))
+;;; Options
+
(defcustom notmuch-address-command 'internal
"Determines how address completion candidates are generated.
-If it is a string then that string should be an external program
-which must take a single argument (searched string) and output a
-list of completion candidates, one per line.
+If this is a string, then that string should be an external
+program, which must take a single argument (searched string)
+and output a list of completion candidates, one per line.
+
+If this is the symbol `internal', then an implementation is used
+that relies on the \"notmuch address\" command, but does not use
+any third-party (i.e. \"external\") programs.
-Alternatively, it can be the symbol `internal', in which case
-internal completion is used; the variable
-`notmuch-address-internal-completion' can be used to customize
-this case.
+If this is the symbol `as-is', then Notmuch does not modify the
+value of `message-completion-alist'. This option has to be set to
+this value before `notmuch' is loaded, otherwise the modification
+to `message-completion-alist' may already have taken place. This
+setting obviously does not prevent `message-completion-alist'
+from being modified at all; the user or some third-party package
+may still modify it.
-Finally, if this variable is nil then address completion is
-disabled."
+Finally, if this is nil, then address completion is disabled."
:type '(radio
- (const :tag "Use internal address completion" internal)
- (const :tag "Disable address completion" nil)
- (string :tag "Use external completion command"))
+ (const :tag "Use internal address completion" internal)
+ (string :tag "Use external completion command")
+ (const :tag "Disable address completion" nil)
+ (const :tag "Use default or third-party mechanism" as-is))
:group 'notmuch-send
:group 'notmuch-address
:group 'notmuch-external)
:group 'notmuch-address
:group 'notmuch-hooks)
+(defcustom notmuch-address-use-company t
+ "If available, use company mode for address completion."
+ :type 'boolean
+ :group 'notmuch-send
+ :group 'notmuch-address)
+
+;;; Setup
+
(defun notmuch-address-selection-function (prompt collection initial-input)
"Call (`completing-read'
PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)"
(defun notmuch-address-message-insinuate ()
(message "calling notmuch-address-message-insinuate is no longer needed"))
-(defcustom notmuch-address-use-company t
- "If available, use company mode for address completion."
- :type 'boolean
- :group 'notmuch-send
- :group 'notmuch-address)
-
(defun notmuch-address-setup ()
- (let* ((setup-company (and notmuch-address-use-company
- (require 'company nil t)))
- (pair (cons notmuch-address-completion-headers-regexp
- #'notmuch-address-expand-name)))
- (when setup-company
+ (unless (eq notmuch-address-command 'as-is)
+ (when (and notmuch-address-use-company
+ (require 'company nil t))
(notmuch-company-setup))
- (unless (member pair message-completion-alist)
- (setq message-completion-alist
- (push pair message-completion-alist)))))
+ (cl-pushnew (cons notmuch-address-completion-headers-regexp
+ #'notmuch-address-expand-name)
+ message-completion-alist :test #'equal)))
(defun notmuch-address-toggle-internal-completion ()
"Toggle use of internal completion for current buffer.
(kill-local-variable 'company-idle-delay)
(setq-local company-idle-delay nil))))
+;;; Completion
+
(defun notmuch-address-matching (substring)
"Returns a list of completion candidates matching SUBSTRING.
The candidates are taken from `notmuch-address-completions'."
(let ((candidates)
(re (regexp-quote substring)))
- (maphash (lambda (key val)
+ (maphash (lambda (key _val)
(when (string-match re key)
(push key candidates)))
notmuch-address-completions)
(t
(funcall notmuch-address-selection-function
(format "Address (%s matches): " num-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))))))
+ options
+ orig)))))
(if chosen
(progn
(push chosen notmuch-address-history)
(ding))))
(t nil)))
-;; Copied from `w3m-which-command'.
-(defun notmuch-address-locate-command (command)
- "Return non-nil if `command' is an executable either on
-`exec-path' or an absolute pathname."
- (and (stringp command)
- (if (and (file-name-absolute-p command)
- (file-executable-p command))
- command
- (setq command (file-name-nondirectory command))
- (catch 'found-command
- (let (bin)
- (dolist (dir exec-path)
- (setq bin (expand-file-name command dir))
- (when (or (and (file-executable-p bin)
- (not (file-directory-p bin)))
- (and (file-executable-p (setq bin (concat bin ".exe")))
- (not (file-directory-p bin))))
- (throw 'found-command bin))))))))
+;;; Harvest
(defun notmuch-address-harvest-addr (result)
- (let ((name-addr (plist-get result :name-addr)))
- (puthash name-addr t notmuch-address-completions)))
-
-(defun notmuch-address-harvest-handle-result (obj)
- (notmuch-address-harvest-addr obj))
+ (puthash (plist-get result :name-addr)
+ t notmuch-address-completions))
(defun notmuch-address-harvest-filter (proc string)
(when (buffer-live-p (process-buffer proc))
(goto-char (point-max))
(insert string))
(notmuch-sexp-parse-partial-list
- 'notmuch-address-harvest-handle-result (process-buffer proc)))))
+ 'notmuch-address-harvest-addr (process-buffer proc)))))
(defvar notmuch-address-harvest-procs '(nil . nil)
"The currently running harvests.
"Collect addresses completion candidates.
It queries the notmuch database for messages sent/received (as
-configured with `notmuch-address-command`) by the user, collects
+configured with `notmuch-address-command') by the user, collects
destination/source addresses from those messages and stores them
in `notmuch-address-completions'.
(defun notmuch-address--save-address-hash ()
(when notmuch-address-save-filename
(if (or (not (file-exists-p notmuch-address-save-filename))
- ;; The file exists, check it is a file we saved
+ ;; The file exists, check it is a file we saved.
(notmuch-address--get-address-hash))
(with-temp-file notmuch-address-save-filename
(let ((save-plist
(setq notmuch-address-last-harvest now)
(notmuch-address-harvest
nil nil
- (lambda (proc event)
+ (lambda (_proc event)
;; If harvest fails, we want to try
- ;; again when the trigger is next
- ;; called
+ ;; again when the trigger is next called.
(if (string= event "finished\n")
(progn
(notmuch-address--save-address-hash)
(setq notmuch-address-full-harvest-finished t))
(setq notmuch-address-last-harvest 0)))))))
-;;
+;;; Standalone completion
(defun notmuch-address-from-minibuffer (prompt)
(if (not notmuch-address-command)
(let ((minibuffer-local-map rmap))
(read-string prompt)))))
-;;
+;;; _
(provide 'notmuch-address)
;;; Code:
-(eval-when-compile (require 'cl-lib))
-
(require 'notmuch-lib)
-(defvar notmuch-company-last-prefix nil)
-(make-variable-buffer-local 'notmuch-company-last-prefix)
+(defvar-local notmuch-company-last-prefix nil)
+
(declare-function company-begin-backend "company")
(declare-function company-grab "company")
(declare-function company-mode "company")
;;;###autoload
(defun notmuch-company-setup ()
(company-mode)
- (make-local-variable 'company-backends)
- (setq company-backends '(notmuch-company))
+ (setq-local company-backends '(notmuch-company))
;; Disable automatic company completion unless an internal
;; completion method is configured. Company completion (using
;; internal completion) can still be accessed via standard company
(run-hook-with-args 'notmuch-address-post-completion-functions arg))
(no-cache t))))
-
(provide 'notmuch-company)
;;; notmuch-company.el ends here
-;;; notmuch-compat.el --- compatibility functions for earlier versions of emacs
+;;; notmuch-compat.el --- compatibility functions for earlier versions of emacs -*- lexical-binding: t -*-
;;
;; The functions in this file are copied from more modern versions of
;; emacs and are Copyright (C) 1985-1986, 1992, 1994-1995, 1999-2017
;;; Code:
-;; emacs master has a bugfix for folding long headers when sending
-;; messages. Include the fix for earlier versions of emacs. To avoid
-;; interfering with gnus we only run the hook when called from
-;; notmuch-message-mode.
+;; Before Emacs 26.1 lines that are longer than 998 octets were not.
+;; folded. Commit 77bbca8c82f6e553c42abbfafca28f55fc995d00 fixed
+;; that. Until we drop support for Emacs 25 we have to backport that
+;; fix. To avoid interfering with Gnus we only run the hook when
+;; called from notmuch-message-mode.
(declare-function mail-header-fold-field "mail-parse" nil)
(unless (fboundp 'message--fold-long-headers)
(add-hook 'message-header-hook 'notmuch-message--fold-long-headers))
-;; End of compatibility functions
-
(provide 'notmuch-compat)
;;; notmuch-compat.el ends here
-;;; notmuch-crypto.el --- functions for handling display of cryptographic metadata
+;;; notmuch-crypto.el --- functions for handling display of cryptographic metadata -*- lexical-binding: t -*-
;;
;; Copyright © Jameson Rollins
;;
(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare))
+;;; Options
+
(defcustom notmuch-crypto-process-mime t
- "Should cryptographic MIME parts be processed?
+ "Whether to process cryptographic MIME parts.
If this variable is non-nil signatures in multipart/signed
messages will be verified and multipart/encrypted parts will be
:group 'notmuch-crypto)
(defcustom notmuch-crypto-get-keys-asynchronously t
- "Retrieve gpg keys asynchronously."
+ "Whether to retrieve openpgp keys asynchronously."
:type 'boolean
:group 'notmuch-crypto)
:type 'string
:group 'notmuch-crypto)
+;;; Faces
+
(defface notmuch-crypto-part-header
'((((class color)
(background dark))
:group 'notmuch-crypto
:group 'notmuch-faces)
+;;; Functions
+
(define-button-type 'notmuch-crypto-status-button-type
- 'action (lambda (button) (message (button-get button 'help-echo)))
+ 'action (lambda (button) (message "%s" (button-get button 'help-echo)))
'follow-link t
'help-echo "Set notmuch-crypto-process-mime to process cryptographic mime parts."
:supertype 'notmuch-button-type)
(defun notmuch-crypto-insert-sigstatus-button (sigstatus from)
- "Insert a button describing the signature status SIGSTATUS sent
-by user FROM."
+ "Insert a button describing the signature status SIGSTATUS sent by user FROM."
(let* ((status (plist-get sigstatus :status))
(show-button t)
(face 'notmuch-crypto-signature-unknown)
(declare-function notmuch-show-refresh-view "notmuch-show" (&optional reset-state))
(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare))
-(defun notmuch-crypto--async-key-sentinel (process event)
+(defun notmuch-crypto--async-key-sentinel (process _event)
"When the user asks for a GPG key to be retrieved
asynchronously, handle completion of that task.
'mouse-face 'notmuch-crypto-decryption)
(insert "\n"))
-;;
+;;; _
(provide 'notmuch-crypto)
-;;; notmuch-draft.el --- functions for postponing and editing drafts
+;;; notmuch-draft.el --- functions for postponing and editing drafts -*- lexical-binding: t -*-
;;
;; Copyright © Mark Walters
;; Copyright © David Bremner
;;; Code:
+(require 'cl-lib)
+(require 'pcase)
+(require 'subr-x)
+
(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")
+;;; Options
+
(defgroup notmuch-draft nil
"Saving and editing drafts in Notmuch."
:group 'notmuch)
: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)."
+ "Whether to allow saving plaintext when it seems encryption is intended.
+When a message contains mml tags, then that suggest it is
+intended to be encrypted. If the user requests that such a
+message is saved locally, then this option controls whether
+that is allowed. Beside a boolean, this can also be `ask'."
:type '(radio
(const :tag "Never" nil)
(const :tag "Ask every time" ask)
:group 'notmuch-draft
:group 'notmuch-crypto)
+;;; Internal
+
(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
+(defvar-local 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.
(notmuch-tag notmuch-draft-id '("+deleted"))))
(defun notmuch-draft-quote-some-mml ()
- "Quote the mml tags in `notmuch-draft-quoted-tags`."
+ "Quote the mml tags in `notmuch-draft-quoted-tags'."
(save-excursion
;; First we deal with any secure tag separately.
(message-goto-body)
(insert "!"))))))
(defun notmuch-draft-unquote-some-mml ()
- "Unquote the mml tags in `notmuch-draft-quoted-tags`."
+ "Unquote the mml tags in `notmuch-draft-quoted-tags'."
(save-excursion
(when notmuch-draft-quoted-tags
(let ((re (concat "<#!+/?\\("
(let (secure-tag)
(save-restriction
(message-narrow-to-headers)
- (setq secure-tag (message-fetch-field "X-Notmuch-Emacs-Secure" 't))
+ (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."
+ "Return non-nil if there is an mml secure tag."
(save-excursion
(message-goto-body)
- (re-search-forward notmuch-draft-encryption-tag-regex nil 't)))
+ (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.
+ "Return non-nil if we should save a message that should be encrypted.
`notmuch-draft-save-plaintext' controls the behaviour."
(cl-case notmuch-draft-save-plaintext
;; but notmuch doesn't want that form, so remove them.
(concat "draft-" (substring (message-make-message-id) 1 -1)))
+;;; Commands
+
(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
+`notmuch-draft-tags' (in addition to any default tags
applied to newly inserted messages)."
(interactive)
(when (notmuch-draft--has-encryption-tag)
;; 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)
+ ;; 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.
(notmuch-draft-quote-some-mml)
(notmuch-maildir-setup-message-for-saving)
(notmuch-maildir-notmuch-insert-current-buffer
- notmuch-draft-folder 't notmuch-draft-tags))
+ 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
(defun notmuch-draft-resume (id)
"Resume editing of message with id ID."
+ ;; Used by command `notmuch-show-resume-message'.
(let* ((tags (process-lines notmuch-command "search" "--output=tags"
"--exclude=false" id))
(draft (equal tags (notmuch-update-tags tags notmuch-draft-tags))))
;; message is resaved or sent.
(setq notmuch-draft-id (and draft id)))))
+;;; _
(add-hook 'message-send-hook 'notmuch-draft--mark-deleted)
-
(provide 'notmuch-draft)
;;; notmuch-draft.el ends here
-;;; notmuch-hello.el --- welcome to notmuch, a frontend
+;;; notmuch-hello.el --- welcome to notmuch, a frontend -*- lexical-binding: t -*-
;;
;; Copyright © David Edmondson
;;
;;; Code:
-(eval-when-compile (require 'cl-lib))
-
(require 'widget)
(require 'wid-edit) ; For `widget-forward'.
(&optional query query-context target buffer-name open-target))
+;;; Options
+
(defun notmuch-saved-search-get (saved-search field)
"Get FIELD from SAVED-SEARCH.
shown. If not present then the :query property
is used.
:sort-order Specify the sort order to be used for the search.
- Possible values are 'oldest-first 'newest-first or
- nil. Nil means use the default sort order.
+ Possible values are `oldest-first', `newest-first'
+ or nil. Nil means use the default sort order.
:search-type Specify whether to run the search in search-mode,
tree mode or unthreaded mode. Set to 'tree to specify tree
mode, 'unthreaded to specify unthreaded mode, and set to nil
(defvar notmuch-hello-indent 4
"How much to indent non-headers.")
+(defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png")))
+
(defcustom notmuch-show-logo t
"Should the notmuch logo be shown?"
:type 'boolean
:group 'notmuch-hello
:group 'notmuch-hooks)
-(defvar notmuch-hello-url "https://notmuchmail.org"
+(defconst notmuch-hello-url "https://notmuchmail.org"
"The `notmuch' web site.")
(defvar notmuch-hello-custom-section-options
:group 'notmuch-hello
:type 'boolean)
+;;; Internal variables
+
(defvar notmuch-hello-hidden-sections nil
"List of sections titles whose contents are hidden.")
(defvar notmuch-hello-first-run t
- "True if `notmuch-hello' is run for the first time, set to nil
-afterwards.")
-
-(defun notmuch-hello-nice-number (n)
- (let (result)
- (while (> n 0)
- (push (% n 1000) result)
- (setq n (/ n 1000)))
- (setq result (or result '(0)))
- (apply #'concat
- (number-to-string (car result))
- (mapcar (lambda (elem)
- (format "%s%03d" notmuch-hello-thousands-separator elem))
- (cdr result)))))
-
-(defun notmuch-hello-trim (search)
- "Trim whitespace."
- (if (string-match "^[[:space:]]*\\(.*[^[:space:]]\\)[[:space:]]*$" search)
- (match-string 1 search)
- search))
-
-(defun notmuch-hello-search (&optional search)
- (unless (null search)
- (setq search (notmuch-hello-trim search))
- (let ((history-delete-duplicates t))
- (add-to-history 'notmuch-search-history search)))
- (notmuch-search search notmuch-search-oldest-first))
-
-(defun notmuch-hello-add-saved-search (widget)
- (interactive)
- (let ((search (widget-value
- (symbol-value
- (widget-get widget :notmuch-saved-search-widget))))
+ "True if `notmuch-hello' is run for the first time, set to nil afterwards.")
+
+;;; Widgets for inserters
+
+(define-widget 'notmuch-search-item 'item
+ "A recent search."
+ :format "%v\n"
+ :value-create 'notmuch-search-item-value-create)
+
+(defun notmuch-search-item-value-create (widget)
+ (let ((value (widget-get widget :value)))
+ (widget-insert (make-string notmuch-hello-indent ?\s))
+ (widget-create 'editable-field
+ :size (widget-get widget :size)
+ :parent widget
+ :action #'notmuch-hello-search
+ value)
+ (widget-insert " ")
+ (widget-create 'push-button
+ :parent widget
+ :notify #'notmuch-hello-add-saved-search
+ "save")
+ (widget-insert " ")
+ (widget-create 'push-button
+ :parent widget
+ :notify #'notmuch-hello-delete-search-from-history
+ "del")))
+
+(defun notmuch-search-item-field-width ()
+ (max 8 ; Don't let the search boxes be less than 8 characters wide.
+ (- (window-width)
+ notmuch-hello-indent ; space at bol
+ notmuch-hello-indent ; space at eol
+ 1 ; for the space before the [save] button
+ 6 ; for the [save] button
+ 1 ; for the space before the [del] button
+ 5))) ; for the [del] button
+
+;;; Widget actions
+
+(defun notmuch-hello-search (widget &rest _event)
+ (let ((search (widget-value widget)))
+ (when search
+ (setq search (string-trim search))
+ (let ((history-delete-duplicates t))
+ (add-to-history 'notmuch-search-history search)))
+ (notmuch-search search notmuch-search-oldest-first)))
+
+(defun notmuch-hello-add-saved-search (widget &rest _event)
+ (let ((search (widget-value (widget-get widget :parent)))
(name (completing-read "Name for saved search: "
notmuch-saved-searches)))
;; If an existing saved search with this name exists, remove it.
(setq notmuch-saved-searches
(cl-loop for elem in notmuch-saved-searches
- if (not (equal name
- (notmuch-saved-search-get elem :name)))
+ unless (equal name (notmuch-saved-search-get elem :name))
collect elem))
;; Add the new one.
(customize-save-variable 'notmuch-saved-searches
(message "Saved '%s' as '%s'." search name)
(notmuch-hello-update)))
-(defun notmuch-hello-delete-search-from-history (widget)
- (interactive)
- (let ((search (widget-value
- (symbol-value
- (widget-get widget :notmuch-saved-search-widget)))))
- (setq notmuch-search-history (delete search
- notmuch-search-history))
+(defun notmuch-hello-delete-search-from-history (widget &rest _event)
+ (when (y-or-n-p "Are you sure you want to delete this search? ")
+ (let ((search (widget-value (widget-get widget :parent))))
+ (setq notmuch-search-history
+ (delete search notmuch-search-history)))
(notmuch-hello-update)))
+;;; Button utilities
+
+;; `notmuch-hello-query-counts', `notmuch-hello-nice-number' and
+;; `notmuch-hello-insert-buttons' are used outside this section.
+;; All other functions that are defined in this section are only
+;; used by these two functions.
+
(defun notmuch-hello-longest-label (searches-alist)
(or (cl-loop for elem in searches-alist
maximize (length (notmuch-saved-search-get elem :name)))
(cl-loop for row from 0 to (- nrows 1)
append (notmuch-hello-reflect-generate-row ncols nrows row list))))
-(defun notmuch-hello-widget-search (widget &rest ignore)
- (cond
- ((eq (widget-get widget :notmuch-search-type) 'tree)
- (notmuch-tree (widget-get widget
- :notmuch-search-terms)))
- ((eq (widget-get widget :notmuch-search-type) 'unthreaded)
- (notmuch-unthreaded (widget-get widget
- :notmuch-search-terms)))
+(defun notmuch-hello-widget-search (widget &rest _ignore)
+ (cl-case (widget-get widget :notmuch-search-type)
+ (tree
+ (notmuch-tree (widget-get widget :notmuch-search-terms)))
+ (unthreaded
+ (notmuch-unthreaded (widget-get widget :notmuch-search-terms)))
(t
- (notmuch-search (widget-get widget
- :notmuch-search-terms)
- (widget-get widget
- :notmuch-search-oldest-first)))))
+ (notmuch-search (widget-get widget :notmuch-search-terms)
+ (widget-get widget :notmuch-search-oldest-first)))))
(defun notmuch-saved-search-count (search)
(car (process-lines notmuch-command "count" search)))
--batch'. In general we recommend running matching versions of
the CLI and emacs interface."))
(goto-char (point-min))
- (notmuch-remove-if-not
- #'identity
- (mapcar
- (lambda (elem)
- (let* ((elem-plist (notmuch-hello-saved-search-to-plist elem))
- (search-query (plist-get elem-plist :query))
- (filtered-query (notmuch-hello-filtered-query
- search-query (plist-get options :filter)))
- (message-count (prog1 (read (current-buffer))
- (forward-line 1))))
- (when (and filtered-query (or (plist-get options :show-empty-searches)
- (> message-count 0)))
- (setq elem-plist (plist-put elem-plist :query filtered-query))
- (plist-put elem-plist :count message-count))))
- query-list))))
+ (cl-mapcan
+ (lambda (elem)
+ (let* ((elem-plist (notmuch-hello-saved-search-to-plist elem))
+ (search-query (plist-get elem-plist :query))
+ (filtered-query (notmuch-hello-filtered-query
+ search-query (plist-get options :filter)))
+ (message-count (prog1 (read (current-buffer))
+ (forward-line 1))))
+ (when (and filtered-query (or (plist-get options :show-empty-searches)
+ (> message-count 0)))
+ (setq elem-plist (plist-put elem-plist :query filtered-query))
+ (list (plist-put elem-plist :count message-count)))))
+ query-list)))
+
+(defun notmuch-hello-nice-number (n)
+ (let (result)
+ (while (> n 0)
+ (push (% n 1000) result)
+ (setq n (/ n 1000)))
+ (setq result (or result '(0)))
+ (apply #'concat
+ (number-to-string (car result))
+ (mapcar (lambda (elem)
+ (format "%s%03d" notmuch-hello-thousands-separator elem))
+ (cdr result)))))
(defun notmuch-hello-insert-buttons (searches)
"Insert buttons for SEARCHES.
(unless (eq (% count tags-per-line) 0)
(widget-insert "\n"))))
-(defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png")))
+;;; Mode
(defun notmuch-hello-update ()
"Update the notmuch-hello buffer."
;; Refresh hello as soon as we get back to redisplay. On Emacs
;; 24, we can't do it right here because something in this
;; hook's call stack overrides hello's point placement.
+ ;; FIXME And on Emacs releases that we still support?
(run-at-time nil nil #'notmuch-hello t))
(unless hello-buf
;; Clean up hook
(remove-hook 'window-configuration-change-hook
#'notmuch-hello-window-configuration-change))))
-;; the following variable is defined as being defconst in notmuch-version.el
-(defvar notmuch-emacs-version)
-
-(defun notmuch-hello-versions ()
- "Display the notmuch version(s)."
- (interactive)
- (let ((notmuch-cli-version (notmuch-cli-version)))
- (message "notmuch version %s"
- (if (string= notmuch-emacs-version notmuch-cli-version)
- notmuch-cli-version
- (concat notmuch-cli-version
- " (emacs mua version " notmuch-emacs-version ")")))))
-
(defvar notmuch-hello-mode-map
- (let ((map (if (fboundp 'make-composed-keymap)
- ;; Inherit both widget-keymap and
- ;; notmuch-common-keymap. We have to use
- ;; make-sparse-keymap to force this to be a new
- ;; keymap (so that when we modify map it does not
- ;; modify widget-keymap).
- (make-composed-keymap (list (make-sparse-keymap) widget-keymap))
- ;; Before Emacs 24, keymaps didn't support multiple
- ;; inheritance,, so just copy the widget keymap since
- ;; it's unlikely to change.
- (copy-keymap widget-keymap))))
+ ;; Inherit both widget-keymap and notmuch-common-keymap. We have
+ ;; to use make-sparse-keymap to force this to be a new keymap (so
+ ;; that when we modify map it does not modify widget-keymap).
+ (let ((map (make-composed-keymap (list (make-sparse-keymap) widget-keymap))))
(set-keymap-parent map notmuch-common-keymap)
- (define-key map "v" 'notmuch-hello-versions)
(define-key map (kbd "<C-tab>") 'widget-backward)
map)
"Keymap for \"notmuch hello\" buffers.")
Complete list of currently available key bindings:
\\{notmuch-hello-mode-map}"
- (setq notmuch-buffer-refresh-function #'notmuch-hello-update)
- ;;(setq buffer-read-only t)
- )
+ (setq notmuch-buffer-refresh-function #'notmuch-hello-update))
+
+;;; Inserters
(defun notmuch-hello-generate-tag-alist (&optional hide-tags)
"Return an alist from tags to queries to display in the all-tags section."
- (mapcar (lambda (tag)
- (cons tag (concat "tag:" (notmuch-escape-boolean-term tag))))
- (notmuch-remove-if-not
- (lambda (tag)
- (not (member tag hide-tags)))
- (process-lines notmuch-command "search" "--output=tags" "*"))))
+ (cl-mapcan (lambda (tag)
+ (and (not (member tag hide-tags))
+ (list (cons tag
+ (concat "tag:"
+ (notmuch-escape-boolean-term tag))))))
+ (process-lines notmuch-command "search" "--output=tags" "*")))
(defun notmuch-hello-insert-header ()
"Insert the default notmuch-hello header."
(let ((widget-link-prefix "")
(widget-link-suffix ""))
(widget-create 'link
- :notify (lambda (&rest ignore)
+ :notify (lambda (&rest _ignore)
(browse-url notmuch-hello-url))
:help-echo "Visit the notmuch website."
"notmuch")
(widget-insert ". ")
(widget-insert "You have ")
(widget-create 'link
- :notify (lambda (&rest ignore)
+ :notify (lambda (&rest _ignore)
(notmuch-hello-update))
:help-echo "Refresh"
(notmuch-hello-nice-number
(when searches
(widget-insert "Saved searches: ")
(widget-create 'push-button
- :notify (lambda (&rest ignore)
+ :notify (lambda (&rest _ignore)
(customize-variable 'notmuch-saved-searches))
"edit")
(widget-insert "\n\n")
;; search boxes.
:size (max 8 (- (window-width) notmuch-hello-indent
(length "Search: ")))
- :action (lambda (widget &rest ignore)
- (notmuch-hello-search (widget-value widget))))
+ :action #'notmuch-hello-search)
;; Add an invisible dot to make `widget-end-of-line' ignore
;; trailing spaces in the search widget field. A dot is used
;; instead of a space to make `show-trailing-whitespace'
;; happy, i.e. avoid it marking the whole line as trailing
;; spaces.
- (widget-insert ".")
- (put-text-property (1- (point)) (point) 'invisible t)
+ (widget-insert (propertize "." 'invisible t))
(widget-insert "\n"))
(defun notmuch-hello-insert-recent-searches ()
"Insert recent searches."
(when notmuch-search-history
(widget-insert "Recent searches: ")
- (widget-create 'push-button
- :notify (lambda (&rest ignore)
- (when (y-or-n-p "Are you sure you want to clear the searches? ")
- (setq notmuch-search-history nil)
- (notmuch-hello-update)))
- "clear")
+ (widget-create
+ 'push-button
+ :notify (lambda (&rest _ignore)
+ (when (y-or-n-p "Are you sure you want to clear the searches? ")
+ (setq notmuch-search-history nil)
+ (notmuch-hello-update)))
+ "clear")
(widget-insert "\n\n")
- (let ((start (point)))
- (cl-loop for i from 1 to notmuch-hello-recent-searches-max
- for search in notmuch-search-history do
- (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i))))
- (set widget-symbol
- (widget-create 'editable-field
- ;; Don't let the search boxes be
- ;; less than 8 characters wide.
- :size (max 8
- (- (window-width)
- ;; Leave some space
- ;; at the start and
- ;; end of the
- ;; boxes.
- (* 2 notmuch-hello-indent)
- ;; 1 for the space
- ;; before the
- ;; `[save]' button. 6
- ;; for the `[save]'
- ;; button.
- 1 6
- ;; 1 for the space
- ;; before the `[del]'
- ;; button. 5 for the
- ;; `[del]' button.
- 1 5))
- :action (lambda (widget &rest ignore)
- (notmuch-hello-search (widget-value widget)))
- search))
- (widget-insert " ")
- (widget-create 'push-button
- :notify (lambda (widget &rest ignore)
- (notmuch-hello-add-saved-search widget))
- :notmuch-saved-search-widget widget-symbol
- "save")
- (widget-insert " ")
- (widget-create 'push-button
- :notify (lambda (widget &rest ignore)
- (when (y-or-n-p "Are you sure you want to delete this search? ")
- (notmuch-hello-delete-search-from-history widget)))
- :notmuch-saved-search-widget widget-symbol
- "del"))
- (widget-insert "\n"))
- (indent-rigidly start (point) notmuch-hello-indent))
- nil))
+ (let ((width (notmuch-search-item-field-width)))
+ (dolist (search (seq-take notmuch-search-history
+ notmuch-hello-recent-searches-max))
+ (widget-create 'notmuch-search-item :value search :size width)))))
(defun notmuch-hello-insert-searches (title query-list &rest options)
"Insert a section with TITLE showing a list of buttons made from QUERY-LIST.
(start (point)))
(if is-hidden
(widget-create 'push-button
- :notify `(lambda (widget &rest ignore)
+ :notify `(lambda (widget &rest _ignore)
(setq notmuch-hello-hidden-sections
(delete ,title notmuch-hello-hidden-sections))
(notmuch-hello-update))
"show")
(widget-create 'push-button
- :notify `(lambda (widget &rest ignore)
+ :notify `(lambda (widget &rest _ignore)
(add-to-list 'notmuch-hello-hidden-sections
,title)
(notmuch-hello-update))
(widget-insert "Hit `?' for context-sensitive help in any Notmuch screen.\n")
(widget-insert "Customize ")
(widget-create 'link
- :notify (lambda (&rest ignore)
+ :notify (lambda (&rest _ignore)
(customize-group 'notmuch))
:button-prefix "" :button-suffix ""
"Notmuch")
(widget-insert " or ")
(widget-create 'link
- :notify (lambda (&rest ignore)
+ :notify (lambda (&rest _ignore)
(customize-variable 'notmuch-hello-sections))
:button-prefix "" :button-suffix ""
"this page.")
(let ((fill-column (- (window-width) notmuch-hello-indent)))
(center-region start (point)))))
+;;; Hello!
+
;;;###autoload
(defun notmuch-hello (&optional no-display)
"Run notmuch and display saved searches, known tags, etc."
(run-hooks 'notmuch-hello-refresh-hook)
(setq notmuch-hello-first-run nil))
-(defun notmuch-folder ()
- "Deprecated function for invoking notmuch---calling `notmuch' is preferred now."
- (interactive)
- (notmuch-hello))
-
-;;
+;;; _
(provide 'notmuch-hello)
-;;; notmuch-jump.el --- User-friendly shortcut keys
+;;; notmuch-jump.el --- User-friendly shortcut keys -*- lexical-binding: t -*-
;;
;; Copyright © Austin Clements
;;
;;; Code:
-(eval-when-compile
- (require 'cl-lib)
- (require 'pcase))
-
(require 'notmuch-lib)
(require 'notmuch-hello)
-(eval-and-compile
- (unless (fboundp 'window-body-width)
- ;; Compatibility for Emacs pre-24
- (defalias 'window-body-width 'window-width)))
-
;;;###autoload
(defun notmuch-jump-search ()
"Jump to a saved search by shortcut key.
(setq action-map (nreverse action-map))
(if action-map
(notmuch-jump action-map "Search: ")
- (error "To use notmuch-jump, \
-please customize shortcut keys in notmuch-saved-searches."))))
+ (error "To use notmuch-jump, %s"
+ "please customize shortcut keys in notmuch-saved-searches."))))
(defvar notmuch-jump--action nil)
buffer."
;; Compute the maximum key description width
(let ((key-width 1))
- (pcase-dolist (`(,key ,desc) action-map)
+ (pcase-dolist (`(,key ,_desc) action-map)
(setq key-width
(max key-width
(string-width (format-kbd-macro key)))))
"Translate ACTION-MAP into a minibuffer keymap."
(let ((map (make-sparse-keymap)))
(set-keymap-parent map notmuch-jump-minibuffer-map)
- (pcase-dolist (`(,key ,name ,fn) action-map)
+ (pcase-dolist (`(,key ,_name ,fn) action-map)
(when (= (length key) 1)
(define-key map key
`(lambda () (interactive)
;; 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.
- (pcase-dolist (`(,key ,name ,fn) action-map)
+ (pcase-dolist (`(,key ,_name ,_fn) action-map)
(when (> (length key) 1)
(let* ((key (elt key 0))
(keystr (string key))
(exit-minibuffer)))))))
map))
-;;
-
(provide 'notmuch-jump)
;;; notmuch-jump.el ends here
-;;; notmuch-lib.el --- common variables, functions and function declarations
+;;; notmuch-lib.el --- common variables, functions and function declarations -*- lexical-binding: t -*-
;;
;; Copyright © Carl Worth
;;
;;; Code:
(require 'cl-lib)
+(require 'pcase)
+(require 'subr-x)
(require 'mm-util)
(require 'mm-view)
(defconst notmuch-emacs-version "unknown"
"Placeholder variable when notmuch-version.el[c] is not available."))
+;;; Groups
+
(defgroup notmuch nil
"Notmuch mail reader for Emacs."
:group 'mail)
"Graphical attributes for displaying text"
:group 'notmuch)
+;;; Options
+
(defcustom notmuch-command "notmuch"
"Name of the notmuch binary.
search."
:type 'boolean
:group 'notmuch-search)
+(make-variable-buffer-local 'notmuch-search-oldest-first)
(defcustom notmuch-poll-script nil
"[Deprecated] Command to run to incorporate new mail into the notmuch database.
(string :tag "Custom script"))
:group 'notmuch-external)
-;;
-
-(defvar notmuch-search-history nil
- "Variable to store notmuch searches history.")
-
(defcustom notmuch-archive-tags '("-inbox")
"List of tag changes to apply to a message or a thread when it is archived.
:group 'notmuch-search
:group 'notmuch-show)
+;;; Variables
+
+(defvar notmuch-search-history nil
+ "Variable to store notmuch searches history.")
+
(defvar notmuch-common-keymap
(let ((map (make-sparse-keymap)))
(define-key map "?" 'notmuch-help)
+ (define-key map "v" 'notmuch-version)
(define-key map "q" 'notmuch-bury-or-kill-this-buffer)
(define-key map "s" 'notmuch-search)
(define-key map "t" 'notmuch-search-by-tag)
(select-window (posn-window (event-start last-input-event)))
(button-activate button)))
+;;; CLI Utilities
+
(defun notmuch-command-to-string (&rest args)
"Synchronously invoke \"notmuch\" with the given list of arguments.
Otherwise the output will be returned."
(with-temp-buffer
- (let* ((status (apply #'call-process notmuch-command nil t nil args))
- (output (buffer-string)))
+ (let ((status (apply #'call-process notmuch-command nil t nil args))
+ (output (buffer-string)))
(notmuch-check-exit-status status (cons notmuch-command args) output)
output)))
(match-string 2 long-string)
"unknown")))
+(defvar notmuch-emacs-version)
+
+(defun notmuch-version ()
+ "Display the notmuch version.
+The versions of the Emacs package and the `notmuch' executable
+should match, but if and only if they don't, then this command
+displays both values separately."
+ (interactive)
+ (let ((cli-version (notmuch-cli-version)))
+ (message "notmuch version %s"
+ (if (string= notmuch-emacs-version cli-version)
+ cli-version
+ (concat cli-version
+ " (emacs mua version " notmuch-emacs-version ")")))))
+
+;;; Notmuch Configuration
+
(defun notmuch-config-get (item)
"Return a value from the notmuch configuration."
(let* ((val (notmuch-command-to-string "config" "get" item))
(len (length val)))
;; Trim off the trailing newline (if the value is empty or not
- ;; configured, there will be no newline)
- (if (and (> len 0) (= (aref val (- len 1)) ?\n))
+ ;; configured, there will be no newline).
+ (if (and (> len 0)
+ (= (aref val (- len 1)) ?\n))
(substring val 0 -1)
val)))
(defun notmuch-user-emails ()
(cons (notmuch-user-primary-email) (notmuch-user-other-email)))
+;;; Commands
+
(defun notmuch-poll ()
"Run \"notmuch new\" or an external script to import mail.
(interactive)
(message "Polling mail...")
(if (stringp notmuch-poll-script)
- (unless (string= notmuch-poll-script "")
+ (unless (string-empty-p notmuch-poll-script)
(unless (equal (call-process notmuch-poll-script nil nil) 0)
(error "Notmuch: poll script `%s' failed!" notmuch-poll-script)))
(notmuch-call-notmuch-process "new"))
(bury-buffer)
(kill-buffer)))
-(defun notmuch-documentation-first-line (symbol)
- "Return the first line of the documentation string for SYMBOL."
- (let ((doc (documentation symbol)))
- (if doc
- (with-temp-buffer
- (insert (documentation symbol t))
- (goto-char (point-min))
- (let ((beg (point)))
- (end-of-line)
- (buffer-substring beg (point))))
- "")))
+;;; Describe Key Bindings
(defun notmuch-prefix-key-description (key)
"Given a prefix key code, return a human-readable string representation.
"M-"
(concat desc " "))))
-
(defun notmuch-describe-key (actual-key binding prefix ua-keys tail)
"Prepend cons cells describing prefix-arg ACTUAL-KEY and ACTUAL-KEY to TAIL.
(or (and (symbolp binding)
(get binding 'notmuch-doc))
(and (functionp binding)
- (notmuch-documentation-first-line binding))))
+ (let ((doc (documentation binding)))
+ (and doc
+ (string-match "\\`.+" doc)
+ (match-string 0 doc))))))
tail)))
tail)
its prefixed behavior by setting the 'notmuch-prefix-doc property
of its command symbol."
(interactive)
- (let* ((mode major-mode)
- (doc (substitute-command-keys
- (notmuch-substitute-command-keys (documentation mode t)))))
+ (let ((doc (substitute-command-keys
+ (notmuch-substitute-command-keys
+ (documentation major-mode t)))))
(with-current-buffer (generate-new-buffer "*notmuch-help*")
(insert doc)
(goto-char (point-min))
(insert desc)))
(pop-to-buffer (help-buffer)))))
-(defvar notmuch-buffer-refresh-function nil
+;;; Refreshing Buffers
+
+(defvar-local notmuch-buffer-refresh-function nil
"Function to call to refresh the current buffer.")
-(make-variable-buffer-local 'notmuch-buffer-refresh-function)
(defun notmuch-refresh-this-buffer ()
"Refresh the current buffer."
(with-current-buffer buffer
(notmuch-refresh-this-buffer))))))
+;;; String Utilities
+
(defun notmuch-prettify-subject (subject)
- ;; This function is used by `notmuch-search-process-filter' which
- ;; requires that we not disrupt its' matching state.
+ ;; This function is used by `notmuch-search-process-filter',
+ ;; which requires that we not disrupt its matching state.
(save-match-data
(if (and subject
(string-match "^[ \t]*$" subject))
(replace-regexp-in-string
"[ %\"]" (lambda (match) (format "%%%02x" (aref match 0))) str))
-;;
-
(defun notmuch-common-do-stash (text)
"Common function to stash text in kill ring, and display in minibuffer."
(if text
(kill-new "")
(message "Nothing to stash!")))
-;;
-
-(defun notmuch-remove-if-not (predicate list)
- "Return a copy of LIST with all items not satisfying PREDICATE removed."
- (let (out)
- (while list
- (when (funcall predicate (car list))
- (push (car list) out))
- (setq list (cdr list)))
- (nreverse out)))
+;;; Generic Utilities
(defun notmuch-plist-delete (plist property)
- (let* ((xplist (cons nil plist))
- (pred xplist))
- (while (cdr pred)
- (when (eq (cadr pred) property)
- (setcdr pred (cdddr pred)))
- (setq pred (cddr pred)))
- (cdr xplist)))
-
-(defun notmuch-split-content-type (content-type)
- "Split content/type into 'content' and 'type'."
- (split-string content-type "/"))
+ (let (p)
+ (while plist
+ (unless (eq property (car plist))
+ (setq p (plist-put p (car plist) (cadr plist))))
+ (setq plist (cddr plist)))
+ p))
+
+;;; MML Utilities
(defun notmuch-match-content-type (t1 t2)
- "Return t if t1 and t2 are matching content types, taking wildcards into account."
- (let ((st1 (notmuch-split-content-type t1))
- (st2 (notmuch-split-content-type t2)))
- (if (or (string= (cadr st1) "*")
- (string= (cadr st2) "*"))
- ;; Comparison of content types should be case insensitive.
- (string= (downcase (car st1)) (downcase (car st2)))
- (string= (downcase t1) (downcase t2)))))
+ "Return t if t1 and t2 are matching content types.
+Take wildcards into account."
+ (and (stringp t1)
+ (stringp t2)
+ (let ((st1 (split-string t1 "/"))
+ (st2 (split-string t2 "/")))
+ (if (or (string= (cadr st1) "*")
+ (string= (cadr st2) "*"))
+ ;; Comparison of content types should be case insensitive.
+ (string= (downcase (car st1))
+ (downcase (car st2)))
+ (string= (downcase t1)
+ (downcase t2))))))
(defvar notmuch-multipart/alternative-discouraged
'(;; Avoid HTML parts.
MSG (if it isn't already)."
(notmuch--get-bodypart-raw msg part process-crypto nil cache))
-;; Workaround: The call to `mm-display-part' below triggers a bug in
-;; Emacs 24 if it attempts to use the shr renderer to display an HTML
-;; part with images in it (demonstrated in 24.1 and 24.2 on Debian and
-;; Fedora 17, though unreproducible in other configurations).
-;; `mm-shr' references the variable `gnus-inhibit-images' without
-;; first loading gnus-art, which defines it, resulting in a
-;; void-variable error. Hence, we advise `mm-shr' to ensure gnus-art
-;; is loaded.
-(define-advice mm-shr (:before (_handle) notmuch--load-gnus-args)
- "Require `gnus-art' since we use its variables."
- (require 'gnus-art nil t))
-
(defun notmuch-mm-display-part-inline (msg part content-type process-crypto)
"Use the mm-decode/mm-view functions to display a part in the
current buffer, if possible."
(mm-display-part handle)
t))))))
+;;; Generic Utilities
+
;; Converts a plist of headers to an alist of headers. The input plist should
;; have symbols of the form :Header as keys, and the resulting alist will have
;; symbols of the form 'Header as keys.
(put-text-property start next prop (funcall func value) object)
(setq start next))))
+;;; Running Notmuch
+
(defun notmuch-logged-error (msg &optional extra)
"Log MSG and EXTRA to *Notmuch errors* and signal MSG.
Emacs requested a newer output format than supported by the notmuch CLI.
You may need to restart Emacs or upgrade your notmuch package."))
(t
- (let* ((command-string
- (mapconcat (lambda (arg)
- (shell-quote-argument
- (cond ((stringp arg) arg)
- ((symbolp arg) (symbol-name arg))
- (t "*UNKNOWN ARGUMENT*"))))
- command " "))
- (extra
- (concat "command: " command-string "\n"
- (if (integerp exit-status)
- (format "exit status: %s\n" exit-status)
- (format "exit signal: %s\n" exit-status))
- (and err (concat "stderr:\n" err))
- (and output (concat "stdout:\n" output)))))
+ (pcase-let*
+ ((`(,command . ,args) command)
+ (command (if (equal (file-name-nondirectory command)
+ notmuch-command)
+ notmuch-command
+ command))
+ (command-string
+ (mapconcat (lambda (arg)
+ (shell-quote-argument
+ (cond ((stringp arg) arg)
+ ((symbolp arg) (symbol-name arg))
+ (t "*UNKNOWN ARGUMENT*"))))
+ (cons command args)
+ " "))
+ (extra
+ (concat "command: " command-string "\n"
+ (if (integerp exit-status)
+ (format "exit status: %s\n" exit-status)
+ (format "exit signal: %s\n" exit-status))
+ (and err (concat "stderr:\n" err))
+ (and output (concat "stdout:\n" output)))))
(if err
;; We have an error message straight from the CLI.
(notmuch-logged-error
;; We only have combined output from the CLI; don't inundate
;; the user with it. Mimic `process-lines'.
(notmuch-logged-error (format "%s exited with status %s"
- (car command) exit-status)
+ command exit-status)
extra))
;; `notmuch-logged-error' does not return.
))))
invoke `set-process-sentinel' directly on the returned process,
as that will interfere with the handling of stderr and the exit
status."
- (let (err-file err-buffer proc err-proc
- ;; Find notmuch using Emacs' `exec-path'
- (command (or (executable-find notmuch-command)
- (error "Command not found: %s" notmuch-command))))
- (if (fboundp 'make-process)
- (progn
- (setq err-buffer (generate-new-buffer " *notmuch-stderr*"))
- ;; Emacs 25 and newer has `make-process', which allows
- ;; redirecting stderr independently from stdout to a
- ;; separate buffer. As this allows us to avoid using a
- ;; temporary file and shell invocation, use it when
- ;; available.
- (setq proc (make-process
- :name name
- :buffer buffer
- :command (cons command args)
- :connection-type 'pipe
- :stderr err-buffer))
- (setq err-proc (get-buffer-process err-buffer))
- (process-put proc 'err-buffer err-buffer)
-
- (process-put err-proc 'err-file err-file)
- (process-put err-proc 'err-buffer err-buffer)
- (set-process-sentinel err-proc #'notmuch-start-notmuch-error-sentinel))
- ;; On Emacs versions before 25, there is no way to capture
- ;; stdout and stderr separately for asynchronous processes, or
- ;; even to redirect stderr to a file, so we use a trivial shell
- ;; wrapper to send stderr to a temporary file and clean things
- ;; up in the sentinel.
- (setq err-file (make-temp-file "nmerr"))
- (let ((process-connection-type nil)) ;; Use a pipe
- (setq proc (apply #'start-process name buffer
- "/bin/sh" "-c"
- "exec 2>\"$1\"; shift; exec \"$0\" \"$@\""
- command err-file args)))
- (process-put proc 'err-file err-file))
+ (let* ((command (or (executable-find notmuch-command)
+ (error "Command not found: %s" notmuch-command)))
+ (err-buffer (generate-new-buffer " *notmuch-stderr*"))
+ (proc (make-process
+ :name name
+ :buffer buffer
+ :command (cons command args)
+ :connection-type 'pipe
+ :stderr err-buffer))
+ (err-proc (get-buffer-process err-buffer)))
+ (process-put proc 'err-buffer err-buffer)
(process-put proc 'sub-sentinel sentinel)
- (process-put proc 'real-command (cons notmuch-command args))
(set-process-sentinel proc #'notmuch-start-notmuch-sentinel)
+ (set-process-sentinel err-proc #'notmuch-start-notmuch-error-sentinel)
proc))
(defun notmuch-start-notmuch-sentinel (proc event)
"Process sentinel function used by `notmuch-start-notmuch'."
- (let* ((err-file (process-get proc 'err-file))
- (err-buffer (or (process-get proc 'err-buffer)
- (find-file-noselect err-file)))
- (err (and (not (zerop (buffer-size err-buffer)))
+ (let* ((err-buffer (process-get proc 'err-buffer))
+ (err (and (buffer-live-p err-buffer)
+ (not (zerop (buffer-size err-buffer)))
(with-current-buffer err-buffer (buffer-string))))
- (sub-sentinel (process-get proc 'sub-sentinel))
- (real-command (process-get proc 'real-command)))
+ (sub-sentinel (process-get proc 'sub-sentinel)))
(condition-case err
(progn
;; Invoke the sub-sentinel, if any
;; and there's no point in telling the user that (but we
;; still check for and report stderr output below).
(when (buffer-live-p (process-buffer proc))
- (notmuch-check-async-exit-status proc event real-command err))
+ (notmuch-check-async-exit-status proc event nil err))
;; If that didn't signal an error, then any error output was
;; really warning output. Show warnings, if any.
(let ((warnings
(error
;; Emacs behaves strangely if an error escapes from a sentinel,
;; so turn errors into messages.
- (message "%s" (error-message-string err))))
- (when err-file (ignore-errors (delete-file err-file)))))
-
-(defun notmuch-start-notmuch-error-sentinel (proc event)
- (let* ((err-file (process-get proc 'err-file))
- ;; When `make-process' is available, use the error buffer
- ;; associated with the process, otherwise the error file.
- (err-buffer (or (process-get proc 'err-buffer)
- (find-file-noselect err-file))))
- (when err-buffer (kill-buffer err-buffer))))
-
-;; This variable is used only buffer local, but it needs to be
-;; declared globally first to avoid compiler warnings.
-(defvar notmuch-show-process-crypto nil)
-(make-variable-buffer-local 'notmuch-show-process-crypto)
+ (message "%s" (error-message-string err))))))
+
+(defun notmuch-start-notmuch-error-sentinel (proc _event)
+ (unless (process-live-p proc)
+ (let ((buffer (process-buffer proc)))
+ (when (buffer-live-p buffer)
+ (kill-buffer buffer)))))
+
+(defvar-local notmuch-show-process-crypto nil)
+
+;;; Generic Utilities
(defun notmuch-interactive-region ()
"Return the bounds of the current interactive region.
'notmuch-interactive-region
"notmuch 0.29")
+;;; _
+
(provide 'notmuch-lib)
;;; notmuch-lib.el ends here
-;;; notmuch-maildir-fcc.el --- inserting using a fcc handler
+;;; notmuch-maildir-fcc.el --- inserting using a fcc handler -*- lexical-binding: t -*-
;; Copyright © Jesse Rosenthal
;;
;;; Code:
-(eval-when-compile (require 'cl-lib))
-
(require 'message)
(require 'notmuch-lib)
(defvar notmuch-maildir-fcc-count 0)
+;;; Options
+
(defcustom notmuch-fcc-dirs "sent"
"Determines the Fcc Header which says where to save outgoing mail.
:require 'notmuch-fcc-initialization
:group 'notmuch-send)
-(defcustom notmuch-maildir-use-notmuch-insert 't
+(defcustom notmuch-maildir-use-notmuch-insert t
"Should fcc use notmuch insert instead of simple fcc."
:type '(choice :tag "Fcc Method"
(const :tag "Use notmuch insert" t)
(const :tag "Use simple fcc" nil))
:group 'notmuch-send)
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Functions which set up the fcc header in the message buffer.
+;;; Functions which set up the fcc header in the message buffer.
(defun notmuch-fcc-header-setup ()
"Add an Fcc header to the current message buffer.
-Sets the Fcc header based on the values of `notmuch-fcc-dirs'.
-
-Originally intended to be use a hook function, but now called directly
-by notmuch-mua-mail."
+If the Fcc header is already set, then keep it as-is.
+Otherwise set it according to `notmuch-fcc-dirs'."
(let ((subdir
(cond
((or (not notmuch-fcc-dirs)
;; Old style - no longer works.
(error "Invalid `notmuch-fcc-dirs' setting (old style)"))
((listp notmuch-fcc-dirs)
- (let* ((from (message-field-value "From"))
- (match
- (catch 'first-match
- (dolist (re-folder notmuch-fcc-dirs)
- (when (string-match-p (car re-folder) from)
- (throw 'first-match re-folder))))))
- (if match
- (cdr match)
- (message "No Fcc header added.")
- nil)))
+ (or (seq-some (let ((from (message-field-value "From")))
+ (pcase-lambda (`(,regexp . ,folder))
+ (and (string-match-p regexp from)
+ folder)))
+ notmuch-fcc-dirs)
+ (progn (message "No Fcc header added.")
+ nil)))
(t
(error "Invalid `notmuch-fcc-dirs' setting (neither string nor list)")))))
(when subdir
;; Notmuch insert does not accept absolute paths, so check the user
;; really want this header inserted.
(when (or (not (= (elt subdir 0) ?/))
- (y-or-n-p
- (format "Fcc header %s is an absolute path and notmuch insert is requested.
-Insert header anyway? " subdir)))
+ (y-or-n-p (format "Fcc header %s is an absolute path %s %s" subdir
+ "and notmuch insert is requested."
+ "Insert header anyway? ")))
(message-add-header (concat "Fcc: " subdir))))
(defun notmuch-maildir-add-file-style-fcc-header (subdir)
subdir
(concat (notmuch-database-path) "/" subdir))))))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Functions for saving a message either using notmuch insert or file
-;; fcc. First functions common to the two cases.
+;;; Functions for saving a message using either method.
(defmacro with-temporary-notmuch-message-buffer (&rest body)
"Set-up a temporary copy of the current message-mode buffer."
,@body)))
(defun notmuch-maildir-setup-message-for-saving ()
- "Setup message for saving. Should be called on a temporary copy.
+ "Setup message for saving.
+This should be called on a temporary copy.
This is taken from the function message-do-fcc."
(message-encode-message-body)
(save-restriction
"Process Fcc headers in the current buffer.
This is a rearranged version of message mode's message-do-fcc."
- (let (list file)
+ (let (files file)
(save-excursion
(save-restriction
(message-narrow-to-headers)
(save-restriction
(message-narrow-to-headers)
(while (setq file (message-fetch-field "fcc" t))
- (push file list)
+ (push file files)
(message-remove-header "fcc" nil t)))
(notmuch-maildir-setup-message-for-saving)
;; Process FCC operations.
- (while list
- (setq file (pop list))
- (notmuch-fcc-handler file))
+ (mapc #'notmuch-fcc-handler files)
(kill-buffer (current-buffer)))))))
(defun notmuch-fcc-handler (fcc-header)
"Store message with notmuch insert or normal (file) fcc.
-If `notmuch-maildir-use-notmuch-insert` is set then store the
+If `notmuch-maildir-use-notmuch-insert' is set then store the
message using notmuch insert. Otherwise store the message using
normal fcc."
(message "Doing Fcc...")
(if notmuch-maildir-use-notmuch-insert
(notmuch-maildir-fcc-with-notmuch-insert fcc-header)
- (notmuch-maildir-fcc-file-fcc fcc-header)))
+ (notmuch-maildir-fcc-file-fcc fcc-header))
+ (message "Doing Fcc...done"))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Functions for saving a message using notmuch insert.
+;;; Functions for saving a message using notmuch insert.
(defun notmuch-maildir-notmuch-insert-current-buffer (folder &optional create tags)
"Use notmuch insert to put the current buffer in the database.
database in folder FOLDER. If CREATE is non-nil it will supply
the --create-folder flag to create the folder if necessary. TAGS
should be a list of tag changes to apply to the inserted message."
- (let* ((args (append (and create (list "--create-folder"))
- (list (concat "--folder=" folder))
- tags)))
- (apply 'notmuch-call-notmuch-process
- :stdin-string (buffer-string) "insert" args)))
+ (apply 'notmuch-call-notmuch-process
+ :stdin-string (buffer-string) "insert"
+ (append (and create (list "--create-folder"))
+ (list (concat "--folder=" folder))
+ tags)))
(defun notmuch-maildir-fcc-with-notmuch-insert (fcc-header &optional create)
"Store message with notmuch insert.
or surrounding the entire folder name in double quotes.
If CREATE is non-nil then create the folder if necessary."
- (let* ((args (split-string-and-unquote fcc-header))
- (folder (car args))
- (tags (cdr args)))
+ (pcase-let ((`(,folder . ,tags)
+ (split-string-and-unquote fcc-header)))
(condition-case nil
(notmuch-maildir-notmuch-insert-current-buffer folder create tags)
;; Since there are many reasons notmuch insert could fail, e.g.,
\(r)etry, (c)reate folder, (i)gnore, or (e)dit the header? " '(?r ?c ?i ?e))))
(cl-case response
(?r (notmuch-maildir-fcc-with-notmuch-insert fcc-header))
- (?c (notmuch-maildir-fcc-with-notmuch-insert fcc-header 't))
- (?i 't)
+ (?c (notmuch-maildir-fcc-with-notmuch-insert fcc-header t))
+ (?i t)
(?e (notmuch-maildir-fcc-with-notmuch-insert
(read-from-minibuffer "Fcc header: " fcc-header)))))))))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Functions for saving a message using file fcc.
+;;; Functions for saving a message using file fcc.
(defun notmuch-maildir-fcc-host-fixer (hostname)
(replace-regexp-in-string "/\\|:"
(let* ((ftime (float-time))
(microseconds (mod (* 1000000 ftime) 1000000))
(hostname (notmuch-maildir-fcc-host-fixer (system-name))))
- (setq notmuch-maildir-fcc-count (+ notmuch-maildir-fcc-count 1))
+ (cl-incf notmuch-maildir-fcc-count)
(format "%d.%d_%d_%d.%s"
ftime
(emacs-pid)
(write-file (concat destdir "/tmp/" msg-id))
msg-id)
(t
- (error (format "Can't write to %s. Not a maildir."
- destdir))
- nil))))
+ (error "Can't write to %s. Not a maildir." destdir)))))
(defun notmuch-maildir-fcc-move-tmp-to-new (destdir msg-id)
(add-name-to-file
(defun notmuch-maildir-fcc-file-fcc (fcc-header)
"Write the message to the file specified by FCC-HEADER.
-It offers the user a chance to correct the header, or filesystem,
-if needed."
+If that fails, then offer the user a chance to correct the header
+or filesystem."
(if (notmuch-maildir-fcc-dir-is-maildir-p fcc-header)
- (notmuch-maildir-fcc-write-buffer-to-maildir fcc-header 't)
+ (notmuch-maildir-fcc-write-buffer-to-maildir fcc-header t)
;; The fcc-header is not a valid maildir see if the user wants to
;; fix it in some way.
(let* ((prompt (format "Fcc %s is not a maildir: \
(message "No permission to create %s." fcc-header)
(sit-for 2))
(notmuch-maildir-fcc-file-fcc fcc-header))
- (?i 't)
+ (?i t)
(?e (notmuch-maildir-fcc-file-fcc
(read-from-minibuffer "Fcc header: " fcc-header)))))))
(defun notmuch-maildir-fcc-write-buffer-to-maildir (destdir &optional mark-seen)
- "Writes the current buffer to maildir destdir. If mark-seen is
-non-nil, it will write it to cur/, and mark it as read. It should
-return t if successful, and nil otherwise."
+ "Write the current buffer to maildir destdir.
+
+If mark-seen is non-nil, then write it to \"cur/\", and mark it
+as read, otherwise write it to \"new/\". Return t if successful,
+and nil otherwise."
(let ((orig-buffer (buffer-name)))
(with-temp-buffer
(insert-buffer-substring orig-buffer)
(catch 'link-error
(let ((msg-id (notmuch-maildir-fcc-save-buffer-to-tmp destdir)))
(when msg-id
- (cond (mark-seen
- (condition-case err
- (notmuch-maildir-fcc-move-tmp-to-cur destdir msg-id t)
- (file-already-exists
- (throw 'link-error nil))))
- (t
- (condition-case err
- (notmuch-maildir-fcc-move-tmp-to-new destdir msg-id)
- (file-already-exists
- (throw 'link-error nil))))))
+ (condition-case nil
+ (if mark-seen
+ (notmuch-maildir-fcc-move-tmp-to-cur destdir msg-id t)
+ (notmuch-maildir-fcc-move-tmp-to-new destdir msg-id))
+ (file-already-exists
+ (throw 'link-error nil))))
(delete-file (concat destdir "/tmp/" msg-id))))
t)))
+;;; _
+
(provide 'notmuch-maildir-fcc)
;;; notmuch-maildir-fcc.el ends here
-;;; notmuch-message.el --- message-mode functions specific to notmuch
+;;; notmuch-message.el --- message-mode functions specific to notmuch -*- lexical-binding: t -*-
;;
;; Copyright © Jesse Rosenthal
;;
;;; Code:
+(require 'cl-lib)
+(require 'pcase)
+(require 'subr-x)
+
(require 'message)
(require 'notmuch-tag)
:type '(repeat string)
:group 'notmuch-send)
-(defconst notmuch-message-queued-tag-changes nil
- "List of messages and corresponding tag-changes to be applied when sending a message.
+(defvar-local notmuch-message-queued-tag-changes nil
+ "List of tag changes to be applied when sending a message.
-This variable is overridden by buffer-local versions in message
-buffers where tag changes should be triggered when sending off
-the message. Each item in this list is a list of strings, where
-the first is a notmuch query and the rest are the tag changes to
-be applied to the matching messages.")
+A list of queries and tag changes that are to be applied to them
+when the message that was composed in the current buffer is being
+send. Each item in this list is a list of strings, where the
+first is a notmuch query and the rest are the tag changes to be
+applied to the matching messages.")
(defun notmuch-message-apply-queued-tag-changes ()
;; Apply the tag changes queued in the buffer-local variable
;; notmuch-message-queued-tag-changes.
- (dolist (query-and-tags notmuch-message-queued-tag-changes)
- (notmuch-tag (car query-and-tags)
- (cdr query-and-tags))))
+ (pcase-dolist (`(,query . ,tags) notmuch-message-queued-tag-changes)
+ (notmuch-tag query tags)))
(add-hook 'message-send-hook 'notmuch-message-apply-queued-tag-changes)
-;;; notmuch-mua.el --- emacs style mail-user-agent
+;;; notmuch-mua.el --- emacs style mail-user-agent -*- lexical-binding: t -*-
;;
;; Copyright © David Edmondson
;;
;;; Code:
-(eval-when-compile (require 'cl-lib))
-
(require 'message)
(require 'mm-view)
(require 'format-spec)
(declare-function notmuch-draft-postpone "notmuch-draft" ())
(declare-function notmuch-draft-save "notmuch-draft" ())
-;;
+(defvar notmuch-show-indent-multipart)
+(defvar notmuch-show-insert-header-p-function)
+(defvar notmuch-show-max-text-part-size)
+(defvar notmuch-show-insert-text/plain-hook)
+
+;;; Options
(defcustom notmuch-mua-send-hook nil
"Hook run before sending messages."
:group 'notmuch-hooks)
(defcustom notmuch-mua-compose-in 'current-window
- (concat
- "Where to create the mail buffer used to compose a new message.
+ "Where to create the mail buffer used to compose a new message.
Possible values are `current-window' (default), `new-window' and
`new-frame'. If set to `current-window', the mail buffer will be
displayed in the current window, so the old buffer will be
window/frame that will be destroyed when the buffer is killed.
You may want to customize `message-kill-buffer-on-exit'
accordingly."
- (when (< emacs-major-version 24)
- " Due to a known bug in Emacs 23, you should not set
-this to `new-window' if `message-kill-buffer-on-exit' is
-disabled: this would result in an incorrect behavior."))
:group 'notmuch-send
:type '(choice (const :tag "Compose in the current window" current-window)
(const :tag "Compose mail in a new window" new-window)
(const :tag "Compose mail in a new frame" new-frame)))
(defcustom notmuch-mua-user-agent-function nil
- "Function used to generate a `User-Agent:' string. If this is
-`nil' then no `User-Agent:' will be generated."
+ "Function used to generate a `User-Agent:' string.
+If this is `nil' then no `User-Agent:' will be generated."
:type '(choice (const :tag "No user agent string" nil)
(const :tag "Full" notmuch-mua-user-agent-full)
(const :tag "Notmuch" notmuch-mua-user-agent-notmuch)
:group 'notmuch-send)
(defcustom notmuch-mua-hidden-headers nil
- "Headers that are added to the `message-mode' hidden headers
-list."
+ "Headers that are added to the `message-mode' hidden headers list."
+ :type '(repeat string)
+ :group 'notmuch-send)
+
+(defcustom notmuch-identities nil
+ "Identities that can be used as the From: address when composing a new message.
+
+If this variable is left unset, then a list will be constructed from the
+name and addresses configured in the notmuch configuration file."
:type '(repeat string)
:group 'notmuch-send)
+(defcustom notmuch-always-prompt-for-sender nil
+ "Always prompt for the From: address when composing or forwarding a message.
+
+This is not taken into account when replying to a message, because in that case
+the From: header is already filled in by notmuch."
+ :type 'boolean
+ :group 'notmuch-send)
+
(defgroup notmuch-reply nil
- "Replying to messages in notmuch"
+ "Replying to messages in notmuch."
:group 'notmuch)
(defcustom notmuch-mua-cite-function 'message-cite-original
- "*Function for citing an original message.
+ "Function for citing an original message.
+
Predefined functions include `message-cite-original' and
-`message-cite-original-without-signature'.
-Note that these functions use `mail-citation-hook' if that is non-nil."
+`message-cite-original-without-signature'. Note that these
+functions use `mail-citation-hook' if that is non-nil."
:type '(radio (function-item message-cite-original)
(function-item message-cite-original-without-signature)
(function-item sc-cite-original)
:type 'regexp
:group 'notmuch-send)
-;;
+;;; Various functions
(defun notmuch-mua-attachment-check ()
- "Signal an error if the message text indicates that an
-attachment is expected but no MML referencing an attachment is
-found.
+ "Signal an error an attachement is expected but missing.
+
+Signal an error if the message text indicates that an attachment
+is expected but no MML referencing an attachment is found.
Typically this is added to `notmuch-mua-send-hook'."
(when (and
(defun notmuch-mua-get-switch-function ()
"Get a switch function according to `notmuch-mua-compose-in'."
- (cond ((eq notmuch-mua-compose-in 'current-window)
- 'switch-to-buffer)
- ((eq notmuch-mua-compose-in 'new-window)
- 'switch-to-buffer-other-window)
- ((eq notmuch-mua-compose-in 'new-frame)
- 'switch-to-buffer-other-frame)
- (t (error "Invalid value for `notmuch-mua-compose-in'"))))
+ (pcase notmuch-mua-compose-in
+ ('current-window 'switch-to-buffer)
+ ('new-window 'switch-to-buffer-other-window)
+ ('new-frame 'switch-to-buffer-other-frame)
+ (_ (error "Invalid value for `notmuch-mua-compose-in'"))))
(defun notmuch-mua-maybe-set-window-dedicated ()
- "Set the selected window as dedicated according to
-`notmuch-mua-compose-in'."
+ "Set the selected window as dedicated according to `notmuch-mua-compose-in'."
(when (or (eq notmuch-mua-compose-in 'new-frame)
(eq notmuch-mua-compose-in 'new-window))
(set-window-dedicated-p (selected-window) t)))
(defun notmuch-mua-reply-crypto (parts)
"Add mml sign-encrypt flag if any part of original message is encrypted."
(cl-loop for part in parts
- if (notmuch-match-content-type (plist-get part :content-type)
- "multipart/encrypted")
+ for type = (plist-get part :content-type)
+ if (notmuch-match-content-type type "multipart/encrypted")
do (mml-secure-message-sign-encrypt)
- else if (notmuch-match-content-type (plist-get part :content-type)
- "multipart/*")
+ else if (notmuch-match-content-type type "multipart/*")
do (notmuch-mua-reply-crypto (plist-get part :content))))
-;; There is a bug in emacs 23's message.el that results in a newline
+;; There is a bug in Emacs' message.el that results in a newline
;; not being inserted after the References header, so the next header
;; is concatenated to the end of it. This function fixes the problem,
;; while guarding against the possibility that some current or future
(funcall original-func header references)
(unless (bolp) (insert "\n")))
+;;; Mua reply
+
(defun notmuch-mua-reply (query-string &optional sender reply-all)
(let ((args '("reply" "--format=sexp" "--format-version=4"))
(process-crypto notmuch-show-process-crypto)
;; Create a buffer-local queue for tag changes triggered when
;; sending the reply.
(when notmuch-message-replied-tags
- (setq-local notmuch-message-queued-tag-changes
- (list (cons query-string notmuch-message-replied-tags))))
+ (setq notmuch-message-queued-tag-changes
+ (list (cons query-string notmuch-message-replied-tags))))
;; Insert the message body - but put it in front of the signature
;; if one is present, and after any other content
;; message*setup-hooks may have added to the message body already.
(message-goto-body)
(set-buffer-modified-p nil))
+;;; Mode and keymap
+
+(defvar notmuch-message-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-c C-c") #'notmuch-mua-send-and-exit)
+ (define-key map (kbd "C-c C-s") #'notmuch-mua-send)
+ (define-key map (kbd "C-c C-p") #'notmuch-draft-postpone)
+ (define-key map (kbd "C-x C-s") #'notmuch-draft-save)
+ map)
+ "Keymap for `notmuch-message-mode'.")
+
(define-derived-mode notmuch-message-mode message-mode "Message[Notmuch]"
"Notmuch message composition mode. Mostly like `message-mode'."
(notmuch-address-setup))
(put 'notmuch-message-mode 'flyspell-mode-predicate 'mail-mode-flyspell-verify)
-(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)
+;;; New messages
(defun notmuch-mua-pop-to-buffer (name switch-function)
- "Pop to buffer NAME, and warn if it already exists and is
-modified. This function is notmuch adaptation of
-`message-pop-to-buffer'."
+ "Pop to buffer NAME, and warn if it already exists and is modified.
+Like `message-pop-to-buffer' but enable `notmuch-message-mode'
+instead of `message-mode' and SWITCH-FUNCTION is mandatory."
(let ((buffer (get-buffer name)))
(if (and buffer
(buffer-name buffer))
(select-window window))
(funcall switch-function buffer)
(set-buffer buffer))
- (when (and (buffer-modified-p)
- (not (prog1
- (y-or-n-p
- "Message already being composed; erase? ")
- (message nil))))
- (error "Message being composed")))
+ (when (buffer-modified-p)
+ (if (y-or-n-p "Message already being composed; erase? ")
+ (message nil)
+ (error "Message being composed"))))
(funcall switch-function name)
(set-buffer name))
(erase-buffer)
(notmuch-message-mode)))
-(defun notmuch-mua-mail (&optional to subject other-headers continue
+(defun notmuch-mua-mail (&optional to subject other-headers _continue
switch-function yank-action send-actions
return-action &rest ignored)
"Invoke the notmuch mail composition window."
(interactive)
(when notmuch-mua-user-agent-function
(let ((user-agent (funcall notmuch-mua-user-agent-function)))
- (unless (string= "" user-agent)
+ (unless (string-empty-p user-agent)
(push (cons 'User-Agent user-agent) other-headers))))
(unless (assq 'From other-headers)
(push (cons 'From (message-make-from
(dolist (h other-headers other-headers)
(when (stringp (car h))
(setcar h (intern (capitalize (car h))))))))
- (args (list yank-action send-actions))
;; Cause `message-setup-1' to do things relevant for mail,
;; such as observe `message-default-mail-headers'.
(message-this-is-mail t))
- ;; message-setup-1 in Emacs 23 does not accept return-action
- ;; argument. Pass it only if it is supplied by the caller. This
- ;; will never be the case when we're called by `compose-mail' in
- ;; Emacs 23.
- (when return-action (nconc args '(return-action)))
- (apply 'message-setup-1 headers args))
+ (message-setup-1 headers yank-action send-actions return-action))
(notmuch-fcc-header-setup)
(message-sort-headers)
(message-hide-headers)
(notmuch-mua-maybe-set-window-dedicated)
(message-goto-to))
-(defcustom notmuch-identities nil
- "Identities that can be used as the From: address when composing a new message.
-
-If this variable is left unset, then a list will be constructed from the
-name and addresses configured in the notmuch configuration file."
- :type '(repeat string)
- :group 'notmuch-send)
-
-(defcustom notmuch-always-prompt-for-sender nil
- "Always prompt for the From: address when composing or forwarding a message.
-
-This is not taken into account when replying to a message, because in that case
-the From: header is already filled in by notmuch."
- :type 'boolean
- :group 'notmuch-send)
-
(defvar notmuch-mua-sender-history nil)
(defun notmuch-mua-prompt-for-sender ()
"Prompt for a sender from the user's configured identities."
(if notmuch-identities
- (ido-completing-read "Send mail from: " notmuch-identities
- nil nil nil 'notmuch-mua-sender-history
- (car notmuch-identities))
+ (completing-read "Send mail from: " notmuch-identities
+ nil nil nil 'notmuch-mua-sender-history
+ (car notmuch-identities))
(let* ((name (notmuch-user-name))
(addrs (cons (notmuch-user-primary-email)
(notmuch-user-other-email)))
(address
- (ido-completing-read (concat "Sender address for " name ": ") addrs
- nil nil nil 'notmuch-mua-sender-history
- (car addrs))))
+ (completing-read (concat "Sender address for " name ": ") addrs
+ nil nil nil 'notmuch-mua-sender-history
+ (car addrs))))
(message-make-from name address))))
(put 'notmuch-mua-new-mail 'notmuch-prefix-doc "... and prompt for sender")
;; Create a buffer-local queue for tag changes triggered when
;; sending the message.
(when notmuch-message-forwarded-tags
- (setq-local notmuch-message-queued-tag-changes
- (cl-loop for id in forward-queries
- collect
- (cons id notmuch-message-forwarded-tags))))
+ (setq notmuch-message-queued-tag-changes
+ (cl-loop for id in forward-queries
+ collect
+ (cons id notmuch-message-forwarded-tags))))
;; `message-forward-make-body' shows the User-agent header. Hide
;; it again.
(message-hide-headers)
If PROMPT-FOR-SENDER is non-nil, the user will be prompted for
the From: address first. If REPLY-ALL is non-nil, the message
will be addressed to all recipients of the source message."
- ;; In current emacs (24.3) select-active-regions is set to t by
- ;; default. The reply insertion code sets the region to the quoted
- ;; message to make it easy to delete (kill-region or C-w). These two
- ;; things combine to put the quoted message in the primary selection.
+ ;; `select-active-regions' is t by default. The reply insertion code
+ ;; sets the region to the quoted message to make it easy to delete
+ ;; (kill-region or C-w). These two things combine to put the quoted
+ ;; message in the primary selection.
;;
;; This is not what the user wanted and is a privacy risk (accidental
;; pasting of the quoted message). We can avoid some of the problems
(notmuch-mua-reply query-string sender reply-all)
(deactivate-mark)))
+;;; Checks
+
(defun notmuch-mua-check-no-misplaced-secure-tag ()
"Query user if there is a misplaced secure mml tag.
(goto-char (point-max))
(or
;; We are always fine if there is no secure tag.
- (not (search-backward "<#secure" nil 't))
+ (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)))
+ (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
newline. It is likely that the message will be sent unsigned and
unencrypted. Really send? "))))
+;;; Finishing commands
+
(defun notmuch-mua-send-common (arg &optional exit)
(interactive "P")
(run-hooks 'notmuch-mua-send-hook)
(defun notmuch-mua-send-and-exit (&optional arg)
(interactive "P")
- (notmuch-mua-send-common arg 't))
+ (notmuch-mua-send-common arg t))
(defun notmuch-mua-send (&optional arg)
(interactive "P")
(interactive)
(message-kill-buffer))
-;;
+;;; _
(define-mail-user-agent 'notmuch-user-agent
- 'notmuch-mua-mail 'notmuch-mua-send-and-exit
- 'notmuch-mua-kill-buffer 'notmuch-mua-send-hook)
+ 'notmuch-mua-mail
+ 'notmuch-mua-send-and-exit
+ 'notmuch-mua-kill-buffer
+ 'notmuch-mua-send-hook)
;; Add some more headers to the list that `message-mode' hides when
;; composing a message.
(notmuch-mua-add-more-hidden-headers)
-;;
-
(provide 'notmuch-mua)
;;; notmuch-mua.el ends here
-;;; notmuch-parser.el --- streaming S-expression parser
+;;; notmuch-parser.el --- streaming S-expression parser -*- lexical-binding: t -*-
;;
;; Copyright © Austin Clements
;;
;;; Code:
-(eval-when-compile (require 'cl-lib))
+(require 'cl-lib)
+(require 'pcase)
+(require 'subr-x)
(defun notmuch-sexp-create-parser ()
"Return a new streaming S-expression parser.
(forward-char)
(signal 'invalid-read-syntax (list (string (char-before)))))))
-(defun notmuch-sexp-eof (sp)
- "Signal an error if there is more data in SP's buffer.
-
-Moves point to the beginning of any trailing data or to the end
-of the buffer if there is only trailing whitespace."
- (skip-chars-forward " \n\r\t")
- (unless (eobp)
- (error "Trailing garbage following expression")))
-
(defvar notmuch-sexp--parser nil
"The buffer-local notmuch-sexp-parser instance.
move point in the input buffer."
;; Set up the initial state
(unless (local-variable-p 'notmuch-sexp--parser)
- (set (make-local-variable 'notmuch-sexp--parser)
- (notmuch-sexp-create-parser))
- (set (make-local-variable 'notmuch-sexp--state) 'begin))
+ (setq-local notmuch-sexp--parser (notmuch-sexp-create-parser))
+ (setq-local notmuch-sexp--state 'begin))
(let (done)
(while (not done)
(cl-case notmuch-sexp--state
(t (with-current-buffer result-buffer
(funcall result-function result))))))
(end
- ;; Any trailing data is unexpected
- (notmuch-sexp-eof notmuch-sexp--parser)
+ ;; Skip over trailing whitespace.
+ (skip-chars-forward " \n\r\t")
+ ;; Any trailing data is unexpected.
+ (unless (eobp)
+ (error "Trailing garbage following expression"))
(setq done t)))))
;; Clear out what we've parsed
(delete-region (point-min) (point)))
-;;; notmuch-print.el --- printing messages from notmuch
+;;; notmuch-print.el --- printing messages from notmuch -*- lexical-binding: t -*-
;;
;; Copyright © David Edmondson
;;
(declare-function notmuch-show-get-prop "notmuch-show" (prop &optional props))
+;;; Options
+
(defcustom notmuch-print-mechanism 'notmuch-print-lpr
"How should printing be done?"
:group 'notmuch-show
(function :tag "Use muttprint then evince" notmuch-print-muttprint/evince)
(function :tag "Using a custom function")))
-;; Utility functions:
+;;; Utility functions
(defun notmuch-print-run-evince (file)
"View FILE using 'evince'."
"--printed-headers" "Date_To_From_CC_Newsgroups_*Subject*_/Tags/"
output))
-;; User-visible functions:
+;;; User-visible functions
-(defun notmuch-print-lpr (msg)
+(defun notmuch-print-lpr (_msg)
"Print a message buffer using lpr."
(lpr-buffer))
(ps-print-buffer ps-file)
(notmuch-print-run-evince ps-file)))
-(defun notmuch-print-muttprint (msg)
+(defun notmuch-print-muttprint (_msg)
"Print a message using muttprint."
(notmuch-print-run-muttprint))
-(defun notmuch-print-muttprint/evince (msg)
+(defun notmuch-print-muttprint/evince (_msg)
"Preview a message buffer using muttprint and evince."
(let ((ps-file (make-temp-file "notmuch" nil ".ps")))
(notmuch-print-run-muttprint (list "--printer" (concat "TO_FILE:" ps-file)))
(set-buffer-modified-p nil)
(funcall notmuch-print-mechanism msg))
+;;; _
+
(provide 'notmuch-print)
;;; notmuch-print.el ends here
-;;; notmuch-query.el --- provide an emacs api to query notmuch
+;;; notmuch-query.el --- provide an emacs api to query notmuch -*- lexical-binding: t -*-
;;
;; Copyright © David Bremner
;;
(require 'notmuch-lib)
+;;; Basic query function
+
(defun notmuch-query-get-threads (search-terms)
"Return a list of threads of messages matching SEARCH-TERMS.
(setq args (append args search-terms))
(apply #'notmuch-call-notmuch-sexp args)))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Mapping functions across collections of messages.
+;;; Mapping functions across collections of messages
(defun notmuch-query-map-aux (mapper function seq)
"Private function to do the actual mapping and flattening."
- (apply 'append
- (mapcar
- (lambda (tree)
- (funcall mapper function tree))
- seq)))
+ (cl-mapcan (lambda (tree)
+ (funcall mapper function tree))
+ seq))
(defun notmuch-query-map-threads (fn threads)
"Apply function FN to every thread in THREADS.
"Apply function FN to every message in TREE.
Flatten results to a list. See the function
`notmuch-query-get-threads' for more information."
- (cons (funcall fn (car tree)) (notmuch-query-map-forest fn (cadr tree))))
+ (cons (funcall fn (car tree))
+ (notmuch-query-map-forest fn (cadr tree))))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Predefined queries
+;;; Predefined queries
(defun notmuch-query-get-message-ids (&rest search-terms)
"Return a list of message-ids of messages that match SEARCH-TERMS."
-;;; notmuch-show.el --- displaying notmuch forests
+;;; notmuch-show.el --- displaying notmuch forests -*- lexical-binding: t -*-
;;
;; Copyright © Carl Worth
;; Copyright © David Edmondson
;;; Code:
-(eval-when-compile
- (require 'cl-lib)
- (require 'pcase))
-
(require 'mm-view)
(require 'message)
(require 'mm-decode)
(declare-function notmuch-read-query "notmuch" (prompt))
(declare-function notmuch-draft-resume "notmuch-draft" (id))
+(defvar shr-blocked-images)
+(defvar gnus-blocked-images)
+(defvar shr-content-function)
+
+;;; Options
+
(defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
"Headers that should be shown in a message, in this order.
:type '(choice (const nil) regexp)
:group 'notmuch-show)
-(defvar notmuch-show-thread-id nil)
-(make-variable-buffer-local 'notmuch-show-thread-id)
+;;; Variables
+
+(defvar-local notmuch-show-thread-id nil)
-(defvar notmuch-show-parent-buffer nil)
-(make-variable-buffer-local 'notmuch-show-parent-buffer)
+(defvar-local notmuch-show-parent-buffer nil)
-(defvar notmuch-show-query-context nil)
-(make-variable-buffer-local 'notmuch-show-query-context)
+(defvar-local notmuch-show-query-context nil)
-(defvar notmuch-show-process-crypto nil)
-(make-variable-buffer-local 'notmuch-show-process-crypto)
+(defvar-local notmuch-show-process-crypto nil)
-(defvar notmuch-show-elide-non-matching-messages nil)
-(make-variable-buffer-local 'notmuch-show-elide-non-matching-messages)
+(defvar-local notmuch-show-elide-non-matching-messages nil)
-(defvar notmuch-show-indent-content t)
-(make-variable-buffer-local 'notmuch-show-indent-content)
+(defvar-local notmuch-show-indent-content t)
(defvar notmuch-show-attachment-debug nil
"If t log stdout and stderr from attachment handlers.
When set to nil (the default) stdout and stderr from attachment
handlers is discarded. When set to t the stdout and stderr from
each attachment handler is logged in buffers with names beginning
-\" *notmuch-part*\". This option requires emacs version at least
-24.3 to work.")
+\" *notmuch-part*\".")
+
+;;; Options
(defcustom notmuch-show-stash-mlarchive-link-alist
'(("Gmane" . "https://mid.gmane.org/")
:type 'boolean
:group 'notmuch-show)
+;;; Utilities
+
(defmacro with-current-notmuch-show-message (&rest body)
"Evaluate body with current buffer set to the text of current message."
`(save-excursion
"Enable Visual Line mode."
(visual-line-mode t))
+;;; Commands
+
;; DEPRECATED in Notmuch 0.16 since we now have convenient part
;; commands. We'll keep the command around for a version or two in
;; case people want to bind it themselves.
(header (concat
"Subject: " subject "\n"
"To: " to "\n"
- (if (not (string= cc ""))
+ (if (not (string-empty-p cc))
(concat "Cc: " cc "\n")
"")
"From: " from "\n"
(interactive)
(notmuch-show-with-message-as-text 'notmuch-print-message))
+;;; Headers
+
(defun notmuch-show-fontify-header ()
(let ((face (cond
((looking-at "[Tt]o:")
(narrow-to-region start (point-max))
(run-hooks 'notmuch-show-markup-headers-hook)))))
+;;; Parts
+
(define-button-type 'notmuch-show-part-button-type
'action 'notmuch-show-part-button-default
'follow-link t
'face 'message-mml
:supertype 'notmuch-button-type)
-(defun notmuch-show-insert-part-header (nth content-type declared-type
+(defun notmuch-show-insert-part-header (_nth content-type declared-type
&optional name comment)
(let ((base-label (concat (and name (concat name ": "))
declared-type
(overlay-put overlay 'invisible (not show))
t)))))))
-;; Part content ID handling
+;;; Part content ID handling
(defvar notmuch-show--cids nil
"Alist from raw content ID to (MSG PART).")
;; alternative (even if we can't render it).
(push (list content-id msg part) notmuch-show--cids)))
;; Recurse on sub-parts
- (let ((ctype (notmuch-split-content-type
- (downcase (plist-get part :content-type)))))
- (cond ((equal (car ctype) "multipart")
- (mapc (apply-partially #'notmuch-show--register-cids msg)
- (plist-get part :content)))
- ((equal ctype '("message" "rfc822"))
- (notmuch-show--register-cids
- msg
- (car (plist-get (car (plist-get part :content)) :body)))))))
+ (when-let ((type (plist-get part :content-type)))
+ (pcase-let ((`(,type ,subtype)
+ (split-string (downcase type) "/")))
+ (cond ((equal type "multipart")
+ (mapc (apply-partially #'notmuch-show--register-cids msg)
+ (plist-get part :content)))
+ ((and (equal type "message")
+ (equal subtype "rfc822"))
+ (notmuch-show--register-cids
+ msg
+ (car (plist-get (car (plist-get part :content)) :body))))))))
(defun notmuch-show--get-cid-content (cid)
"Return a list (CID-content content-type) or nil.
into the current buffer. CID must be a raw content ID, without
enclosing angle brackets, a cid: prefix, or URL encoding. This
will return nil if the CID is unknown or cannot be retrieved."
- (let ((descriptor (cdr (assoc cid notmuch-show--cids))))
- (when descriptor
- (let* ((msg (car descriptor))
- (part (cadr descriptor))
- ;; Request caching for this content, as some messages
- ;; reference the same cid: part many times (hundreds!).
- (content (notmuch-get-bodypart-binary
- msg part notmuch-show-process-crypto 'cache))
- (content-type (plist-get part :content-type)))
- (list content content-type)))))
+ (when-let ((descriptor (cdr (assoc cid notmuch-show--cids))))
+ (pcase-let ((`(,msg ,part) descriptor))
+ ;; Request caching for this content, as some messages
+ ;; reference the same cid: part many times (hundreds!).
+ (list (notmuch-get-bodypart-binary
+ msg part notmuch-show-process-crypto 'cache)
+ (plist-get part :content-type)))))
(defun notmuch-show-setup-w3m ()
"Instruct w3m how to retrieve content from a \"related\" part of a message."
(setq mm-html-inhibit-images nil))
(defvar w3m-current-buffer) ;; From `w3m.el'.
-(defun notmuch-show--cid-w3m-retrieve (url &rest args)
+(defun notmuch-show--cid-w3m-retrieve (url &rest _args)
;; url includes the cid: prefix and is URL encoded (see RFC 2392).
(let* ((cid (url-unhex-string (substring url 4)))
(content-and-type
(mapcar (lambda (inner-part) (plist-get inner-part :content-type))
(plist-get part :content)))
-(defun notmuch-show-insert-part-multipart/alternative (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-multipart/alternative (msg part _content-type _nth depth _button)
(let ((chosen-type (car (notmuch-multipart/alternative-choose
msg (notmuch-show-multipart/*-to-list part))))
(inner-parts (plist-get part :content))
(indent-rigidly start (point) 1)))
t)
-(defun notmuch-show-insert-part-multipart/related (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-multipart/related (msg part _content-type _nth depth _button)
(let ((inner-parts (plist-get part :content))
(start (point)))
;; Render the primary part. FIXME: Support RFC 2387 Start header.
(indent-rigidly start (point) 1)))
t)
-(defun notmuch-show-insert-part-multipart/signed (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-multipart/signed (msg part _content-type _nth depth button)
(when button
(button-put button 'face 'notmuch-crypto-part-header))
;; Insert a button detailing the signature status.
(indent-rigidly start (point) 1)))
t)
-(defun notmuch-show-insert-part-multipart/encrypted (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-multipart/encrypted (msg part _content-type _nth depth button)
(when button
(button-put button 'face 'notmuch-crypto-part-header))
;; Insert a button detailing the encryption status.
(indent-rigidly start (point) 1)))
t)
-(defun notmuch-show-insert-part-application/pgp-encrypted (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-application/pgp-encrypted (_msg _part _content-type _nth _depth _button)
t)
-(defun notmuch-show-insert-part-multipart/* (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-multipart/* (msg part _content-type _nth depth _button)
(let ((inner-parts (plist-get part :content))
(start (point)))
;; Show all of the parts.
(indent-rigidly start (point) 1)))
t)
-(defun notmuch-show-insert-part-message/rfc822 (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-message/rfc822 (msg part _content-type _nth depth _button)
(let* ((message (car (plist-get part :content)))
(body (car (plist-get message :body)))
(start (point)))
(indent-rigidly start (point) 1)))
t)
-(defun notmuch-show-insert-part-text/plain (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-text/plain (msg part _content-type _nth depth button)
;; For backward compatibility we want to apply the text/plain hook
;; to the whole of the part including the part button if there is
;; one.
(run-hook-with-args 'notmuch-show-insert-text/plain-hook msg depth))))
t)
-(defun notmuch-show-insert-part-text/calendar (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-text/calendar (msg part _content-type _nth _depth _button)
(insert (with-temp-buffer
(insert (notmuch-get-bodypart-text msg part notmuch-show-process-crypto))
;; notmuch-get-bodypart-text does no newline conversion.
t)
;; For backwards compatibility.
-(defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth button)
- (notmuch-show-insert-part-text/calendar msg part content-type nth depth button))
+(defun notmuch-show-insert-part-text/x-vcalendar (msg part _content-type _nth depth _button)
+ (notmuch-show-insert-part-text/calendar msg part nil nil depth nil))
(when (version< emacs-version "25.3")
;; https://bugs.gnu.org/28350
;; the first time).
(require 'enriched)
(cl-letf (((symbol-function 'enriched-decode-display-prop)
- (lambda (start end &optional param) (list start end))))
+ (lambda (start end &optional _param) (list start end))))
(notmuch-show-insert-part-*/* msg part content-type nth depth button))))
(defun notmuch-show-get-mime-type-of-application/octet-stream (part)
(gnus-blocked-images notmuch-show-text/html-blocked-images))
(notmuch-show-insert-part-*/* msg part content-type nth depth button))))
-;; These functions are used by notmuch-show--insert-part-text/html-shr
+;;; Functions used by notmuch-show--insert-part-text/html-shr
+
(declare-function libxml-parse-html-region "xml.c")
(declare-function shr-insert-document "shr")
(shr-insert-document dom)
t))
-(defun notmuch-show-insert-part-*/* (msg part content-type nth depth button)
+(defun notmuch-show-insert-part-*/* (msg part content-type _nth _depth _button)
;; This handler _must_ succeed - it is the handler of last resort.
(notmuch-mm-display-part-inline msg part content-type notmuch-show-process-crypto)
t)
-;; Functions for determining how to handle MIME parts.
+;;; Functions for determining how to handle MIME parts.
(defun notmuch-show-handlers-for (content-type)
"Return a list of content handlers for a part of type CONTENT-TYPE."
(push func result)))
;; Reverse order of prefrence.
(list (intern (concat "notmuch-show-insert-part-*/*"))
- (intern (concat
- "notmuch-show-insert-part-"
- (car (notmuch-split-content-type content-type))
- "/*"))
+ (intern (concat "notmuch-show-insert-part-"
+ (car (split-string content-type "/"))
+ "/*"))
(intern (concat "notmuch-show-insert-part-" content-type))))
result))
-;; \f
+;;; Parts
(defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth button)
;; Run the handlers until one of them succeeds.
(defun notmuch-show-mime-type (part)
"Return the correct mime-type to use for PART."
- (let ((content-type (downcase (plist-get part :content-type))))
+ (when-let ((content-type (plist-get part :content-type)))
+ (setq content-type (downcase content-type))
(or (and (string= content-type "application/octet-stream")
(notmuch-show-get-mime-type-of-application/octet-stream part))
(and (string= content-type "inline patch")
should return non-NIL if a header button should be inserted for
this part.")
-(defun notmuch-show-insert-header-p (part hide)
+(defun notmuch-show-insert-header-p (part _hide)
;; Show all part buttons except for the first part if it is text/plain.
(let ((mime-type (notmuch-show-mime-type part)))
(not (and (string= mime-type "text/plain")
(<= (plist-get part :id) 1)))))
-(defun notmuch-show-reply-insert-header-p-never (part hide)
+(defun notmuch-show-reply-insert-header-p-never (_part _hide)
nil)
(defun notmuch-show-reply-insert-header-p-trimmed (part hide)
HIDE determines whether to show or hide the part and the button
as follows: If HIDE is nil, show the part and the button. If HIDE
is t, hide the part initially and show the button."
- (let* ((content-type (downcase (plist-get part :content-type)))
+ (let* ((content-type (plist-get part :content-type))
(mime-type (notmuch-show-mime-type part))
(nth (plist-get part :id))
(long (and (notmuch-match-content-type mime-type "text/*")
;; the first (or only) part if this is text/plain.
(button (and (funcall notmuch-show-insert-header-p-function part hide)
(notmuch-show-insert-part-header
- nth mime-type content-type
+ nth mime-type
+ (and content-type (downcase content-type))
(plist-get part :filename))))
;; Hide the part initially if HIDE is t, or if it is too long
;; and we have a button to allow toggling.
(notmuch-show-message-visible msg (and (plist-get msg :match)
(not (plist-get msg :excluded))))))
+;;; Toggle commands
+
(defun notmuch-show-toggle-process-crypto ()
"Toggle the processing of cryptographic MIME parts."
(interactive)
"Content is not indented."))
(notmuch-show-refresh-view))
+;;; Main insert functions
+
(defun notmuch-show-insert-tree (tree depth)
"Insert the message tree TREE at depth DEPTH in the current thread."
(let ((msg (car tree))
"Insert the forest of threads FOREST."
(mapc (lambda (thread) (notmuch-show-insert-thread thread 0)) forest))
+;;; Link buttons
+
(defvar notmuch-id-regexp
(concat
;; Match the id: prefix only if it begins a word (to disallow, for
'help-echo "Mouse-1, RET: search for this message"
'face goto-address-mail-face)))))
+;;; Show command
+
;;;###autoload
(defun notmuch-show (thread-id &optional elide-toggle parent-buffer query-context buffer-name)
"Run \"notmuch show\" with the given thread ID and display results.
;; Report back to the caller whether any messages matched.
forest))
+;;; Refresh command
+
(defun notmuch-show-capture-state ()
"Capture the state of the current buffer.
(ding)
(message "Refreshing the buffer resulted in no messages!"))))
+;;; Keymaps
+
(defvar notmuch-show-stash-map
(let ((map (make-sparse-keymap)))
(define-key map "c" 'notmuch-show-stash-cc)
(define-key map "B" 'notmuch-show-browse-urls)
map)
"Keymap for \"notmuch show\" buffers.")
-(fset 'notmuch-show-mode-map notmuch-show-mode-map)
+
+;;; Mode
(define-derived-mode notmuch-show-mode fundamental-mode "notmuch-show"
"Major mode for viewing a thread with notmuch.
(setq imenu-extract-index-name-function
#'notmuch-show-imenu-extract-index-name-function))
+;;; Tree commands
+
(defun notmuch-tree-from-show-current-query ()
"Call notmuch tree with the current query."
(interactive)
notmuch-show-query-context
(notmuch-show-get-message-id)))
+;;; Movement related functions.
+
(defun notmuch-show-move-to-message-top ()
(goto-char (notmuch-show-message-top)))
(defun notmuch-show-move-to-message-bottom ()
(goto-char (notmuch-show-message-bottom)))
-(defun notmuch-show-message-adjust ()
- (recenter 0))
-
-;; Movement related functions.
-
;; There's some strangeness here where a text property applied to a
;; region a->b is not found when point is at b. We walk backwards
;; until finding the property.
(cl-loop do (funcall function)
while (notmuch-show-goto-message-next))))
-;; Functions relating to the visibility of messages and their
-;; components.
+;;; Functions relating to the visibility of messages and their components.
(defun notmuch-show-message-visible (props visible-p)
(overlay-put (plist-get props :message-overlay) 'invisible (not visible-p))
(overlay-put (plist-get props :headers-overlay) 'invisible (not visible-p))
(notmuch-show-set-prop :headers-visible visible-p props))
-;; Functions for setting and getting attributes of the current
-;; message.
+;;; Functions for setting and getting attributes of the current message.
(defun notmuch-show-set-message-properties (props)
(save-excursion
message in either tree or show. This means that several utility
functions in notmuch-show can be used directly by notmuch-tree as
they just need the correct message properties."
- (let ((props (or props
- (cond ((eq major-mode 'notmuch-show-mode)
- (notmuch-show-get-message-properties))
- ((eq major-mode 'notmuch-tree-mode)
- (notmuch-tree-get-message-properties))
- (t nil)))))
- (plist-get props prop)))
+ (plist-get (or props
+ (cond ((eq major-mode 'notmuch-show-mode)
+ (notmuch-show-get-message-properties))
+ ((eq major-mode 'notmuch-tree-mode)
+ (notmuch-tree-get-message-properties))
+ (t nil)))
+ prop))
(defun notmuch-show-get-message-id (&optional bare)
"Return an id: query for the Message-Id of the current message.
(apply 'notmuch-show-tag-message
(notmuch-tag-change-list notmuch-show-mark-read-tags unread))))
-(defun notmuch-show-seen-current-message (start end)
+(defun notmuch-show-seen-current-message (_start _end)
"Mark the current message read if it is open.
We only mark it read once: if it is changed back then that is a
;; We need to redisplay to get window-start and window-end correct.
(redisplay)
(save-excursion
- (condition-case err
+ (condition-case nil
(funcall notmuch-show-mark-read-function (window-start) (window-end))
((debug error)
(unless notmuch-show--seen-has-errored
- (setq notmuch-show--seen-has-errored 't)
+ (setq notmuch-show--seen-has-errored t)
(setq header-line-format
(concat header-line-format
(propertize
Reshows the current thread with matches defined by the new query-string."
(interactive (list (notmuch-read-query "Filter thread: ")))
(let ((msg-id (notmuch-show-get-message-id)))
- (setq notmuch-show-query-context (if (string= query "") nil query))
+ (setq notmuch-show-query-context (if (string-empty-p query) nil query))
(notmuch-show-refresh-view t)
(notmuch-show-goto-message msg-id)))
-;; Functions for getting attributes of several messages in the current
-;; thread.
+;;; Functions for getting attributes of several messages in the current thread.
(defun notmuch-show-get-message-ids-for-open-messages ()
"Return a list of all id: queries for open messages in the current thread."
(setq done (not (notmuch-show-goto-message-next))))
message-ids)))
-;; Commands typically bound to keys.
+;;; Commands typically bound to keys.
(defun notmuch-show-advance ()
"Advance through thread.
(message-resend addresses)
(notmuch-bury-or-kill-this-buffer)))
+(defun notmuch-show-message-adjust ()
+ (recenter 0))
+
(defun notmuch-show-next-message (&optional pop-at-end)
"Show the next message.
(browse-url (current-kill 0 t)))
(defun notmuch-show-stash-git-helper (addresses prefix)
- "Escape, trim, quote, and add PREFIX to each address in list of ADDRESSES, and return the result as a single string."
+ "Normalize all ADDRESSES while adding PREFIX.
+Escape, trim, quote and add PREFIX to each address in list
+of ADDRESSES, and return the result as a single string."
(mapconcat (lambda (x)
(concat prefix "\""
;; escape double-quotes
addresses " "))
(put 'notmuch-show-stash-git-send-email 'notmuch-prefix-doc
- "Copy From/To/Cc of current message to kill-ring in a form suitable for pasting to git send-email command line.")
+ "Copy From/To/Cc of current message to kill-ring.
+Use a form suitable for pasting to git send-email command line.")
(defun notmuch-show-stash-git-send-email (&optional no-in-reply-to)
- "Copy From/To/Cc/Message-Id of current message to kill-ring in a form suitable for pasting to git send-email command line.
+ "Copy From/To/Cc/Message-Id of current message to kill-ring.
+Use a form suitable for pasting to git send-email command line.
If invoked with a prefix argument (or NO-IN-REPLY-TO is non-nil),
omit --in-reply-to=<Message-Id>."
(list (notmuch-show-get-message-id t)) "--in-reply-to="))))
" ")))
-;; Interactive part functions and their helpers
+;;; Interactive part functions and their helpers
(defun notmuch-show-generate-part-buffer (msg part)
"Return a temporary buffer containing the specified part's content."
is destroyed when FN returns. If MIME-TYPE is given then force
part to be treated as if it had that mime-type."
(let ((handle (notmuch-show-current-part-handle mime-type)))
- ;; emacs 24.3+ puts stdout/stderr into the calling buffer so we
- ;; call it from a temp-buffer, unless
- ;; notmuch-show-attachment-debug is non-nil in which case we put
- ;; it in " *notmuch-part*".
+ ;; Emacs puts stdout/stderr into the calling buffer so we call
+ ;; it from a temp-buffer, unless notmuch-show-attachment-debug
+ ;; is non-nil, in which case we put it in " *notmuch-part*".
(unwind-protect
(if notmuch-show-attachment-debug
(with-current-buffer (generate-new-buffer " *notmuch-part*")
(funcall fn (completing-read prompt urls nil nil nil nil (car urls)))
(message "No URLs found."))))
+;;; _
+
(provide 'notmuch-show)
;;; notmuch-show.el ends here
-;;; notmuch-tag.el --- tag messages within emacs
+;;; notmuch-tag.el --- tag messages within emacs -*- lexical-binding: t -*-
;;
;; Copyright © Damien Cassou
;; Copyright © Carl Worth
;;; Code:
-(require 'cl-lib)
-(eval-when-compile
- (require 'pcase))
-
(require 'crm)
(require 'notmuch-lib)
(declare-function notmuch-tree-tag "notmuch-tree" (tag-changes))
(declare-function notmuch-jump "notmuch-jump" (action-map prompt))
+;;; Keys
+
(define-widget 'notmuch-tag-key-type 'list
"A single key tagging binding."
:format "%v"
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.
+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
+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."
:type '(repeat notmuch-tag-key-type)
:group 'notmuch-tag)
+;;; Faces and Formats
+
(define-widget 'notmuch-tag-format-type 'lazy
"Customize widget for notmuch-tag-format and friends."
:type '(alist :key-type (regexp :tag "Tag")
'((t :foreground "red"))
"Default face used for the unread tag.
-Used in the default value of `notmuch-tag-formats`."
+Used in the default value of `notmuch-tag-formats'."
:group 'notmuch-faces)
(defface notmuch-tag-flagged
(:foreground "blue")))
"Face used for the flagged tag.
-Used in the default value of `notmuch-tag-formats`."
+Used in the default value of `notmuch-tag-formats'."
:group 'notmuch-faces)
(defcustom notmuch-tag-formats
(notmuch-tag-format-image-data tag (notmuch-tag-star-icon))))
"Custom formats for individual tags.
-This is an association list that maps from tag name regexps to
-lists of formatting expressions. The first entry whose car
-regexp-matches a tag will be used to format that tag. The regexp
-is implicitly anchored, so to match a literal tag name, just use
-that tag name (if it contains special regexp characters like
-\".\" or \"*\", these have to be escaped). The cdr of the
-matching entry gives a list of Elisp expressions that modify the
-tag. If the list is empty, the tag will simply be hidden.
-Otherwise, each expression will be evaluated in order: for the
-first expression, the variable `tag' will be bound to the tag
-name; for each later expression, the variable `tag' will be bound
-to the result of the previous expression. In this way, each
+This is an association list of the form ((MATCH EXPR...)...),
+mapping tag name regexps to lists of formatting expressions.
+
+The first entry whose MATCH regexp-matches a tag is used to
+format that tag. The regexp is implicitly anchored, so to match
+a literal tag name, just use that tag name (if it contains
+special regexp characters like \".\" or \"*\", these have to be
+escaped).
+
+The cdr of the matching entry gives a list of Elisp expressions
+that modify the tag. If the list is empty, the tag is simply
+hidden. Otherwise, each expression EXPR is evaluated in order:
+for the first expression, the variable `tag' is bound to the tag
+name; for each later expression, the variable `tag' is bound to
+the result of the previous expression. In this way, each
expression can build on the formatting performed by the previous
-expression. The result of the last expression will displayed in
+expression. The result of the last expression is displayed in
place of the tag.
For example, to replace a tag with another string, simply use
(t :inverse-video t))
"Face used to display deleted tags.
-Used in the default value of `notmuch-tag-deleted-formats`."
+Used in the default value of `notmuch-tag-deleted-formats'."
:group 'notmuch-faces)
(defcustom notmuch-tag-deleted-formats
(".*" (notmuch-apply-face tag `notmuch-tag-deleted)))
"Custom formats for tags when deleted.
-For deleted tags the formats in `notmuch-tag-formats` are applied
+For deleted tags the formats in `notmuch-tag-formats' are applied
first and then these formats are applied on top; that is `tag'
passed to the function is the tag with all these previous
formattings applied. The formatted can access the original
'((t :underline "green"))
"Default face used for added tags.
-Used in the default value for `notmuch-tag-added-formats`."
+Used in the default value for `notmuch-tag-added-formats'."
:group 'notmuch-faces)
(defcustom notmuch-tag-added-formats
'((".*" (notmuch-apply-face tag 'notmuch-tag-added)))
"Custom formats for tags when added.
-For added tags the formats in `notmuch-tag-formats` are applied
+For added tags the formats in `notmuch-tag-formats' are applied
first and then these formats are applied on top.
To disable special formatting of added tags, set this variable to
:group 'notmuch-faces
:type 'notmuch-tag-format-type)
+;;; Icons
+
(defun notmuch-tag-format-image-data (tag data)
"Replace TAG with image DATA, if available.
</g>
</svg>")
+;;; Format Handling
+
(defvar notmuch-tag--format-cache (make-hash-table :test 'equal)
"Cache of tag format lookup. Internal to `notmuch-tag-format-tag'.")
"Clear the internal cache of tag formats."
(clrhash notmuch-tag--format-cache))
-(defun notmuch-tag--get-formats (tag format-alist)
+(defun notmuch-tag--get-formats (tag alist)
"Find the first item whose car regexp-matches TAG."
(save-match-data
;; Don't use assoc-default since there's no way to distinguish a
;; missing key from a present key with a null cdr.
- (cl-assoc tag format-alist
+ (cl-assoc tag alist
:test (lambda (tag key)
(and (eq (string-match key tag) 0)
(= (match-end 0) (length tag)))))))
-(defun notmuch-tag--do-format (tag formatted-tag formats)
+(defun notmuch-tag--do-format (bare-tag tag formats)
"Apply a tag-formats entry to TAG."
(cond ((null formats) ;; - Tag not in `formats',
- formatted-tag) ;; the format is the tag itself.
+ tag) ;; the format is the tag itself.
((null (cdr formats)) ;; - Tag was deliberately hidden,
nil) ;; no format must be returned
(t
;; Tag was found and has formats, we must apply all the
;; formats. TAG may be null so treat that as a special case.
- (let ((bare-tag tag)
- (tag (copy-sequence (or formatted-tag ""))))
+ (let ((return-tag (copy-sequence (or tag ""))))
(dolist (format (cdr formats))
- (setq tag (eval format)))
- (if (and (null formatted-tag) (equal tag ""))
+ (setq return-tag
+ (eval format
+ `((bare-tag . ,bare-tag)
+ (tag . ,return-tag)))))
+ (if (and (null tag) (equal return-tag ""))
nil
- tag)))))
+ return-tag)))))
(defun notmuch-tag-format-tag (tags orig-tags tag)
"Format TAG according to `notmuch-tag-formats'.
face
t)))
+;;; Hooks
+
(defcustom notmuch-before-tag-hook nil
"Hooks that are run before tags of a message are modified.
:options '(notmuch-hl-line-mode)
:group 'notmuch-hooks)
+;;; User Input
+
(defvar notmuch-select-tag-history nil
- "Variable to store minibuffer history for
-`notmuch-select-tag-with-completion' function.")
+ "Minibuffer history of `notmuch-select-tag-with-completion' function.")
(defvar notmuch-read-tag-changes-history nil
- "Variable to store minibuffer history for
-`notmuch-read-tag-changes' function.")
+ "Minibuffer history of `notmuch-read-tag-changes' function.")
(defun notmuch-tag-completions (&rest search-terms)
"Return a list of tags for messages matching SEARCH-TERMS.
-Returns all tags if no search terms are given."
+Return all tags if no search terms are given."
(unless search-terms
(setq search-terms (list "*")))
(split-string
"\n+" t))
(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
- (let ((tag-list (apply #'notmuch-tag-completions search-terms)))
- (completing-read prompt tag-list nil nil nil 'notmuch-select-tag-history)))
+ (completing-read prompt
+ (apply #'notmuch-tag-completions search-terms)
+ nil nil nil 'notmuch-select-tag-history))
(defun notmuch-read-tag-changes (current-tags &optional prompt initial-input)
"Prompt for tag changes in the minibuffer.
-CURRENT-TAGS is a list of tags that are present on the message or
-messages to be changed. These are offered as tag removal
+CURRENT-TAGS is a list of tags that are present on the message
+or messages to be changed. These are offered as tag removal
completions. CURRENT-TAGS may contain duplicates. PROMPT, if
non-nil, is the query string to present in the minibuffer. It
defaults to \"Tags\". INITIAL-INPUT, if non-nil, will be the
nil nil initial-input
'notmuch-read-tag-changes-history))))
+;;; Tagging
+
(defun notmuch-update-tags (tags tag-changes)
"Return a copy of TAGS with additions and removals from TAG-CHANGES.
from TAGS if present."
(let ((result-tags (copy-sequence tags)))
(dolist (tag-change tag-changes)
- (let ((op (string-to-char tag-change))
- (tag (unless (string= tag-change "") (substring tag-change 1))))
- (cl-case op
+ (let ((tag (and (not (string-empty-p tag-change))
+ (substring tag-change 1))))
+ (cl-case (aref tag-change 0)
(?+ (unless (member tag result-tags)
(push tag result-tags)))
(?- (setq result-tags (delete tag result-tags)))
directly, so that hooks specified in notmuch-before-tag-hook and
notmuch-after-tag-hook will be run."
;; Perform some validation
- (mapc (lambda (tag-change)
- (unless (string-match-p "^[-+]\\S-+$" tag-change)
- (error "Tag must be of the form `+this_tag' or `-that_tag'")))
- tag-changes)
+ (dolist (tag-change tag-changes)
+ (unless (string-match-p "^[-+]\\S-+$" tag-change)
+ (error "Tag must be of the form `+this_tag' or `-that_tag'")))
(unless query
(error "Nothing to tag!"))
- (unless (null tag-changes)
+ (when tag-changes
(run-hooks 'notmuch-before-tag-hook)
(if (<= (length query) notmuch-tag-argument-limit)
(apply 'notmuch-call-notmuch-process "tag"
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`
+`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
(symbol-value tag)
tag))
(tag-change (if reverse
- (notmuch-tag-change-list tag 't)
+ (notmuch-tag-change-list tag t)
tag))
(name (or (and (not (string= name ""))
name)
(setq action-map (nreverse action-map))
(notmuch-jump action-map "Tag: ")))
-;;
+;;; _
(provide 'notmuch-tag)
-;;; notmuch-tree.el --- displaying notmuch forests
+;;; notmuch-tree.el --- displaying notmuch forests -*- lexical-binding: t -*-
;;
;; Copyright © Carl Worth
;; Copyright © David Edmondson
;;; Code:
-(eval-when-compile (require 'cl-lib))
-
(require 'mail-parse)
(require 'notmuch-lib)
(declare-function notmuch-search-find-thread-id "notmuch" (&optional bare))
(declare-function notmuch-search-find-subject "notmuch" ())
+;; For `notmuch-tree-next-thread-from-search'.
+(declare-function notmuch-search-next-thread "notmuch" ())
+(declare-function notmuch-search-previous-thread "notmuch" ())
+(declare-function notmuch-tree-from-search-thread "notmuch" ())
+
;; the following variable is defined in notmuch.el
(defvar notmuch-search-query-string)
;; this variable distinguishes the unthreaded display from the normal tree display
-(defvar notmuch-tree-unthreaded nil
+(defvar-local notmuch-tree-unthreaded nil
"A buffer local copy of argument unthreaded to the function notmuch-tree.")
-(make-variable-buffer-local 'notmuch-tree-unthreaded)
+
+;;; Options
(defgroup notmuch-tree nil
"Showing message and thread structure."
notmuch-unthreaded-result-format
notmuch-tree-result-format))
-;; Faces for messages that match the query.
+;;; Faces
+;;;; Faces for messages that match the query
+
(defface notmuch-tree-match-face
'((t :inherit default))
"Default face used in tree mode face for matching messages"
:group 'notmuch-tree
:group 'notmuch-faces)
-;; Faces for messages that do not match the query.
+;;;; Faces for messages that do not match the query
+
(defface notmuch-tree-no-match-face
'((t (:foreground "gray")))
"Default face used in tree mode face for non-matching messages."
:group 'notmuch-tree
:group 'notmuch-faces)
-(defvar notmuch-tree-previous-subject
+;;; Variables
+
+(defvar-local notmuch-tree-previous-subject
"The subject of the most recent result shown during the async display.")
-(make-variable-buffer-local 'notmuch-tree-previous-subject)
-(defvar notmuch-tree-basic-query nil
+(defvar-local notmuch-tree-basic-query nil
"A buffer local copy of argument query to the function notmuch-tree.")
-(make-variable-buffer-local 'notmuch-tree-basic-query)
-(defvar notmuch-tree-query-context nil
+(defvar-local notmuch-tree-query-context nil
"A buffer local copy of argument query-context to the function notmuch-tree.")
-(make-variable-buffer-local 'notmuch-tree-query-context)
-(defvar notmuch-tree-target-msg nil
+(defvar-local notmuch-tree-target-msg nil
"A buffer local copy of argument target to the function notmuch-tree.")
-(make-variable-buffer-local 'notmuch-tree-target-msg)
-(defvar notmuch-tree-open-target nil
+(defvar-local notmuch-tree-open-target nil
"A buffer local copy of argument open-target to the function notmuch-tree.")
-(make-variable-buffer-local 'notmuch-tree-open-target)
-(defvar notmuch-tree-parent-buffer nil)
-(make-variable-buffer-local 'notmuch-tree-parent-buffer)
+(defvar-local notmuch-tree-parent-buffer nil)
-(defvar notmuch-tree-message-window nil
+(defvar-local notmuch-tree-message-window nil
"The window of the message pane.
It is set in both the tree buffer and the child show buffer. It
is used to try and close the message pane when quitting tree view
or the child show buffer.")
-(make-variable-buffer-local 'notmuch-tree-message-window)
(put 'notmuch-tree-message-window 'permanent-local t)
-(defvar notmuch-tree-message-buffer nil
+(defvar-local notmuch-tree-message-buffer nil
"The buffer name of the show buffer in the message pane.
This is used to try and make sure we don't close the message pane
if the user has loaded a different buffer in that window.")
-(make-variable-buffer-local 'notmuch-tree-message-buffer)
(put 'notmuch-tree-message-buffer 'permanent-local t)
-(defun notmuch-tree-to-message-pane (func)
- "Execute FUNC in message pane.
+;;; Tree wrapper commands
-This function returns a function (so can be used as a keybinding)
-which executes function FUNC in the message pane if it is
-open (if the message pane is closed it does nothing)."
- `(lambda ()
- ,(concat "(In message pane) " (documentation func t))
+(defmacro notmuch-tree--define-do-in-message-window (name cmd)
+ "Define NAME as a command that calls CMD interactively in the message window.
+If the message pane is closed then this command does nothing.
+Avoid using this macro in new code; it will be removed."
+ `(defun ,name ()
+ ,(concat "(In message window) " (documentation cmd t))
(interactive)
(when (window-live-p notmuch-tree-message-window)
(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."
+ (call-interactively #',cmd)))))
+
+(notmuch-tree--define-do-in-message-window
+ notmuch-tree-previous-message-button
+ notmuch-show-previous-button)
+(notmuch-tree--define-do-in-message-window
+ notmuch-tree-next-message-button
+ notmuch-show-next-button)
+(notmuch-tree--define-do-in-message-window
+ notmuch-tree-toggle-message-process-crypto
+ notmuch-show-toggle-process-crypto)
+
+(defun notmuch-tree--message-process-crypto ()
+ "Return value of `notmuch-show-process-crypto' in the message window.
+If that window isn't alive, then return the current value.
+Avoid using this function in new code; it will be removed."
(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.
-
-This function does not give an error if there is no button."
- (interactive)
- (let ((button (or button (button-at (point)))))
- (when button (button-activate button))))
-
-(defun notmuch-tree-close-message-pane-and (func)
- "Close message pane and execute FUNC.
-
-This function returns a function (so can be used as a keybinding)
-which closes the message pane if open and then executes function
-FUNC."
- `(lambda ()
- ,(concat "(Close message pane and) " (documentation func t))
+ notmuch-show-process-crypto)
+ notmuch-show-process-crypto))
+
+(defmacro notmuch-tree--define-close-message-window-and (name cmd)
+ "Define NAME as a variant of CMD.
+
+NAME determines the value of `notmuch-show-process-crypto' in the
+message window, closes the window, and then call CMD interactively
+with that value let-bound. If the message window does not exist,
+then NAME behaves like CMD."
+ `(defun ,name ()
+ ,(concat "(Close message pane and) " (documentation cmd t))
(interactive)
(let ((notmuch-show-process-crypto
- (notmuch-tree-inherit-from-message-pane 'notmuch-show-process-crypto)))
+ (notmuch-tree--message-process-crypto)))
(notmuch-tree-close-message-window)
- (call-interactively #',func))))
+ (call-interactively #',cmd))))
+
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-help
+ notmuch-help)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-new-mail
+ notmuch-mua-new-mail)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-jump-search
+ notmuch-jump-search)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-forward-message
+ notmuch-show-forward-message)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-reply-sender
+ notmuch-show-reply-sender)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-reply
+ notmuch-show-reply)
+(notmuch-tree--define-close-message-window-and
+ notmuch-tree-view-raw-message
+ notmuch-show-view-raw-message)
+
+;;; Keymap
(defvar notmuch-tree-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map notmuch-common-keymap)
- ;; The following override the global keymap.
- ;; Override because we want to close message pane first.
- (define-key map [remap notmuch-help]
- (notmuch-tree-close-message-pane-and #'notmuch-help))
- ;; Override because we first close message pane and then close tree buffer.
+ ;; These bindings shadow common bindings with variants
+ ;; that additionally close the message window.
(define-key map [remap notmuch-bury-or-kill-this-buffer] 'notmuch-tree-quit)
- ;; Override because we close message pane after the search query is entered.
- (define-key map [remap notmuch-search] 'notmuch-tree-to-search)
- ;; Override because we want to close message pane first.
- (define-key map [remap notmuch-mua-new-mail]
- (notmuch-tree-close-message-pane-and #'notmuch-mua-new-mail))
- ;; Override because we want to close message pane first.
- (define-key map [remap notmuch-jump-search]
- (notmuch-tree-close-message-pane-and #'notmuch-jump-search))
+ (define-key map [remap notmuch-search] 'notmuch-tree-to-search)
+ (define-key map [remap notmuch-help] 'notmuch-tree-help)
+ (define-key map [remap notmuch-mua-new-mail] 'notmuch-tree-new-mail)
+ (define-key map [remap notmuch-jump-search] 'notmuch-tree-jump-search)
(define-key map "S" 'notmuch-search-from-tree-current-query)
(define-key map "U" 'notmuch-unthreaded-from-tree-current-query)
(define-key map "b" 'notmuch-show-resend-message)
;; these apply to the message pane
- (define-key map (kbd "M-TAB")
- (notmuch-tree-to-message-pane #'notmuch-show-previous-button))
- (define-key map (kbd "<backtab>")
- (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 "$"
- (notmuch-tree-to-message-pane #'notmuch-show-toggle-process-crypto))
+ (define-key map (kbd "M-TAB") 'notmuch-tree-previous-message-button)
+ (define-key map (kbd "<backtab>") 'notmuch-tree-previous-message-button)
+ (define-key map (kbd "TAB") 'notmuch-tree-next-message-button)
+ (define-key map "$" 'notmuch-tree-toggle-message-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))
- (define-key map "r"
- (notmuch-tree-close-message-pane-and #'notmuch-show-reply-sender))
- (define-key map "R"
- (notmuch-tree-close-message-pane-and #'notmuch-show-reply))
- (define-key map "V"
- (notmuch-tree-close-message-pane-and #'notmuch-show-view-raw-message))
+ (define-key map "f" 'notmuch-tree-forward-message)
+ (define-key map "r" 'notmuch-tree-reply-sender)
+ (define-key map "R" 'notmuch-tree-reply)
+ (define-key map "V" 'notmuch-tree-view-raw-message)
;; The main tree view bindings
(define-key map (kbd "RET") 'notmuch-tree-show-message)
(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)
+ map)
+ "Keymap for \"notmuch tree\" buffers.")
+
+;;; Message properties
(defun notmuch-tree-get-message-properties ()
"Return the properties of the current message as a plist.
(notmuch-tree-set-message-properties props)))
(defun notmuch-tree-get-prop (prop &optional props)
- (let ((props (or props
- (notmuch-tree-get-message-properties))))
- (plist-get props prop)))
+ (plist-get (or props (notmuch-tree-get-message-properties))
+ prop))
(defun notmuch-tree-set-tags (tags)
"Set the tags of the current message."
(defun notmuch-tree-get-match ()
"Return whether the current message is a match."
- (interactive)
(notmuch-tree-get-prop :match))
+;;; Update display
+
(defun notmuch-tree-refresh-result ()
"Redisplay the current message line.
(when (string= tree-msg-id (notmuch-show-get-message-id))
(notmuch-show-update-tags new-tags)))))))
+;;; Commands (and some helper functions used by them)
+
(defun notmuch-tree-tag (tag-changes)
"Change tags for the current message."
(interactive
(let ((buffer (current-buffer)))
(when (and (window-live-p notmuch-tree-message-window)
(eq (window-buffer notmuch-tree-message-window) buffer))
- ;; We do not want an error if this is the sole window in the
- ;; frame and I do not know how to test for that in emacs pre
- ;; 24. Hence we just ignore-errors.
+ ;; We could check whether this is the only window in its frame,
+ ;; but simply ignoring the error that is thrown otherwise is
+ ;; what we had to do for Emacs 24 and we stick to that because
+ ;; it is still the simplest approach.
(ignore-errors
(delete-window notmuch-tree-message-window)))))
"Show the current message (in whole window)."
(interactive)
(let ((id (notmuch-tree-get-message-id))
- (inhibit-read-only t)
- buffer)
+ (inhibit-read-only t))
(when id
;; We close the window to kill off un-needed buffers.
(notmuch-tree-close-message-window)
(notmuch-tree-from-search-thread))))
(defun notmuch-tree-next-thread (&optional previous)
- "Move to the next thread in the current tree or parent search
-results
+ "Move to the next thread in the current tree or parent search results.
If PREVIOUS is non-nil, move to the previous thread in the tree or
search results instead."
(notmuch-tree-next-thread-from-search previous)))
(defun notmuch-tree-prev-thread ()
- "Move to the previous thread in the current tree or parent search
-results"
+ "Move to the previous thread in the current tree or parent search results."
(interactive)
(notmuch-tree-next-thread t))
(defun notmuch-tree-thread-mapcar (function)
- "Iterate through all messages in the current thread
- and call FUNCTION for side effects."
+ "Call FUNCTION for each message in the current thread.
+FUNCTION is called for side effects only."
(save-excursion
(notmuch-tree-thread-top)
(cl-loop collect (funcall function)
(notmuch-tree-tag-thread
(notmuch-tag-change-list notmuch-archive-tags unarchive))))
-;; Functions below here display the tree buffer itself.
+;;; Functions for displaying the tree buffer itself
(defun notmuch-tree-clean-address (address)
"Try to clean a single email ADDRESS for display. Return
(setq buffer-read-only t)
(setq truncate-lines t))
-(defun notmuch-tree-process-sentinel (proc msg)
+(defun notmuch-tree-process-sentinel (proc _msg)
"Add a message to let user know when \"notmuch tree\" exits."
(let ((buffer (process-buffer proc))
(status (process-status proc))
- (exit-status (process-exit-status proc))
- (never-found-target-thread nil))
+ (exit-status (process-exit-status proc)))
(when (memq status '(exit signal))
(kill-buffer (process-get proc 'parse-buf))
(when (buffer-live-p buffer)
(with-current-buffer buffer
(save-excursion
- (let ((inhibit-read-only t)
- (atbob (bobp)))
+ (let ((inhibit-read-only t))
(goto-char (point-max))
(when (eq status 'signal)
(insert "Incomplete search results (tree view process was killed).\n"))
"Process and filter the output of \"notmuch show\" for tree view."
(let ((results-buf (process-buffer proc))
(parse-buf (process-get proc 'parse-buf))
- (inhibit-read-only t)
- done)
+ (inhibit-read-only t))
(if (not (buffer-live-p results-buf))
(delete-process proc)
(with-current-buffer parse-buf
(inhibit-read-only t))
(pop-to-buffer-same-window buffer))
;; Don't track undo information for this buffer
- (set 'buffer-undo-list t)
+ (setq buffer-undo-list t)
(notmuch-tree-worker query query-context target open-target unthreaded)
(setq notmuch-tree-parent-buffer parent-buffer)
(setq truncate-lines t))
(interactive)
(notmuch-tree query query-context target buffer-name open-target t))
-;;
+;;; _
(provide 'notmuch-tree)
-;;; notmuch-wash.el --- cleaning up message bodies
+;;; notmuch-wash.el --- cleaning up message bodies -*- lexical-binding: t -*-
;;
;; Copyright © Carl Worth
;; Copyright © David Edmondson
;;; Code:
(require 'coolj)
+(require 'diff-mode)
(require 'notmuch-lib)
(declare-function notmuch-show-insert-bodypart "notmuch-show"
(msg part depth &optional hide))
(defvar notmuch-show-indent-messages-width)
-;;
+;;; Options
(defgroup notmuch-wash nil
"Cleaning up messages for display."
(integer :tag "number of characters"))
:group 'notmuch-wash)
+;;; Faces
+
(defface notmuch-wash-toggle-button
'((t (:inherit font-lock-comment-face)))
"Face used for buttons toggling the visibility of washed away
:group 'notmuch-wash
:group 'notmuch-faces)
+;;; Buttons
+
(defun notmuch-wash-toggle-invisible-action (cite-button)
;; Toggle overlay visibility
(let ((overlay (button-get cite-button 'overlay)))
(overlay-end overlay))))
(format label-format lines-count)))
-(defun notmuch-wash-region-to-button (msg beg end type &optional prefix)
+(defun notmuch-wash-region-to-button (beg end type &optional prefix)
"Auxiliary function to do the actual making of overlays and buttons.
BEG and END are buffer locations. TYPE should a string, either
:type button-type)))
(overlay-put overlay 'notmuch-wash-button button))))))
-(defun notmuch-wash-excerpt-citations (msg depth)
+;;; Hook functions
+
+(defun notmuch-wash-excerpt-citations (_msg _depth)
"Excerpt citations and up to one signature."
(goto-char (point-min))
(beginning-of-line)
(when (and (< (point) (point-max))
(re-search-forward notmuch-wash-original-regexp nil t))
- (let* ((msg-start (match-beginning 0))
- (msg-end (point-max))
- (msg-lines (count-lines msg-start msg-end)))
- (notmuch-wash-region-to-button
- msg msg-start msg-end "original")))
+ (notmuch-wash-region-to-button (match-beginning 0)
+ (point-max)
+ "original"))
(while (and (< (point) (point-max))
(re-search-forward notmuch-wash-citation-regexp nil t))
(let* ((cite-start (match-beginning 0))
(goto-char cite-end)
(forward-line (- notmuch-wash-citation-lines-suffix))
(notmuch-wash-region-to-button
- msg hidden-start (point-marker)
+ hidden-start (point-marker)
"citation")))))
(when (and (not (eobp))
(re-search-forward notmuch-wash-signature-regexp nil t))
- (let* ((sig-start (match-beginning 0))
- (sig-end (match-end 0))
- (sig-lines (count-lines sig-start (point-max))))
- (when (<= sig-lines notmuch-wash-signature-lines-max)
+ (let ((sig-start (match-beginning 0)))
+ (when (<= (count-lines sig-start (point-max))
+ notmuch-wash-signature-lines-max)
(let ((sig-start-marker (make-marker))
(sig-end-marker (make-marker)))
(set-marker sig-start-marker sig-start)
(overlay-put (make-overlay sig-start-marker sig-end-marker)
'face 'message-cited-text)
(notmuch-wash-region-to-button
- msg sig-start-marker sig-end-marker
+ sig-start-marker sig-end-marker
"signature"))))))
-;;
-
-(defun notmuch-wash-elide-blank-lines (msg depth)
+(defun notmuch-wash-elide-blank-lines (_msg _depth)
"Elide leading, trailing and successive blank lines."
;; Algorithm derived from `article-strip-multiple-blank-lines' in
;; `gnus-art.el'.
(when (looking-at "\n")
(delete-region (match-beginning 0) (match-end 0))))
-;;
-
-(defun notmuch-wash-tidy-citations (msg depth)
+(defun notmuch-wash-tidy-citations (_msg _depth)
"Improve the display of cited regions of a message.
Perform several transformations on the message body:
(while (re-search-forward "\\(^>[> ]*\n\\)\\(^$\\|^[^>].*\\)" nil t)
(replace-match "\\2")))
-;;
-
-(defun notmuch-wash-wrap-long-lines (msg depth)
+(defun notmuch-wash-wrap-long-lines (_msg depth)
"Wrap long lines in the message.
If `notmuch-wash-wrap-lines-length' is a number, this will wrap
2)))
(coolj-wrap-region (point-min) (point-max))))
-;;
-
-(require 'diff-mode)
-
-(defvar diff-file-header-re) ; From `diff-mode.el'.
+;;;; Convert Inline Patches
(defun notmuch-wash-subject-to-filename (subject &optional maxlen)
"Convert a mail SUBJECT into a filename.
(delete-region (point-min) (point-max))
(notmuch-show-insert-bodypart nil part depth)))))
-;;
+;;; _
(provide 'notmuch-wash)
-;;; notmuch.el --- run notmuch within emacs
+;;; notmuch.el --- run notmuch within emacs -*- lexical-binding: t -*-
;;
;; Copyright © Carl Worth
;;
;;; Code:
-(eval-when-compile (require 'cl-lib))
-
(require 'mm-view)
(require 'message)
+(require 'hl-line)
+
(require 'notmuch-lib)
(require 'notmuch-tag)
(require 'notmuch-show)
(require 'notmuch-message)
(require 'notmuch-parser)
+;;; Options
+
(defcustom notmuch-search-result-format
`(("date" . "%12s ")
("count" . "%-7s ")
:type 'file
:group 'notmuch)
-(defvar notmuch-query-history nil
- "Variable to store minibuffer history for notmuch queries.")
+(defcustom notmuch-search-hook '(notmuch-hl-line-mode)
+ "List of functions to call when notmuch displays the search results."
+ :type 'hook
+ :options '(notmuch-hl-line-mode)
+ :group 'notmuch-search
+ :group 'notmuch-hooks)
+
+;;; Mime Utilities
(defun notmuch-foreach-mime-part (function mm-handle)
(cond ((stringp (car mm-handle))
(mm-save-part p))))
mm-handle))
-(require 'hl-line)
-
-(defun notmuch-hl-line-mode ()
- (prog1 (hl-line-mode)
- (when hl-line-overlay
- (overlay-put hl-line-overlay 'priority 1))))
-
-(defcustom notmuch-search-hook '(notmuch-hl-line-mode)
- "List of functions to call when notmuch displays the search results."
- :type 'hook
- :options '(notmuch-hl-line-mode)
- :group 'notmuch-search
- :group 'notmuch-hooks)
+;;; Keymap
(defvar notmuch-search-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map notmuch-common-keymap)
(define-key map "x" 'notmuch-bury-or-kill-this-buffer)
- (define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
+ (define-key map (kbd "DEL") 'notmuch-search-scroll-down)
(define-key map "b" 'notmuch-search-scroll-down)
(define-key map " " 'notmuch-search-scroll-up)
(define-key map "<" 'notmuch-search-first-thread)
(define-key map "U" 'notmuch-unthreaded-from-search-current-query)
map)
"Keymap for \"notmuch search\" buffers.")
-(fset 'notmuch-search-mode-map notmuch-search-mode-map)
+
+;;; Internal Variables
+
+(defvar notmuch-query-history nil
+ "Variable to store minibuffer history for notmuch queries.")
+
+(defvar-local notmuch-search-query-string nil)
+(defvar-local notmuch-search-target-thread nil)
+(defvar-local notmuch-search-target-line nil)
+
+;;; Stashing
(defvar notmuch-search-stash-map
(let ((map (make-sparse-keymap)))
(defun notmuch-stash-query ()
"Copy current query to kill-ring."
(interactive)
- (notmuch-common-do-stash (notmuch-search-get-query)))
-
-(defvar notmuch-search-query-string)
-(defvar notmuch-search-target-thread)
-(defvar notmuch-search-target-line)
+ (notmuch-common-do-stash notmuch-search-query-string))
-(defvar notmuch-search-disjunctive-regexp "\\<[oO][rR]\\>")
+;;; Movement
(defun notmuch-search-scroll-up ()
"Move forward through search results by one window's worth."
(interactive)
(goto-char (point-min)))
+;;; Faces
+
(defface notmuch-message-summary-face
`((((class color) (background light))
,@(and (>= emacs-major-version 27) '(:extend t))
"Face used in search mode face for flagged threads.
This face is the default value for the \"flagged\" tag in
-`notmuch-search-line-faces`."
+`notmuch-search-line-faces'."
:group 'notmuch-search
:group 'notmuch-faces)
"Face used in search mode for unread threads.
This face is the default value for the \"unread\" tag in
-`notmuch-search-line-faces`."
+`notmuch-search-line-faces'."
:group 'notmuch-search
:group 'notmuch-faces)
+;;; Mode
+
(define-derived-mode notmuch-search-mode fundamental-mode "notmuch-search"
"Major mode displaying results of a notmuch search.
Complete list of currently available key bindings:
\\{notmuch-search-mode-map}"
- (make-local-variable 'notmuch-search-query-string)
- (make-local-variable 'notmuch-search-oldest-first)
- (make-local-variable 'notmuch-search-target-thread)
- (make-local-variable 'notmuch-search-target-line)
(setq notmuch-buffer-refresh-function #'notmuch-search-refresh-view)
- (set (make-local-variable 'scroll-preserve-screen-position) t)
+ (setq-local scroll-preserve-screen-position t)
(add-to-invisibility-spec (cons 'ellipsis t))
(setq truncate-lines t)
(setq buffer-read-only t)
(setq imenu-extract-index-name-function
#'notmuch-search-imenu-extract-index-name-function))
+;;; Search Results
+
(defun notmuch-search-get-result (&optional pos)
"Return the result object for the thread at POS (or point).
(defun notmuch-search-find-stable-query ()
"Return the stable queries for the current thread.
-This returns a list (MATCHED-QUERY UNMATCHED-QUERY) for the
+Return a list (MATCHED-QUERY UNMATCHED-QUERY) for the
matched and unmatched messages in the current thread."
(plist-get (notmuch-search-get-result) :query))
`notmuch-show-only-matching-messages' when displaying the
thread."
(interactive "P")
- (let ((thread-id (notmuch-search-find-thread-id))
- (subject (notmuch-search-find-subject)))
- (if (> (length thread-id) 0)
+ (let ((thread-id (notmuch-search-find-thread-id)))
+ (if thread-id
(notmuch-show thread-id
elide-toggle
(current-buffer)
notmuch-search-query-string
;; Name the buffer based on the subject.
- (concat "*"
- (truncate-string-to-width subject 30 nil nil t)
- "*"))
+ (format "*%s*" (truncate-string-to-width
+ (notmuch-search-find-subject)
+ 30 nil nil t)))
(message "End of search results."))))
(defun notmuch-tree-from-search-current-query ()
(defun notmuch-search-reply-to-thread (&optional prompt-for-sender)
"Begin composing a reply-all to the entire current thread in a new buffer."
(interactive "P")
- (let ((message-id (notmuch-search-find-thread-id)))
- (notmuch-mua-new-reply message-id prompt-for-sender t)))
+ (notmuch-mua-new-reply (notmuch-search-find-thread-id)
+ prompt-for-sender t))
(defun notmuch-search-reply-to-thread-sender (&optional prompt-for-sender)
"Begin composing a reply to the entire current thread in a new buffer."
(interactive "P")
- (let ((message-id (notmuch-search-find-thread-id)))
- (notmuch-mua-new-reply message-id prompt-for-sender nil)))
+ (notmuch-mua-new-reply (notmuch-search-find-thread-id)
+ prompt-for-sender nil))
+
+;;; Tags
(defun notmuch-search-set-tags (tags &optional pos)
- (let ((new-result (plist-put (notmuch-search-get-result pos) :tags tags)))
- (notmuch-search-update-result new-result pos)))
+ (notmuch-search-update-result
+ (plist-put (notmuch-search-get-result pos) :tags tags)
+ pos))
(defun notmuch-search-get-tags (&optional pos)
(plist-get (notmuch-search-get-result pos) :tags))
(notmuch-search-foreach-result beg end
(lambda (pos)
(setq output (append output (notmuch-search-get-tags pos)))))
- output))
+ (delete-dups output)))
(defun notmuch-search-interactive-tag-changes (&optional initial-input)
"Prompt for tag changes for the current thread or region.
-Returns (TAG-CHANGES REGION-BEGIN REGION-END)."
+Return (TAG-CHANGES REGION-BEGIN REGION-END)."
(pcase-let ((`(,beg ,end) (notmuch-interactive-region)))
(list (notmuch-read-tag-changes (notmuch-search-get-tags-region beg end)
(if (= beg end) "Tag thread" "Tag region")
(when (eq beg end)
(notmuch-search-next-thread)))
+;;; Search Results
+
(defun notmuch-search-update-result (result &optional pos)
"Replace the result object of the thread at POS (or point) by
RESULT and redraw it.
(min init-point (- new-end 1)))))
(goto-char new-point)))))
-(defun notmuch-search-process-sentinel (proc msg)
+(defun notmuch-search-process-sentinel (proc _msg)
"Add a message to let user know when \"notmuch search\" exits."
(let ((buffer (process-buffer proc))
(status (process-status proc))
(throw 'return nil))
(when (and atbob
(not (string= notmuch-search-target-thread "found")))
- (set 'never-found-target-thread t)))))
+ (setq never-found-target-thread t)))))
(when (and never-found-target-thread
notmuch-search-target-line)
(goto-char (point-min))
(setq invisible-string (notmuch-search-author-propertize invisible-string)))
;; If there is any invisible text, add it as a tooltip to the
;; visible text.
- (unless (string= invisible-string "")
+ (unless (string-empty-p invisible-string)
(setq visible-string
(propertize visible-string
'help-echo (concat "..." invisible-string))))
;; Insert the visible and, if present, invisible author strings.
(insert visible-string)
- (unless (string= invisible-string "")
+ (unless (string-empty-p invisible-string)
(let ((start (point))
overlay)
(insert invisible-string)
"Process and filter the output of \"notmuch search\"."
(let ((results-buf (process-buffer proc))
(parse-buf (process-get proc 'parse-buf))
- (inhibit-read-only t)
- done)
+ (inhibit-read-only t))
(when (buffer-live-p results-buf)
(with-current-buffer parse-buf
;; Insert new data
(notmuch-sexp-parse-partial-list 'notmuch-search-append-result
results-buf)))))
+;;; Commands (and some helper functions used by them)
+
(defun notmuch-search-tag-all (tag-changes)
"Add/remove tags from all messages in current search buffer.
"Read a notmuch-query from the minibuffer with completion.
PROMPT is the string to prompt with."
- (let*
- ((all-tags
- (mapcar (lambda (tag) (notmuch-escape-boolean-term tag))
- (process-lines notmuch-command "search" "--output=tags" "*")))
- (completions
- (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:"
- "subject:" "attachment:")
- (mapcar (lambda (tag) (concat "tag:" 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 (cl-case major-mode
- (notmuch-search-mode (notmuch-search-get-query))
- (notmuch-show-mode (notmuch-show-get-query))
- (notmuch-tree-mode (notmuch-tree-get-query))))
- (minibuffer-completion-table
- (completion-table-dynamic
- (lambda (string)
- ;; generate a list of possible completions for the current input
- (cond
- ;; this ugly regexp is used to get the last word of the input
- ;; possibly preceded by a '('
- ((string-match "\\(^\\|.* (?\\)\\([^ ]*\\)$" string)
- (mapcar (lambda (compl)
- (concat (match-string-no-properties 1 string) compl))
- (all-completions (match-string-no-properties 2 string)
- completions)))
- (t (list string)))))))
- ;; this was simpler than convincing completing-read to accept spaces:
- (define-key keymap (kbd "TAB") 'minibuffer-complete)
- (let ((history-delete-duplicates t))
- (read-from-minibuffer prompt nil keymap nil
- 'notmuch-search-history current-query nil)))))
+ (let* ((all-tags
+ (mapcar (lambda (tag) (notmuch-escape-boolean-term tag))
+ (process-lines notmuch-command "search" "--output=tags" "*")))
+ (completions
+ (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:"
+ "subject:" "attachment:")
+ (mapcar (lambda (tag) (concat "tag:" tag)) all-tags)
+ (mapcar (lambda (tag) (concat "is:" tag)) all-tags)
+ (mapcar (lambda (mimetype) (concat "mimetype:" mimetype))
+ (mailcap-mime-types))))
+ (keymap (copy-keymap minibuffer-local-map))
+ (current-query (cl-case major-mode
+ (notmuch-search-mode (notmuch-search-get-query))
+ (notmuch-show-mode (notmuch-show-get-query))
+ (notmuch-tree-mode (notmuch-tree-get-query))))
+ (minibuffer-completion-table
+ (completion-table-dynamic
+ (lambda (string)
+ ;; Generate a list of possible completions for the current input.
+ (cond
+ ;; This ugly regexp is used to get the last word of the input
+ ;; possibly preceded by a '('.
+ ((string-match "\\(^\\|.* (?\\)\\([^ ]*\\)$" string)
+ (mapcar (lambda (compl)
+ (concat (match-string-no-properties 1 string) compl))
+ (all-completions (match-string-no-properties 2 string)
+ completions)))
+ (t (list string)))))))
+ ;; This was simpler than convincing completing-read to accept spaces:
+ (define-key keymap (kbd "TAB") 'minibuffer-complete)
+ (let ((history-delete-duplicates t))
+ (read-from-minibuffer prompt nil keymap nil
+ 'notmuch-search-history current-query nil))))
(defun notmuch-search-get-query ()
"Return the current query in this search buffer."
(if no-display
(set-buffer buffer)
(pop-to-buffer-same-window buffer))
- ;; avoid wiping out third party buffer-local variables in the case
- ;; where we're just refreshing or changing the sort order of an
- ;; existing search results buffer
- (unless (eq major-mode 'notmuch-search-mode)
- (notmuch-search-mode))
+ (notmuch-search-mode)
;; Don't track undo information for this buffer
- (set 'buffer-undo-list t)
- (set 'notmuch-search-query-string query)
- (set 'notmuch-search-oldest-first oldest-first)
- (set 'notmuch-search-target-thread target-thread)
- (set 'notmuch-search-target-line target-line)
+ (setq buffer-undo-list t)
+ (setq notmuch-search-query-string query)
+ (setq notmuch-search-oldest-first oldest-first)
+ (setq notmuch-search-target-thread target-thread)
+ (setq notmuch-search-target-line target-line)
(notmuch-tag-clear-cache)
- (let ((proc (get-buffer-process (current-buffer)))
- (inhibit-read-only t))
- (when proc
- (error "notmuch search process already running for query `%s'" query))
+ (when (get-buffer-process buffer)
+ (error "notmuch search process already running for query `%s'" query))
+ (let ((inhibit-read-only t))
(erase-buffer)
(goto-char (point-min))
(save-excursion
(if oldest-first
"--sort=oldest-first"
"--sort=newest-first")
- query))
- ;; Use a scratch buffer to accumulate partial output.
- ;; This buffer will be killed by the sentinel, which
- ;; should be called no matter how the process dies.
- (parse-buf (generate-new-buffer " *notmuch search parse*")))
- (process-put proc 'parse-buf parse-buf)
+ query)))
+ ;; Use a scratch buffer to accumulate partial output.
+ ;; This buffer will be killed by the sentinel, which
+ ;; should be called no matter how the process dies.
+ (process-put proc 'parse-buf
+ (generate-new-buffer " *notmuch search parse*"))
(set-process-filter proc 'notmuch-search-process-filter)
(set-process-query-on-exit-flag proc nil))))
(run-hooks 'notmuch-search-hook)))
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-search erases the current buffer.
- (notmuch-search query oldest-first target-thread target-line t)
- (goto-char (point-min))))
+ (notmuch-search notmuch-search-query-string
+ notmuch-search-oldest-first
+ (notmuch-search-find-thread-id 'bare)
+ (line-number-at-pos)
+ t)
+ (goto-char (point-min)))
(defun notmuch-search-toggle-order ()
"Toggle the current search order.
This command toggles the sort order for the current search. The
default sort order is defined by `notmuch-search-oldest-first'."
(interactive)
- (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
+ (setq notmuch-search-oldest-first (not notmuch-search-oldest-first))
(notmuch-search-refresh-view))
(defun notmuch-group-disjunctive-query-string (query-string)
"Group query if it contains a complex expression.
-
-Enclose QUERY-STRING in parentheses if it matches
-`notmuch-search-disjunctive-regexp'."
- (if (string-match-p notmuch-search-disjunctive-regexp query-string)
+Enclose QUERY-STRING in parentheses if contains \"OR\" operators."
+ (if (string-match-p "\\<[oO][rR]\\>" query-string)
(concat "( " query-string " )")
query-string))
notmuch-search-oldest-first)))
(defun notmuch-search-filter-by-tag (tag)
- "Filter the current search results based on a single tag.
+ "Filter the current search results based on a single TAG.
-Runs a new search matching only messages that match both the
-current search results AND that are tagged with the given tag."
+Run a new search matching only messages that match the current
+search results and that are also tagged with the given TAG."
(interactive
(list (notmuch-select-tag-with-completion "Filter by tag: "
notmuch-search-query-string)))
(notmuch-hello))
(defun notmuch-interesting-buffer (b)
- "Is the current buffer of interest to a notmuch user?"
+ "Whether the current buffer's major-mode is a notmuch mode."
(with-current-buffer b
(memq major-mode '(notmuch-show-mode
notmuch-search-mode
(defun notmuch-cycle-notmuch-buffers ()
"Cycle through any existing notmuch buffers (search, show or hello).
-If the current buffer is the only notmuch buffer, bury it. If no
-notmuch buffers exist, run `notmuch'."
+If the current buffer is the only notmuch buffer, bury it.
+If no notmuch buffers exist, run `notmuch'."
(interactive)
(let (start first)
;; If the current buffer is a notmuch buffer, remember it and then
(pop-to-buffer-same-window first))
(notmuch))))
+;;; Integrations
+;;;; Hl-line Support
+
+(defun notmuch-hl-line-mode ()
+ (prog1 (hl-line-mode)
+ (when hl-line-overlay
+ (overlay-put hl-line-overlay 'priority 1))))
+
;;;; Imenu Support
(defun notmuch-search-imenu-prev-index-position-function ()
"Move point to previous message in notmuch-search buffer.
-This function is used as a value for
-`imenu-prev-index-position-function'."
+Used as`imenu-prev-index-position-function' in notmuch buffers."
(notmuch-search-previous-thread))
(defun notmuch-search-imenu-extract-index-name-function ()
"Return imenu name for line at point.
-This function is used as a value for
-`imenu-extract-index-name-function'. Point should be at the
-beginning of the line."
- (let ((subject (notmuch-search-find-subject))
- (author (notmuch-search-find-authors)))
- (format "%s (%s)" subject author)))
+Used as `imenu-extract-index-name-function' in notmuch buffers.
+Point should be at the beginning of the line."
+ (format "%s (%s)"
+ (notmuch-search-find-subject)
+ (notmuch-search-find-authors)))
+
+;;; _
(setq mail-user-agent 'notmuch-user-agent)
-;;; rstdoc.el --- help generate documentation from docstrings -*-lexical-binding: t-*-
+;;; rstdoc.el --- help generate documentation from docstrings -*- lexical-binding: t -*-
;; Copyright (C) 2018 David Bremner
#include <sys/wait.h>
int
-notmuch_run_hook (const char *db_path, const char *hook)
+notmuch_run_hook (notmuch_database_t *notmuch, const char *hook)
{
char *hook_path;
int status = 0;
pid_t pid;
- hook_path = talloc_asprintf (NULL, "%s/%s/%s/%s", db_path, ".notmuch",
- "hooks", hook);
+ hook_path = talloc_asprintf (notmuch, "%s/%s",
+ notmuch_config_get (notmuch, NOTMUCH_CONFIG_HOOK_DIR),
+ hook);
if (hook_path == NULL) {
fprintf (stderr, "Out of memory\n");
return 1;
$(dir)/config.cc \
$(dir)/regexp-fields.cc \
$(dir)/thread.cc \
- $(dir)/thread-fp.cc
+ $(dir)/thread-fp.cc \
+ $(dir)/features.cc \
+ $(dir)/prefix.cc \
+ $(dir)/open.cc
libnotmuch_modules := $(libnotmuch_c_srcs:.c=.o) $(libnotmuch_cxx_srcs:.cc=.o)
char *current_val;
};
+struct _notmuch_config_values {
+ const char *iterator;
+ size_t tok_len;
+ const char *string;
+ void *children; /* talloc_context */
+};
+
+static const char * _notmuch_config_key_to_string (notmuch_config_key_t key);
+
static int
_notmuch_config_list_destroy (notmuch_config_list_t *list)
{
if (status)
return status;
+ if (! notmuch->config) {
+ if ((status = _notmuch_config_load_from_database (notmuch)))
+ return status;
+ }
+
try {
notmuch->writable_xapian_db->set_metadata (CONFIG_PREFIX + key, value);
} catch (const Xapian::Error &error) {
_notmuch_database_log (notmuch, "Error: A Xapian exception occurred setting metadata: %s\n",
error.get_msg ().c_str ());
}
- return status;
+
+ if (status)
+ return status;
+
+ _notmuch_string_map_set (notmuch->config, key, value);
+
+ return NOTMUCH_STATUS_SUCCESS;
}
static notmuch_status_t
const char *key,
char **value)
{
- std::string strval;
+ const char* stored_val;
notmuch_status_t status;
+ if (! notmuch->config) {
+ if ((status = _notmuch_config_load_from_database (notmuch)))
+ return status;
+ }
+
if (! value)
return NOTMUCH_STATUS_NULL_POINTER;
- status = _metadata_value (notmuch, key, strval);
- if (status)
- return status;
-
- *value = strdup (strval.c_str ());
+ stored_val = _notmuch_string_map_get (notmuch->config, key);
+ if (! stored_val) {
+ /* XXX in principle this API should be fixed so empty string
+ * is distinguished from not found */
+ *value = strdup("");
+ } else {
+ *value = strdup (stored_val);
+ }
return NOTMUCH_STATUS_SUCCESS;
}
{
talloc_free (list);
}
+
+notmuch_status_t
+_notmuch_config_load_from_database (notmuch_database_t *notmuch)
+{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+ notmuch_config_list_t *list;
+
+ if (notmuch->config == NULL)
+ notmuch->config = _notmuch_string_map_create (notmuch);
+
+ if (unlikely(notmuch->config == NULL))
+ return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+ status = notmuch_database_get_config_list (notmuch, "", &list);
+ if (status)
+ return status;
+
+ for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
+ _notmuch_string_map_append (notmuch->config,
+ notmuch_config_list_key (list),
+ notmuch_config_list_value (list));
+ }
+
+ return status;
+}
+
+notmuch_config_values_t *
+notmuch_config_get_values (notmuch_database_t *notmuch, notmuch_config_key_t key)
+{
+ notmuch_config_values_t *values = NULL;
+ bool ok = false;
+
+ const char *key_str = _notmuch_config_key_to_string (key);
+
+ if (! key_str)
+ goto DONE;
+
+ values = talloc (notmuch, notmuch_config_values_t);
+ if (unlikely(! values))
+ goto DONE;
+
+ values->children = talloc_new (values);
+
+ values->string = _notmuch_string_map_get (notmuch->config, key_str);
+ if (! values->string)
+ goto DONE;
+
+ values->iterator = strsplit_len (values->string, ';', &(values->tok_len));
+ ok = true;
+
+ DONE:
+ if (!ok) {
+ if (values)
+ talloc_free(values);
+ return NULL;
+ }
+ return values;
+}
+
+notmuch_bool_t
+notmuch_config_values_valid (notmuch_config_values_t *values) {
+ if (! values)
+ return false;
+
+ return (values->iterator != NULL);
+}
+
+const char *
+notmuch_config_values_get (notmuch_config_values_t *values) {
+ return talloc_strndup (values, values->iterator, values->tok_len);
+}
+
+void
+notmuch_config_values_start (notmuch_config_values_t *values) {
+ if (values == NULL)
+ return;
+ if (values->children) {
+ talloc_free (values->children);
+ }
+
+ values->children = talloc_new (values);
+
+ values->iterator = strsplit_len (values->string, ';', &(values->tok_len));
+}
+
+void
+notmuch_config_values_move_to_next (notmuch_config_values_t *values) {
+ values->iterator += values->tok_len;
+ values->iterator = strsplit_len (values->iterator, ';', &(values->tok_len));
+}
+
+void
+notmuch_config_values_destroy (notmuch_config_values_t *values) {
+ talloc_free (values);
+}
+
+notmuch_status_t
+_notmuch_config_load_from_file (notmuch_database_t *notmuch,
+ GKeyFile *file)
+{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+ gchar **groups, **keys, *val;
+
+ if (notmuch->config == NULL)
+ notmuch->config = _notmuch_string_map_create (notmuch);
+
+ if (unlikely(notmuch->config == NULL)) {
+ status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ for (groups = g_key_file_get_groups (file, NULL); *groups; groups++) {
+ for (keys = g_key_file_get_keys (file, *groups, NULL, NULL); *keys; keys++) {
+ char *absolute_key = talloc_asprintf(notmuch, "%s.%s", *groups, *keys);
+ val = g_key_file_get_value (file, *groups, *keys, NULL);
+ if (! val) {
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+ _notmuch_string_map_set (notmuch->config, absolute_key, val);
+ talloc_free (absolute_key);
+ if (status)
+ goto DONE;
+ }
+ }
+
+ DONE:
+ return status;
+}
+
+notmuch_status_t
+notmuch_config_get_bool (notmuch_database_t *notmuch, notmuch_config_key_t key, notmuch_bool_t *val)
+{
+ const char *key_string, *val_string;
+
+ key_string = _notmuch_config_key_to_string (key);
+ if (! key_string) {
+ return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
+ }
+
+ val_string = _notmuch_string_map_get (notmuch->config, key_string);
+ if (! val_string) {
+ *val = FALSE;
+ return NOTMUCH_STATUS_SUCCESS;
+ }
+
+ if (strcase_equal (val_string, "false") || strcase_equal (val_string, "no"))
+ *val = FALSE;
+ else if (strcase_equal (val_string, "true") || strcase_equal (val_string, "yes"))
+ *val = TRUE;
+ else
+ return NOTMUCH_STATUS_ILLEGAL_ARGUMENT;
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+static const char *
+_notmuch_config_key_to_string (notmuch_config_key_t key) {
+ switch (key) {
+ case NOTMUCH_CONFIG_DATABASE_PATH:
+ return "database.path";
+ case NOTMUCH_CONFIG_HOOK_DIR:
+ return "database.hook_dir";
+ case NOTMUCH_CONFIG_EXCLUDE_TAGS:
+ return "search.exclude_tags";
+ case NOTMUCH_CONFIG_NEW_TAGS:
+ return "new.tags";
+ case NOTMUCH_CONFIG_NEW_IGNORE:
+ return "new.ignore";
+ case NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS:
+ return "maildir.synchronize_flags";
+ case NOTMUCH_CONFIG_PRIMARY_EMAIL:
+ return "user.primary_email";
+ case NOTMUCH_CONFIG_OTHER_EMAIL:
+ return "user.other_email";
+ case NOTMUCH_CONFIG_USER_NAME:
+ return "user.name";
+ default:
+ return NULL;
+ }
+}
+
+static const char *
+_notmuch_config_default (void *ctx, notmuch_config_key_t key) {
+ char *path;
+
+ switch (key) {
+ case NOTMUCH_CONFIG_DATABASE_PATH:
+ path = getenv ("MAILDIR");
+ if (path)
+ path = talloc_strdup (ctx, path);
+ else
+ path = talloc_asprintf (ctx, "%s/mail",
+ getenv ("HOME"));
+ return path;
+ case NOTMUCH_CONFIG_EXCLUDE_TAGS:
+ return "";
+ case NOTMUCH_CONFIG_NEW_TAGS:
+ return "inbox;unread";
+ case NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS:
+ return "true";
+ case NOTMUCH_CONFIG_HOOK_DIR:
+ case NOTMUCH_CONFIG_NEW_IGNORE:
+ case NOTMUCH_CONFIG_USER_NAME:
+ case NOTMUCH_CONFIG_PRIMARY_EMAIL:
+ case NOTMUCH_CONFIG_OTHER_EMAIL:
+ return NULL;
+ default:
+ case NOTMUCH_CONFIG_LAST:
+ INTERNAL_ERROR ("illegal key enum %d", key);
+ }
+}
+
+notmuch_status_t
+_notmuch_config_load_defaults (notmuch_database_t *notmuch) {
+ notmuch_config_key_t key;
+ for (key = NOTMUCH_CONFIG_FIRST;
+ key < NOTMUCH_CONFIG_LAST;
+ key = notmuch_config_key_t(key + 1)) {
+ const char *val = notmuch_config_get (notmuch, key);
+ const char *key_string = _notmuch_config_key_to_string (key);
+
+ val = _notmuch_string_map_get (notmuch->config, key_string);
+ if (! val) {
+ _notmuch_string_map_set (notmuch->config, key_string, _notmuch_config_default (notmuch, key));
+ }
+ }
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+const char *
+notmuch_config_get (notmuch_database_t *notmuch, notmuch_config_key_t key) {
+
+ return _notmuch_string_map_get (notmuch->config, _notmuch_config_key_to_string (key));
+}
+
+notmuch_status_t
+notmuch_config_set (notmuch_database_t *notmuch, notmuch_config_key_t key, const char *val) {
+
+ return notmuch_database_set_config (notmuch, _notmuch_config_key_to_string (key), val);
+}
+
+void
+_notmuch_config_cache (notmuch_database_t *notmuch, notmuch_config_key_t key, const char *val) {
+ _notmuch_string_map_set (notmuch->config, _notmuch_config_key_to_string (key), val);
+}
#include "notmuch-private.h"
+#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
+
#ifdef SILENCE_XAPIAN_DEPRECATION_WARNINGS
#define XAPIAN_DEPRECATED(D) D
#endif
* here, but at least they are small */
notmuch_string_map_t *user_prefix;
notmuch_string_map_t *user_header;
+
+ /* Cached and possibly overridden configuration */
+ notmuch_string_map_t *config;
};
/* Prior to database version 3, features were implied by the database
const char *value,
Xapian::PostingIterator *begin,
Xapian::PostingIterator *end);
+
+#define NOTMUCH_DATABASE_VERSION 3
+
+/* features.cc */
+
+_notmuch_features
+_notmuch_database_parse_features (const void *ctx, const char *features, unsigned int version,
+ char mode, char **incompat_out);
+
+char *
+_notmuch_database_print_features (const void *ctx, unsigned int features);
+
+/* prefix.cc */
+notmuch_status_t
+_notmuch_database_setup_standard_query_fields (notmuch_database_t *notmuch);
+
+notmuch_status_t
+_notmuch_database_setup_user_query_fields (notmuch_database_t *notmuch);
+
#endif
*/
#include "database-private.h"
-#include "parse-time-vrp.h"
-#include "query-fp.h"
-#include "thread-fp.h"
-#include "regexp-fields.h"
#include "string-util.h"
#include <iostream>
using namespace std;
-#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
-
typedef struct {
const char *name;
const char *prefix;
#define STRINGIFY(s) _SUB_STRINGIFY (s)
#define _SUB_STRINGIFY(s) #s
-#if HAVE_XAPIAN_DB_RETRY_LOCK
-#define DB_ACTION (Xapian::DB_CREATE_OR_OPEN | Xapian::DB_RETRY_LOCK)
-#else
-#define DB_ACTION Xapian::DB_CREATE_OR_OPEN
-#endif
-
#define LOG_XAPIAN_EXCEPTION(message, error) _log_xapian_exception (__location__, message, error)
static void
* same thread.
*/
-/* With these prefix values we follow the conventions published here:
- *
- * https://xapian.org/docs/omega/termprefixes.html
- *
- * as much as makes sense. Note that I took some liberty in matching
- * the reserved prefix values to notmuch concepts, (for example, 'G'
- * is documented as "newsGroup (or similar entity - e.g. a web forum
- * name)", for which I think the thread is the closest analogue in
- * notmuch. This in spite of the fact that we will eventually be
- * storing mailing-list messages where 'G' for "mailing list name"
- * might be even a closer analogue. I'm treating the single-character
- * prefixes preferentially for core notmuch concepts (which will be
- * nearly universal to all mail messages).
- */
-
-static const
-prefix_t prefix_table[] = {
- /* name term prefix flags */
- { "type", "T", NOTMUCH_FIELD_NO_FLAGS },
- { "reference", "XREFERENCE", NOTMUCH_FIELD_NO_FLAGS },
- { "replyto", "XREPLYTO", NOTMUCH_FIELD_NO_FLAGS },
- { "directory", "XDIRECTORY", NOTMUCH_FIELD_NO_FLAGS },
- { "file-direntry", "XFDIRENTRY", NOTMUCH_FIELD_NO_FLAGS },
- { "directory-direntry", "XDDIRENTRY", NOTMUCH_FIELD_NO_FLAGS },
- { "body", "", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROBABILISTIC },
- { "thread", "G", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROCESSOR },
- { "tag", "K", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROCESSOR },
- { "is", "K", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROCESSOR },
- { "id", "Q", NOTMUCH_FIELD_EXTERNAL },
- { "mid", "Q", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROCESSOR },
- { "path", "P", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROCESSOR },
- { "property", "XPROPERTY", NOTMUCH_FIELD_EXTERNAL },
- /*
- * Unconditionally add ':' to reduce potential ambiguity with
- * overlapping prefixes and/or terms that start with capital
- * letters. See Xapian document termprefixes.html for related
- * discussion.
- */
- { "folder", "XFOLDER:", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROCESSOR },
- { "date", NULL, NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROCESSOR },
- { "query", NULL, NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROCESSOR },
- { "from", "XFROM", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROBABILISTIC |
- NOTMUCH_FIELD_PROCESSOR },
- { "to", "XTO", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROBABILISTIC },
- { "attachment", "XATTACHMENT", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROBABILISTIC },
- { "mimetype", "XMIMETYPE", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROBABILISTIC },
- { "subject", "XSUBJECT", NOTMUCH_FIELD_EXTERNAL |
- NOTMUCH_FIELD_PROBABILISTIC |
- NOTMUCH_FIELD_PROCESSOR },
-};
-
-static void
-_setup_query_field_default (const prefix_t *prefix, notmuch_database_t *notmuch)
-{
- if (prefix->prefix)
- notmuch->query_parser->add_prefix ("", prefix->prefix);
- if (prefix->flags & NOTMUCH_FIELD_PROBABILISTIC)
- notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
- else
- notmuch->query_parser->add_boolean_prefix (prefix->name, prefix->prefix);
-}
notmuch_string_map_iterator_t *
_notmuch_database_user_headers (notmuch_database_t *notmuch)
return _notmuch_string_map_iterator_create (notmuch->user_header, "", false);
}
-const char *
-_user_prefix (void *ctx, const char *name)
-{
- return talloc_asprintf (ctx, "XU%s:", name);
-}
-
-static notmuch_status_t
-_setup_user_query_fields (notmuch_database_t *notmuch)
-{
- notmuch_config_list_t *list;
- notmuch_status_t status;
-
- notmuch->user_prefix = _notmuch_string_map_create (notmuch);
- if (notmuch->user_prefix == NULL)
- return NOTMUCH_STATUS_OUT_OF_MEMORY;
-
- notmuch->user_header = _notmuch_string_map_create (notmuch);
- if (notmuch->user_header == NULL)
- return NOTMUCH_STATUS_OUT_OF_MEMORY;
-
- status = notmuch_database_get_config_list (notmuch, CONFIG_HEADER_PREFIX, &list);
- if (status)
- return status;
-
- for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
-
- prefix_t query_field;
-
- const char *key = notmuch_config_list_key (list)
- + sizeof (CONFIG_HEADER_PREFIX) - 1;
-
- _notmuch_string_map_append (notmuch->user_prefix,
- key,
- _user_prefix (notmuch, key));
-
- _notmuch_string_map_append (notmuch->user_header,
- key,
- notmuch_config_list_value (list));
-
- query_field.name = talloc_strdup (notmuch, key);
- query_field.prefix = _user_prefix (notmuch, key);
- query_field.flags = NOTMUCH_FIELD_PROBABILISTIC
- | NOTMUCH_FIELD_EXTERNAL;
-
- _setup_query_field_default (&query_field, notmuch);
- }
-
- notmuch_config_list_destroy (list);
-
- return NOTMUCH_STATUS_SUCCESS;
-}
-
-static void
-_setup_query_field (const prefix_t *prefix, notmuch_database_t *notmuch)
-{
- if (prefix->flags & NOTMUCH_FIELD_PROCESSOR) {
- Xapian::FieldProcessor *fp;
-
- if (STRNCMP_LITERAL (prefix->name, "date") == 0)
- fp = (new DateFieldProcessor(NOTMUCH_VALUE_TIMESTAMP))->release ();
- else if (STRNCMP_LITERAL(prefix->name, "query") == 0)
- fp = (new QueryFieldProcessor (*notmuch->query_parser, notmuch))->release ();
- else if (STRNCMP_LITERAL (prefix->name, "thread") == 0)
- fp = (new ThreadFieldProcessor (*notmuch->query_parser, notmuch))->release ();
- else
- fp = (new RegexpFieldProcessor (prefix->name, prefix->flags,
- *notmuch->query_parser, notmuch))->release ();
-
- /* we treat all field-processor fields as boolean in order to get the raw input */
- if (prefix->prefix)
- notmuch->query_parser->add_prefix ("", prefix->prefix);
- notmuch->query_parser->add_boolean_prefix (prefix->name, fp);
- } else {
- _setup_query_field_default (prefix, notmuch);
- }
-}
-
-const char *
-_find_prefix (const char *name)
-{
- unsigned int i;
-
- for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
- if (strcmp (name, prefix_table[i].name) == 0)
- return prefix_table[i].prefix;
- }
-
- INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
-
- return "";
-}
-
-/* Like find prefix, but include the possibility of user defined
- * prefixes specific to this database */
-
-const char *
-_notmuch_database_prefix (notmuch_database_t *notmuch, const char *name)
-{
- unsigned int i;
-
- /*XXX TODO: reduce code duplication */
- for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
- if (strcmp (name, prefix_table[i].name) == 0)
- return prefix_table[i].prefix;
- }
-
- if (notmuch->user_prefix)
- return _notmuch_string_map_get (notmuch->user_prefix, name);
-
- return NULL;
-}
-
-static const struct {
- /* NOTMUCH_FEATURE_* value. */
- _notmuch_features value;
- /* Feature name as it appears in the database. This name should
- * be appropriate for displaying to the user if an older version
- * of notmuch doesn't support this feature. */
- const char *name;
- /* Compatibility flags when this feature is declared. */
- const char *flags;
-} feature_names[] = {
- { NOTMUCH_FEATURE_FILE_TERMS,
- "multiple paths per message", "rw" },
- { NOTMUCH_FEATURE_DIRECTORY_DOCS,
- "relative directory paths", "rw" },
- /* Header values are not required for reading a database because a
- * reader can just refer to the message file. */
- { NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES,
- "from/subject/message-ID in database", "w" },
- { NOTMUCH_FEATURE_BOOL_FOLDER,
- "exact folder:/path: search", "rw" },
- { NOTMUCH_FEATURE_GHOSTS,
- "mail documents for missing messages", "w" },
- /* Knowledge of the index mime-types are not required for reading
- * a database because a reader will just be unable to query
- * them. */
- { NOTMUCH_FEATURE_INDEXED_MIMETYPES,
- "indexed MIME types", "w" },
- { NOTMUCH_FEATURE_LAST_MOD,
- "modification tracking", "w" },
- /* Existing databases will work fine for all queries not involving
- * 'body:' */
- { NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY,
- "index body and headers separately", "w" },
-};
-
const char *
notmuch_status_to_string (notmuch_status_t status)
{
}
}
-notmuch_status_t
-notmuch_database_create (const char *path, notmuch_database_t **database)
-{
- char *status_string = NULL;
- notmuch_status_t status;
-
- status = notmuch_database_create_verbose (path, database,
- &status_string);
-
- if (status_string) {
- fputs (status_string, stderr);
- free (status_string);
- }
-
- return status;
-}
-
-notmuch_status_t
-notmuch_database_create_verbose (const char *path,
- notmuch_database_t **database,
- char **status_string)
-{
- notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
- notmuch_database_t *notmuch = NULL;
- char *notmuch_path = NULL;
- char *message = NULL;
- struct stat st;
- int err;
-
- if (path == NULL) {
- message = strdup ("Error: Cannot create a database for a NULL path.\n");
- status = NOTMUCH_STATUS_NULL_POINTER;
- goto DONE;
- }
-
- if (path[0] != '/') {
- message = strdup ("Error: Database path must be absolute.\n");
- status = NOTMUCH_STATUS_PATH_ERROR;
- goto DONE;
- }
-
- err = stat (path, &st);
- if (err) {
- IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: %s.\n",
- path, strerror (errno)));
- status = NOTMUCH_STATUS_FILE_ERROR;
- goto DONE;
- }
-
- if (! S_ISDIR (st.st_mode)) {
- IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: "
- "Not a directory.\n",
- path));
- status = NOTMUCH_STATUS_FILE_ERROR;
- goto DONE;
- }
-
- notmuch_path = talloc_asprintf (NULL, "%s/%s", path, ".notmuch");
-
- err = mkdir (notmuch_path, 0755);
-
- if (err) {
- IGNORE_RESULT (asprintf (&message, "Error: Cannot create directory %s: %s.\n",
- notmuch_path, strerror (errno)));
- status = NOTMUCH_STATUS_FILE_ERROR;
- goto DONE;
- }
-
- status = notmuch_database_open_verbose (path,
- NOTMUCH_DATABASE_MODE_READ_WRITE,
- ¬much, &message);
- if (status)
- goto DONE;
-
- /* Upgrade doesn't add these feature to existing databases, but
- * new databases have them. */
- notmuch->features |= NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES;
- notmuch->features |= NOTMUCH_FEATURE_INDEXED_MIMETYPES;
- notmuch->features |= NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY;
-
- status = notmuch_database_upgrade (notmuch, NULL, NULL);
- if (status) {
- notmuch_database_close (notmuch);
- notmuch = NULL;
- }
-
- DONE:
- if (notmuch_path)
- talloc_free (notmuch_path);
-
- if (message) {
- if (status_string)
- *status_string = message;
- else
- free (message);
- }
- if (database)
- *database = notmuch;
- else
- talloc_free (notmuch);
- return status;
-}
-
notmuch_status_t
_notmuch_database_ensure_writable (notmuch_database_t *notmuch)
{
return new_revision;
}
-/* Parse a database features string from the given database version.
- * Returns the feature bit set.
- *
- * For version < 3, this ignores the features string and returns a
- * hard-coded set of features.
- *
- * If there are unrecognized features that are required to open the
- * database in mode (which should be 'r' or 'w'), return a
- * comma-separated list of unrecognized but required features in
- * *incompat_out suitable for presenting to the user. *incompat_out
- * will be allocated from ctx.
- */
-static _notmuch_features
-_parse_features (const void *ctx, const char *features, unsigned int version,
- char mode, char **incompat_out)
-{
- _notmuch_features res = static_cast<_notmuch_features>(0);
- unsigned int namelen, i;
- size_t llen = 0;
- const char *flags;
-
- /* Prior to database version 3, features were implied by the
- * version number. */
- if (version == 0)
- return NOTMUCH_FEATURES_V0;
- else if (version == 1)
- return NOTMUCH_FEATURES_V1;
- else if (version == 2)
- return NOTMUCH_FEATURES_V2;
-
- /* Parse the features string */
- while ((features = strtok_len_c (features + llen, "\n", &llen)) != NULL) {
- flags = strchr (features, '\t');
- if (! flags || flags > features + llen)
- continue;
- namelen = flags - features;
-
- for (i = 0; i < ARRAY_SIZE (feature_names); ++i) {
- if (strlen (feature_names[i].name) == namelen &&
- strncmp (feature_names[i].name, features, namelen) == 0) {
- res |= feature_names[i].value;
- break;
- }
- }
-
- if (i == ARRAY_SIZE (feature_names) && incompat_out) {
- /* Unrecognized feature */
- const char *have = strchr (flags, mode);
- if (have && have < features + llen) {
- /* This feature is required to access this database in
- * 'mode', but we don't understand it. */
- if (! *incompat_out)
- *incompat_out = talloc_strdup (ctx, "");
- *incompat_out = talloc_asprintf_append_buffer (
- *incompat_out, "%s%.*s", **incompat_out ? ", " : "",
- namelen, features);
- }
- }
- }
-
- return res;
-}
-
-static char *
-_print_features (const void *ctx, unsigned int features)
-{
- unsigned int i;
- char *res = talloc_strdup (ctx, "");
-
- for (i = 0; i < ARRAY_SIZE (feature_names); ++i)
- if (features & feature_names[i].value)
- res = talloc_asprintf_append_buffer (
- res, "%s\t%s\n", feature_names[i].name, feature_names[i].flags);
-
- return res;
-}
-
-notmuch_status_t
-notmuch_database_open (const char *path,
- notmuch_database_mode_t mode,
- notmuch_database_t **database)
-{
- char *status_string = NULL;
- notmuch_status_t status;
-
- status = notmuch_database_open_verbose (path, mode, database,
- &status_string);
-
- if (status_string) {
- fputs (status_string, stderr);
- free (status_string);
- }
-
- return status;
-}
-
-notmuch_status_t
-notmuch_database_open_verbose (const char *path,
- notmuch_database_mode_t mode,
- notmuch_database_t **database,
- char **status_string)
-{
- notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
- void *local = talloc_new (NULL);
- notmuch_database_t *notmuch = NULL;
- char *notmuch_path, *xapian_path, *incompat_features;
- char *message = NULL;
- struct stat st;
- int err;
- unsigned int i, version;
- static int initialized = 0;
-
- if (path == NULL) {
- message = strdup ("Error: Cannot open a database for a NULL path.\n");
- status = NOTMUCH_STATUS_NULL_POINTER;
- goto DONE;
- }
-
- if (path[0] != '/') {
- message = strdup ("Error: Database path must be absolute.\n");
- status = NOTMUCH_STATUS_PATH_ERROR;
- goto DONE;
- }
-
- if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
- message = strdup ("Out of memory\n");
- status = NOTMUCH_STATUS_OUT_OF_MEMORY;
- goto DONE;
- }
-
- err = stat (notmuch_path, &st);
- if (err) {
- IGNORE_RESULT (asprintf (&message, "Error opening database at %s: %s\n",
- notmuch_path, strerror (errno)));
- status = NOTMUCH_STATUS_FILE_ERROR;
- goto DONE;
- }
-
- if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
- message = strdup ("Out of memory\n");
- status = NOTMUCH_STATUS_OUT_OF_MEMORY;
- goto DONE;
- }
-
- /* Initialize the GLib type system and threads */
-#if ! GLIB_CHECK_VERSION (2, 35, 1)
- g_type_init ();
-#endif
-
- /* Initialize gmime */
- if (! initialized) {
- g_mime_init ();
- initialized = 1;
- }
-
- notmuch = talloc_zero (NULL, notmuch_database_t);
- notmuch->exception_reported = false;
- notmuch->status_string = NULL;
- notmuch->path = talloc_strdup (notmuch, path);
-
- strip_trailing (notmuch->path, '/');
-
- notmuch->writable_xapian_db = NULL;
- notmuch->atomic_nesting = 0;
- notmuch->view = 1;
- try {
- string last_thread_id;
- string last_mod;
-
- if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
- notmuch->writable_xapian_db = new Xapian::WritableDatabase (xapian_path,
- DB_ACTION);
- notmuch->xapian_db = notmuch->writable_xapian_db;
- } else {
- notmuch->xapian_db = new Xapian::Database (xapian_path);
- }
-
- /* Check version. As of database version 3, we represent
- * changes in terms of features, so assume a version bump
- * means a dramatically incompatible change. */
- version = notmuch_database_get_version (notmuch);
- if (version > NOTMUCH_DATABASE_VERSION) {
- IGNORE_RESULT (asprintf (&message,
- "Error: Notmuch database at %s\n"
- " has a newer database format version (%u) than supported by this\n"
- " version of notmuch (%u).\n",
- notmuch_path, version, NOTMUCH_DATABASE_VERSION));
- notmuch_database_destroy (notmuch);
- notmuch = NULL;
- status = NOTMUCH_STATUS_FILE_ERROR;
- goto DONE;
- }
-
- /* Check features. */
- incompat_features = NULL;
- notmuch->features = _parse_features (
- local, notmuch->xapian_db->get_metadata ("features").c_str (),
- version, mode == NOTMUCH_DATABASE_MODE_READ_WRITE ? 'w' : 'r',
- &incompat_features);
- if (incompat_features) {
- IGNORE_RESULT (asprintf (&message,
- "Error: Notmuch database at %s\n"
- " requires features (%s)\n"
- " not supported by this version of notmuch.\n",
- notmuch_path, incompat_features));
- notmuch_database_destroy (notmuch);
- notmuch = NULL;
- status = NOTMUCH_STATUS_FILE_ERROR;
- goto DONE;
- }
-
- notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();
- last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
- if (last_thread_id.empty ()) {
- notmuch->last_thread_id = 0;
- } else {
- const char *str;
- char *end;
-
- str = last_thread_id.c_str ();
- notmuch->last_thread_id = strtoull (str, &end, 16);
- if (*end != '\0')
- INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
- }
-
- /* Get current highest revision number. */
- last_mod = notmuch->xapian_db->get_value_upper_bound (
- NOTMUCH_VALUE_LAST_MOD);
- if (last_mod.empty ())
- notmuch->revision = 0;
- else
- notmuch->revision = Xapian::sortable_unserialise (last_mod);
- notmuch->uuid = talloc_strdup (
- notmuch, notmuch->xapian_db->get_uuid ().c_str ());
-
- notmuch->query_parser = new Xapian::QueryParser;
- notmuch->term_gen = new Xapian::TermGenerator;
- notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
- notmuch->value_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
- notmuch->date_range_processor = new ParseTimeRangeProcessor (NOTMUCH_VALUE_TIMESTAMP, "date:");
- notmuch->last_mod_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_LAST_MOD, "lastmod:");
- notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
- notmuch->query_parser->set_database (*notmuch->xapian_db);
- notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
- notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
- notmuch->query_parser->add_rangeprocessor (notmuch->value_range_processor);
- notmuch->query_parser->add_rangeprocessor (notmuch->date_range_processor);
- notmuch->query_parser->add_rangeprocessor (notmuch->last_mod_range_processor);
-
- for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
- const prefix_t *prefix = &prefix_table[i];
- if (prefix->flags & NOTMUCH_FIELD_EXTERNAL) {
- _setup_query_field (prefix, notmuch);
- }
- }
- status = _setup_user_query_fields (notmuch);
- } catch (const Xapian::Error &error) {
- IGNORE_RESULT (asprintf (&message, "A Xapian exception occurred opening database: %s\n",
- error.get_msg ().c_str ()));
- notmuch_database_destroy (notmuch);
- notmuch = NULL;
- status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
- }
-
- DONE:
- talloc_free (local);
-
- if (message) {
- if (status_string)
- *status_string = message;
- else
- free (message);
- }
-
- if (database)
- *database = notmuch;
- else
- talloc_free (notmuch);
-
- if (notmuch)
- notmuch->open = true;
-
- return status;
-}
-
notmuch_status_t
notmuch_database_close (notmuch_database_t *notmuch)
{
notmuch_compact_status_cb_t status_cb,
void *closure)
{
- void *local;
- char *notmuch_path, *xapian_path, *compact_xapian_path;
notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
notmuch_database_t *notmuch = NULL;
- struct stat statbuf;
- bool keep_backup;
char *message = NULL;
- local = talloc_new (NULL);
- if (! local)
- return NOTMUCH_STATUS_OUT_OF_MEMORY;
-
ret = notmuch_database_open_verbose (path,
NOTMUCH_DATABASE_MODE_READ_WRITE,
¬much,
&message);
if (ret) {
if (status_cb) status_cb (message, closure);
- goto DONE;
+ return ret;
}
+ _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_DATABASE_PATH, path);
+
+ return notmuch_database_compact_db (notmuch,
+ backup_path,
+ status_cb,
+ closure);
+}
+
+notmuch_status_t
+notmuch_database_compact_db (notmuch_database_t *notmuch,
+ const char *backup_path,
+ notmuch_compact_status_cb_t status_cb,
+ void *closure) {
+ void *local;
+ char *notmuch_path, *xapian_path, *compact_xapian_path;
+ const char* path;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+ struct stat statbuf;
+ bool keep_backup;
+
+ ret = _notmuch_database_ensure_writable (notmuch);
+ if (ret)
+ return ret;
+
+ path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
+ if (! path)
+ return NOTMUCH_STATUS_PATH_ERROR;
+
+ local = talloc_new (NULL);
+ if (! local)
+ return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) {
ret = NOTMUCH_STATUS_OUT_OF_MEMORY;
goto DONE;
}
status = NOTMUCH_STATUS_SUCCESS;
- db->set_metadata ("features", _print_features (local, notmuch->features));
+ db->set_metadata ("features", _notmuch_database_print_features (local, notmuch->features));
db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
DONE:
--- /dev/null
+#include "database-private.h"
+
+static const struct {
+ /* NOTMUCH_FEATURE_* value. */
+ _notmuch_features value;
+ /* Feature name as it appears in the database. This name should
+ * be appropriate for displaying to the user if an older version
+ * of notmuch doesn't support this feature. */
+ const char *name;
+ /* Compatibility flags when this feature is declared. */
+ const char *flags;
+} feature_names[] = {
+ { NOTMUCH_FEATURE_FILE_TERMS,
+ "multiple paths per message", "rw" },
+ { NOTMUCH_FEATURE_DIRECTORY_DOCS,
+ "relative directory paths", "rw" },
+ /* Header values are not required for reading a database because a
+ * reader can just refer to the message file. */
+ { NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES,
+ "from/subject/message-ID in database", "w" },
+ { NOTMUCH_FEATURE_BOOL_FOLDER,
+ "exact folder:/path: search", "rw" },
+ { NOTMUCH_FEATURE_GHOSTS,
+ "mail documents for missing messages", "w" },
+ /* Knowledge of the index mime-types are not required for reading
+ * a database because a reader will just be unable to query
+ * them. */
+ { NOTMUCH_FEATURE_INDEXED_MIMETYPES,
+ "indexed MIME types", "w" },
+ { NOTMUCH_FEATURE_LAST_MOD,
+ "modification tracking", "w" },
+ /* Existing databases will work fine for all queries not involving
+ * 'body:' */
+ { NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY,
+ "index body and headers separately", "w" },
+};
+
+char *
+_notmuch_database_print_features (const void *ctx, unsigned int features)
+{
+ unsigned int i;
+ char *res = talloc_strdup (ctx, "");
+
+ for (i = 0; i < ARRAY_SIZE (feature_names); ++i)
+ if (features & feature_names[i].value)
+ res = talloc_asprintf_append_buffer (
+ res, "%s\t%s\n", feature_names[i].name, feature_names[i].flags);
+
+ return res;
+}
+
+
+/* Parse a database features string from the given database version.
+ * Returns the feature bit set.
+ *
+ * For version < 3, this ignores the features string and returns a
+ * hard-coded set of features.
+ *
+ * If there are unrecognized features that are required to open the
+ * database in mode (which should be 'r' or 'w'), return a
+ * comma-separated list of unrecognized but required features in
+ * *incompat_out suitable for presenting to the user. *incompat_out
+ * will be allocated from ctx.
+ */
+_notmuch_features
+_notmuch_database_parse_features (const void *ctx, const char *features, unsigned int version,
+ char mode, char **incompat_out)
+{
+ _notmuch_features res = static_cast<_notmuch_features>(0);
+ unsigned int namelen, i;
+ size_t llen = 0;
+ const char *flags;
+
+ /* Prior to database version 3, features were implied by the
+ * version number. */
+ if (version == 0)
+ return NOTMUCH_FEATURES_V0;
+ else if (version == 1)
+ return NOTMUCH_FEATURES_V1;
+ else if (version == 2)
+ return NOTMUCH_FEATURES_V2;
+
+ /* Parse the features string */
+ while ((features = strtok_len_c (features + llen, "\n", &llen)) != NULL) {
+ flags = strchr (features, '\t');
+ if (! flags || flags > features + llen)
+ continue;
+ namelen = flags - features;
+
+ for (i = 0; i < ARRAY_SIZE (feature_names); ++i) {
+ if (strlen (feature_names[i].name) == namelen &&
+ strncmp (feature_names[i].name, features, namelen) == 0) {
+ res |= feature_names[i].value;
+ break;
+ }
+ }
+
+ if (i == ARRAY_SIZE (feature_names) && incompat_out) {
+ /* Unrecognized feature */
+ const char *have = strchr (flags, mode);
+ if (have && have < features + llen) {
+ /* This feature is required to access this database in
+ * 'mode', but we don't understand it. */
+ if (! *incompat_out)
+ *incompat_out = talloc_strdup (ctx, "");
+ *incompat_out = talloc_asprintf_append_buffer (
+ *incompat_out, "%s%.*s", **incompat_out ? ", " : "",
+ namelen, features);
+ }
+ }
+ }
+
+ return res;
+}
#define NOTMUCH_CLEAR_BIT(valp, bit) \
(_NOTMUCH_VALID_BIT (bit) ? (*(valp) &= ~(1ull << (bit))) : *(valp))
-#define unused(x) x __attribute__ ((unused))
+#define unused(x) x ## _unused __attribute__ ((unused))
/* Thanks to Andrew Tridgell's (SAMBA's) talloc for this definition of
* unlikely. The talloc source code comes to us via the GNU LGPL v. 3.
NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY = NOTMUCH_STATUS_OUT_OF_MEMORY,
NOTMUCH_PRIVATE_STATUS_READ_ONLY_DATABASE = NOTMUCH_STATUS_READ_ONLY_DATABASE,
NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION = NOTMUCH_STATUS_XAPIAN_EXCEPTION,
+ NOTMUCH_PRIVATE_STATUS_FILE_ERROR = NOTMUCH_STATUS_FILE_ERROR,
NOTMUCH_PRIVATE_STATUS_FILE_NOT_EMAIL = NOTMUCH_STATUS_FILE_NOT_EMAIL,
NOTMUCH_PRIVATE_STATUS_NULL_POINTER = NOTMUCH_STATUS_NULL_POINTER,
NOTMUCH_PRIVATE_STATUS_TAG_TOO_LONG = NOTMUCH_STATUS_TAG_TOO_LONG,
NOTMUCH_PRIVATE_STATUS_UNBALANCED_FREEZE_THAW = NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
+ NOTMUCH_PRIVATE_STATUS_UNBALANCED_ATOMIC = NOTMUCH_STATUS_UNBALANCED_ATOMIC,
+ NOTMUCH_PRIVATE_STATUS_UNSUPPORTED_OPERATION = NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
+ NOTMUCH_PRIVATE_STATUS_UPGRADE_REQUIRED = NOTMUCH_STATUS_UPGRADE_REQUIRED,
+ NOTMUCH_PRIVATE_STATUS_PATH_ERROR = NOTMUCH_STATUS_PATH_ERROR,
+ NOTMUCH_PRIVATE_STATUS_IGNORED = NOTMUCH_STATUS_IGNORED,
+ NOTMUCH_PRIVATE_STATUS_ILLEGAL_ARGUMENT = NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
+ NOTMUCH_PRIVATE_STATUS_MALFORMED_CRYPTO_PROTOCOL = NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL,
+ NOTMUCH_PRIVATE_STATUS_FAILED_CRYPTO_CONTEXT_CREATION = NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION,
+ NOTMUCH_PRIVATE_STATUS_UNKNOWN_CRYPTO_PROTOCOL = NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL,
+ NOTMUCH_PRIVATE_STATUS_NO_CONFIG = NOTMUCH_STATUS_NO_CONFIG,
+ NOTMUCH_PRIVATE_STATUS_DATABASE_EXISTS = NOTMUCH_STATUS_DATABASE_EXISTS,
/* Then add our own private values. */
NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG = NOTMUCH_STATUS_LAST_STATUS,
const char *key,
const char *value);
+void
+_notmuch_string_map_set (notmuch_string_map_t *map,
+ const char *key,
+ const char *value);
+
const char *
_notmuch_string_map_get (notmuch_string_map_t *map, const char *key);
#define EMPTY_STRING(s) ((s)[0] == '\0')
+/* config.cc */
+notmuch_status_t
+_notmuch_config_load_from_database (notmuch_database_t * db);
+
+notmuch_status_t
+_notmuch_config_load_from_file (notmuch_database_t * db, GKeyFile *file);
+
+notmuch_status_t
+_notmuch_config_load_defaults (notmuch_database_t * db);
+
+void
+_notmuch_config_cache (notmuch_database_t *db, notmuch_config_key_t key, const char* val);
+
NOTMUCH_END_DECLS
#ifdef __cplusplus
* something that notmuch doesn't know how to handle.
*/
NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL,
+ /**
+ * Unable to load a config file
+ */
+ NOTMUCH_STATUS_NO_CONFIG,
+ /**
+ * Database exists, so not (re)-created
+ */
+ NOTMUCH_STATUS_DATABASE_EXISTS,
/**
* Not an actual status value. Just a way to find out how many
* valid status values there are.
typedef struct _notmuch_directory notmuch_directory_t;
typedef struct _notmuch_filenames notmuch_filenames_t;
typedef struct _notmuch_config_list notmuch_config_list_t;
+typedef struct _notmuch_config_values notmuch_config_values_t;
typedef struct _notmuch_indexopts notmuch_indexopts_t;
#endif /* __DOXYGEN__ */
} notmuch_database_mode_t;
/**
- * Open an existing notmuch database located at 'path'.
+ * Deprecated alias for notmuch_database_open_with_config with
+ * config_path=error_message=NULL
+ * @deprecated Deprecated as of libnotmuch 5.4 (notmuch 0.32)
+ */
+/* NOTMUCH_DEPRECATED(5, 4) */
+notmuch_status_t
+notmuch_database_open (const char *path,
+ notmuch_database_mode_t mode,
+ notmuch_database_t **database);
+/**
+ * Deprecated alias for notmuch_database_open_with_config with
+ * config_path=NULL
+ *
+ * @deprecated Deprecated as of libnotmuch 5.4 (notmuch 0.32)
+ *
+ */
+/* NOTMUCH_DEPRECATED(5, 4) */
+notmuch_status_t
+notmuch_database_open_verbose (const char *path,
+ notmuch_database_mode_t mode,
+ notmuch_database_t **database,
+ char **error_message);
+
+/**
+ * Open an existing notmuch database located at 'database_path', using
+ * configuration in 'config_path'.
+ *
+ * @param[in] database_path
+ * @parblock
+ * Path to existing database.
+ *
+ * A notmuch database is a Xapian database containing appropriate
+ * metadata.
*
* The database should have been created at some time in the past,
* (not necessarily by this process), by calling
- * notmuch_database_create with 'path'. By default the database should be
- * opened for reading only. In order to write to the database you need to
- * pass the NOTMUCH_DATABASE_MODE_READ_WRITE mode.
+ * notmuch_database_create.
+ *
+ * If 'database_path' is NULL, use the location specified
+ *
+ * - in the environment variable NOTMUCH_DATABASE, if non-empty
+ *
+ * - in a configuration file, located as described under 'config_path'
+ *
+ * - by $XDG_DATA_HOME/notmuch/$PROFILE where XDG_DATA_HOME defaults
+ * to "$HOME/.local/share" and PROFILE as as discussed in
+ * 'profile'
+ *
+ * If 'database_path' is non-NULL, but does not appear to be a Xapian
+ * database, check for a directory '.notmuch/xapian' below
+ * 'database_path' (this is the behavior of
+ * notmuch_database_open_verbose pre-0.32).
+ *
+ * @endparblock
+ * @param[in] mode
+ * @parblock
+ * Mode to open database. Use one of #NOTMUCH_DATABASE_MODE_READ_WRITE
+ * or #NOTMUCH_DATABASE_MODE_READ_ONLY
+ *
+ * @endparblock
+ * @param[in] config_path
+ * @parblock
+ * Path to config file.
+ *
+ * Config file is key-value, with mandatory sections. See
+ * <em>notmuch-config(5)</em> for more information. The key-value pair
+ * overrides the corresponding configuration data stored in the
+ * database (see <em>notmuch_database_get_config</em>)
+ *
+ * If <em>config_path</em> is NULL use the path specified
+ *
+ * - in environment variable <em>NOTMUCH_CONFIG</em>, if non-empty
+ *
+ * - by <em>XDG_CONFIG_HOME</em>/notmuch/ where
+ * XDG_CONFIG_HOME defaults to "$HOME/.config".
+ *
+ * - by $HOME/.notmuch-config
+ *
+ * If <em>config_path</em> is "" (empty string) then do not
+ * open any configuration file.
+ * @endparblock
+ * @param[in] profile:
+ * @parblock
+ * Name of profile (configuration/database variant).
+ *
+ * If non-NULL, append to the directory / file path determined for
+ * <em>config_path</em> and <em>database_path</em>.
*
- * An existing notmuch database can be identified by the presence of a
- * directory named ".notmuch" below 'path'.
+ * If NULL then use
+ * - environment variable NOTMUCH_PROFILE if defined,
+ * - otherwise "default" for directories and "" (empty string) for paths.
+ *
+ * @endparblock
+ * @param[out] database
+ * @parblock
+ * Pointer to database object. May not be NULL.
*
* The caller should call notmuch_database_destroy when finished with
* this database.
*
* In case of any failure, this function returns an error status and
- * sets *database to NULL (after printing an error message on stderr).
+ * sets *database to NULL.
*
- * Return value:
+ * @endparblock
+ * @param[out] error_message
+ * If non-NULL, store error message from opening the database.
+ * Any such message is allocated by \a malloc(3) and should be freed
+ * by the caller.
*
- * NOTMUCH_STATUS_SUCCESS: Successfully opened the database.
+ * @retval NOTMUCH_STATUS_SUCCESS: Successfully opened the database.
*
- * NOTMUCH_STATUS_NULL_POINTER: The given 'path' argument is NULL.
+ * @retval NOTMUCH_STATUS_NULL_POINTER: The given \a database
+ * argument is NULL.
*
- * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory.
+ * @retval NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory.
*
- * NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the
- * database file (such as permission denied, or file not found,
+ * @retval NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the
+ * database or config file (such as permission denied, or file not found,
* etc.), or the database version is unknown.
*
- * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred.
+ * @retval NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred.
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
*/
+
notmuch_status_t
-notmuch_database_open (const char *path,
- notmuch_database_mode_t mode,
- notmuch_database_t **database);
+notmuch_database_open_with_config (const char *database_path,
+ notmuch_database_mode_t mode,
+ const char *config_path,
+ const char *profile,
+ notmuch_database_t **database,
+ char **error_message);
/**
- * Like notmuch_database_open, except optionally return an error
- * message. This message is allocated by malloc and should be freed by
- * the caller.
+ * Create a new notmuch database located at 'database_path', using
+ * configuration in 'config_path'.
+ *
+ * For description of arguments, @see notmuch_database_open_with_config
+ *
+ * @retval NOTMUCH_STATUS_SUCCESS: Successfully created the database.
+ *
+ * @retval NOTMUCH_STATUS_DATABASE_EXISTS: Database already exists, not created
+ *
+ * @retval NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory.
+ *
+ * @retval NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the
+ * database or config file (such as permission denied, or file not found,
+ * etc.)
+ *
+ * @retval NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred.
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
*/
notmuch_status_t
-notmuch_database_open_verbose (const char *path,
- notmuch_database_mode_t mode,
- notmuch_database_t **database,
- char **error_message);
+notmuch_database_create_with_config (const char *database_path,
+ const char *config_path,
+ const char *profile,
+ notmuch_database_t **database,
+ char **error_message);
/**
* Retrieve last status string for given database.
notmuch_compact_status_cb_t status_cb,
void *closure);
+/**
+ * Like notmuch_database_compact, but take an open database as a
+ * parameter.
+ *
+ * @since libnnotmuch 5.4 (notmuch 0.32)
+ */
+notmuch_status_t
+notmuch_database_compact_db (notmuch_database_t *database,
+ const char *backup_path,
+ notmuch_compact_status_cb_t status_cb,
+ void *closure);
+
/**
* Destroy the notmuch database, closing it if necessary and freeing
* all associated resources.
* set config 'key' to 'value'
*
* @since libnotmuch 4.4 (notmuch 0.23)
+ * @retval #NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in
+ * read-only mode so message cannot be modified.
+ * @retval #NOTMUCH_STATUS_XAPIAN_EXCEPTION: an exception was thrown
+ * accessing the database.
+ * @retval #NOTMUCH_STATUS_SUCCESS
*/
notmuch_status_t
notmuch_database_set_config (notmuch_database_t *db, const char *key, const char *value);
* caller.
*
* @since libnotmuch 4.4 (notmuch 0.23)
+ *
*/
notmuch_status_t
notmuch_database_get_config (notmuch_database_t *db, const char *key, char **value);
notmuch_config_list_destroy (notmuch_config_list_t *config_list);
+/**
+ * Configuration keys known to libnotmuch
+ */
+typedef enum _notmuch_config_key {
+ NOTMUCH_CONFIG_FIRST,
+ NOTMUCH_CONFIG_DATABASE_PATH = NOTMUCH_CONFIG_FIRST,
+ NOTMUCH_CONFIG_HOOK_DIR,
+ NOTMUCH_CONFIG_EXCLUDE_TAGS,
+ NOTMUCH_CONFIG_NEW_TAGS,
+ NOTMUCH_CONFIG_NEW_IGNORE,
+ NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+ NOTMUCH_CONFIG_PRIMARY_EMAIL,
+ NOTMUCH_CONFIG_OTHER_EMAIL,
+ NOTMUCH_CONFIG_USER_NAME,
+ NOTMUCH_CONFIG_LAST
+} notmuch_config_key_t;
+
+/**
+ * get a configuration value from an open database.
+ *
+ * This value reflects all configuration information given at the time
+ * the database was opened.
+ *
+ * @param[in] notmuch database
+ * @param[in] key configuration key
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval NULL if 'key' unknown or if no value is known for
+ * 'key'. Otherwise returns a string owned by notmuch which should
+ * not be modified nor freed by the caller.
+ */
+const char *
+notmuch_config_get (notmuch_database_t *notmuch, notmuch_config_key_t key);
+
+/**
+ * set a configuration value from in an open database.
+ *
+ * This value reflects all configuration information given at the time
+ * the database was opened.
+ *
+ * @param[in,out] notmuch database open read/write
+ * @param[in] key configuration key
+ * @param[in] val configuration value
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval returns any return value for notmuch_database_set_config.
+ */
+notmuch_status_t
+notmuch_config_set (notmuch_database_t *notmuch, notmuch_config_key_t key, const char *val);
+
+/**
+ * Returns an iterator for a ';'-delimited list of configuration values
+ *
+ * These values reflect all configuration information given at the
+ * time the database was opened.
+ *
+ * @param[in] notmuch database
+ * @param[in] key configuration key
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval NULL in case of error.
+ */
+notmuch_config_values_t *
+notmuch_config_get_values (notmuch_database_t *notmuch, notmuch_config_key_t key);
+
+/**
+ * Is the given 'config_values' iterator pointing at a valid element.
+ *
+ * @param[in] values iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval FALSE if passed a NULL pointer, or the iterator is exhausted.
+ *
+ */
+notmuch_bool_t
+notmuch_config_values_valid (notmuch_config_values_t *values);
+
+/**
+ * Get the current value from the 'values' iterator
+ *
+ * @param[in] values iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval a string with the same lifetime as the iterator
+ */
+const char *
+notmuch_config_values_get (notmuch_config_values_t *values);
+
+/**
+ * Move the 'values' iterator to the next element
+ *
+ * @param[in,out] values iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ */
+void
+notmuch_config_values_move_to_next (notmuch_config_values_t *values);
+
+
+/**
+ * reset the 'values' iterator to the first element
+ *
+ * @param[in,out] values iterator. A NULL value is ignored.
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ */
+void
+notmuch_config_values_start (notmuch_config_values_t *values);
+
+/**
+ * Destroy a config values iterator, along with any associated
+ * resources.
+ *
+ * @param[in,out] values iterator
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ */
+void
+notmuch_config_values_destroy (notmuch_config_values_t *values);
+
+/**
+ * get a configuration value from an open database as Boolean
+ *
+ * This value reflects all configuration information given at the time
+ * the database was opened.
+ *
+ * @param[in] notmuch database
+ * @param[in] key configuration key
+ * @param[out] val configuration value, converted to Boolean
+ *
+ * @since libnotmuch 5.4 (notmuch 0.32)
+ *
+ * @retval #NOTMUCH_STATUS_ILLEGAL_ARGUMENT if either key is unknown
+ * or the corresponding value does not convert to Boolean.
+ */
+notmuch_status_t
+notmuch_config_get_bool (notmuch_database_t *notmuch,
+ notmuch_config_key_t key,
+ notmuch_bool_t *val);
/**
* get the current default indexing options for a given database.
*
--- /dev/null
+#include <unistd.h>
+#include "database-private.h"
+#include "parse-time-vrp.h"
+
+#if HAVE_XAPIAN_DB_RETRY_LOCK
+#define DB_ACTION (Xapian::DB_CREATE_OR_OPEN | Xapian::DB_RETRY_LOCK)
+#else
+#define DB_ACTION Xapian::DB_CREATE_OR_OPEN
+#endif
+
+notmuch_status_t
+notmuch_database_open (const char *path,
+ notmuch_database_mode_t mode,
+ notmuch_database_t **database)
+{
+ char *status_string = NULL;
+ notmuch_status_t status;
+
+ status = notmuch_database_open_verbose (path, mode, database,
+ &status_string);
+
+ if (status_string) {
+ fputs (status_string, stderr);
+ free (status_string);
+ }
+
+ return status;
+}
+
+notmuch_status_t
+notmuch_database_open_verbose (const char *path,
+ notmuch_database_mode_t mode,
+ notmuch_database_t **database,
+ char **status_string)
+{
+ return notmuch_database_open_with_config (path, mode, "", NULL,
+ database, status_string);
+}
+
+static const char *
+_xdg_dir (void *ctx,
+ const char *xdg_root_variable,
+ const char *xdg_prefix,
+ const char *profile_name)
+{
+ const char *xdg_root = getenv (xdg_root_variable);
+
+ if (! xdg_root) {
+ const char *home = getenv ("HOME");
+
+ if (! home) return NULL;
+
+ xdg_root = talloc_asprintf (ctx,
+ "%s/%s",
+ home,
+ xdg_prefix);
+ }
+
+ if (! profile_name)
+ profile_name = getenv ("NOTMUCH_PROFILE");
+
+ if (! profile_name)
+ profile_name = "default";
+
+ return talloc_asprintf (ctx,
+ "%s/notmuch/%s",
+ xdg_root,
+ profile_name);
+}
+
+static notmuch_status_t
+_choose_hook_dir (notmuch_database_t *notmuch,
+ const char *profile,
+ char **message)
+{
+ const char *config;
+ const char *hook_dir;
+ struct stat st;
+ int err;
+
+ hook_dir = notmuch_config_get (notmuch, NOTMUCH_CONFIG_HOOK_DIR);
+
+ if (hook_dir)
+ return NOTMUCH_STATUS_SUCCESS;
+
+ config = _xdg_dir (notmuch, "XDG_CONFIG_HOME", ".config", profile);
+ if (! config)
+ return NOTMUCH_STATUS_PATH_ERROR;
+
+ hook_dir = talloc_asprintf (notmuch, "%s/hooks", config);
+
+ err = stat (hook_dir, &st);
+ if (err) {
+ if (errno == ENOENT) {
+ const char *database_path = notmuch_database_get_path (notmuch);
+ hook_dir = talloc_asprintf (notmuch, "%s/.notmuch/hooks", database_path);
+ } else {
+ IGNORE_RESULT (asprintf (message, "Error: Cannot stat %s: %s.\n",
+ hook_dir, strerror (errno)));
+ return NOTMUCH_STATUS_FILE_ERROR;
+ }
+ }
+
+ _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_HOOK_DIR, hook_dir);
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_load_key_file (const char *path,
+ const char *profile,
+ GKeyFile **key_file)
+{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+ void *local = talloc_new (NULL);
+
+ if (path && EMPTY_STRING (path))
+ goto DONE;
+
+ if (! path)
+ path = getenv ("NOTMUCH_CONFIG");
+
+ if (! path) {
+ const char *dir = _xdg_dir (local, "XDG_CONFIG_HOME", ".config", profile);
+
+ if (dir) {
+ path = talloc_asprintf (local, "%s/config", dir);
+ if (access (path, R_OK) !=0)
+ path = NULL;
+ }
+ }
+
+ if (! path) {
+ const char *home = getenv ("HOME");
+
+ path = talloc_asprintf (local, "%s/.notmuch-config", home);
+
+ if (! profile)
+ profile = getenv ("NOTMUCH_PROFILE");
+
+ if (profile)
+ path = talloc_asprintf (local, "%s.%s", path, profile);
+ }
+
+ *key_file = g_key_file_new ();
+ if (! g_key_file_load_from_file (*key_file, path, G_KEY_FILE_NONE, NULL)) {
+ status = NOTMUCH_STATUS_NO_CONFIG;
+ }
+
+DONE:
+ talloc_free (local);
+ return status;
+}
+
+static notmuch_status_t
+_choose_database_path (const char *config_path,
+ const char *profile,
+ GKeyFile **key_file,
+ const char **database_path,
+ char **message)
+{
+ notmuch_status_t status;
+
+ status =_load_key_file (config_path, profile, key_file);
+ if (status) {
+ *message = strdup ("Error: cannot load config file.\n");
+ return status;
+ }
+
+ if (! *database_path && *key_file)
+ *database_path = g_key_file_get_value (*key_file, "database", "path", NULL);
+
+ if (*database_path == NULL) {
+ *message = strdup ("Error: Cannot open a database for a NULL path.\n");
+ return NOTMUCH_STATUS_NULL_POINTER;
+ }
+
+ if (*database_path[0] != '/') {
+ *message = strdup ("Error: Database path must be absolute.\n");
+ return NOTMUCH_STATUS_PATH_ERROR;
+ }
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_database_open_with_config (const char *database_path,
+ notmuch_database_mode_t mode,
+ const char *config_path,
+ const char *profile,
+ notmuch_database_t **database,
+ char **status_string)
+{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+ void *local = talloc_new (NULL);
+ notmuch_database_t *notmuch = NULL;
+ char *notmuch_path, *xapian_path, *incompat_features;
+ char *message = NULL;
+ struct stat st;
+ int err;
+ unsigned int version;
+ GKeyFile *key_file = NULL;
+ static int initialized = 0;
+
+ if ((status = _choose_database_path (config_path, profile, &key_file, &database_path, &message)))
+ goto DONE;
+
+ if (! (notmuch_path = talloc_asprintf (local, "%s/%s", database_path, ".notmuch"))) {
+ message = strdup ("Out of memory\n");
+ status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ err = stat (notmuch_path, &st);
+ if (err) {
+ IGNORE_RESULT (asprintf (&message, "Error opening database at %s: %s\n",
+ notmuch_path, strerror (errno)));
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) {
+ message = strdup ("Out of memory\n");
+ status = NOTMUCH_STATUS_OUT_OF_MEMORY;
+ goto DONE;
+ }
+
+ /* Initialize the GLib type system and threads */
+#if ! GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+
+ /* Initialize gmime */
+ if (! initialized) {
+ g_mime_init ();
+ initialized = 1;
+ }
+
+ notmuch = talloc_zero (NULL, notmuch_database_t);
+ notmuch->exception_reported = false;
+ notmuch->status_string = NULL;
+ notmuch->path = talloc_strdup (notmuch, database_path);
+
+ strip_trailing (notmuch->path, '/');
+
+ notmuch->writable_xapian_db = NULL;
+ notmuch->atomic_nesting = 0;
+ notmuch->view = 1;
+ try {
+ std::string last_thread_id;
+ std::string last_mod;
+
+ if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
+ notmuch->writable_xapian_db = new Xapian::WritableDatabase (xapian_path,
+ DB_ACTION);
+ notmuch->xapian_db = notmuch->writable_xapian_db;
+ } else {
+ notmuch->xapian_db = new Xapian::Database (xapian_path);
+ }
+
+ /* Check version. As of database version 3, we represent
+ * changes in terms of features, so assume a version bump
+ * means a dramatically incompatible change. */
+ version = notmuch_database_get_version (notmuch);
+ if (version > NOTMUCH_DATABASE_VERSION) {
+ IGNORE_RESULT (asprintf (&message,
+ "Error: Notmuch database at %s\n"
+ " has a newer database format version (%u) than supported by this\n"
+ " version of notmuch (%u).\n",
+ notmuch_path, version, NOTMUCH_DATABASE_VERSION));
+ notmuch_database_destroy (notmuch);
+ notmuch = NULL;
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ /* Check features. */
+ incompat_features = NULL;
+ notmuch->features = _notmuch_database_parse_features (
+ local, notmuch->xapian_db->get_metadata ("features").c_str (),
+ version, mode == NOTMUCH_DATABASE_MODE_READ_WRITE ? 'w' : 'r',
+ &incompat_features);
+ if (incompat_features) {
+ IGNORE_RESULT (asprintf (&message,
+ "Error: Notmuch database at %s\n"
+ " requires features (%s)\n"
+ " not supported by this version of notmuch.\n",
+ notmuch_path, incompat_features));
+ notmuch_database_destroy (notmuch);
+ notmuch = NULL;
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid ();
+ last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
+ if (last_thread_id.empty ()) {
+ notmuch->last_thread_id = 0;
+ } else {
+ const char *str;
+ char *end;
+
+ str = last_thread_id.c_str ();
+ notmuch->last_thread_id = strtoull (str, &end, 16);
+ if (*end != '\0')
+ INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
+ }
+
+ /* Get current highest revision number. */
+ last_mod = notmuch->xapian_db->get_value_upper_bound (
+ NOTMUCH_VALUE_LAST_MOD);
+ if (last_mod.empty ())
+ notmuch->revision = 0;
+ else
+ notmuch->revision = Xapian::sortable_unserialise (last_mod);
+ notmuch->uuid = talloc_strdup (
+ notmuch, notmuch->xapian_db->get_uuid ().c_str ());
+
+ notmuch->query_parser = new Xapian::QueryParser;
+ notmuch->term_gen = new Xapian::TermGenerator;
+ notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
+ notmuch->value_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
+ notmuch->date_range_processor = new ParseTimeRangeProcessor (NOTMUCH_VALUE_TIMESTAMP, "date:");
+ notmuch->last_mod_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_LAST_MOD, "lastmod:");
+ notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
+ notmuch->query_parser->set_database (*notmuch->xapian_db);
+ notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
+ notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
+ notmuch->query_parser->add_rangeprocessor (notmuch->value_range_processor);
+ notmuch->query_parser->add_rangeprocessor (notmuch->date_range_processor);
+ notmuch->query_parser->add_rangeprocessor (notmuch->last_mod_range_processor);
+
+ /* Configuration information is needed to set up query parser */
+ status = _notmuch_config_load_from_database (notmuch);
+ if (status)
+ goto DONE;
+
+ if (key_file)
+ status = _notmuch_config_load_from_file (notmuch, key_file);
+ if (status)
+ goto DONE;
+
+ status = _choose_hook_dir (notmuch, profile, &message);
+ if (status)
+ goto DONE;
+
+ status = _notmuch_config_load_defaults (notmuch);
+ if (status)
+ goto DONE;
+
+ status = _notmuch_database_setup_standard_query_fields (notmuch);
+ if (status)
+ goto DONE;
+
+ status = _notmuch_database_setup_user_query_fields (notmuch);
+ if (status)
+ goto DONE;
+
+ } catch (const Xapian::Error &error) {
+ IGNORE_RESULT (asprintf (&message, "A Xapian exception occurred opening database: %s\n",
+ error.get_msg ().c_str ()));
+ notmuch_database_destroy (notmuch);
+ notmuch = NULL;
+ status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+ }
+
+ DONE:
+ talloc_free (local);
+
+ if (message) {
+ if (status_string)
+ *status_string = message;
+ else
+ free (message);
+ }
+
+ if (database)
+ *database = notmuch;
+ else
+ talloc_free (notmuch);
+
+ if (notmuch)
+ notmuch->open = true;
+
+ return status;
+}
+
+notmuch_status_t
+notmuch_database_create (const char *path, notmuch_database_t **database)
+{
+ char *status_string = NULL;
+ notmuch_status_t status;
+
+ status = notmuch_database_create_verbose (path, database,
+ &status_string);
+
+ if (status_string) {
+ fputs (status_string, stderr);
+ free (status_string);
+ }
+
+ return status;
+}
+
+notmuch_status_t
+notmuch_database_create_verbose (const char *path,
+ notmuch_database_t **database,
+ char **status_string)
+{
+ return notmuch_database_create_with_config (path, "", NULL, database, status_string);
+}
+
+notmuch_status_t
+notmuch_database_create_with_config (const char *database_path,
+ const char *config_path,
+ const char *profile,
+ notmuch_database_t **database,
+ char **status_string)
+{
+ notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
+ notmuch_database_t *notmuch = NULL;
+ char *notmuch_path = NULL;
+ char *message = NULL;
+ GKeyFile *key_file = NULL;
+ struct stat st;
+ int err;
+
+ if ((status = _choose_database_path (config_path, profile, &key_file, &database_path, &message)))
+ goto DONE;
+
+ err = stat (database_path, &st);
+ if (err) {
+ IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: %s.\n",
+ database_path, strerror (errno)));
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ if (! S_ISDIR (st.st_mode)) {
+ IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: "
+ "Not a directory.\n",
+ database_path));
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ notmuch_path = talloc_asprintf (NULL, "%s/%s", database_path, ".notmuch");
+
+ err = mkdir (notmuch_path, 0755);
+ if (err) {
+ if (errno == EEXIST) {
+ status = NOTMUCH_STATUS_DATABASE_EXISTS;
+ } else {
+ IGNORE_RESULT (asprintf (&message, "Error: Cannot create directory %s: %s.\n",
+ notmuch_path, strerror (errno)));
+ status = NOTMUCH_STATUS_FILE_ERROR;
+ }
+ goto DONE;
+ }
+
+ /* XXX this reads the config file twice, which is a bit wasteful */
+ status = notmuch_database_open_with_config (database_path,
+ NOTMUCH_DATABASE_MODE_READ_WRITE,
+ config_path,
+ profile,
+ ¬much, &message);
+ if (status)
+ goto DONE;
+
+ /* Upgrade doesn't add these feature to existing databases, but
+ * new databases have them. */
+ notmuch->features |= NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES;
+ notmuch->features |= NOTMUCH_FEATURE_INDEXED_MIMETYPES;
+ notmuch->features |= NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY;
+
+ status = notmuch_database_upgrade (notmuch, NULL, NULL);
+ if (status) {
+ notmuch_database_close (notmuch);
+ notmuch = NULL;
+ }
+
+ DONE:
+ if (notmuch_path)
+ talloc_free (notmuch_path);
+
+ if (message) {
+ if (status_string)
+ *status_string = message;
+ else
+ free (message);
+ }
+ if (database)
+ *database = notmuch;
+ else
+ talloc_free (notmuch);
+ return status;
+}
--- /dev/null
+#include "database-private.h"
+#include "query-fp.h"
+#include "thread-fp.h"
+#include "regexp-fields.h"
+#include "parse-time-vrp.h"
+
+typedef struct {
+ const char *name;
+ const char *prefix;
+ notmuch_field_flag_t flags;
+} prefix_t;
+
+/* With these prefix values we follow the conventions published here:
+ *
+ * https://xapian.org/docs/omega/termprefixes.html
+ *
+ * as much as makes sense. Note that I took some liberty in matching
+ * the reserved prefix values to notmuch concepts, (for example, 'G'
+ * is documented as "newsGroup (or similar entity - e.g. a web forum
+ * name)", for which I think the thread is the closest analogue in
+ * notmuch. This in spite of the fact that we will eventually be
+ * storing mailing-list messages where 'G' for "mailing list name"
+ * might be even a closer analogue. I'm treating the single-character
+ * prefixes preferentially for core notmuch concepts (which will be
+ * nearly universal to all mail messages).
+ */
+
+static const
+prefix_t prefix_table[] = {
+ /* name term prefix flags */
+ { "type", "T", NOTMUCH_FIELD_NO_FLAGS },
+ { "reference", "XREFERENCE", NOTMUCH_FIELD_NO_FLAGS },
+ { "replyto", "XREPLYTO", NOTMUCH_FIELD_NO_FLAGS },
+ { "directory", "XDIRECTORY", NOTMUCH_FIELD_NO_FLAGS },
+ { "file-direntry", "XFDIRENTRY", NOTMUCH_FIELD_NO_FLAGS },
+ { "directory-direntry", "XDDIRENTRY", NOTMUCH_FIELD_NO_FLAGS },
+ { "body", "", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROBABILISTIC },
+ { "thread", "G", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROCESSOR },
+ { "tag", "K", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROCESSOR },
+ { "is", "K", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROCESSOR },
+ { "id", "Q", NOTMUCH_FIELD_EXTERNAL },
+ { "mid", "Q", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROCESSOR },
+ { "path", "P", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROCESSOR },
+ { "property", "XPROPERTY", NOTMUCH_FIELD_EXTERNAL },
+ /*
+ * Unconditionally add ':' to reduce potential ambiguity with
+ * overlapping prefixes and/or terms that start with capital
+ * letters. See Xapian document termprefixes.html for related
+ * discussion.
+ */
+ { "folder", "XFOLDER:", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROCESSOR },
+ { "date", NULL, NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROCESSOR },
+ { "query", NULL, NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROCESSOR },
+ { "from", "XFROM", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROBABILISTIC |
+ NOTMUCH_FIELD_PROCESSOR },
+ { "to", "XTO", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROBABILISTIC },
+ { "attachment", "XATTACHMENT", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROBABILISTIC },
+ { "mimetype", "XMIMETYPE", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROBABILISTIC },
+ { "subject", "XSUBJECT", NOTMUCH_FIELD_EXTERNAL |
+ NOTMUCH_FIELD_PROBABILISTIC |
+ NOTMUCH_FIELD_PROCESSOR },
+};
+
+static const char *
+_user_prefix (void *ctx, const char *name)
+{
+ return talloc_asprintf (ctx, "XU%s:", name);
+}
+
+const char *
+_find_prefix (const char *name)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
+ if (strcmp (name, prefix_table[i].name) == 0)
+ return prefix_table[i].prefix;
+ }
+
+ INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
+
+ return "";
+}
+
+/* Like find prefix, but include the possibility of user defined
+ * prefixes specific to this database */
+
+const char *
+_notmuch_database_prefix (notmuch_database_t *notmuch, const char *name)
+{
+ unsigned int i;
+
+ /*XXX TODO: reduce code duplication */
+ for (i = 0; i < ARRAY_SIZE (prefix_table); i++) {
+ if (strcmp (name, prefix_table[i].name) == 0)
+ return prefix_table[i].prefix;
+ }
+
+ if (notmuch->user_prefix)
+ return _notmuch_string_map_get (notmuch->user_prefix, name);
+
+ return NULL;
+}
+
+static void
+_setup_query_field_default (const prefix_t *prefix, notmuch_database_t *notmuch)
+{
+ if (prefix->prefix)
+ notmuch->query_parser->add_prefix ("", prefix->prefix);
+ if (prefix->flags & NOTMUCH_FIELD_PROBABILISTIC)
+ notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
+ else
+ notmuch->query_parser->add_boolean_prefix (prefix->name, prefix->prefix);
+}
+
+static void
+_setup_query_field (const prefix_t *prefix, notmuch_database_t *notmuch)
+{
+ if (prefix->flags & NOTMUCH_FIELD_PROCESSOR) {
+ Xapian::FieldProcessor *fp;
+
+ if (STRNCMP_LITERAL (prefix->name, "date") == 0)
+ fp = (new DateFieldProcessor(NOTMUCH_VALUE_TIMESTAMP))->release ();
+ else if (STRNCMP_LITERAL(prefix->name, "query") == 0)
+ fp = (new QueryFieldProcessor (*notmuch->query_parser, notmuch))->release ();
+ else if (STRNCMP_LITERAL (prefix->name, "thread") == 0)
+ fp = (new ThreadFieldProcessor (*notmuch->query_parser, notmuch))->release ();
+ else
+ fp = (new RegexpFieldProcessor (prefix->name, prefix->flags,
+ *notmuch->query_parser, notmuch))->release ();
+
+ /* we treat all field-processor fields as boolean in order to get the raw input */
+ if (prefix->prefix)
+ notmuch->query_parser->add_prefix ("", prefix->prefix);
+ notmuch->query_parser->add_boolean_prefix (prefix->name, fp);
+ } else {
+ _setup_query_field_default (prefix, notmuch);
+ }
+}
+
+notmuch_status_t
+_notmuch_database_setup_standard_query_fields (notmuch_database_t *notmuch)
+{
+ for (unsigned int i = 0; i < ARRAY_SIZE (prefix_table); i++) {
+ const prefix_t *prefix = &prefix_table[i];
+ if (prefix->flags & NOTMUCH_FIELD_EXTERNAL) {
+ _setup_query_field (prefix, notmuch);
+ }
+ }
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+_notmuch_database_setup_user_query_fields (notmuch_database_t *notmuch)
+{
+ notmuch_string_map_iterator_t *list;
+
+ notmuch->user_prefix = _notmuch_string_map_create (notmuch);
+ if (notmuch->user_prefix == NULL)
+ return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+ notmuch->user_header = _notmuch_string_map_create (notmuch);
+ if (notmuch->user_header == NULL)
+ return NOTMUCH_STATUS_OUT_OF_MEMORY;
+
+ list = _notmuch_string_map_iterator_create (notmuch->config, CONFIG_HEADER_PREFIX, FALSE);
+ if (! list)
+ INTERNAL_ERROR ("unable to read headers from configuration");
+
+ for (; _notmuch_string_map_iterator_valid (list);
+ _notmuch_string_map_iterator_move_to_next (list)) {
+
+ prefix_t query_field;
+
+ const char *key = _notmuch_string_map_iterator_key (list)
+ + sizeof (CONFIG_HEADER_PREFIX) - 1;
+
+ _notmuch_string_map_append (notmuch->user_prefix,
+ key,
+ _user_prefix (notmuch, key));
+
+ _notmuch_string_map_append (notmuch->user_header,
+ key,
+ _notmuch_string_map_iterator_value (list));
+
+ query_field.name = talloc_strdup (notmuch, key);
+ query_field.prefix = _user_prefix (notmuch, key);
+ query_field.flags = NOTMUCH_FIELD_PROBABILISTIC
+ | NOTMUCH_FIELD_EXTERNAL;
+
+ _setup_query_field_default (&query_field, notmuch);
+ }
+
+ _notmuch_string_map_iterator_destroy (list);
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
}
+void
+_notmuch_string_map_set (notmuch_string_map_t *map,
+ const char *key,
+ const char *val)
+{
+ notmuch_string_pair_t *pair;
+
+ /* this means that calling string_map_set invalidates iterators */
+ _notmuch_string_map_sort (map);
+ pair = bsearch_first (map->pairs, map->length, key, true);
+ if (! pair)
+ _notmuch_string_map_append (map, key, val);
+ else {
+ talloc_free (pair->value);
+ pair->value = talloc_strdup (map->pairs, val);
+ }
+}
+
const char *
_notmuch_string_map_get (notmuch_string_map_t *map, const char *key)
{
notmuch_exit_if_unsupported_format (void);
int
-notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_count_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_dump_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_new_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_insert_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_reindex_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_reply_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_restore_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_search_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_address_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_address_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_setup_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_setup_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_show_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_tag_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_config_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_config_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
int
-notmuch_compact_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_compact_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
const char *
notmuch_time_relative_date (const void *ctx, time_t then);
/* notmuch-config.c */
typedef enum {
- NOTMUCH_CONFIG_OPEN = 1 << 0,
- NOTMUCH_CONFIG_CREATE = 1 << 1,
-} notmuch_config_mode_t;
+ NOTMUCH_COMMAND_CONFIG_OPEN = 1 << 0,
+ NOTMUCH_COMMAND_CONFIG_CREATE = 1 << 1,
+ NOTMUCH_COMMAND_DATABASE_EARLY = 1 << 2,
+ NOTMUCH_COMMAND_DATABASE_WRITE = 1 << 3,
+ NOTMUCH_COMMAND_DATABASE_CREATE = 1 << 4,
+} notmuch_command_mode_t;
notmuch_config_t *
notmuch_config_open (void *ctx,
const char *filename,
- notmuch_config_mode_t config_mode);
+ notmuch_command_mode_t config_mode);
void
notmuch_config_close (notmuch_config_t *config);
notmuch_config_set_search_exclude_tags (notmuch_config_t *config,
const char *list[],
size_t length);
+const char *
+_notmuch_config_get_path (notmuch_config_t *config);
int
-notmuch_run_hook (const char *db_path, const char *hook);
+notmuch_run_hook (notmuch_database_t *notmuch, const char *hook);
bool
debugger_is_active (void);
}
int
-notmuch_compact_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_compact_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch, int argc, char *argv[])
{
- const char *path = notmuch_config_get_database_path (config);
const char *backup_path = NULL;
notmuch_status_t ret;
bool quiet = false;
if (! quiet)
printf ("Compacting database...\n");
- ret = notmuch_database_compact (path, backup_path,
- quiet ? NULL : status_update_cb, NULL);
+ ret = notmuch_database_compact_db (notmuch, backup_path,
+ quiet ? NULL : status_update_cb, NULL);
if (ret) {
fprintf (stderr, "Compaction failed: %s\n", notmuch_status_to_string (ret));
return EXIT_FAILURE;
notmuch_config_t *
notmuch_config_open (void *ctx,
const char *filename,
- notmuch_config_mode_t config_mode)
+ notmuch_command_mode_t config_mode)
{
GError *error = NULL;
size_t tmp;
config->key_file = g_key_file_new ();
- if (config_mode & NOTMUCH_CONFIG_OPEN) {
- bool create_new = (config_mode & NOTMUCH_CONFIG_CREATE) != 0;
+ if (config_mode & NOTMUCH_COMMAND_CONFIG_OPEN) {
+ bool create_new = (config_mode & NOTMUCH_COMMAND_CONFIG_CREATE) != 0;
if (! get_config_from_file (config, create_new)) {
talloc_free (config);
talloc_free (config);
}
+const char *_notmuch_config_get_path (notmuch_config_t *config) {
+ return config->filename;
+}
/* Save any changes made to the notmuch configuration.
*
* Any comments originally in the file will be preserved.
}
int
-notmuch_config_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_config_command (notmuch_config_t *config, unused(notmuch_database_t *notmuch), int argc, char *argv[])
{
int ret;
int opt_index;
/* return 0 on success, -1 on failure */
static int
print_count (notmuch_database_t *notmuch, const char *query_str,
- const char **exclude_tags, size_t exclude_tags_length, int output, int print_lastmod)
+ notmuch_config_values_t *exclude_tags, int output, int print_lastmod)
{
notmuch_query_t *query;
- size_t i;
int count;
unsigned int ucount;
unsigned long revision;
return -1;
}
- for (i = 0; i < exclude_tags_length; i++) {
- status = notmuch_query_add_tag_exclude (query, exclude_tags[i]);
- if (status && status != NOTMUCH_STATUS_IGNORED) {
- print_status_query ("notmuch count", query, status);
- return -1;
+ for (notmuch_config_values_start (exclude_tags);
+ notmuch_config_values_valid (exclude_tags);
+ notmuch_config_values_move_to_next (exclude_tags)) {
+
+ status = notmuch_query_add_tag_exclude (query,
+ notmuch_config_values_get (exclude_tags));
+ if (status && status != NOTMUCH_STATUS_IGNORED) {
+ print_status_query ("notmuch count", query, status);
+ ret = -1;
+ goto DONE;
+ }
}
- }
switch (output) {
case OUTPUT_MESSAGES:
}
static int
-count_file (notmuch_database_t *notmuch, FILE *input, const char **exclude_tags,
- size_t exclude_tags_length, int output, int print_lastmod)
+count_file (notmuch_database_t *notmuch, FILE *input, notmuch_config_values_t *exclude_tags,
+ int output, int print_lastmod)
{
char *line = NULL;
ssize_t line_len;
while (! ret && (line_len = getline (&line, &line_size, input)) != -1) {
chomp_newline (line);
- ret = print_count (notmuch, line, exclude_tags, exclude_tags_length,
- output, print_lastmod);
+ ret = print_count (notmuch, line, exclude_tags, output, print_lastmod);
}
if (line)
}
int
-notmuch_count_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_count_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch, int argc, char *argv[])
{
- notmuch_database_t *notmuch;
char *query_str;
int opt_index;
int output = OUTPUT_MESSAGES;
bool exclude = true;
- const char **search_exclude_tags = NULL;
- size_t search_exclude_tags_length = 0;
+ notmuch_config_values_t *exclude_tags = NULL;
bool batch = false;
bool print_lastmod = false;
FILE *input = stdin;
return EXIT_FAILURE;
}
- if (notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much))
- return EXIT_FAILURE;
-
notmuch_exit_if_unmatched_db_uuid (notmuch);
- query_str = query_string_from_args (config, argc - opt_index, argv + opt_index);
+ query_str = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
if (query_str == NULL) {
fprintf (stderr, "Out of memory.\n");
return EXIT_FAILURE;
}
if (exclude) {
- search_exclude_tags = notmuch_config_get_search_exclude_tags
- (config, &search_exclude_tags_length);
+ exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS);
}
if (batch)
- ret = count_file (notmuch, input, search_exclude_tags,
- search_exclude_tags_length, output, print_lastmod);
+ ret = count_file (notmuch, input, exclude_tags, output, print_lastmod);
else
- ret = print_count (notmuch, query_str, search_exclude_tags,
- search_exclude_tags_length, output, print_lastmod);
+ ret = print_count (notmuch, query_str, exclude_tags, output, print_lastmod);
notmuch_database_destroy (notmuch);
}
int
-notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_dump_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch , int argc, char *argv[])
{
- notmuch_database_t *notmuch;
const char *query_str = NULL;
int ret;
- if (notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much))
- return EXIT_FAILURE;
-
notmuch_exit_if_unmatched_db_uuid (notmuch);
const char *output_file_name = NULL;
}
int
-notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_insert_command (unused(notmuch_config_t *config),notmuch_database_t *notmuch, int argc, char *argv[])
{
notmuch_status_t status, close_status;
- notmuch_database_t *notmuch;
struct sigaction action;
const char *db_path;
- const char **new_tags;
- size_t new_tags_length;
+ notmuch_config_values_t *new_tags = NULL;
tag_op_list_t *tag_ops;
char *query_string = NULL;
const char *folder = "";
bool keep = false;
bool hooks = true;
bool world_readable = false;
- bool synchronize_flags;
+ notmuch_bool_t synchronize_flags;
char *maildir;
char *newpath;
int opt_index;
- unsigned int i;
+ void *local = talloc_new (NULL);
notmuch_opt_desc_t options[] = {
{ .opt_string = &folder, .name = "folder", .allow_empty = true },
notmuch_process_shared_options (argv[0]);
- db_path = notmuch_config_get_database_path (config);
- new_tags = notmuch_config_get_new_tags (config, &new_tags_length);
- synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config);
- tag_ops = tag_op_list_create (config);
+ db_path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
+
+ if (! db_path)
+ INTERNAL_ERROR ("Unable to retrieve database path");
+ else
+ db_path = talloc_strdup (local, db_path);
+
+ new_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_TAGS);
+
+ if (print_status_database (
+ "notmuch insert",
+ notmuch,
+ notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+ &synchronize_flags)))
+ return EXIT_FAILURE;
+
+ tag_ops = tag_op_list_create (local);
if (tag_ops == NULL) {
fprintf (stderr, "Out of memory.\n");
return EXIT_FAILURE;
}
- for (i = 0; i < new_tags_length; i++) {
+ for (;
+ notmuch_config_values_valid (new_tags);
+ notmuch_config_values_move_to_next (new_tags)) {
const char *error_msg;
-
- error_msg = illegal_tag (new_tags[i], false);
+ const char *tag = notmuch_config_values_get (new_tags);
+ error_msg = illegal_tag (tag, false);
if (error_msg) {
fprintf (stderr, "Error: tag '%s' in new.tags: %s\n",
- new_tags[i], error_msg);
+ tag, error_msg);
return EXIT_FAILURE;
}
- if (tag_op_list_append (tag_ops, new_tags[i], false))
+ if (tag_op_list_append (tag_ops, tag, false))
return EXIT_FAILURE;
}
- if (parse_tag_command_line (config, argc - opt_index, argv + opt_index,
+ if (parse_tag_command_line (local, argc - opt_index, argv + opt_index,
&query_string, tag_ops))
return EXIT_FAILURE;
return EXIT_FAILURE;
}
- maildir = talloc_asprintf (config, "%s/%s", db_path, folder);
+ maildir = talloc_asprintf (local, "%s/%s", db_path, folder);
if (! maildir) {
fprintf (stderr, "Out of memory\n");
return EXIT_FAILURE;
}
strip_trailing (maildir, '/');
- if (create_folder && ! maildir_create_folder (config, maildir, world_readable))
+ if (create_folder && ! maildir_create_folder (local, maildir, world_readable))
return EXIT_FAILURE;
/* Set up our handler for SIGINT. We do not set SA_RESTART so that copying
sigaction (SIGINT, &action, NULL);
/* Write the message to the Maildir new directory. */
- newpath = maildir_write_new (config, STDIN_FILENO, maildir, world_readable);
+ newpath = maildir_write_new (local, STDIN_FILENO, maildir, world_readable);
if (! newpath) {
return EXIT_FAILURE;
}
- status = notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much);
- if (status)
- return keep ? NOTMUCH_STATUS_SUCCESS : status_to_exit (status);
-
notmuch_exit_if_unmatched_db_uuid (notmuch);
status = notmuch_process_shared_indexing_options (notmuch);
status = add_file (notmuch, newpath, tag_ops, synchronize_flags, keep, indexing_cli_choices.opts);
/* Commit changes. */
- close_status = notmuch_database_destroy (notmuch);
+ close_status = notmuch_database_close (notmuch);
if (close_status) {
/* Hold on to the first error, if any. */
if (! status)
if (hooks && status == NOTMUCH_STATUS_SUCCESS) {
/* Ignore hook failures. */
- notmuch_run_hook (db_path, "post-insert");
+ notmuch_run_hook (notmuch, "post-insert");
}
+ notmuch_database_destroy (notmuch);
+
+ talloc_free (local);
+
return status_to_exit (status);
}
enum verbosity verbosity;
bool debug;
bool full_scan;
- const char **new_tags;
- size_t new_tags_length;
+ notmuch_config_values_t *new_tags;
const char **ignore_verbatim;
size_t ignore_verbatim_length;
regex_t *ignore_regex;
_filename_list_t *removed_directories;
_filename_list_t *directory_mtimes;
- bool synchronize_flags;
+ notmuch_bool_t synchronize_flags;
} add_files_state_t;
static volatile sig_atomic_t do_print_progress = 0;
}
static bool
-_setup_ignore (notmuch_config_t *config, add_files_state_t *state)
+_setup_ignore (notmuch_database_t *notmuch, add_files_state_t *state)
{
- const char **ignore_list, **ignore;
+ notmuch_config_values_t *ignore_list;
int nregex = 0, nverbatim = 0;
const char **verbatim = NULL;
regex_t *regex = NULL;
- ignore_list = notmuch_config_get_new_ignore (config, NULL);
- if (! ignore_list)
- return true;
-
- for (ignore = ignore_list; *ignore; ignore++) {
- const char *s = *ignore;
+ for (ignore_list = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_IGNORE);
+ notmuch_config_values_valid (ignore_list);
+ notmuch_config_values_move_to_next (ignore_list)) {
+ const char *s = notmuch_config_values_get (ignore_list);
size_t len = strlen (s);
if (len == 0) {
return false;
}
- r = talloc_strndup (config, s + 1, len - 2);
- regex = talloc_realloc (config, regex, regex_t, nregex + 1);
+ r = talloc_strndup (notmuch, s + 1, len - 2);
+ regex = talloc_realloc (notmuch, regex, regex_t, nregex + 1);
preg = ®ex[nregex];
rerr = regcomp (preg, r, REG_EXTENDED | REG_NOSUB);
talloc_free (r);
} else {
- verbatim = talloc_realloc (config, verbatim, const char *,
+ verbatim = talloc_realloc (notmuch, verbatim, const char *,
nverbatim + 1);
verbatim[nverbatim++] = s;
}
add_files_state_t *state)
{
notmuch_message_t *message = NULL;
- const char **tag;
+ const char *tag;
notmuch_status_t status;
status = notmuch_database_begin_atomic (notmuch);
if (state->synchronize_flags)
notmuch_message_maildir_flags_to_tags (message);
- for (tag = state->new_tags; *tag != NULL; tag++) {
+ for (notmuch_config_values_start (state->new_tags);
+ notmuch_config_values_valid (state->new_tags);
+ notmuch_config_values_move_to_next (state->new_tags)) {
notmuch_bool_t is_set;
+
+ tag = notmuch_config_values_get (state->new_tags);
/* Currently all errors from has_maildir_flag are fatal */
if ((status = notmuch_message_has_maildir_flag_st (message, 'S', &is_set)))
goto DONE;
- if (strcmp ("unread", *tag) != 0 || ! is_set) {
- notmuch_message_add_tag (message, *tag);
+ if (strcmp ("unread", tag) != 0 || ! is_set) {
+ notmuch_message_add_tag (message, tag);
}
}
/* Recursively remove all filenames from the database referring to
* 'path' (or to any of its children). */
static notmuch_status_t
-_remove_directory (void *ctx,
- notmuch_database_t *notmuch,
+_remove_directory (notmuch_database_t *notmuch,
const char *path,
add_files_state_t *add_files_state)
{
for (files = notmuch_directory_get_child_files (directory);
notmuch_filenames_valid (files);
notmuch_filenames_move_to_next (files)) {
- absolute = talloc_asprintf (ctx, "%s/%s", path,
+ absolute = talloc_asprintf (notmuch, "%s/%s", path,
notmuch_filenames_get (files));
status = remove_filename (notmuch, absolute, add_files_state);
talloc_free (absolute);
for (subdirs = notmuch_directory_get_child_directories (directory);
notmuch_filenames_valid (subdirs);
notmuch_filenames_move_to_next (subdirs)) {
- absolute = talloc_asprintf (ctx, "%s/%s", path,
+ absolute = talloc_asprintf (notmuch, "%s/%s", path,
notmuch_filenames_get (subdirs));
- status = _remove_directory (ctx, notmuch, absolute, add_files_state);
+ status = _remove_directory (notmuch, absolute, add_files_state);
talloc_free (absolute);
if (status)
goto DONE;
printf ("\n");
}
+static int
+_maybe_upgrade (notmuch_database_t *notmuch, add_files_state_t *state) {
+ if (notmuch_database_needs_upgrade (notmuch)) {
+ time_t now = time (NULL);
+ struct tm *gm_time = gmtime (&now);
+ notmuch_status_t status;
+ char *dot_notmuch_path = talloc_asprintf (notmuch, "%s/%s", state->db_path, ".notmuch");
+
+ /* since dump files are written atomically, the amount of
+ * harm from overwriting one within a second seems
+ * relatively small. */
+
+ const char *backup_name =
+ talloc_asprintf (notmuch, "%s/dump-%04d%02d%02dT%02d%02d%02d.gz",
+ dot_notmuch_path,
+ gm_time->tm_year + 1900,
+ gm_time->tm_mon + 1,
+ gm_time->tm_mday,
+ gm_time->tm_hour,
+ gm_time->tm_min,
+ gm_time->tm_sec);
+
+ if (state->verbosity >= VERBOSITY_NORMAL) {
+ printf ("Welcome to a new version of notmuch! Your database will now be upgraded.\n");
+ printf ("This process is safe to interrupt.\n");
+ printf ("Backing up tags to %s...\n", backup_name);
+ }
+
+ if (notmuch_database_dump (notmuch, backup_name, "",
+ DUMP_FORMAT_BATCH_TAG, DUMP_INCLUDE_DEFAULT, true)) {
+ fprintf (stderr, "Backup failed. Aborting upgrade.");
+ return EXIT_FAILURE;
+ }
+
+ gettimeofday (&state->tv_start, NULL);
+ status = notmuch_database_upgrade (
+ notmuch,
+ state->verbosity >= VERBOSITY_NORMAL ? upgrade_print_progress : NULL,
+ state);
+ if (status) {
+ printf ("Upgrade failed: %s\n",
+ notmuch_status_to_string (status));
+ notmuch_database_destroy (notmuch);
+ return EXIT_FAILURE;
+ }
+ if (state->verbosity >= VERBOSITY_NORMAL)
+ printf ("Your notmuch database has now been upgraded.\n");
+ }
+ return EXIT_SUCCESS;
+}
+
int
-notmuch_new_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_new_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch, int argc, char *argv[])
{
- notmuch_database_t *notmuch;
add_files_state_t add_files_state = {
.verbosity = VERBOSITY_NORMAL,
.debug = false,
};
struct timeval tv_start;
int ret = 0;
- struct stat st;
const char *db_path;
- char *dot_notmuch_path;
struct sigaction action;
_filename_node_t *f;
int opt_index;
else if (verbose)
add_files_state.verbosity = VERBOSITY_VERBOSE;
- add_files_state.new_tags = notmuch_config_get_new_tags (config, &add_files_state.new_tags_length);
- add_files_state.synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config);
- db_path = notmuch_config_get_database_path (config);
+ add_files_state.new_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_TAGS);
+
+ if (print_status_database (
+ "notmuch new",
+ notmuch,
+ notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+ &add_files_state.synchronize_flags)))
+ return EXIT_FAILURE;
+
+ db_path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH);
add_files_state.db_path = db_path;
- if (! _setup_ignore (config, &add_files_state))
+ if (! _setup_ignore (notmuch, &add_files_state))
return EXIT_FAILURE;
- for (i = 0; i < add_files_state.new_tags_length; i++) {
- const char *error_msg;
+ for (notmuch_config_values_start (add_files_state.new_tags);
+ notmuch_config_values_valid (add_files_state.new_tags);
+ notmuch_config_values_move_to_next (add_files_state.new_tags)) {
+ const char *tag,*error_msg;
- error_msg = illegal_tag (add_files_state.new_tags[i], false);
+ tag = notmuch_config_values_get (add_files_state.new_tags);
+ error_msg = illegal_tag (tag, false);
if (error_msg) {
- fprintf (stderr, "Error: tag '%s' in new.tags: %s\n",
- add_files_state.new_tags[i], error_msg);
+ fprintf (stderr, "Error: tag '%s' in new.tags: %s\n", tag, error_msg);
return EXIT_FAILURE;
}
}
if (hooks) {
- ret = notmuch_run_hook (db_path, "pre-new");
+ ret = notmuch_run_hook (notmuch, "pre-new");
if (ret)
return EXIT_FAILURE;
}
- dot_notmuch_path = talloc_asprintf (config, "%s/%s", db_path, ".notmuch");
-
- if (stat (dot_notmuch_path, &st)) {
- int count;
+ notmuch_exit_if_unmatched_db_uuid (notmuch);
- count = 0;
+ if (notmuch_database_get_revision (notmuch, NULL) == 0) {
+ int count = 0;
count_files (db_path, &count, &add_files_state);
if (interrupted)
return EXIT_FAILURE;
if (add_files_state.verbosity >= VERBOSITY_NORMAL)
printf ("Found %d total files (that's not much mail).\n", count);
- if (notmuch_database_create (db_path, ¬much))
- return EXIT_FAILURE;
+
add_files_state.total_files = count;
} else {
- char *status_string = NULL;
- if (notmuch_database_open_verbose (db_path, NOTMUCH_DATABASE_MODE_READ_WRITE,
- ¬much, &status_string)) {
- if (status_string) {
- fputs (status_string, stderr);
- free (status_string);
- }
+ if (_maybe_upgrade (notmuch, &add_files_state))
return EXIT_FAILURE;
- }
-
- notmuch_exit_if_unmatched_db_uuid (notmuch);
-
- if (notmuch_database_needs_upgrade (notmuch)) {
- time_t now = time (NULL);
- struct tm *gm_time = gmtime (&now);
-
- /* since dump files are written atomically, the amount of
- * harm from overwriting one within a second seems
- * relatively small. */
-
- const char *backup_name =
- talloc_asprintf (notmuch, "%s/dump-%04d%02d%02dT%02d%02d%02d.gz",
- dot_notmuch_path,
- gm_time->tm_year + 1900,
- gm_time->tm_mon + 1,
- gm_time->tm_mday,
- gm_time->tm_hour,
- gm_time->tm_min,
- gm_time->tm_sec);
-
- if (add_files_state.verbosity >= VERBOSITY_NORMAL) {
- printf ("Welcome to a new version of notmuch! Your database will now be upgraded.\n");
- printf ("This process is safe to interrupt.\n");
- printf ("Backing up tags to %s...\n", backup_name);
- }
-
- if (notmuch_database_dump (notmuch, backup_name, "",
- DUMP_FORMAT_BATCH_TAG, DUMP_INCLUDE_DEFAULT, true)) {
- fprintf (stderr, "Backup failed. Aborting upgrade.");
- return EXIT_FAILURE;
- }
-
- gettimeofday (&add_files_state.tv_start, NULL);
- status = notmuch_database_upgrade (
- notmuch,
- add_files_state.verbosity >= VERBOSITY_NORMAL ? upgrade_print_progress : NULL,
- &add_files_state);
- if (status) {
- printf ("Upgrade failed: %s\n",
- notmuch_status_to_string (status));
- notmuch_database_destroy (notmuch);
- return EXIT_FAILURE;
- }
- if (add_files_state.verbosity >= VERBOSITY_NORMAL)
- printf ("Your notmuch database has now been upgraded.\n");
- }
add_files_state.total_files = 0;
}
action.sa_flags = SA_RESTART;
sigaction (SIGINT, &action, NULL);
- talloc_free (dot_notmuch_path);
- dot_notmuch_path = NULL;
-
gettimeofday (&add_files_state.tv_start, NULL);
- add_files_state.removed_files = _filename_list_create (config);
- add_files_state.removed_directories = _filename_list_create (config);
- add_files_state.directory_mtimes = _filename_list_create (config);
+ add_files_state.removed_files = _filename_list_create (notmuch);
+ add_files_state.removed_directories = _filename_list_create (notmuch);
+ add_files_state.directory_mtimes = _filename_list_create (notmuch);
if (add_files_state.verbosity == VERBOSITY_NORMAL &&
add_files_state.output_is_a_tty && ! debugger_is_active ()) {
gettimeofday (&tv_start, NULL);
for (f = add_files_state.removed_directories->head, i = 0; f && ! interrupted; f = f->next, i++) {
- ret = _remove_directory (config, notmuch, f->filename, &add_files_state);
+ ret = _remove_directory (notmuch, f->filename, &add_files_state);
if (ret)
goto DONE;
if (do_print_progress) {
fprintf (stderr, "Note: A fatal error was encountered: %s\n",
notmuch_status_to_string (ret));
- notmuch_database_destroy (notmuch);
+ notmuch_database_close (notmuch);
if (hooks && ! ret && ! interrupted)
- ret = notmuch_run_hook (db_path, "post-new");
+ ret = notmuch_run_hook (notmuch, "post-new");
+
+ notmuch_database_destroy (notmuch);
if (ret || interrupted)
return EXIT_FAILURE;
}
int
-notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_reindex_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch, int argc, char *argv[])
{
char *query_string = NULL;
- notmuch_database_t *notmuch;
struct sigaction action;
int opt_index;
int ret;
notmuch_process_shared_options (argv[0]);
- if (notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much))
- return EXIT_FAILURE;
-
notmuch_exit_if_unmatched_db_uuid (notmuch);
status = notmuch_process_shared_indexing_options (notmuch);
return EXIT_FAILURE;
}
- query_string = query_string_from_args (config, argc - opt_index, argv + opt_index);
+ query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
if (query_string == NULL) {
fprintf (stderr, "Out of memory\n");
return EXIT_FAILURE;
/* Match given string against user's configured "primary" and "other"
* addresses according to mode. */
static const char *
-address_match (const char *str, notmuch_config_t *config, address_match_t mode)
+address_match (const char *str, notmuch_database_t *notmuch, address_match_t mode)
{
const char *primary;
- const char **other;
- size_t i, other_len;
+ notmuch_config_values_t *other = NULL;
if (! str || *str == '\0')
return NULL;
- primary = notmuch_config_get_user_primary_email (config);
+ primary = notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL);
if (match_address (str, primary, mode))
return primary;
- other = notmuch_config_get_user_other_email (config, &other_len);
- for (i = 0; i < other_len; i++) {
- if (match_address (str, other[i], mode))
- return other[i];
- }
+ for (other = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_OTHER_EMAIL);
+ notmuch_config_values_valid (other);
+ notmuch_config_values_move_to_next (other)) {
+ const char *addr = notmuch_config_values_get (other);
+ if (match_address (str, addr, mode))
+ return addr;
+ }
return NULL;
}
* user's "primary" or "other" addresses. If so, return the matching
* address, NULL otherwise. */
static const char *
-user_address_in_string (const char *str, notmuch_config_t *config)
+user_address_in_string (const char *str, notmuch_database_t *notmuch)
{
- return address_match (str, config, USER_ADDRESS_IN_STRING);
+ return address_match (str, notmuch, USER_ADDRESS_IN_STRING);
}
/* Do any of the addresses configured as one of the user's "primary"
* or "other" addresses contain the given string. If so, return the
* matching address, NULL otherwise. */
static const char *
-string_in_user_address (const char *str, notmuch_config_t *config)
+string_in_user_address (const char *str, notmuch_database_t *notmuch)
{
- return address_match (str, config, STRING_IN_USER_ADDRESS);
+ return address_match (str, notmuch, STRING_IN_USER_ADDRESS);
}
/* Is the given address configured as one of the user's "primary" or
* "other" addresses. */
static bool
-address_is_users (const char *address, notmuch_config_t *config)
+address_is_users (const char *address, notmuch_database_t *notmuch)
{
- return address_match (address, config, STRING_IS_USER_ADDRESS) != NULL;
+ return address_match (address, notmuch, STRING_IS_USER_ADDRESS) != NULL;
}
/* Scan addresses in 'list'.
*/
static unsigned int
scan_address_list (InternetAddressList *list,
- notmuch_config_t *config,
+ notmuch_database_t *notmuch,
GMimeMessage *message,
GMimeAddressType type,
const char **user_from)
group = INTERNET_ADDRESS_GROUP (address);
group_list = internet_address_group_get_members (group);
- n += scan_address_list (group_list, config, message, type, user_from);
+ n += scan_address_list (group_list, notmuch, message, type, user_from);
} else {
InternetAddressMailbox *mailbox;
const char *name;
name = internet_address_get_name (address);
addr = internet_address_mailbox_get_addr (mailbox);
- if (address_is_users (addr, config)) {
+ if (address_is_users (addr, notmuch)) {
if (user_from && *user_from == NULL)
*user_from = addr;
} else if (message) {
*/
static const char *
add_recipients_from_message (GMimeMessage *reply,
- notmuch_config_t *config,
+ notmuch_database_t *notmuch,
GMimeMessage *message,
bool reply_all)
{
recipients = reply_to_map[i].get_header (message);
- n += scan_address_list (recipients, config, reply,
+ n += scan_address_list (recipients, notmuch, reply,
reply_to_map[i].recipient_type, &from_addr);
if (! reply_all && n) {
* Return the address that was found, if any, and NULL otherwise.
*/
static const char *
-guess_from_in_received_for (notmuch_config_t *config, const char *received)
+guess_from_in_received_for (notmuch_database_t *notmuch, const char *received)
{
const char *ptr;
if (! ptr)
return NULL;
- return user_address_in_string (ptr, config);
+ return user_address_in_string (ptr, notmuch);
}
/*
* Return the address that was found, if any, and NULL otherwise.
*/
static const char *
-guess_from_in_received_by (notmuch_config_t *config, const char *received)
+guess_from_in_received_by (notmuch_database_t *notmuch, const char *received)
{
const char *addr;
const char *by = received;
*/
*(tld - 1) = '.';
- addr = string_in_user_address (domain, config);
+ addr = string_in_user_address (domain, notmuch);
if (addr) {
free (mta);
return addr;
* Return the address that was found, if any, and NULL otherwise.
*/
static const char *
-guess_from_in_received_headers (notmuch_config_t *config,
- notmuch_message_t *message)
+guess_from_in_received_headers (notmuch_message_t *message)
{
const char *received, *addr;
char *sanitized;
+ notmuch_database_t *notmuch = notmuch_message_get_database (message);
+
received = notmuch_message_get_header (message, "received");
if (! received)
return NULL;
if (! sanitized)
return NULL;
- addr = guess_from_in_received_for (config, sanitized);
+ addr = guess_from_in_received_for (notmuch, sanitized);
if (! addr)
- addr = guess_from_in_received_by (config, sanitized);
+ addr = guess_from_in_received_by (notmuch, sanitized);
talloc_free (sanitized);
* Return the address that was found, if any, and NULL otherwise.
*/
static const char *
-get_from_in_to_headers (notmuch_config_t *config, notmuch_message_t *message)
+get_from_in_to_headers (notmuch_message_t *message)
{
size_t i;
const char *tohdr, *addr;
"Delivered-To",
};
+ notmuch_database_t *notmuch = notmuch_message_get_database (message);
+
for (i = 0; i < ARRAY_SIZE (to_headers); i++) {
tohdr = notmuch_message_get_header (message, to_headers[i]);
/* Note: tohdr potentially contains a list of email addresses. */
- addr = user_address_in_string (tohdr, config);
+ addr = user_address_in_string (tohdr, notmuch);
if (addr)
return addr;
}
static GMimeMessage *
create_reply_message (void *ctx,
- notmuch_config_t *config,
notmuch_message_t *message,
GMimeMessage *mime_message,
bool reply_all,
{
const char *subject, *from_addr = NULL;
const char *in_reply_to, *orig_references, *references;
-
+ notmuch_database_t *notmuch = notmuch_message_get_database (message);
/*
* Use the below header order for limited headers, "pretty" order
* otherwise.
g_mime_object_set_header (GMIME_OBJECT (reply), "References", references, NULL);
- from_addr = add_recipients_from_message (reply, config,
+ from_addr = add_recipients_from_message (reply, notmuch,
mime_message, reply_all);
/* The above is all that is needed for limited headers. */
* Delivered-To: headers.
*/
if (from_addr == NULL)
- from_addr = get_from_in_to_headers (config, message);
+ from_addr = get_from_in_to_headers (message);
/*
* Check for a (for <email@add.res>) clause in Received: headers,
* of Received: headers
*/
if (from_addr == NULL)
- from_addr = guess_from_in_received_headers (config, message);
+ from_addr = guess_from_in_received_headers (message);
/* Default to user's primary address. */
if (from_addr == NULL)
- from_addr = notmuch_config_get_user_primary_email (config);
+ from_addr = notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL);
from_addr = talloc_asprintf (ctx, "%s <%s>",
- notmuch_config_get_user_name (config),
+ notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME),
from_addr);
g_mime_object_set_header (GMIME_OBJECT (reply), "From", from_addr, NULL);
};
static int
-do_reply (notmuch_config_t *config,
+do_reply (notmuch_database_t *notmuch,
notmuch_query_t *query,
notmuch_show_params_t *params,
int format,
}
if (format == FORMAT_JSON)
- sp = sprinter_json_create (config, stdout);
+ sp = sprinter_json_create (notmuch, stdout);
else
- sp = sprinter_sexp_create (config, stdout);
+ sp = sprinter_sexp_create (notmuch, stdout);
}
status = notmuch_query_search_messages (query, &messages);
notmuch_messages_move_to_next (messages)) {
message = notmuch_messages_get (messages);
- if (mime_node_open (config, message, ¶ms->crypto, &node))
+ if (mime_node_open (notmuch, message, ¶ms->crypto, &node))
return 1;
- reply = create_reply_message (config, config, message,
+ reply = create_reply_message (notmuch, message,
GMIME_MESSAGE (node->part), reply_all,
format == FORMAT_HEADERS_ONLY);
if (! reply)
/* Start the original */
sp->map_key (sp, "original");
- format_part_sprinter (config, sp, node, true, false);
+ format_part_sprinter (notmuch, sp, node, true, false);
/* End */
sp->end (sp);
}
int
-notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_reply_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch, int argc, char *argv[])
{
- notmuch_database_t *notmuch;
notmuch_query_t *query;
char *query_string;
int opt_index;
notmuch_exit_if_unsupported_format ();
- query_string = query_string_from_args (config, argc - opt_index, argv + opt_index);
+ query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
if (query_string == NULL) {
fprintf (stderr, "Out of memory\n");
return EXIT_FAILURE;
return EXIT_FAILURE;
}
- if (notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much))
- return EXIT_FAILURE;
-
notmuch_exit_if_unmatched_db_uuid (notmuch);
query = notmuch_query_create (notmuch, query_string);
return EXIT_FAILURE;
}
- if (do_reply (config, query, ¶ms, format, reply_all) != 0)
+ if (do_reply (notmuch, query, ¶ms, format, reply_all) != 0)
return EXIT_FAILURE;
_notmuch_crypto_cleanup (¶ms.crypto);
}
int
-notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_restore_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch, int argc, char *argv[])
{
- notmuch_database_t *notmuch;
bool accumulate = false;
tag_op_flag_t flags = 0;
tag_op_list_t *tag_ops;
int include = 0;
int input_format = DUMP_FORMAT_AUTO;
int errnum;
+ notmuch_bool_t synchronize_flags;
- if (notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much))
+ if (print_status_database (
+ "notmuch restore",
+ notmuch,
+ notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+ &synchronize_flags)))
return EXIT_FAILURE;
- if (notmuch_config_get_maildir_synchronize_flags (config))
+ if (synchronize_flags)
flags |= TAG_FLAG_MAILDIR_SYNC;
notmuch_opt_desc_t options[] = {
goto DONE;
}
- tag_ops = tag_op_list_create (config);
+ tag_ops = tag_op_list_create (notmuch);
if (tag_ops == NULL) {
fprintf (stderr, "Out of memory.\n");
ret = EXIT_FAILURE;
if (line_ctx != NULL)
talloc_free (line_ctx);
- line_ctx = talloc_new (config);
+ line_ctx = talloc_new (notmuch);
if ((include & DUMP_INCLUDE_PROPERTIES) && line_len >= 2 && line[0] == '#' && line[1] == '=') {
ret = process_properties_line (notmuch, line + 2);
typedef struct {
notmuch_database_t *notmuch;
+ void *talloc_ctx;
int format_sel;
sprinter_t *format;
int exclude;
}
static int
-_notmuch_search_prepare (search_context_t *ctx, notmuch_config_t *config, int argc, char *argv[])
+_notmuch_search_prepare (search_context_t *ctx, int argc, char *argv[])
{
char *query_str;
- unsigned int i;
- char *status_string = NULL;
+
+ if (! ctx->talloc_ctx)
+ ctx->talloc_ctx = talloc_new (NULL);
switch (ctx->format_sel) {
case NOTMUCH_FORMAT_TEXT:
- ctx->format = sprinter_text_create (config, stdout);
+ ctx->format = sprinter_text_create (ctx->talloc_ctx, stdout);
break;
case NOTMUCH_FORMAT_TEXT0:
if (ctx->output == OUTPUT_SUMMARY) {
fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n");
return EXIT_FAILURE;
}
- ctx->format = sprinter_text0_create (config, stdout);
+ ctx->format = sprinter_text0_create (ctx->talloc_ctx, stdout);
break;
case NOTMUCH_FORMAT_JSON:
- ctx->format = sprinter_json_create (config, stdout);
+ ctx->format = sprinter_json_create (ctx->talloc_ctx, stdout);
break;
case NOTMUCH_FORMAT_SEXP:
- ctx->format = sprinter_sexp_create (config, stdout);
+ ctx->format = sprinter_sexp_create (ctx->talloc_ctx, stdout);
break;
default:
/* this should never happen */
notmuch_exit_if_unsupported_format ();
- if (notmuch_database_open_verbose (
- notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_ONLY, &ctx->notmuch, &status_string)) {
-
- if (status_string) {
- fputs (status_string, stderr);
- free (status_string);
- }
-
- return EXIT_FAILURE;
- }
-
notmuch_exit_if_unmatched_db_uuid (ctx->notmuch);
query_str = query_string_from_args (ctx->notmuch, argc, argv);
}
if (ctx->exclude != NOTMUCH_EXCLUDE_FALSE) {
- const char **search_exclude_tags;
- size_t search_exclude_tags_length;
+ notmuch_config_values_t *exclude_tags;
notmuch_status_t status;
- search_exclude_tags = notmuch_config_get_search_exclude_tags (
- config, &search_exclude_tags_length);
+ for (exclude_tags = notmuch_config_get_values (ctx->notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS);
+ notmuch_config_values_valid (exclude_tags);
+ notmuch_config_values_move_to_next (exclude_tags)) {
- for (i = 0; i < search_exclude_tags_length; i++) {
- status = notmuch_query_add_tag_exclude (ctx->query, search_exclude_tags[i]);
+ status = notmuch_query_add_tag_exclude (ctx->query,
+ notmuch_config_values_get (exclude_tags));
if (status && status != NOTMUCH_STATUS_IGNORED) {
print_status_query ("notmuch search", ctx->query, status);
return EXIT_FAILURE;
}
}
-
notmuch_query_set_omit_excluded (ctx->query, ctx->exclude);
}
};
int
-notmuch_search_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_search_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch, int argc, char *argv[])
{
search_context_t *ctx = &search_context;
int opt_index, ret;
{ }
};
+ ctx->notmuch = notmuch;
ctx->output = OUTPUT_SUMMARY;
opt_index = parse_arguments (argc, argv, options, 1);
if (opt_index < 0)
return EXIT_FAILURE;
}
- if (_notmuch_search_prepare (ctx, config,
- argc - opt_index, argv + opt_index))
+ if (_notmuch_search_prepare (ctx, argc - opt_index, argv + opt_index))
return EXIT_FAILURE;
switch (ctx->output) {
}
int
-notmuch_address_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_address_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch, int argc, char *argv[])
{
search_context_t *ctx = &search_context;
int opt_index, ret;
{ }
};
+ ctx->notmuch = notmuch;
+
opt_index = parse_arguments (argc, argv, options, 1);
if (opt_index < 0)
return EXIT_FAILURE;
return EXIT_FAILURE;
}
- if (_notmuch_search_prepare (ctx, config,
- argc - opt_index, argv + opt_index))
+ if (_notmuch_search_prepare (ctx, argc - opt_index, argv + opt_index))
return EXIT_FAILURE;
ctx->addresses = g_hash_table_new_full (strcase_hash, strcase_equal,
int
notmuch_setup_command (notmuch_config_t *config,
+ unused(notmuch_database_t *notmuch),
int argc, char *argv[])
{
char *response = NULL;
}
if (ssize > 0 && fwrite (buf, ssize, 1, stdout) != 1) {
- fprintf (stderr, "Error: Write %ld chars to stdout failed\n", ssize);
+ fprintf (stderr, "Error: Write %zd chars to stdout failed\n", ssize);
goto DONE;
}
}
};
int
-notmuch_show_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_show_command (notmuch_config_t *config, unused(notmuch_database_t *notmuch), int argc, char *argv[])
{
notmuch_database_t *notmuch;
notmuch_query_t *query;
bool entire_thread_set = false;
bool single_message;
bool unthreaded = FALSE;
+ char *status_string = NULL;
notmuch_opt_desc_t options[] = {
{ .opt_keyword = &format, .name = "format", .keywords =
fprintf (stderr, "Warning: --include-html only implemented for format=text, format=json and format=sexp\n");
}
- query_string = query_string_from_args (config, argc - opt_index, argv + opt_index);
+ notmuch_database_mode_t mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
+ if (params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
+ mode = NOTMUCH_DATABASE_MODE_READ_WRITE;
+ if (notmuch_database_open_with_config (NULL,
+ mode,
+ _notmuch_config_get_path (config),
+ NULL,
+ ¬much,
+ &status_string)) {
+ if (status_string) {
+ fputs (status_string, stderr);
+ free (status_string);
+ }
+
+ return EXIT_FAILURE;
+ }
+
+ config = NULL;
+
+ notmuch_exit_if_unmatched_db_uuid (notmuch);
+
+ query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index);
if (query_string == NULL) {
fprintf (stderr, "Out of memory\n");
return EXIT_FAILURE;
return EXIT_FAILURE;
}
- notmuch_database_mode_t mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
- if (params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE)
- mode = NOTMUCH_DATABASE_MODE_READ_WRITE;
- if (notmuch_database_open (notmuch_config_get_database_path (config),
- mode, ¬much))
- return EXIT_FAILURE;
-
- notmuch_exit_if_unmatched_db_uuid (notmuch);
-
query = notmuch_query_create (notmuch, query_string);
if (query == NULL) {
fprintf (stderr, "Out of memory\n");
/* Create structure printer. */
formatter = formatters[format];
- sprinter = formatter->new_sprinter (config, stdout);
+ sprinter = formatter->new_sprinter (notmuch, stdout);
params.out_stream = g_mime_stream_stdout_new ();
/* If a single message is requested we do not use search_excludes. */
if (single_message) {
- ret = do_show_single (config, query, formatter, sprinter, ¶ms);
+ ret = do_show_single (notmuch, query, formatter, sprinter, ¶ms);
} else {
/* We always apply set the exclude flag. The
* exclude=true|false option controls whether or not we return
* threads that only match in an excluded message */
- const char **search_exclude_tags;
- size_t search_exclude_tags_length;
- unsigned int i;
+ notmuch_config_values_t *exclude_tags;
notmuch_status_t status;
- search_exclude_tags = notmuch_config_get_search_exclude_tags
- (config, &search_exclude_tags_length);
+ for (exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS);
+ notmuch_config_values_valid (exclude_tags);
+ notmuch_config_values_move_to_next (exclude_tags)) {
- for (i = 0; i < search_exclude_tags_length; i++) {
- status = notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
+ status = notmuch_query_add_tag_exclude (query,
+ notmuch_config_values_get (exclude_tags));
if (status && status != NOTMUCH_STATUS_IGNORED) {
print_status_query ("notmuch show", query, status);
ret = -1;
}
if (unthreaded)
- ret = do_show_unthreaded (config, query, formatter, sprinter, ¶ms);
+ ret = do_show_unthreaded (notmuch, query, formatter, sprinter, ¶ms);
else
- ret = do_show_threaded (config, query, formatter, sprinter, ¶ms);
+ ret = do_show_threaded (notmuch, query, formatter, sprinter, ¶ms);
}
DONE:
}
int
-notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[])
+notmuch_tag_command (unused(notmuch_config_t *config), notmuch_database_t *notmuch, int argc, char *argv[])
{
tag_op_list_t *tag_ops = NULL;
char *query_string = NULL;
- notmuch_database_t *notmuch;
struct sigaction action;
tag_op_flag_t tag_flags = TAG_FLAG_NONE;
bool batch = false;
const char *input_file_name = NULL;
int opt_index;
int ret;
+ notmuch_bool_t synchronize_flags;
/* Set up our handler for SIGINT */
memset (&action, 0, sizeof (struct sigaction));
return EXIT_FAILURE;
}
} else {
- tag_ops = tag_op_list_create (config);
+ tag_ops = tag_op_list_create (notmuch);
if (tag_ops == NULL) {
fprintf (stderr, "Out of memory.\n");
return EXIT_FAILURE;
}
- if (parse_tag_command_line (config, argc - opt_index, argv + opt_index,
+ if (parse_tag_command_line (notmuch, argc - opt_index, argv + opt_index,
&query_string, tag_ops))
return EXIT_FAILURE;
}
}
- if (notmuch_database_open (notmuch_config_get_database_path (config),
- NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much))
- return EXIT_FAILURE;
-
notmuch_exit_if_unmatched_db_uuid (notmuch);
- if (notmuch_config_get_maildir_synchronize_flags (config))
+ if (print_status_database (
+ "notmuch restore",
+ notmuch,
+ notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+ &synchronize_flags)))
+ return EXIT_FAILURE;
+
+ if (synchronize_flags)
tag_flags |= TAG_FLAG_MAILDIR_SYNC;
if (remove_all)
tag_flags |= TAG_FLAG_REMOVE_ALL;
if (batch)
- ret = tag_file (config, notmuch, tag_flags, input);
+ ret = tag_file (notmuch, notmuch, tag_flags, input);
else
- ret = tag_query (config, notmuch, query_string, tag_ops, tag_flags);
+ ret = tag_query (notmuch, notmuch, query_string, tag_ops, tag_flags);
notmuch_database_destroy (notmuch);
*
* The return value will be used as notmuch exit status code,
* preferably EXIT_SUCCESS or EXIT_FAILURE.
+ *
+ * Each subcommand should be passed either a config object, or an open
+ * database
*/
-typedef int (*command_function_t) (notmuch_config_t *config, int argc, char *argv[]);
+typedef int (*command_function_t) (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
typedef struct command {
const char *name;
command_function_t function;
- notmuch_config_mode_t config_mode;
+ notmuch_command_mode_t mode;
const char *summary;
} command_t;
static int
-notmuch_help_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_help_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
static int
-notmuch_command (notmuch_config_t *config, int argc, char *argv[]);
+notmuch_command (notmuch_config_t *config, notmuch_database_t *notmuch, int argc, char *argv[]);
static int
_help_for (const char *topic);
static command_t commands[] = {
- { NULL, notmuch_command, NOTMUCH_CONFIG_OPEN | NOTMUCH_CONFIG_CREATE,
+ { NULL, notmuch_command, NOTMUCH_COMMAND_CONFIG_OPEN | NOTMUCH_COMMAND_CONFIG_CREATE,
"Notmuch main command." },
- { "setup", notmuch_setup_command, NOTMUCH_CONFIG_OPEN | NOTMUCH_CONFIG_CREATE,
+ { "setup", notmuch_setup_command, NOTMUCH_COMMAND_CONFIG_OPEN | NOTMUCH_COMMAND_CONFIG_CREATE,
"Interactively set up notmuch for first use." },
- { "new", notmuch_new_command, NOTMUCH_CONFIG_OPEN,
+ { "new", notmuch_new_command,
+ NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE | NOTMUCH_COMMAND_DATABASE_CREATE,
"Find and import new messages to the notmuch database." },
- { "insert", notmuch_insert_command, NOTMUCH_CONFIG_OPEN,
+ { "insert", notmuch_insert_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
"Add a new message into the maildir and notmuch database." },
- { "search", notmuch_search_command, NOTMUCH_CONFIG_OPEN,
+ { "search", notmuch_search_command, NOTMUCH_COMMAND_DATABASE_EARLY,
"Search for messages matching the given search terms." },
- { "address", notmuch_address_command, NOTMUCH_CONFIG_OPEN,
+ { "address", notmuch_address_command, NOTMUCH_COMMAND_DATABASE_EARLY,
"Get addresses from messages matching the given search terms." },
- { "show", notmuch_show_command, NOTMUCH_CONFIG_OPEN,
+ { "show", notmuch_show_command, NOTMUCH_COMMAND_CONFIG_OPEN,
"Show all messages matching the search terms." },
- { "count", notmuch_count_command, NOTMUCH_CONFIG_OPEN,
+ { "count", notmuch_count_command, NOTMUCH_COMMAND_DATABASE_EARLY,
"Count messages matching the search terms." },
- { "reply", notmuch_reply_command, NOTMUCH_CONFIG_OPEN,
+ { "reply", notmuch_reply_command, NOTMUCH_COMMAND_DATABASE_EARLY,
"Construct a reply template for a set of messages." },
- { "tag", notmuch_tag_command, NOTMUCH_CONFIG_OPEN,
+ { "tag", notmuch_tag_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
"Add/remove tags for all messages matching the search terms." },
- { "dump", notmuch_dump_command, NOTMUCH_CONFIG_OPEN,
+ { "dump", notmuch_dump_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
"Create a plain-text dump of the tags for each message." },
- { "restore", notmuch_restore_command, NOTMUCH_CONFIG_OPEN,
+ { "restore", notmuch_restore_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
"Restore the tags from the given dump file (see 'dump')." },
- { "compact", notmuch_compact_command, NOTMUCH_CONFIG_OPEN,
+ { "compact", notmuch_compact_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
"Compact the notmuch database." },
- { "reindex", notmuch_reindex_command, NOTMUCH_CONFIG_OPEN,
+ { "reindex", notmuch_reindex_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
"Re-index all messages matching the search terms." },
- { "config", notmuch_config_command, NOTMUCH_CONFIG_OPEN,
+ { "config", notmuch_config_command, NOTMUCH_COMMAND_CONFIG_OPEN,
"Get or set settings in the notmuch configuration file." },
#if WITH_EMACS
{ "emacs-mua", NULL, 0,
"send mail with notmuch and emacs." },
#endif
- { "help", notmuch_help_command, NOTMUCH_CONFIG_CREATE, /* create but don't save config */
+ { "help", notmuch_help_command, NOTMUCH_COMMAND_CONFIG_CREATE, /* create but don't save config */
"This message, or more detailed help for the named command." }
};
}
static int
-notmuch_help_command (unused (notmuch_config_t *config), int argc, char *argv[])
+notmuch_help_command (unused (notmuch_config_t *config), unused(notmuch_database_t *notmuch), int argc, char *argv[])
{
int opt_index;
*/
static int
notmuch_command (notmuch_config_t *config,
+ unused(notmuch_database_t *notmuch),
unused(int argc), unused(char **argv))
{
char *db_path;
* notmuch_setup_command which will give a nice welcome message,
* and interactively guide the user through the configuration. */
if (notmuch_config_is_new (config))
- return notmuch_setup_command (config, 0, NULL);
+ return notmuch_setup_command (config, NULL, 0, NULL);
/* Notmuch is already configured, but is there a database? */
db_path = talloc_asprintf (config, "%s/%s",
command_t *command;
const char *config_file_name = NULL;
notmuch_config_t *config = NULL;
+ notmuch_database_t *notmuch = NULL;
int opt_index;
int ret;
goto DONE;
}
- config = notmuch_config_open (local, config_file_name, command->config_mode);
- if (! config) {
- ret = EXIT_FAILURE;
- goto DONE;
- }
+ if (command->mode & NOTMUCH_COMMAND_DATABASE_EARLY) {
+ char *status_string = NULL;
+ notmuch_database_mode_t mode;
+ notmuch_status_t status;
- ret = (command->function)(config, argc - opt_index, argv + opt_index);
+ if (command->mode & NOTMUCH_COMMAND_DATABASE_WRITE ||
+ command->mode & NOTMUCH_COMMAND_DATABASE_CREATE)
+ mode = NOTMUCH_DATABASE_MODE_READ_WRITE;
+ else
+ mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
+
+ if (command->mode & NOTMUCH_COMMAND_DATABASE_CREATE) {
+ status = notmuch_database_create_with_config (NULL,
+ config_file_name,
+ NULL,
+ ¬much,
+ &status_string);
+ if (status && status != NOTMUCH_STATUS_DATABASE_EXISTS) {
+ if (status_string) {
+ fputs (status_string, stderr);
+ free (status_string);
+ }
+
+ if (status == NOTMUCH_STATUS_NO_CONFIG)
+ fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
+
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (notmuch == NULL) {
+ status = notmuch_database_open_with_config (NULL,
+ mode,
+ config_file_name,
+ NULL,
+ ¬much,
+ &status_string);
+ if (status) {
+ if (status_string) {
+ fputs (status_string, stderr);
+ free (status_string);
+ }
+
+ return EXIT_FAILURE;
+ }
+ }
+ } else {
+ config = notmuch_config_open (local, config_file_name, command->mode);
+ if (! config) {
+ ret = EXIT_FAILURE;
+ goto DONE;
+ }
+ }
+ ret = (command->function)(config, notmuch, argc - opt_index, argv + opt_index);
DONE:
if (config)
- gdb(1)
- gpg(1)
- python(1)
+ - xapian-metadata(1)
If your system lacks these tools or have older, non-upgradable versions
of these, please (possibly compile and) install these to some other
--- /dev/null
+#!/usr/bin/env bash
+test_description='Various options for reading configuration'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+backup_config () {
+ local test_name=$(basename $0 .sh)
+ cp ${NOTMUCH_CONFIG} notmuch-config-backup.${test_name}
+}
+
+xdg_config () {
+ local dir
+ local profile=${1:-default}
+ if [[ $profile != default ]]; then
+ export NOTMUCH_PROFILE=$profile
+ fi
+ backup_config
+ dir="${HOME}/.config/notmuch/${profile}"
+ rm -rf $dir
+ mkdir -p $dir
+ CONFIG_PATH=$dir/config
+ mv ${NOTMUCH_CONFIG} ${CONFIG_PATH}
+ unset NOTMUCH_CONFIG
+}
+
+restore_config () {
+ local test_name=$(basename $0 .sh)
+ export NOTMUCH_CONFIG="${TMP_DIRECTORY}/notmuch-config"
+ unset CONFIG_PATH
+ unset NOTMUCH_PROFILE
+ cp notmuch-config-backup.${test_name} ${NOTMUCH_CONFIG}
+}
+
+add_email_corpus
+
+test_begin_subtest "count with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> notmuch-config
+notmuch count query:$query_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "count with saved query from config file (xdg)"
+query_name="test${RANDOM}"
+xdg_config
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch count query:$query_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "count with saved query from config file (xdg+profile)"
+query_name="test${RANDOM}"
+xdg_config work
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch count query:$query_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+cat <<EOF > EXPECTED
+Before:
+#notmuch-dump batch-tag:3 tags
+
+After:
+#notmuch-dump batch-tag:3 tags
++attachment +inbox +signed +unread -- id:20091118005829.GB25380@dottiness.seas.harvard.edu
++attachment +inbox +signed +unread -- id:20091118010116.GC25380@dottiness.seas.harvard.edu
++inbox +signed +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu
++inbox +signed +unread -- id:20091117203301.GV3165@dottiness.seas.harvard.edu
++inbox +signed +unread -- id:20091118002059.067214ed@hikari
++inbox +signed +unread -- id:20091118005040.GA25380@dottiness.seas.harvard.edu
++inbox +signed +unread -- id:87iqd9rn3l.fsf@vertex.dottedmag
+EOF
+
+test_begin_subtest "dump with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+CONFIG_PATH=notmuch-config
+printf "Before:\n" > OUTPUT
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+printf "\nAfter:\n" >> OUTPUT
+printf "\n[query]\n${query_name} = tag:signed\n" >> ${CONFIG_PATH}
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "dump with saved query from config file (xdg)"
+backup_config
+query_name="test${RANDOM}"
+xdg_config
+printf "Before:\n" > OUTPUT
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+printf "\nAfter:\n" >> OUTPUT
+printf "\n[query]\n${query_name} = tag:signed\n" >> ${CONFIG_PATH}
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "dump with saved query from config file (xdg+profile)"
+backup_config
+query_name="test${RANDOM}"
+xdg_config work
+printf "Before:\n" > OUTPUT
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+printf "\nAfter:\n" >> OUTPUT
+printf "\n[query]\n${query_name} = tag:signed\n" >> ${CONFIG_PATH}
+notmuch dump --include=tags query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "restore with xdg config"
+backup_config
+notmuch dump '*' > EXPECTED
+notmuch tag -inbox '*'
+xdg_config
+notmuch restore --input=EXPECTED
+notmuch dump > OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "restore with xdg+profile config"
+backup_config
+notmuch dump '*' > EXPECTED
+notmuch tag -inbox '*'
+xdg_config work
+notmuch restore --input=EXPECTED
+notmuch dump > OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Insert message with custom new.tags (xdg)"
+backup_config
+xdg_config
+tag=test${RANDOM}
+notmuch --config=${CONFIG_PATH} config set new.tags $tag
+generate_message \
+ "[subject]=\"insert-subject\"" \
+ "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \
+ "[body]=\"insert-message\""
+mkdir -p ${MAIL_DIR}/{cur,new,tmp}
+notmuch insert < "$gen_msg_filename"
+notmuch dump id:$gen_msg_id > OUTPUT
+cat <<EOF > EXPECTED
+#notmuch-dump batch-tag:3 config,properties,tags
++$tag -- id:$gen_msg_id
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "Insert message with custom new.tags (xdg+profile)"
+backup_config
+tag=test${RANDOM}
+xdg_config $tag
+notmuch --config=${CONFIG_PATH} config set new.tags $tag
+generate_message \
+ "[subject]=\"insert-subject\"" \
+ "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \
+ "[body]=\"insert-message\""
+mkdir -p ${MAIL_DIR}/{cur,new,tmp}
+notmuch insert < "$gen_msg_filename"
+notmuch dump id:$gen_msg_id > OUTPUT
+cat <<EOF > EXPECTED
+#notmuch-dump batch-tag:3 config,properties,tags
++$tag -- id:$gen_msg_id
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reindex with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+count1=$(notmuch count --lastmod '*' | cut -f3)
+printf "\n[query]\n${query_name} = tag:inbox\n" >> notmuch-config
+notmuch reindex query:$query_name
+count2=$(notmuch count --lastmod '*' | cut -f3)
+restore_config
+test_expect_success "test '$count2 -gt $count1'"
+
+test_begin_subtest "reindex with saved query from config file (xdg)"
+query_name="test${RANDOM}"
+count1=$(notmuch count --lastmod '*' | cut -f3)
+xdg_config
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch reindex query:$query_name
+count2=$(notmuch count --lastmod '*' | cut -f3)
+restore_config
+test_expect_success "test '$count2 -gt $count1'"
+
+test_begin_subtest "reindex with saved query from config file (xdg+profile)"
+query_name="test${RANDOM}"
+count1=$(notmuch count --lastmod '*' | cut -f3)
+xdg_config $query_name
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch reindex query:$query_name
+count2=$(notmuch count --lastmod '*' | cut -f3)
+restore_config
+test_expect_success "test '$count2 -gt $count1'"
+
+
+
+add_message '[from]="Sender <sender@example.com>"' \
+ [to]=test_suite@notmuchmail.org \
+ '[cc]="Other Parties <cc@example.com>"' \
+ [subject]=notmuch-reply-test \
+ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \
+ '[body]="reply with CC"'
+
+cat <<EOF > EXPECTED
+Before:
+After:
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>
+Cc: Other Parties <cc@example.com>
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0000, Sender <sender@example.com> wrote:
+> reply with CC
+EOF
+
+test_begin_subtest "reply with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+printf "Before:\n" > OUTPUT
+notmuch reply query:$query_name 2>&1 >> OUTPUT
+printf "\n[query]\n${query_name} = id:${gen_msg_id}\n" >> notmuch-config
+printf "After:\n" >> OUTPUT
+notmuch reply query:$query_name >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reply with saved query from config file (xdg)"
+backup_config
+query_name="test${RANDOM}"
+xdg_config
+printf "Before:\n" > OUTPUT
+notmuch reply query:$query_name 2>&1 >> OUTPUT
+printf "\n[query]\n${query_name} = id:${gen_msg_id}\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch reply query:$query_name >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reply with saved query from config file (xdg+profile)"
+backup_config
+query_name="test${RANDOM}"
+xdg_config $query_name
+printf "Before:\n" > OUTPUT
+notmuch reply query:$query_name 2>&1 >> OUTPUT
+printf "\n[query]\n${query_name} = id:${gen_msg_id}\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch reply query:$query_name >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+backup_database
+test_begin_subtest "search with alternate config"
+notmuch tag -- +foobar17 '*'
+cp notmuch-config alt-config
+notmuch --config=alt-config config set search.exclude_tags foobar17
+output=$(notmuch --config=alt-config count '*')
+test_expect_equal "$output" "0"
+restore_database
+
+cat <<EOF > EXPECTED
+Before:
+After:
+thread:XXX 2009-11-18 [1/2] Carl Worth| Alex Botero-Lowry; [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread)
+thread:XXX 2009-11-18 [1/2] Carl Worth| Ingmar Vanhassel; [notmuch] [PATCH] Typsos (inbox unread)
+thread:XXX 2009-11-18 [1/3] Carl Worth| Adrian Perez de Castro, Keith Packard; [notmuch] Introducing myself (inbox signed unread)
+thread:XXX 2009-11-18 [1/3] Carl Worth| Israel Herraiz, Keith Packard; [notmuch] New to the list (inbox unread)
+thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread)
+thread:XXX 2009-11-18 [1/2] Carl Worth| Jan Janak; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
+thread:XXX 2009-11-18 [1/3(4)] Carl Worth| Aron Griffis, Keith Packard; [notmuch] archive (inbox unread)
+thread:XXX 2009-11-18 [1/2] Carl Worth| Keith Packard; [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread)
+thread:XXX 2009-11-18 [1/7] Carl Worth| Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard; [notmuch] Working with Maildir storage? (inbox signed unread)
+thread:XXX 2009-11-18 [2/5] Carl Worth| Mikhail Gusarov, Keith Packard; [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread)
+thread:XXX 2009-11-17 [1/2] Carl Worth| Alex Botero-Lowry; [notmuch] preliminary FreeBSD support (attachment inbox unread)
+EOF
+
+test_begin_subtest "search with saved query from config file"
+query_name="test${RANDOM}"
+backup_config
+printf "Before:\n" > OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+printf "\n[query]\n${query_name} = from:cworth\n" >> notmuch-config
+printf "After:\n" >> OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search with saved query from config file (xdg)"
+query_name="test${RANDOM}"
+xdg_config
+printf "Before:\n" > OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+printf "\n[query]\n${query_name} = from:cworth\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "search with saved query from config file (xdg + profile)"
+query_name="test${RANDOM}"
+xdg_config $query_name
+printf "Before:\n" > OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+printf "\n[query]\n${query_name} = from:cworth\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+cat <<EOF > EXPECTED
+Before:
+After:
+Alex Botero-Lowry <alex.boterolowry@gmail.com>
+Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+François Boulogne <boulogne.f@gmail.com>
+Jjgod Jiang <gzjjgod@gmail.com>
+EOF
+
+test_begin_subtest "address: saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+printf "Before:\n" > OUTPUT
+notmuch address --deduplicate=no --output=sender query:$query_name 2>&1 | sort >> OUTPUT
+printf "\n[query]\n${query_name} = from:gmail.com\n" >> notmuch-config
+printf "After:\n" >> OUTPUT
+notmuch address --output=sender query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "address: saved query from config file (xdg)"
+query_name="test${RANDOM}"
+xdg_config
+printf "Before:\n" > OUTPUT
+notmuch address --deduplicate=no --output=sender query:$query_name 2>&1 | sort >> OUTPUT
+printf "\n[query]\n${query_name} = from:gmail.com\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch address --output=sender query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "address: saved query from config file (xdg+profile)"
+query_name="test${RANDOM}"
+xdg_config $query_name
+printf "Before:\n" > OUTPUT
+notmuch address --deduplicate=no --output=sender query:$query_name 2>&1 | sort >> OUTPUT
+printf "\n[query]\n${query_name} = from:gmail.com\n" >> ${CONFIG_PATH}
+printf "After:\n" >> OUTPUT
+notmuch address --output=sender query:$query_name | sort >> OUTPUT
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "show with alternate config"
+backup_database
+cp notmuch-config alt-config
+notmuch --config=alt-config config set search.exclude_tags foobar17
+notmuch tag -- +foobar17 '*'
+output=$(notmuch --config=alt-config show '*' && echo OK)
+restore_database
+test_expect_equal "$output" "OK"
+
+test_begin_subtest "show with alternate config (xdg)"
+test_subtest_known_broken
+backup_database
+notmuch tag -- +foobar17 '*'
+xdg_config
+notmuch --config=${CONFIG_PATH} config set search.exclude_tags foobar17
+output=$(notmuch show '*' && echo OK)
+restore_database
+restore_config
+test_expect_equal "$output" "OK"
+
+test_begin_subtest "show with alternate config (xdg+profile)"
+test_subtest_known_broken
+backup_database
+notmuch tag -- +foobar17 '*'
+xdg_config foobar17
+notmuch --config=${CONFIG_PATH} config set search.exclude_tags foobar17
+output=$(notmuch show '*' && echo OK)
+restore_database
+restore_config
+test_expect_equal "$output" "OK"
+
+# reset to known state
+add_email_corpus
+
+test_begin_subtest "tag with saved query from config file"
+backup_config
+query_name="test${RANDOM}"
+tag_name="tag${RANDOM}"
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> notmuch-config
+notmuch tag +$tag_name -- query:${query_name}
+notmuch count tag:$tag_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "tag with saved query from config file (xdg)"
+xdg_config
+query_name="test${RANDOM}"
+tag_name="tag${RANDOM}"
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch tag +$tag_name -- query:${query_name}
+notmuch count tag:$tag_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "tag with saved query from config file (xdg+profile)"
+query_name="test${RANDOM}"
+xdg_config ${query_name}
+tag_name="tag${RANDOM}"
+notmuch count query:$query_name > OUTPUT
+printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH}
+notmuch tag +$tag_name -- query:${query_name}
+notmuch count tag:$tag_name >> OUTPUT
+cat <<EOF > EXPECTED
+0
+52
+EOF
+restore_config
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "running compact (xdg)"
+xdg_config
+notmuch compact
+output=$(notmuch count '*')
+restore_config
+test_expect_equal "52" "$output"
+
+test_begin_subtest "running compact (xdg + profile)"
+xdg_config ${RANDOM}
+notmuch compact
+output=$(notmuch count '*')
+restore_config
+test_expect_equal "52" "$output"
+
+test_begin_subtest "run notmuch-new (xdg)"
+xdg_config
+generate_message
+output=$(NOTMUCH_NEW --debug)
+restore_config
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_begin_subtest "run notmuch-new (xdg + profile)"
+xdg_config ${RANDOM}
+generate_message
+output=$(NOTMUCH_NEW --debug)
+restore_config
+test_expect_equal "$output" "Added 1 new message to the database."
+
+test_done
test_begin_subtest "Notmuch new without a config suggests notmuch setup"
output=$(notmuch --config=new-notmuch-config new 2>&1)
test_expect_equal "$output" "\
-Configuration file new-notmuch-config not found.
+Error: cannot load config file.
Try running 'notmuch setup' to create a configuration."
test_begin_subtest "Create a new config interactively"
OLDCONFIG=$(notmuch config get new.tags)
-test_begin_subtest "Empty tags in new.tags are forbidden"
+test_begin_subtest "Empty tags in new.tags are ignored"
notmuch config set new.tags "foo;;bar"
-output=$(NOTMUCH_NEW --debug 2>&1)
-test_expect_equal "$output" "Error: tag '' in new.tags: empty tag forbidden"
+output=$(NOTMUCH_NEW --quiet 2>&1)
+test_expect_equal "$output" ""
test_begin_subtest "Tags starting with '-' in new.tags are forbidden"
notmuch config set new.tags "-foo;bar"
OLDCONFIG=$(notmuch config get new.tags)
-test_begin_subtest "Empty tags in new.tags are forbidden"
+test_begin_subtest "Empty tags in new.tags are ignored"
notmuch config set new.tags "foo;;bar"
gen_insert_msg
-output=$(notmuch insert < $gen_msg_filename 2>&1)
-test_expect_equal "$output" "Error: tag '' in new.tags: empty tag forbidden"
+notmuch insert < $gen_msg_filename
+output=$(notmuch show --format=json id:$gen_msg_id)
+test_json_nodes <<<"$output" \
+ 'new_tags:[0][0][0]["tags"] = ["bar", "foo"]'
test_begin_subtest "Tags starting with '-' in new.tags are forbidden"
notmuch config set new.tags "-foo;bar"
output=$(notmuch search subject:deleted | notmuch_search_sanitize)
test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)"
+test_begin_subtest "Search, exclude \"deleted\" messages; alternate config file"
+cp ${NOTMUCH_CONFIG} alt-config
+notmuch config set search.exclude_tags
+notmuch --config=alt-config search subject:deleted | notmuch_search_sanitize > OUTPUT
+cp alt-config ${NOTMUCH_CONFIG}
+cat <<EOF > EXPECTED
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
test_begin_subtest "Search, exclude \"deleted\" messages from message search"
output=$(notmuch search --output=messages subject:deleted | notmuch_search_sanitize)
test_expect_equal "$output" "id:$not_deleted_id"
output=$(notmuch count --output=threads tag:test and tag:deleted)
test_expect_equal "$output" "3"
+test_begin_subtest "Count, default exclusion, batch"
+notmuch count --batch --output=messages<<EOF > OUTPUT
+tag:test
+tag:test and tag:deleted
+tag:test
+tag:test and tag:deleted
+EOF
+cat <<EOF >EXPECTED
+2
+4
+2
+4
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
test_begin_subtest "Count, exclude=true: tag in query (messages)"
output=$(notmuch count --exclude=true tag:test and tag:deleted)
test_expect_equal "$output" "4"
test_expect_equal_file EXPECTED OUTPUT
+backup_database
test_begin_subtest 'roundtripping random message-ids and tags'
${TEST_DIRECTORY}/random-corpus --config-path=${NOTMUCH_CONFIG} \
sort > OUTPUT.$test_count
test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count
+restore_database
test_done
-# Note the database is "poisoned" for sup format at this point.
test_begin_subtest "python cffi tests"
pytest_dir=$NOTMUCH_BUILDDIR/bindings/python-cffi/build/stage
printf "[pytest]\nminversion = 3.0\naddopts = -ra\n" > $pytest_dir/pytest.ini
-test_expect_success "(cd $pytest_dir && ${NOTMUCH_PYTHON} -m pytest --log-file=$TMP_DIRECTORY/test.output)"
+test_expect_success "(cd $pytest_dir && ${NOTMUCH_PYTHON} -m pytest --verbose --log-file=$TMP_DIRECTORY/test.output)"
test_done
test_description='hooks'
. $(dirname "$0")/test-lib.sh || exit 1
-HOOK_DIR=${MAIL_DIR}/.notmuch/hooks
-
create_echo_hook () {
local TOKEN="${RANDOM}"
mkdir -p ${HOOK_DIR}
}
create_failing_hook () {
+ local HOOK_DIR=${2}
mkdir -p ${HOOK_DIR}
cat <<EOF >"${HOOK_DIR}/${1}"
#!/bin/sh
chmod +x "${HOOK_DIR}/${1}"
}
-rm_hooks () {
- rm -rf ${HOOK_DIR}
-}
-
# add a message to generate mail dir and database
add_message
# create maildir structure for notmuch-insert
mkdir -p "$MAIL_DIR"/{cur,new,tmp}
-test_begin_subtest "pre-new is run"
-rm_hooks
-generate_message
-create_echo_hook "pre-new" expected output
-notmuch new > /dev/null
-test_expect_equal_file expected output
-
-test_begin_subtest "post-new is run"
-rm_hooks
-generate_message
-create_echo_hook "post-new" expected output
-notmuch new > /dev/null
-test_expect_equal_file expected output
-
-test_begin_subtest "post-insert hook is run"
-rm_hooks
-generate_message
-create_echo_hook "post-insert" expected output
-notmuch insert < "$gen_msg_filename"
-test_expect_equal_file expected output
-
-test_begin_subtest "pre-new is run before post-new"
-rm_hooks
-generate_message
-create_echo_hook "pre-new" pre-new.expected pre-new.output
-create_echo_hook "post-new" post-new.expected post-new.output
-notmuch new > /dev/null
-test_expect_equal_file post-new.expected post-new.output
-
-test_begin_subtest "pre-new non-zero exit status (hook status)"
-rm_hooks
-generate_message
-create_failing_hook "pre-new"
-output=`notmuch new 2>&1`
-test_expect_equal "$output" "Error: pre-new hook failed with status 13"
-
-# depends on the previous subtest leaving broken hook behind
-test_begin_subtest "pre-new non-zero exit status (notmuch status)"
-test_expect_code 1 "notmuch new"
-
-# depends on the previous subtests leaving 1 new message behind
-test_begin_subtest "pre-new non-zero exit status aborts new"
-rm_hooks
-output=$(NOTMUCH_NEW)
-test_expect_equal "$output" "Added 1 new message to the database."
-
-test_begin_subtest "post-new non-zero exit status (hook status)"
-rm_hooks
-generate_message
-create_failing_hook "post-new"
-NOTMUCH_NEW 2>output.stderr >output
-cat output.stderr >> output
-echo "Added 1 new message to the database." > expected
-echo "Error: post-new hook failed with status 13" >> expected
-test_expect_equal_file expected output
-
-# depends on the previous subtest leaving broken hook behind
-test_begin_subtest "post-new non-zero exit status (notmuch status)"
-test_expect_code 1 "notmuch new"
-
-test_begin_subtest "post-insert hook does not affect insert status"
-rm_hooks
-generate_message
-create_failing_hook "post-insert"
-test_expect_success "notmuch insert < \"$gen_msg_filename\" > /dev/null"
-
-test_begin_subtest "hook without executable permissions"
-rm_hooks
-mkdir -p ${HOOK_DIR}
-cat <<EOF >"${HOOK_DIR}/pre-new"
-#!/bin/sh
-echo foo
+for config in traditional profile explicit XDG; do
+ unset NOTMUCH_PROFILE
+ notmuch config set database.hook_dir
+ case $config in
+ traditional)
+ HOOK_DIR=${MAIL_DIR}/.notmuch/hooks
+ ;;
+ profile)
+ dir=${HOME}/.config/notmuch/other
+ mkdir -p ${dir}
+ HOOK_DIR=${dir}/hooks
+ cp ${NOTMUCH_CONFIG} ${dir}/config
+ export NOTMUCH_PROFILE=other
+ ;;
+ explicit)
+ HOOK_DIR=${HOME}/.notmuch-hooks
+ mkdir -p $HOOK_DIR
+ notmuch config set database.hook_dir $HOOK_DIR
+ ;;
+ XDG)
+ HOOK_DIR=${HOME}/.config/notmuch/default/hooks
+ ;;
+ esac
+
+ test_begin_subtest "pre-new is run [${config}]"
+ rm -rf ${HOOK_DIR}
+ generate_message
+ create_echo_hook "pre-new" expected output $HOOK_DIR
+ notmuch new > /dev/null
+ test_expect_equal_file expected output
+
+ test_begin_subtest "post-new is run [${config}]"
+ rm -rf ${HOOK_DIR}
+ generate_message
+ create_echo_hook "post-new" expected output $HOOK_DIR
+ notmuch new > /dev/null
+ test_expect_equal_file expected output
+
+ test_begin_subtest "post-insert hook is run [${config}]"
+ rm -rf ${HOOK_DIR}
+ generate_message
+ create_echo_hook "post-insert" expected output $HOOK_DIR
+ notmuch insert < "$gen_msg_filename"
+ test_expect_equal_file expected output
+
+ test_begin_subtest "pre-new is run before post-new [${config}]"
+ rm -rf ${HOOK_DIR}
+ generate_message
+ create_echo_hook "pre-new" pre-new.expected pre-new.output $HOOK_DIR
+ create_echo_hook "post-new" post-new.expected post-new.output $HOOK_DIR
+ notmuch new > /dev/null
+ test_expect_equal_file post-new.expected post-new.output
+
+ test_begin_subtest "pre-new non-zero exit status (hook status) [${config}]"
+ rm -rf ${HOOK_DIR}
+ generate_message
+ create_failing_hook "pre-new" $HOOK_DIR
+ output=`notmuch new 2>&1`
+ test_expect_equal "$output" "Error: pre-new hook failed with status 13"
+
+ # depends on the previous subtest leaving broken hook behind
+ test_begin_subtest "pre-new non-zero exit status (notmuch status) [${config}]"
+ test_expect_code 1 "notmuch new"
+
+ # depends on the previous subtests leaving 1 new message behind
+ test_begin_subtest "pre-new non-zero exit status aborts new [${config}]"
+ rm -rf ${HOOK_DIR}
+ output=$(NOTMUCH_NEW)
+ test_expect_equal "$output" "Added 1 new message to the database."
+
+ test_begin_subtest "post-new non-zero exit status (hook status) [${config}]"
+ rm -rf ${HOOK_DIR}
+ generate_message
+ create_failing_hook "post-new" $HOOK_DIR
+ NOTMUCH_NEW 2>output.stderr >output
+ cat output.stderr >> output
+ echo "Added 1 new message to the database." > expected
+ echo "Error: post-new hook failed with status 13" >> expected
+ test_expect_equal_file expected output
+
+ # depends on the previous subtest leaving broken hook behind
+ test_begin_subtest "post-new non-zero exit status (notmuch status) [${config}]"
+ test_expect_code 1 "notmuch new"
+
+ test_begin_subtest "post-insert hook does not affect insert status [${config}]"
+ rm -rf ${HOOK_DIR}
+ generate_message
+ create_failing_hook "post-insert" $HOOK_DIR
+ test_expect_success "notmuch insert < \"$gen_msg_filename\" > /dev/null"
+
+ test_begin_subtest "hook without executable permissions [${config}]"
+ rm -rf ${HOOK_DIR}
+ mkdir -p ${HOOK_DIR}
+ cat <<EOF >"${HOOK_DIR}/pre-new"
+ #!/bin/sh
+ echo foo
EOF
-output=`notmuch new 2>&1`
-test_expect_code 1 "notmuch new"
-
-test_begin_subtest "hook execution failure"
-rm_hooks
-mkdir -p ${HOOK_DIR}
-cat <<EOF >"${HOOK_DIR}/pre-new"
-no hashbang, execl fails
+ output=`notmuch new 2>&1`
+ test_expect_code 1 "notmuch new"
+
+ test_begin_subtest "hook execution failure [${config}]"
+ rm -rf ${HOOK_DIR}
+ mkdir -p ${HOOK_DIR}
+ cat <<EOF >"${HOOK_DIR}/pre-new"
+ no hashbang, execl fails
EOF
-chmod +x "${HOOK_DIR}/pre-new"
-test_expect_code 1 "notmuch new"
+ chmod +x "${HOOK_DIR}/pre-new"
+ test_expect_code 1 "notmuch new"
+ rm -rf ${HOOK_DIR}
+done
test_done
--- /dev/null
+#!/usr/bin/env bash
+test_description='database upgrades'
+. $(dirname "$0")/test-lib.sh || exit 1
+
+test_require_external_prereq xapian-metadata
+
+XAPIAN_PATH=$MAIL_DIR/.notmuch/xapian
+BACKUP_PATH=$MAIL_DIR/.notmuch
+
+delete_feature () {
+ local key=$1
+ features=$(xapian-metadata get $XAPIAN_PATH features | grep -v "^$key")
+ xapian-metadata set $XAPIAN_PATH features "$features"
+}
+
+add_email_corpus
+
+for key in 'multiple paths per message' \
+ 'relative directory paths' \
+ 'exact folder:/path: search' \
+ 'mail documents for missing messages' \
+ 'modification tracking'; do
+ backup_database
+ test_begin_subtest "upgrade is triggered by missing '$key'"
+ delete_feature "$key"
+ output=$(notmuch new | grep Welcome)
+ test_expect_equal \
+ "$output" \
+ "Welcome to a new version of notmuch! Your database will now be upgraded."
+
+ restore_database
+
+ backup_database
+ test_begin_subtest "backup can be restored ['$key']"
+ notmuch dump > BEFORE
+ delete_feature "$key"
+ notmuch new
+ notmuch tag -inbox '*'
+ dump_file=$(echo ${BACKUP_PATH}/dump*)
+ notmuch restore --input=$dump_file
+ notmuch dump > AFTER
+ test_expect_equal_file BEFORE AFTER
+ restore_database
+done
+
+for key in 'from/subject/message-ID in database' \
+ 'indexed MIME types' \
+ 'index body and headers separately'; do
+ backup_database
+ test_begin_subtest "upgrade not triggered by missing '$key'"
+ delete_feature "$key"
+ output=$(notmuch new | grep Welcome)
+ test_expect_equal "$output" ""
+ restore_database
+done
+
+test_done
cat <<'EOF' >EXPECTED
== stdout ==
== stderr ==
-Error: Cannot create a database for a NULL path.
+Error: Cannot open a database for a NULL path.
EOF
test_expect_equal_file EXPECTED OUTPUT
test_begin_subtest "get config from closed database"
cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
{
- const char *result;
+ char *result;
EXPECT0(notmuch_database_close (db));
stat = notmuch_database_get_config (db, "foo", &result);
- printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION);
+ printf("%d\n", stat == NOTMUCH_STATUS_SUCCESS);
}
EOF
cat <<EOF > EXPECTED
== stdout ==
1
== stderr ==
-Error: A Xapian exception occurred getting metadata: Database has been closed
EOF
test_expect_equal_file EXPECTED OUTPUT
notmuch_indexopts_t *result;
EXPECT0(notmuch_database_close (db));
result = notmuch_database_get_default_indexopts (db);
- printf("%d\n", result == NULL);
+ printf("%d\n", result != NULL);
}
EOF
cat <<EOF > EXPECTED
notmuch_database_t *db;
char *val;
notmuch_status_t stat;
+ char *msg = NULL;
- EXPECT0(notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db));
+ for (int i = 1; i < argc; i++)
+ if (strcmp (argv[i], "%NULL%") == 0) argv[i] = NULL;
+ stat = notmuch_database_open_with_config (argv[1],
+ NOTMUCH_DATABASE_MODE_READ_WRITE,
+ argv[2],
+ argv[3],
+ &db,
+ &msg);
+ if (stat != NOTMUCH_STATUS_SUCCESS) {
+ fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : "");
+ exit (1);
+ }
EOF
cat <<EOF > c_tail
EOF
test_begin_subtest "notmuch_database_{set,get}_config"
-cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG}
{
- EXPECT0(notmuch_database_set_config (db, "testkey1", "testvalue1"));
- EXPECT0(notmuch_database_set_config (db, "testkey2", "testvalue2"));
- EXPECT0(notmuch_database_get_config (db, "testkey1", &val));
- printf("testkey1 = %s\n", val);
- EXPECT0(notmuch_database_get_config (db, "testkey2", &val));
- printf("testkey2 = %s\n", val);
+ EXPECT0(notmuch_database_set_config (db, "test.key1", "testvalue1"));
+ EXPECT0(notmuch_database_set_config (db, "test.key2", "testvalue2"));
+ EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+ printf("test.key1 = %s\n", val);
+ EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+ printf("test.key2 = %s\n", val);
}
EOF
cat <<'EOF' >EXPECTED
== stdout ==
-testkey1 = testvalue1
-testkey2 = testvalue2
+test.key1 = testvalue1
+test.key2 = testvalue2
== stderr ==
EOF
test_expect_equal_file EXPECTED OUTPUT
test_begin_subtest "notmuch_database_get_config_list: empty list"
-cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
{
notmuch_config_list_t *list;
EXPECT0(notmuch_database_get_config_list (db, "nonexistent", &list));
test_expect_equal_file EXPECTED OUTPUT
test_begin_subtest "notmuch_database_get_config_list: all pairs"
-cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
{
notmuch_config_list_t *list;
EXPECT0(notmuch_database_set_config (db, "zzzafter", "afterval"));
cat <<'EOF' >EXPECTED
== stdout ==
aaabefore beforeval
-testkey1 testvalue1
-testkey2 testvalue2
+test.key1 testvalue1
+test.key2 testvalue2
zzzafter afterval
== stderr ==
EOF
cat <<'EOF' >EXPECTED
== stdout ==
aaabefore 1
-testkey1 1
-testkey2 1
+test.key1 1
+test.key2 1
zzzafter 1
== stderr ==
EOF
test_expect_equal_file EXPECTED OUTPUT
test_begin_subtest "notmuch_database_get_config_list: one prefix"
-cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
{
notmuch_config_list_t *list;
- EXPECT0(notmuch_database_get_config_list (db, "testkey", &list));
+ EXPECT0(notmuch_database_get_config_list (db, "test.key", &list));
for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) {
printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list));
}
EOF
cat <<'EOF' >EXPECTED
== stdout ==
-testkey1 testvalue1
-testkey2 testvalue2
+test.key1 testvalue1
+test.key2 testvalue2
== stderr ==
EOF
test_expect_equal_file EXPECTED OUTPUT
test_begin_subtest "dump config"
-cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
{
EXPECT0(notmuch_database_set_config (db, "key with spaces", "value, with, spaces!"));
}
#notmuch-dump batch-tag:3 config
#@ aaabefore beforeval
#@ key%20with%20spaces value,%20with,%20spaces%21
-#@ testkey1 testvalue1
-#@ testkey2 testvalue2
+#@ test.key1 testvalue1
+#@ test.key2 testvalue2
#@ zzzafter afterval
EOF
test_expect_equal_file EXPECTED OUTPUT
test_begin_subtest "restore config"
notmuch dump --include=config >EXPECTED
-cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
{
- EXPECT0(notmuch_database_set_config (db, "testkey1", "mutatedvalue"));
+ EXPECT0(notmuch_database_set_config (db, "test.key1", "mutatedvalue"));
}
EOF
notmuch restore --include=config <EXPECTED
notmuch dump --include=config >OUTPUT
test_expect_equal_file EXPECTED OUTPUT
+backup_database
+test_begin_subtest "override config from file"
+notmuch config set test.key1 overridden
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG}
+{
+ EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+ printf("test.key1 = %s\n", val);
+ EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+ printf("test.key2 = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_begin_subtest "NOTMUCH_CONFIG_HOOK_DIR: traditional"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+ const char *val = notmuch_config_get (db, NOTMUCH_CONFIG_HOOK_DIR);
+ printf("database.hook_dir = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+database.hook_dir = MAIL_DIR/.notmuch/hooks
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "NOTMUCH_CONFIG_HOOK_DIR: xdg"
+dir="${HOME}/.config/notmuch/default/hooks"
+mkdir -p $dir
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+ const char *val = notmuch_config_get (db, NOTMUCH_CONFIG_HOOK_DIR);
+ printf("database.hook_dir = %s\n", val);
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+database.hook_dir = CWD/home/.config/notmuch/default/hooks
+== stderr ==
+EOF
+rmdir $dir
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "notmuch_config_get_values"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+ notmuch_config_values_t *values;
+ EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_NEW_TAGS, "a;b;c"));
+ for (values = notmuch_config_get_values (db, NOTMUCH_CONFIG_NEW_TAGS);
+ notmuch_config_values_valid (values);
+ notmuch_config_values_move_to_next (values))
+ {
+ puts (notmuch_config_values_get (values));
+ }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+a
+b
+c
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_begin_subtest "notmuch_config_get_values (restart)"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+ notmuch_config_values_t *values;
+ EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_NEW_TAGS, "a;b;c"));
+ for (values = notmuch_config_get_values (db, NOTMUCH_CONFIG_NEW_TAGS);
+ notmuch_config_values_valid (values);
+ notmuch_config_values_move_to_next (values))
+ {
+ puts (notmuch_config_values_get (values));
+ }
+ for (notmuch_config_values_start (values);
+ notmuch_config_values_valid (values);
+ notmuch_config_values_move_to_next (values))
+ {
+ puts (notmuch_config_values_get (values));
+ }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+a
+b
+c
+a
+b
+c
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "notmuch_config_get_values, trailing ;"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL%
+{
+ notmuch_config_values_t *values;
+ EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_NEW_TAGS, "a;b;c"));
+ for (values = notmuch_config_get_values (db, NOTMUCH_CONFIG_NEW_TAGS);
+ notmuch_config_values_valid (values);
+ notmuch_config_values_move_to_next (values))
+ {
+ puts (notmuch_config_values_get (values));
+ }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+a
+b
+c
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "get config by key"
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG}
+{
+ printf("before = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS));
+ EXPECT0(notmuch_database_set_config (db, "maildir.synchronize_flags", "false"));
+ printf("after = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS));
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+before = true
+after = false
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "set config by key"
+notmuch config set test.key1 overridden
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG}
+{
+ printf("before = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS));
+ EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS, "false"));
+ printf("after = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS));
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+before = true
+after = false
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+test_begin_subtest "load default values"
+export MAILDIR=${MAIL_DIR}
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} '' %NULL%
+{
+ notmuch_config_key_t key;
+ for (key = NOTMUCH_CONFIG_FIRST;
+ key < NOTMUCH_CONFIG_LAST;
+ key = (notmuch_config_key_t)(key + 1)) {
+ const char *val = notmuch_config_get (db, key);
+ printf("%s\n", val ? val : "NULL" );
+ }
+}
+EOF
+cat <<'EOF' >EXPECTED
+== stdout ==
+MAIL_DIR
+MAIL_DIR/.notmuch/hooks
+
+inbox;unread
+NULL
+true
+NULL
+NULL
+NULL
+== stderr ==
+EOF
+unset MAILDIR
+test_expect_equal_file EXPECTED OUTPUT
+
+backup_database
+test_begin_subtest "override config from \${NOTMUCH_CONFIG}"
+notmuch config set test.key1 overridden
+# second argument omitted to make argv[2] == NULL
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR}
+{
+ EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+ printf("test.key1 = %s\n", val);
+ EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+ printf("test.key2 = %s\n", val);
+}
+EOF
+notmuch config set test.key1
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "override config from \${HOME}/.notmuch-config"
+ovconfig=${HOME}/.notmuch-config
+cp ${NOTMUCH_CONFIG} ${ovconfig}
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+notmuch --config=${ovconfig} config set test.key1 overridden-home
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
+{
+ EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+ printf("test.key1 = %s\n", val);
+ EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+ printf("test.key2 = %s\n", val);
+}
+EOF
+rm -f ${ovconfig}
+NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden-home
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "override config from \${XDG_CONFIG_HOME}/notmuch"
+ovconfig=${HOME}/.config/notmuch/default/config
+mkdir -p $(dirname ${ovconfig})
+cp ${NOTMUCH_CONFIG} ${ovconfig}
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+notmuch --config=${ovconfig} config set test.key1 overridden-xdg
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL%
+{
+ EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+ printf("test.key1 = %s\n", val);
+ EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+ printf("test.key2 = %s\n", val);
+}
+EOF
+rm -f ${ovconfig}
+NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden-xdg
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "override config from \${XDG_CONFIG_HOME}/notmuch with profile"
+ovconfig=${HOME}/.config/notmuch/work/config
+mkdir -p $(dirname ${ovconfig})
+cp ${NOTMUCH_CONFIG} ${ovconfig}
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+notmuch --config=${ovconfig} config set test.key1 overridden-xdg-profile
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% work
+{
+ EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+ printf("test.key1 = %s\n", val);
+ EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+ printf("test.key2 = %s\n", val);
+}
+EOF
+rm -f ${ovconfig}
+NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden-xdg-profile
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
+backup_database
+test_begin_subtest "override config from \${HOME}/.notmuch-config.work (via args)"
+ovconfig=${HOME}/.notmuch-config.work
+cp ${NOTMUCH_CONFIG} ${ovconfig}
+old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG}
+unset NOTMUCH_CONFIG
+notmuch --config=${ovconfig} config set test.key1 overridden-profile
+cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% work
+{
+ EXPECT0(notmuch_database_get_config (db, "test.key1", &val));
+ printf("test.key1 = %s\n", val);
+ EXPECT0(notmuch_database_get_config (db, "test.key2", &val));
+ printf("test.key2 = %s\n", val);
+}
+EOF
+#rm -f ${ovconfig}
+NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG}
+cat <<'EOF' >EXPECTED
+== stdout ==
+test.key1 = overridden-profile
+test.key2 = testvalue2
+== stderr ==
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+restore_database
+
test_done
add_email_corpus lkml
test_begin_subtest "new doesn't run out of file descriptors with many gzipped files"
ulimit -n 200
-gzip --recursive ${MAIL_DIR}
+find ${MAIL_DIR} -name .notmuch -prune -o -type f -print0 | xargs -0 gzip --
test_expect_success "notmuch new"
test_done
EOF
test_expect_equal_file EXPECTED OUTPUT
+test_begin_subtest "index user header, config from file"
+field_name="Test"
+printf "\n[index]\nheader.${field_name} = List-Id\n" >> notmuch-config
+notmuch reindex '*'
+notmuch search --output=files ${field_name}:notmuch | notmuch_search_files_sanitize | sort > OUTPUT
+cat <<EOF > EXPECTED
+MAIL_DIR/bar/baz/05:2,
+MAIL_DIR/bar/baz/23:2,
+MAIL_DIR/bar/baz/24:2,
+MAIL_DIR/bar/cur/20:2,
+MAIL_DIR/bar/new/21:2,
+MAIL_DIR/bar/new/22:2,
+MAIL_DIR/foo/cur/08:2,
+MAIL_DIR/foo/new/03:2,
+MAIL_DIR/new/04:2,
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
test_done
-#!/usr/bin/env python
+#!/usr/bin/env python3
import re
import sys
import json
-;; test-lib.el --- auxiliary stuff for Notmuch Emacs tests.
+;;; test-lib.el --- auxiliary stuff for Notmuch Emacs tests
;;
;; Copyright © Carl Worth
;; Copyright © David Edmondson
;;
;; Authors: Dmitry Kurochkin <dmitry.kurochkin@gmail.com>
+;;; Code:
+
(require 'cl-lib)
;; Ensure that the dynamic variables that are defined by this library
test_declare_external_prereq openssl
test_declare_external_prereq gpgsm
test_declare_external_prereq ${NOTMUCH_PYTHON}
+test_declare_external_prereq xapian-metadata
#include <ctype.h>
#include <errno.h>
+#include <stdbool.h>
char *
strtok_len (char *s, const char *delim, size_t *len)
return *len ? s : NULL;
}
+const char *
+strsplit_len (const char *s, char delim, size_t *len)
+{
+ bool escaping = false;
+ size_t count = 0;
+
+ /* Skip initial unescaped delimiters */
+ while (*s && *s == delim)
+ s++;
+
+ while (s[count] && (escaping || s[count] != delim)) {
+ escaping = (s[count] == '\\');
+ count++;
+ }
+
+ if (count==0)
+ return NULL;
+
+ *len = count;
+ return s;
+}
+
const char *
strtok_len_c (const char *s, const char *delim, size_t *len)
{
/* Const version of strtok_len. */
const char *strtok_len_c (const char *s, const char *delim, size_t *len);
+/* Simplified version of strtok_len, with a single delimiter.
+ * Handles escaping delimiters with \
+ * Usage pattern:
+ *
+ * const char *tok = input;
+ * const char *delim = ';';
+ * size_t tok_len = 0;
+ *
+ * while ((tok = strsplit_len (tok + tok_len, delim, &tok_len)) != NULL) {
+ * // do stuff with string tok of length tok_len
+ * }
+ */
+const char *strsplit_len (const char *s, char delim, size_t *len);
+
/* Return a talloced string with str sanitized.
*
* Whitespace characters (tabs and newlines) are replaced with spaces,