From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp1 ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms11 with LMTPS id qEXJAqzGu14ubQAA0tVLHw (envelope-from ) for ; Wed, 13 May 2020 10:06:36 +0000 Received: from aspmx1.migadu.com ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp1 with LMTPS id 6FnAKrrGu15hOwAAbx9fmQ (envelope-from ) for ; Wed, 13 May 2020 10:06:50 +0000 Received: from arlo.cworth.org (unknown [50.126.95.6]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) server-signature RSA-PSS (4096 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id 4E300940B30 for ; Wed, 13 May 2020 10:06:44 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by arlo.cworth.org (Postfix) with ESMTP id 2CC4F6DE14D7; Wed, 13 May 2020 03:06:36 -0700 (PDT) X-Virus-Scanned: Debian amavisd-new at cworth.org Received: from arlo.cworth.org ([127.0.0.1]) by localhost (arlo.cworth.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id zIOhj9hWm8H0; Wed, 13 May 2020 03:06:35 -0700 (PDT) Received: from arlo.cworth.org (localhost [IPv6:::1]) by arlo.cworth.org (Postfix) with ESMTP id C3DD56DE14D1; Wed, 13 May 2020 03:06:34 -0700 (PDT) Received: from localhost (localhost [127.0.0.1]) by arlo.cworth.org (Postfix) with ESMTP id D689A6DE14D1 for ; Wed, 13 May 2020 03:06:32 -0700 (PDT) X-Virus-Scanned: Debian amavisd-new at cworth.org Received: from arlo.cworth.org ([127.0.0.1]) by localhost (arlo.cworth.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id 8TaT0NFHYjqX for ; Wed, 13 May 2020 03:06:30 -0700 (PDT) X-Greylist: delayed 352 seconds by postgrey-1.36 at arlo; Wed, 13 May 2020 03:06:29 PDT Received: from out2-smtp.messagingengine.com (out2-smtp.messagingengine.com [66.111.4.26]) by arlo.cworth.org (Postfix) with ESMTPS id D14336DE14C7 for ; Wed, 13 May 2020 03:06:29 -0700 (PDT) Received: from compute2.internal (compute2.nyi.internal [10.202.2.42]) by mailout.nyi.internal (Postfix) with ESMTP id C044E5C01CE; Wed, 13 May 2020 06:00:34 -0400 (EDT) Received: from mailfrontend2 ([10.202.2.163]) by compute2.internal (MEProxy); Wed, 13 May 2020 06:00:34 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= tom-fitzhenry.me.uk; h=from:to:cc:subject:date:message-id :mime-version:content-transfer-encoding; s=fm1; bh=v68IdzWwVrQSU 5ghSnzeJ40rML1se1dSyoSbf5XM1h4=; b=JDn1DpJ+FHk1jxxK05mmjAjGs57nX 8h+iPq0LlLv0qpMZZTDE8j/Al4vuJjOcVdTMObqGQDXpWWUdg0QJA5sLwAUlfiBK MQ8EVBkS2ctCyx/rJlY2WriroJ/zkI3iIw5kRQ5KAaNE8RLh0V3Wm3jbZ7tdqGGc QclVSOk0AKP+DXH2DljehuJwg3hWse71gpHoes8P27LctZ01jp0aa2BNocY34WAx scaMj2gcqbBye1T4TGT07AdK3CLOUSE0NTCy8SJHd5TL0jdwgdLFfH8U7u/pHzZI NN6NCgKxlB0NloWurXJlultCkx34vQxEkjb/NCj80sr0RF4HmZpJMWy/Q== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:date:from :message-id:mime-version:subject:to:x-me-proxy:x-me-proxy :x-me-sender:x-me-sender:x-sasl-enc; s=fm2; bh=v68IdzWwVrQSU5ghS nzeJ40rML1se1dSyoSbf5XM1h4=; b=ucA7FDVzMCa50II5EJftxuY8wwXU8stOt 5mCuz6euPioiWUIRVERA+9uBrmBCvttBlxvA7SX3kmCENKNqvxHwxK/NR22TDzd+ J5fIj3CKgL7c1+uXe9OKVIygg7R2BSnhXCOEnODnLRYAgFPWl40iC7crQiYdYTox nwhGVG/GoPJfhnfuOh6PHCMr7OxkVSeVydMVvkvEoPcY7zH8bzXDppXYNZCDUKll Bv1lQNHFUFt34pBBwucAMgTdcDJTMDonPxizA9YL04ZipijJmhys7JtXSnSgyQO3 +D2GgehYC/Xpt/0OpWduXoga6RKJJfC8i81HfNOhEq15FFDKNdFuA== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeduhedrleeggddvvdcutefuodetggdotefrodftvf curfhrohhfihhlvgemucfhrghsthforghilhdpqfgfvfdpuffrtefokffrpgfnqfghnecu uegrihhlohhuthemuceftddtnecunecujfgurhephffvufffkffoggfgsedtkeertdertd dtnecuhfhrohhmpefvohhmucfhihhtiihhvghnrhihuceothhomhesthhomhdqfhhithii hhgvnhhrhidrmhgvrdhukheqnecuggftrfgrthhtvghrnhepgfetheehhfduledvgfehvd dvgfduheffveevffduuedtgfelleetieevtdekveefnecuffhomhgrihhnpehnohhtmhhu tghhmhgrihhlrdhorhhgnecukfhppeehledrudeijedrudefhedrvddvnecuvehluhhsth gvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomhepthhomhesthhomhdqfhhi thiihhgvnhhrhidrmhgvrdhukh 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 72A1C3066302; Wed, 13 May 2020 06:00:32 -0400 (EDT) From: Tom Fitzhenry To: notmuch@notmuchmail.org Subject: [PATCH] emacs: add notmuch-expr, sexp-style queries Date: Wed, 13 May 2020 20:00:24 +1000 Message-Id: <20200513100024.8474-1-tom@tom-fitzhenry.me.uk> X-Mailer: git-send-email 2.23.1 MIME-Version: 1.0 X-BeenThere: notmuch@notmuchmail.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "Use and development of the notmuch mail system." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Tom Fitzhenry Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: notmuch-bounces@notmuchmail.org Sender: "notmuch" X-Scanner: scn0 X-Spam-Score: 4.59 Authentication-Results: aspmx1.migadu.com; dkim=fail (body hash did not verify) header.d=tom-fitzhenry.me.uk header.s=fm1 header.b=JDn1DpJ+; dkim=fail (body hash did not verify) header.d=messagingengine.com header.s=fm2 header.b=ucA7FDVz; 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 50.126.95.6 as permitted sender) smtp.mailfrom=notmuch-bounces@notmuchmail.org X-Scan-Result: default: False [4.59 / 13.00]; RCVD_VIA_SMTP_AUTH(0.00)[]; GENERIC_REPUTATION(0.00)[-0.44461441125222]; RDNS_NONE(1.00)[]; TO_DN_SOME(0.00)[]; R_SPF_ALLOW(-0.20)[+a:c]; IP_REPUTATION_HAM(0.00)[asn: 27017(-0.17), country: US(-0.00), ip: 50.126.95.6(-0.44)]; DWL_DNSWL_FAIL(0.00)[50.126.95.6:server fail]; R_DKIM_REJECT(1.00)[tom-fitzhenry.me.uk:s=fm1,messagingengine.com:s=fm2]; FORGED_SENDER_MAILLIST(0.00)[]; MX_GOOD(-0.50)[cached: notmuchmail.org]; RCPT_COUNT_TWO(0.00)[2]; DKIM_TRACE(0.00)[tom-fitzhenry.me.uk:-,messagingengine.com:-]; MAILLIST(-0.20)[mailman]; RCVD_IN_DNSWL_FAIL(0.00)[50.126.95.6:server fail]; MIME_TRACE(0.00)[0:+]; RCVD_TLS_LAST(0.00)[]; ASN(0.00)[asn:27017, ipnet:50.126.64.0/18, country:US]; FROM_NEQ_ENVFROM(0.00)[tom@tom-fitzhenry.me.uk,notmuch-bounces@notmuchmail.org]; RDNS_DNSFAIL(0.00)[]; ARC_NA(0.00)[]; URIBL_BLOCKED(0.00)[notmuchmail.org:email]; FROM_HAS_DN(0.00)[]; MIME_GOOD(-0.10)[text/plain]; PREVIOUSLY_DELIVERED(0.00)[notmuch@notmuchmail.org]; HAS_LIST_UNSUB(-0.01)[]; MID_CONTAINS_FROM(1.00)[]; RCVD_COUNT_SEVEN(0.00)[10]; HFILTER_HOSTNAME_UNKNOWN(2.50)[]; DMARC_POLICY_SOFTFAIL(0.10)[tom-fitzhenry.me.uk : SPF not aligned (relaxed),none] X-TUID: LJq5FCecpXmA 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 | 75 ++++++++++++++++++++++++ emacs/notmuch-expr.el | 117 +++++++++++++++++++++++++++++++++++++ emacs/notmuch.el | 1 + 4 files changed, 194 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 141f5868..32f55388 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..3e13f545 --- /dev/null +++ b/emacs/notmuch-expr-test.el @@ -0,0 +1,75 @@ +(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"))))) diff --git a/emacs/notmuch-expr.el b/emacs/notmuch-expr.el new file mode 100644 index 00000000..b6ba442a --- /dev/null +++ b/emacs/notmuch-expr.el @@ -0,0 +1,117 @@ +;;; 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 "folder: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) + ;; FIXME Escape s. + (concat "\"" s "\"")) + +(provide 'notmuch-expr) +;;; notmuch-expr.el ends here diff --git a/emacs/notmuch.el b/emacs/notmuch.el index a980c7a2..d25a28ea 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.23.1