From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp0 ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms11 with LMTPS id gMcUJtl3rl9xPwAA0tVLHw (envelope-from ) for ; Fri, 13 Nov 2020 12:11:05 +0000 Received: from aspmx1.migadu.com ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp0 with LMTPS id gIL5Idl3rl+sUwAA1q6Kng (envelope-from ) for ; Fri, 13 Nov 2020 12:11:05 +0000 Received: from mail.notmuchmail.org (nmbug.tethera.net [144.217.243.247]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) server-signature RSA-PSS (2048 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id 227909403A8 for ; Fri, 13 Nov 2020 12:11:02 +0000 (UTC) Received: from nmbug.tethera.net (localhost [127.0.0.1]) by mail.notmuchmail.org (Postfix) with ESMTP id A529228C41; Fri, 13 Nov 2020 07:10:54 -0500 (EST) X-Greylist: delayed 537 seconds by postgrey-1.36 at nmbug; Fri, 13 Nov 2020 07:10:51 EST Received: from out3-smtp.messagingengine.com (out3-smtp.messagingengine.com [66.111.4.27]) by mail.notmuchmail.org (Postfix) with ESMTPS id B88BA203D6 for ; Fri, 13 Nov 2020 07:10:51 -0500 (EST) Received: from compute3.internal (compute3.nyi.internal [10.202.2.43]) by mailout.nyi.internal (Postfix) with ESMTP id 160165C00BC; Fri, 13 Nov 2020 07:01:54 -0500 (EST) Received: from mailfrontend1 ([10.202.2.162]) by compute3.internal (MEProxy); Fri, 13 Nov 2020 07:01:54 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= tom-fitzhenry.me.uk; h=from:to:cc:subject:date:message-id :in-reply-to:references:mime-version:content-transfer-encoding; s=fm3; bh=bE4KGPbdoSJbM8a6bBWf6F33ZzcioPfW8qCXVCnfncI=; b=22bh9 skPY0uTjSm8N1a3lSLeQdnvnh0X2SpEDGYyO2peG64AaH78PWjXU88M5Qp84egD9 PzO9BBQOrLNnzF7mONjV+NHj60U1ptcFHFk3En/fV3LD0+Z2VmIjUus0j6DlsHuT P076QLcQcEl98xZ2J+CyaATgR46KLqfau3c7V8Ur67a5xtAcc2fpjeq/yzH4ln28 H0EBu0jbuHUCAcyge8ljaSOUZkJX5SK47BnKkr4wxhw67iNt8wPCHZTBzUkMrJsI ea69jmRF6hotzdq0mLejpvb6DhSn5PEPpkfo0lBQ7SDn4chLi+f/lWpAxhLtLAFc 548mQkjT5d3RZtevA== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:date:from :in-reply-to:message-id:mime-version:references:subject:to :x-me-proxy:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s= fm1; bh=bE4KGPbdoSJbM8a6bBWf6F33ZzcioPfW8qCXVCnfncI=; b=Pb6kgOje LpNRJTOc9zPoreqnrLXfvAZtBqbBmuZXVbNC6GevdVFc7HndSwy1IR2xM88z4r7v dWULHMSkfkjbF7KA2qqzel7EArl/r70+yzZHa9E3TnMji8psB4FnfaPVsHoFdeuA Y7U0c1NwQPFtmIYUr9lj0u64/GSYq3zszk70ACa21LgI98FJCShaEriBi9GRF8SY yWp/a4BpsNQEtbpyvEstYo7PtU6/PzloPBIDWvcI0X+q94YeU+psHRaUXo6NhTIg /+fnf9MxeBdGgS3JNpP2FBgXYe3rPA/eYCbHfXr4VBQ2kTiXRt1F86LsWU6uA2c+ Ye+I1tHWrUMiSQ== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedujedruddvhedgfeehucetufdoteggodetrfdotf fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen uceurghilhhouhhtmecufedttdenucenucfjughrpefhvffufffkofgjfhgggfestdekre dtredttdenucfhrhhomhepvfhomhcuhfhithiihhgvnhhrhicuoehtohhmsehtohhmqdhf ihhtiihhvghnrhihrdhmvgdruhhkqeenucggtffrrghtthgvrhhnpeefueevfffgudffke dvtdfgkeefteehjefhgefftedvgffhveevieefledugfevveenucffohhmrghinhepnhho thhmuhgthhhmrghilhdrohhrghenucfkphepheelrdduieejrddufeehrddvvdenucevlh hushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpehtohhmsehtohhm qdhfihhtiihhvghnrhihrdhmvgdruhhk X-ME-Proxy: Received: from oztop.theymay.com (ppp59-167-135-22.static.internode.on.net [59.167.135.22]) by mail.messagingengine.com (Postfix) with ESMTPA id 74E183280060; Fri, 13 Nov 2020 07:01:51 -0500 (EST) From: Tom Fitzhenry To: notmuch@notmuchmail.org Subject: [PATCH v2] emacs: add notmuch-expr, sexp-style queries Date: Fri, 13 Nov 2020 23:01:22 +1100 Message-Id: <20201113120122.26105-1-tom@tom-fitzhenry.me.uk> X-Mailer: git-send-email 2.28.0 In-Reply-To: <20200513100024.8474-1-tom@tom-fitzhenry.me.uk> References: <20200513100024.8474-1-tom@tom-fitzhenry.me.uk> MIME-Version: 1.0 Message-ID-Hash: JBNFJD3HNOT5EH6UBOU74Z36AK6NL2XX X-Message-ID-Hash: JBNFJD3HNOT5EH6UBOU74Z36AK6NL2XX X-MailFrom: tom@tom-fitzhenry.me.uk X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; header-match-notmuch.notmuchmail.org-0; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; suspicious-header CC: Tom Fitzhenry X-Mailman-Version: 3.2.1 Precedence: list List-Id: "Use and development of the notmuch mail system." List-Help: List-Post: List-Subscribe: List-Unsubscribe: Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit X-Scanner: ns3122888.ip-94-23-21.eu Authentication-Results: aspmx1.migadu.com; dkim=fail (body hash did not verify) header.d=tom-fitzhenry.me.uk header.s=fm3 header.b=22bh9 sk; dkim=fail (body hash did not verify) header.d=messagingengine.com header.s=fm1 header.b=Pb6kgOje; dmarc=fail reason="SPF not aligned (relaxed)" header.from=tom-fitzhenry.me.uk (policy=none); spf=pass (aspmx1.migadu.com: domain of notmuch-bounces@notmuchmail.org designates 144.217.243.247 as permitted sender) smtp.mailfrom=notmuch-bounces@notmuchmail.org X-Spam-Score: 1.09 X-TUID: bTLJrzeT3nXY From: Tom Fitzhenry notmuch-expr allows you to write notmuch search queries in sexp style like: (notmuch-expr '(and (to "emacs-devel") "info manual" (or (not (is "spam")) (is "important")))) which will generate the textual query: "to:emacs-devel AND (NOT is:spam OR is:important) AND \"info manual\"" --- emacs/Makefile.local | 1 + emacs/notmuch-expr-test.el | 96 ++++++++++++++++++++++++++++ emacs/notmuch-expr.el | 124 +++++++++++++++++++++++++++++++++++++ emacs/notmuch.el | 1 + 4 files changed, 222 insertions(+) create mode 100644 emacs/notmuch-expr-test.el create mode 100644 emacs/notmuch-expr.el diff --git a/emacs/Makefile.local b/emacs/Makefile.local index d1b320c3..f68e6e31 100644 --- a/emacs/Makefile.local +++ b/emacs/Makefile.local @@ -22,6 +22,7 @@ emacs_sources := \ $(dir)/notmuch-version.el \ $(dir)/notmuch-jump.el \ $(dir)/notmuch-company.el \ + $(dir)/notmuch-expr.el \ $(dir)/notmuch-draft.el elpa_sources := ${emacs_sources} $(dir)/notmuch-pkg.el diff --git a/emacs/notmuch-expr-test.el b/emacs/notmuch-expr-test.el new file mode 100644 index 00000000..92029fec --- /dev/null +++ b/emacs/notmuch-expr-test.el @@ -0,0 +1,96 @@ +(require 'ert) +(require 'notmuch-expr) + +(ert-deftest and () + (should + (equal + "(\"valued\" AND is:unread AND from:spam@example.com)" + (notmuch-expr + '(and + "valued" + (is "unread") + (from "spam@example.com")))))) + +(ert-deftest body () + (should + (equal + "(body:wallace AND from:gromit)" + (notmuch-expr + '(and + (body "wallace") + (from "gromit")))))) + +(ert-deftest regex () + (should + (equal + "(subject:\"/Ca+sh/\" AND NOT is:important)" + (notmuch-expr + '(and + (subject "/Ca+sh/") + (not (is "important"))))))) + +(ert-deftest precedence () + (should + (equal + "(to:emacs-devel AND (NOT is:spam OR is:important))" + (notmuch-expr + '(and + (to "emacs-devel") + (or + (not (is "spam")) + (is "important"))))))) + +(ert-deftest xor () + (should + (equal + "is:inbox XOR is:sent" + (notmuch-expr + '(xor + (is "inbox") + (is "sent")))))) + +(ert-deftest literal () + (should + (equal + "(is:inbox OR from:foo)" + (notmuch-expr + '(or + (is "inbox") + (literal "from:foo")))))) + +(ert-deftest string () + (should + (equal + "(is:inbox OR \"from:foo\")" + (notmuch-expr + '(or + (is "inbox") + "from:foo"))))) + +(ert-deftest tag-with-spaces () + (should + (equal + "is:\"a tag\"" + (notmuch-expr + '(tag "a tag"))))) + +(ert-deftest quoted-spaces () + (should + (equal + "subject:\"Hello there\"" + (notmuch-expr + '(subject "Hello there"))))) + +(ert-deftest quoted-backslash () + (should + (equal + "subject:\"A celebration! \\o/ Woo.\"" + (notmuch-expr + '(subject "A celebration! \\o/ Woo."))))) + +(ert-deftest quoted-quote () + (should + (equal + "subject:\"Gandalf: \\\"Use the force!\\\" 2001\"" + (notmuch-expr + '(subject "Gandalf: \"Use the force!\" 2001"))))) diff --git a/emacs/notmuch-expr.el b/emacs/notmuch-expr.el new file mode 100644 index 00000000..f5a3429f --- /dev/null +++ b/emacs/notmuch-expr.el @@ -0,0 +1,124 @@ +;;; notmuch-expr.el --- An S-exp library for building notmuch search queries -*- lexical-binding: t; -*- + +;; Author: Tom Fitzhenry +;; Package-Requires: ((emacs "24.1")) +;; URL: https://notmuchmail.org + +;;; Commentary: + +;; This package provides a way to build notmuch search queries via s-expressions. +;; +;; For example, rather than write: + +;; "to:emacs-devel AND (NOT is:spam OR is:important) AND \"info manual\"" +;; +;; this package allows you to generate the same query via s-expressions: +;; +;; (notmuch-expr +;; '(and +;; (to "emacs-devel") +;; "info manual" +;; (or +;; (not (is "spam")) +;; (is "important")))) +;; +;; See notmuch-expr-test.el for more examples. +;; +;; Some search terms are unsupported. To use those, use the `literal' atom. +;; For example: (literal "path:spam") +;; +;; man page: notmuch-search-terms(7). +;; The generated search query may change across different versions. + +;;; Code: + +(defmacro notmuch-expr (query) + "Compile an sexp QUERY into a textual notmuch query." + `(notmuch-expr--eval ,query)) + +(defun notmuch-expr--eval (expr) + (pcase expr + (`(tag ,s) (notmuch-expr--is s)) + (`(is ,s) (notmuch-expr--is s)) + (`(from ,s) (notmuch-expr--from s)) + (`(to ,s) (notmuch-expr--to s)) + (`(body ,s) (notmuch-expr--body s)) + (`(subject ,s) (notmuch-expr--subject s)) + + ;; Boolean operators. + (`(and . ,clauses) (notmuch-expr--and clauses)) + (`(or . ,clauses) (notmuch-expr--or clauses)) + (`(not ,clause) (notmuch-expr--not clause)) + (`(xor ,c1 ,c2) (notmuch-expr--xor c1 c2)) + + ;; Provide an escape-hatch. + (`(literal ,s) (notmuch-expr--literal s)) + + ;; Otherwise, quote. + (s (notmuch-expr--quote s)))) + +(defun notmuch-expr--and (clauses) + (concat + "(" + (mapconcat 'notmuch-expr--eval clauses " AND ") + ")")) + +(defun notmuch-expr--or (clauses) + (concat + "(" + (mapconcat 'notmuch-expr--eval clauses " OR ") + ")")) + +(defun notmuch-expr--not (clauses) + (concat "NOT " (notmuch-expr--eval clauses))) + +(defun notmuch-expr--xor (c1 c2) + (concat + (notmuch-expr--eval c1) + " XOR " + (notmuch-expr--eval c2))) + +(defun notmuch-expr--body (s) + (concat "body:" + (notmuch-expr--leaf s))) + +(defun notmuch-expr--subject (s) + (concat "subject:" + (notmuch-expr--leaf s))) + +(defun notmuch-expr--from (f) + (concat "from:" + (notmuch-expr--leaf f))) + +(defun notmuch-expr--to (f) + (concat "to:" + (notmuch-expr--leaf f))) + +(defun notmuch-expr--is (expr) + (concat "is:" + (notmuch-expr--leaf expr))) + +(defun notmuch-expr--leaf (s) + (if (string-match-p "^[a-zA-Z0-9.@-]+$" s) + ;; Avoid ugly quoting. + ;; This is safe because the string is bound to a prefix + ;; and thus it won't be misinterpreted by notmuch. + s + (notmuch-expr--quote s))) + +(defun notmuch-expr--literal (s) + s) + +(defun notmuch-expr--quote (s) + "Return a quoted version of S." + (concat "\"" + (replace-regexp-in-string + (rx "\"") + ;; We must double escape the backslash to avoid it being + ;; interpreted as a back-reference. + "\\\\\"" + s) + "\"")) + +(provide 'notmuch-expr) +;;; notmuch-expr.el ends here diff --git a/emacs/notmuch.el b/emacs/notmuch.el index 165aaa43..8c5843e5 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -79,6 +79,7 @@ (require 'notmuch-maildir-fcc) (require 'notmuch-message) (require 'notmuch-parser) +(require 'notmuch-expr) (defcustom notmuch-search-result-format `(("date" . "%12s ") -- 2.28.0