config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

notmuch-address.el (16156B)


      1 ;;; notmuch-address.el --- address completion with notmuch  -*- lexical-binding: t -*-
      2 ;;
      3 ;; Copyright © David Edmondson
      4 ;;
      5 ;; This file is part of Notmuch.
      6 ;;
      7 ;; Notmuch is free software: you can redistribute it and/or modify it
      8 ;; under the terms of the GNU General Public License as published by
      9 ;; the Free Software Foundation, either version 3 of the License, or
     10 ;; (at your option) any later version.
     11 ;;
     12 ;; Notmuch is distributed in the hope that it will be useful, but
     13 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
     14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     15 ;; General Public License for more details.
     16 ;;
     17 ;; You should have received a copy of the GNU General Public License
     18 ;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
     19 ;;
     20 ;; Authors: David Edmondson <dme@dme.org>
     21 
     22 ;;; Code:
     23 
     24 (require 'message)
     25 (require 'notmuch-parser)
     26 (require 'notmuch-lib)
     27 (require 'notmuch-company)
     28 
     29 (declare-function company-manual-begin "company")
     30 
     31 ;;; Cache internals
     32 
     33 (defvar notmuch-address-last-harvest 0
     34   "Time of last address harvest.")
     35 
     36 (defvar notmuch-address-completions (make-hash-table :test 'equal)
     37   "Hash of email addresses for completion during email composition.
     38 This variable is set by calling `notmuch-address-harvest'.")
     39 
     40 (defvar notmuch-address-full-harvest-finished nil
     41   "Whether full completion address harvesting has finished.
     42 Use `notmuch-address--harvest-ready' to access as that will load
     43 a saved hash if necessary (and available).")
     44 
     45 (defun notmuch-address--harvest-ready ()
     46   "Return t if there is a full address hash available.
     47 
     48 If the hash is not present it attempts to load a saved hash."
     49   (or notmuch-address-full-harvest-finished
     50       (notmuch-address--load-address-hash)))
     51 
     52 ;;; Options
     53 
     54 (defcustom notmuch-address-command 'internal
     55   "Determines how address completion candidates are generated.
     56 
     57 If this is a string, then that string should be an external
     58 program, which must take a single argument (searched string)
     59 and output a list of completion candidates, one per line.
     60 
     61 If this is the symbol `internal', then an implementation is used
     62 that relies on the \"notmuch address\" command, but does not use
     63 any third-party (i.e. \"external\") programs.
     64 
     65 If this is the symbol `as-is', then Notmuch does not modify the
     66 value of `message-completion-alist'. This option has to be set to
     67 this value before `notmuch' is loaded, otherwise the modification
     68 to `message-completion-alist' may already have taken place. This
     69 setting obviously does not prevent `message-completion-alist'
     70 from being modified at all; the user or some third-party package
     71 may still modify it.
     72 
     73 Finally, if this is nil, then address completion is disabled."
     74   :type '(radio
     75 	  (const  :tag "Use internal address completion" internal)
     76 	  (string :tag "Use external completion command")
     77 	  (const  :tag "Disable address completion" nil)
     78 	  (const  :tag "Use default or third-party mechanism" as-is))
     79   :group 'notmuch-send
     80   :group 'notmuch-address
     81   :group 'notmuch-external)
     82 
     83 (defcustom notmuch-address-internal-completion '(sent nil)
     84   "Determines how internal address completion generates candidates.
     85 
     86 This should be a list of the form (DIRECTION FILTER), where
     87 DIRECTION is either sent or received and specifies whether the
     88 candidates are searched in messages sent by the user or received
     89 by the user (note received by is much faster), and FILTER is
     90 either nil or a filter-string, such as \"date:1y..\" to append to
     91 the query."
     92   :type '(list :tag "Use internal address completion"
     93 	       (radio
     94 		:tag "Base completion on messages you have"
     95 		:value sent
     96 		(const :tag "sent (more accurate)" sent)
     97 		(const :tag "received (faster)" received))
     98 	       (radio :tag "Filter messages used for completion"
     99 		      (const :tag "Use all messages" nil)
    100 		      (string :tag "Filter query")))
    101   ;; We override set so that we can clear the cache when this changes
    102   :set (lambda (symbol value)
    103 	 (set-default symbol value)
    104 	 (setq notmuch-address-last-harvest 0)
    105 	 (setq notmuch-address-completions (clrhash notmuch-address-completions))
    106 	 (setq notmuch-address-full-harvest-finished nil))
    107   :group 'notmuch-send
    108   :group 'notmuch-address
    109   :group 'notmuch-external)
    110 
    111 (defcustom notmuch-address-save-filename nil
    112   "Filename to save the cached completion addresses.
    113 
    114 All the addresses notmuch uses for address completion will be
    115 cached in this file.  This has obvious privacy implications so
    116 you should make sure it is not somewhere publicly readable."
    117   :type '(choice (const :tag "Off" nil)
    118 		 (file :tag "Filename"))
    119   :group 'notmuch-send
    120   :group 'notmuch-address
    121   :group 'notmuch-external)
    122 
    123 (defcustom notmuch-address-selection-function 'notmuch-address-selection-function
    124   "The function to select address from given list.
    125 
    126 The function is called with PROMPT, COLLECTION, and INITIAL-INPUT
    127 as arguments (subset of what `completing-read' can be called
    128 with).  While executed the value of `completion-ignore-case'
    129 is t.  See documentation of function
    130 `notmuch-address-selection-function' to know how address
    131 selection is made by default."
    132   :type 'function
    133   :group 'notmuch-send
    134   :group 'notmuch-address
    135   :group 'notmuch-external)
    136 
    137 (defcustom notmuch-address-post-completion-functions nil
    138   "Functions called after completing address.
    139 
    140 The completed address is passed as an argument to each function.
    141 Note that this hook will be invoked for completion in headers
    142 matching `notmuch-address-completion-headers-regexp'."
    143   :type 'hook
    144   :group 'notmuch-address
    145   :group 'notmuch-hooks)
    146 
    147 (defcustom notmuch-address-use-company t
    148   "If available, use company mode for address completion."
    149   :type 'boolean
    150   :group 'notmuch-send
    151   :group 'notmuch-address)
    152 
    153 ;;; Setup
    154 
    155 (defun notmuch-address-selection-function (prompt collection initial-input)
    156   "Default address selection function: delegate to completing read."
    157   (completing-read
    158    prompt collection nil nil initial-input 'notmuch-address-history))
    159 
    160 (defvar notmuch-address-completion-headers-regexp
    161   "^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):")
    162 
    163 (defvar notmuch-address-history nil)
    164 
    165 (defun notmuch-address-message-insinuate ()
    166   (message "calling notmuch-address-message-insinuate is no longer needed"))
    167 
    168 (defun notmuch-address-setup ()
    169   (unless (eq notmuch-address-command 'as-is)
    170     (when (and notmuch-address-use-company
    171 	       (require 'company nil t))
    172       (notmuch-company-setup))
    173     (cl-pushnew (cons notmuch-address-completion-headers-regexp
    174 		      #'notmuch-address-expand-name)
    175 		message-completion-alist :test #'equal)))
    176 
    177 (defun notmuch-address-toggle-internal-completion ()
    178   "Toggle use of internal completion for current buffer.
    179 
    180 This overrides the global setting for address completion and
    181 toggles the setting in this buffer."
    182   (interactive)
    183   (if (local-variable-p 'notmuch-address-command)
    184       (kill-local-variable 'notmuch-address-command)
    185     (setq-local notmuch-address-command 'internal))
    186   (when (boundp 'company-idle-delay)
    187     (if (local-variable-p 'company-idle-delay)
    188 	(kill-local-variable 'company-idle-delay)
    189       (setq-local company-idle-delay nil))))
    190 
    191 ;;; Completion
    192 
    193 (defun notmuch-address-matching (substring)
    194   "Returns a list of completion candidates matching SUBSTRING.
    195 The candidates are taken from `notmuch-address-completions'."
    196   (let ((candidates)
    197 	(re (regexp-quote substring)))
    198     (maphash (lambda (key _val)
    199 	       (when (string-match re key)
    200 		 (push key candidates)))
    201 	     notmuch-address-completions)
    202     candidates))
    203 
    204 (defun notmuch-address-options (original)
    205   "Return a list of completion candidates.
    206 Use either elisp-based implementation or older implementation
    207 requiring external commands."
    208   (cond
    209    ((eq notmuch-address-command 'internal)
    210     (unless (notmuch-address--harvest-ready)
    211       ;; First, run quick synchronous harvest based on what the user
    212       ;; entered so far.
    213       (notmuch-address-harvest original t))
    214     (prog1 (notmuch-address-matching original)
    215       ;; Then start the (potentially long-running) full asynchronous
    216       ;; harvest if necessary.
    217       (notmuch-address-harvest-trigger)))
    218    (t
    219     (notmuch--process-lines notmuch-address-command original))))
    220 
    221 (defun notmuch-address-expand-name ()
    222   (cond
    223    ((and (eq notmuch-address-command 'internal)
    224 	 notmuch-address-use-company
    225 	 (bound-and-true-p company-mode))
    226     (company-manual-begin))
    227    (notmuch-address-command
    228     (let* ((end (point))
    229 	   (beg (save-excursion
    230 		  (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
    231 		  (goto-char (match-end 0))
    232 		  (point)))
    233 	   (orig (buffer-substring-no-properties beg end))
    234 	   (completion-ignore-case t)
    235 	   (options (with-temp-message "Looking for completion candidates..."
    236 		      (notmuch-address-options orig)))
    237 	   (num-options (length options))
    238 	   (chosen (cond
    239 		    ((eq num-options 0)
    240 		     nil)
    241 		    ((eq num-options 1)
    242 		     (car options))
    243 		    (t
    244 		     (funcall notmuch-address-selection-function
    245 			      (format "Address (%s matches): " num-options)
    246 			      options
    247 			      orig)))))
    248       (if chosen
    249 	  (progn
    250 	    (push chosen notmuch-address-history)
    251 	    (delete-region beg end)
    252 	    (insert chosen)
    253 	    (run-hook-with-args 'notmuch-address-post-completion-functions
    254 				chosen))
    255 	(message "No matches.")
    256 	(ding))))
    257    (t nil)))
    258 
    259 ;;; Harvest
    260 
    261 (defun notmuch-address-harvest-addr (result)
    262   (puthash (plist-get result :name-addr)
    263 	   t notmuch-address-completions))
    264 
    265 (defun notmuch-address-harvest-filter (proc string)
    266   (when (buffer-live-p (process-buffer proc))
    267     (with-current-buffer (process-buffer proc)
    268       (save-excursion
    269 	(goto-char (point-max))
    270 	(insert string))
    271       (notmuch-sexp-parse-partial-list
    272        'notmuch-address-harvest-addr (process-buffer proc)))))
    273 
    274 (defvar notmuch-address-harvest-procs '(nil . nil)
    275   "The currently running harvests.
    276 
    277 The car is a partial harvest, and the cdr is a full harvest.")
    278 
    279 (defun notmuch-address-harvest (&optional addr-prefix synchronous callback)
    280   "Collect addresses completion candidates.
    281 
    282 It queries the notmuch database for messages sent/received (as
    283 configured with `notmuch-address-command') by the user, collects
    284 destination/source addresses from those messages and stores them
    285 in `notmuch-address-completions'.
    286 
    287 If ADDR-PREFIX is not nil, only messages with to/from addresses
    288 matching ADDR-PREFIX*' are queried.
    289 
    290 Address harvesting may take some time so the address collection runs
    291 asynchronously unless SYNCHRONOUS is t. In case of asynchronous
    292 execution, CALLBACK is called when harvesting finishes."
    293   (let* ((sent (eq (car notmuch-address-internal-completion) 'sent))
    294 	 (config-query (cadr notmuch-address-internal-completion))
    295 	 (prefix-query (and addr-prefix
    296 			    (format "%s:%s*"
    297 				    (if sent "to" "from")
    298 				    addr-prefix)))
    299 	 (from-or-to-me-query
    300 	  (mapconcat (lambda (x)
    301 		       (concat (if sent "from:" "to:") x))
    302 		     (notmuch-user-emails) " or "))
    303 	 (query (if (or prefix-query config-query)
    304 		    (concat (format "(%s)" from-or-to-me-query)
    305 			    (and prefix-query
    306 				 (format " and (%s)" prefix-query))
    307 			    (and config-query
    308 				 (format " and (%s)" config-query)))
    309 		  from-or-to-me-query))
    310 	 (args `("address" "--format=sexp" "--format-version=5"
    311 		 ,(if sent "--output=recipients" "--output=sender")
    312 		 "--deduplicate=address"
    313 		 ,query)))
    314     (if synchronous
    315 	(mapc #'notmuch-address-harvest-addr
    316 	      (apply 'notmuch-call-notmuch-sexp args))
    317       ;; Asynchronous
    318       (let* ((current-proc (if addr-prefix
    319 			       (car notmuch-address-harvest-procs)
    320 			     (cdr notmuch-address-harvest-procs)))
    321 	     (proc-name (format "notmuch-address-%s-harvest"
    322 				(if addr-prefix "partial" "full")))
    323 	     (proc-buf (concat " *" proc-name "*")))
    324 	;; Kill any existing process
    325 	(when current-proc
    326 	  (kill-buffer (process-buffer current-proc))) ; this also kills the process
    327 	(setq current-proc
    328 	      (apply 'notmuch-start-notmuch proc-name proc-buf
    329 		     callback				; process sentinel
    330 		     args))
    331 	(set-process-filter current-proc 'notmuch-address-harvest-filter)
    332 	(set-process-query-on-exit-flag current-proc nil)
    333 	(if addr-prefix
    334 	    (setcar notmuch-address-harvest-procs current-proc)
    335 	  (setcdr notmuch-address-harvest-procs current-proc)))))
    336   ;; return value
    337   nil)
    338 
    339 (defvar notmuch-address--save-hash-version 1
    340   "Version format of the save hash.")
    341 
    342 (defun notmuch-address--get-address-hash ()
    343   "Return the saved address hash as a plist.
    344 
    345 Returns nil if the save file does not exist, or it does not seem
    346 to be a saved address hash."
    347   (and notmuch-address-save-filename
    348        (condition-case nil
    349 	   (with-temp-buffer
    350 	     (insert-file-contents notmuch-address-save-filename)
    351 	     (let ((name (read (current-buffer)))
    352 		   (plist (read (current-buffer))))
    353 	       ;; We do two simple sanity checks on the loaded file.
    354 	       ;; We just check a version is specified, not that
    355 	       ;; it is the current version, as we are allowed to
    356 	       ;; over-write and a save-file with an older version.
    357 	       (and (string= name "notmuch-address-hash")
    358 		    (plist-get plist :version)
    359 		    plist)))
    360 	 ;; The error case catches any of the reads failing.
    361 	 (error nil))))
    362 
    363 (defun notmuch-address--load-address-hash ()
    364   "Read the saved address hash and set the corresponding variables."
    365   (let ((load-plist (notmuch-address--get-address-hash)))
    366     (when (and load-plist
    367 	       ;; If the user's setting have changed, or the version
    368 	       ;; has changed, return nil to make sure the new settings
    369 	       ;; take effect.
    370 	       (equal (plist-get load-plist :completion-settings)
    371 		      notmuch-address-internal-completion)
    372 	       (equal (plist-get load-plist :version)
    373 		      notmuch-address--save-hash-version))
    374       (setq notmuch-address-last-harvest (plist-get load-plist :last-harvest))
    375       (setq notmuch-address-completions (plist-get load-plist :completions))
    376       (setq notmuch-address-full-harvest-finished t)
    377       ;; Return t to say load was successful.
    378       t)))
    379 
    380 (defun notmuch-address--save-address-hash ()
    381   (when notmuch-address-save-filename
    382     (if (or (not (file-exists-p notmuch-address-save-filename))
    383 	    ;; The file exists, check it is a file we saved.
    384 	    (notmuch-address--get-address-hash))
    385 	(with-temp-file notmuch-address-save-filename
    386 	  (let ((save-plist
    387 		 (list :version notmuch-address--save-hash-version
    388 		       :completion-settings notmuch-address-internal-completion
    389 		       :last-harvest notmuch-address-last-harvest
    390 		       :completions notmuch-address-completions)))
    391 	    (print "notmuch-address-hash" (current-buffer))
    392 	    (print save-plist (current-buffer))))
    393       (message "\
    394 Warning: notmuch-address-save-filename %s exists but doesn't
    395 appear to be an address savefile.  Not overwriting."
    396 	       notmuch-address-save-filename))))
    397 
    398 (defun notmuch-address-harvest-trigger ()
    399   (let ((now (float-time)))
    400     (when (> (- now notmuch-address-last-harvest) 86400)
    401       (setq notmuch-address-last-harvest now)
    402       (notmuch-address-harvest
    403        nil nil
    404        (lambda (_proc event)
    405 	 ;; If harvest fails, we want to try
    406 	 ;; again when the trigger is next called.
    407 	 (if (string= event "finished\n")
    408 	     (progn
    409 	       (notmuch-address--save-address-hash)
    410 	       (setq notmuch-address-full-harvest-finished t))
    411 	   (setq notmuch-address-last-harvest 0)))))))
    412 
    413 ;;; Standalone completion
    414 
    415 (defun notmuch-address-from-minibuffer (prompt)
    416   (if (not notmuch-address-command)
    417       (read-string prompt)
    418     (let ((rmap (copy-keymap minibuffer-local-map))
    419 	  (omap minibuffer-local-map))
    420       ;; Configure TAB to start completion when executing read-string.
    421       ;; "Original" minibuffer keymap is restored just before calling
    422       ;; notmuch-address-expand-name as it may also use minibuffer-local-map
    423       ;; (completing-read probably does not but if something else is used there).
    424       (define-key rmap (kbd "TAB") (lambda ()
    425 				     (interactive)
    426 				     (let ((enable-recursive-minibuffers t)
    427 					   (minibuffer-local-map omap))
    428 				       (notmuch-address-expand-name))))
    429       (let ((minibuffer-local-map rmap))
    430 	(read-string prompt)))))
    431 
    432 ;;; _
    433 
    434 (provide 'notmuch-address)
    435 
    436 ;;; notmuch-address.el ends here