1 ;;; mh-index -- MH-E interface to indexing programs
3 ;; Copyright (C) 2002, 2003, 2004 Free Software Foundation, Inc.
5 ;; Author: Satyaki Das <satyaki@theforce.stanford.edu>
6 ;; Maintainer: Bill Wohler <wohler@newt.com>
10 ;; This file is part of GNU Emacs.
12 ;; GNU Emacs is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation; either version 2, or (at your option)
17 ;; GNU Emacs is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with GNU Emacs; see the file COPYING. If not, write to the
24 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
25 ;; Boston, MA 02111-1307, USA.
29 ;;; (1) The following search engines are supported:
36 ;;; (2) To use this package, you first have to build an index. Please read
37 ;;; the documentation for `mh-index-search' to get started. That
38 ;;; documentation will direct you to the specific instructions for your
39 ;;; particular indexer.
45 (eval-when-compile (require 'mh-acros
))
51 (autoload 'gnus-local-map-property
"gnus-util")
52 (autoload 'gnus-eval-format
"gnus-spec")
53 (autoload 'widget-convert-button
"wid-edit")
54 (autoload 'executable-find
"executable")
56 ;; Support different indexing programs
57 (defvar mh-indexer-choices
59 mh-swish
++-binary mh-swish
++-execute-search mh-swish
++-next-result
60 mh-swish
++-regexp-builder
)
62 mh-swish-binary mh-swish-execute-search mh-swish-next-result nil
)
64 mh-mairix-binary mh-mairix-execute-search mh-mairix-next-result
65 mh-mairix-regexp-builder
)
67 mh-namazu-binary mh-namazu-execute-search mh-namazu-next-result nil
)
69 mh-pick-binary mh-pick-execute-search mh-pick-next-result
70 mh-pick-regexp-builder
)
72 mh-grep-binary mh-grep-execute-search mh-grep-next-result nil
))
73 "List of possible indexer choices.")
74 (defvar mh-indexer nil
75 "Chosen index program.")
76 (defvar mh-index-execute-search-function nil
77 "Function which executes the search program.")
78 (defvar mh-index-next-result-function nil
79 "Function to parse the next line of output.")
80 (defvar mh-index-regexp-builder nil
81 "Function used to construct search regexp.")
83 ;; FIXME: This should be a defcustom...
84 (defvar mh-index-folder
"+mhe-index"
85 "Folder that contains the folders resulting from the index searches.")
87 ;; Temporary buffers for search results
88 (defvar mh-index-temp-buffer
" *mh-index-temp*")
89 (defvar mh-checksum-buffer
" *mh-checksum-buffer*")
93 ;;; A few different checksum programs are supported. The supported programs
99 ;;; To add support for your favorite checksum program add a clause to the cond
100 ;;; statement in mh-checksum-choose. This should set the variable
101 ;;; mh-checksum-cmd to the command line needed to run the checsum program and
102 ;;; should set mh-checksum-parser to a function which returns a cons cell
103 ;;; containing the message number and checksum string.
105 (defvar mh-checksum-cmd
)
106 (defvar mh-checksum-parser
)
108 (defun mh-checksum-choose ()
109 "Check if a program to create a checksum is present."
110 (unless (boundp 'mh-checksum-cmd
)
111 (let ((exec-path (append '("/sbin" "/usr/sbin") exec-path
)))
112 (cond ((executable-find "md5sum")
113 (setq mh-checksum-cmd
(list (executable-find "md5sum")))
114 (setq mh-checksum-parser
#'mh-md5sum-parser
))
115 ((executable-find "openssl")
116 (setq mh-checksum-cmd
(list (executable-find "openssl") "md5"))
117 (setq mh-checksum-parser
#'mh-openssl-parser
))
118 ((executable-find "md5")
119 (setq mh-checksum-cmd
(list (executable-find "md5")))
120 (setq mh-checksum-parser
#'mh-md5-parser
))
121 (t (error "No suitable checksum program"))))))
123 (defun mh-md5sum-parser ()
124 "Parse md5sum output."
125 (let ((begin (line-beginning-position))
126 (end (line-end-position))
127 first-space last-slash
)
128 (setq first-space
(search-forward " " end t
))
130 (setq last-slash
(search-backward "/" begin t
))
131 (cond ((and first-space last-slash
)
132 (cons (car (read-from-string (buffer-substring-no-properties
133 (1+ last-slash
) end
)))
134 (buffer-substring-no-properties begin
(1- first-space
))))
135 (t (cons nil nil
)))))
137 (defun mh-openssl-parser ()
138 "Parse openssl output."
139 (let ((begin (line-beginning-position))
140 (end (line-end-position))
141 last-space last-slash
)
143 (setq last-space
(search-backward " " begin t
))
144 (setq last-slash
(search-backward "/" begin t
))
145 (cond ((and last-slash last-space
)
146 (cons (car (read-from-string (buffer-substring-no-properties
147 (1+ last-slash
) (1- last-space
))))
148 (buffer-substring-no-properties (1+ last-space
) end
))))))
150 (defalias 'mh-md5-parser
'mh-openssl-parser
)
154 ;;; Make sure that we don't produce too long a command line.
156 (defvar mh-index-max-cmdline-args
500
157 "Maximum number of command line args.")
159 (defun mh-index-execute (cmd &rest args
)
160 "Partial imitation of xargs.
161 The current buffer contains a list of strings, one on each line. The function
162 will execute CMD with ARGS and pass the first `mh-index-max-cmdline-args'
163 strings to it. This is repeated till all the strings have been used."
164 (goto-char (point-min))
165 (let ((current-buffer (current-buffer)))
167 (let ((out (current-buffer)))
168 (set-buffer current-buffer
)
170 (let ((arg-list (reverse args
))
172 (while (and (not (eobp)) (< count mh-index-max-cmdline-args
))
173 (push (buffer-substring-no-properties (point) (line-end-position))
177 (apply #'call-process cmd nil
(list out nil
) nil
178 (nreverse arg-list
))))
180 (insert-buffer-substring out
)))))
184 (defun mh-index-update-single-msg (msg checksum origin-map
)
185 "Update various maps for one message.
186 MSG is a index folder message, CHECKSUM its MD5 hash and ORIGIN-MAP, if
187 non-nil, a hashtable containing which maps each message in the index folder to
188 the folder and message that it was copied from. The function updates the hash
189 tables `mh-index-msg-checksum-map' and `mh-index-checksum-origin-map'.
191 This function should only be called in the appropriate index folder buffer."
192 (cond ((and origin-map
(gethash checksum mh-index-checksum-origin-map
))
193 (let* ((intermediate (gethash msg origin-map
))
194 (ofolder (car intermediate
))
195 (omsg (cdr intermediate
)))
196 ;; This is most probably a duplicate. So eliminate it.
197 (call-process "rm" nil nil nil
198 (format "%s%s/%s" mh-user-path
199 (substring mh-current-folder
1) msg
))
200 (when (gethash ofolder mh-index-data
)
201 (remhash omsg
(gethash ofolder mh-index-data
)))))
203 (setf (gethash msg mh-index-msg-checksum-map
) checksum
)
205 (setf (gethash checksum mh-index-checksum-origin-map
)
206 (gethash msg origin-map
))))))
209 (defun mh-index-update-maps (folder &optional origin-map
)
210 "Annotate all as yet unannotated messages in FOLDER with their MD5 hash.
211 As a side effect msg -> checksum map is updated. Optional argument ORIGIN-MAP
212 is a hashtable which maps each message in the index folder to the original
213 folder and message from whence it was copied. If present the
214 checksum -> (origin-folder, origin-index) map is updated too."
215 (clrhash mh-index-msg-checksum-map
)
218 (set-buffer (get-buffer-create mh-checksum-buffer
))
220 ;; Run scan to check if any messages needs MD5 annotations at all
222 (mh-exec-cmd-output mh-scan-prog nil
"-width" "80"
223 "-format" "%(msg)\n%{x-mhe-checksum}\n"
225 (goto-char (point-min))
228 (setq msg
(buffer-substring-no-properties
229 (point) (line-end-position)))
232 (cond ((not (string-match "^[0-9]*$" msg
)))
234 ;; need to compute checksum
235 (set-buffer mh-checksum-buffer
)
236 (insert mh-user-path
(substring folder
1) "/" msg
"\n"))
239 (setq checksum
(buffer-substring-no-properties
240 (point) (line-end-position)))
241 (let ((msg (car (read-from-string msg
))))
243 (mh-index-update-single-msg msg checksum origin-map
)))))
245 ;; Run checksum program if needed
246 (unless (and (eobp) (bobp))
247 (apply #'mh-index-execute mh-checksum-cmd
)
248 (goto-char (point-min))
250 (let* ((intermediate (funcall mh-checksum-parser
))
251 (msg (car intermediate
))
252 (checksum (cdr intermediate
)))
255 (mh-exec-cmd "anno" folder msg
"-component" "X-MHE-Checksum"
256 "-nodate" "-text" checksum
"-inplace")
260 (mh-index-update-single-msg msg checksum origin-map
)))
262 (mh-index-write-data))
264 (defvar mh-unpropagated-sequences
'(cur range subject search
)
265 "List of sequences that aren't preserved.")
267 (defun mh-unpropagated-sequences ()
268 "Return a list of sequences that aren't propagated to the source folders.
269 It is just the sequences in the variable `mh-unpropagated-sequences' in
270 addition to the Previous-Sequence (see mh-profile 5)."
272 (cons mh-previous-seq mh-unpropagated-sequences
)
273 mh-unpropagated-sequences
))
276 (defun mh-create-sequence-map (seq-list)
277 "Return a map from msg number to list of sequences in which it is present.
278 SEQ-LIST is an assoc list whose keys are sequence names and whose cdr is the
279 list of messages in that sequence."
280 (loop with map
= (make-hash-table)
282 when
(and (not (memq (car seq
) (mh-unpropagated-sequences)))
283 (mh-valid-seq-p (car seq
)))
284 do
(loop for msg in
(cdr seq
)
285 do
(push (car seq
) (gethash msg map
)))
289 (defun mh-index-create-sequences ()
290 "Mirror sequences present in source folders in index folder."
291 (let ((seq-hash (make-hash-table :test
#'equal
))
293 (loop for folder being the hash-keys of mh-index-data
294 do
(setf (gethash folder seq-hash
)
295 (mh-create-sequence-map
296 (mh-read-folder-sequences folder nil
))))
297 (dolist (msg (mh-translate-range mh-current-folder
"all"))
298 (let* ((checksum (gethash msg mh-index-msg-checksum-map
))
299 (pair (gethash checksum mh-index-checksum-origin-map
))
302 (loop for seq in
(ignore-errors
303 (gethash omsg
(gethash ofolder seq-hash
)))
304 do
(if (assoc seq seq-list
)
305 (push msg
(cdr (assoc seq seq-list
)))
306 (push (list seq msg
) seq-list
)))))
307 (loop for seq in seq-list
308 do
(apply #'mh-exec-cmd
"mark" mh-current-folder
309 "-sequence" (symbol-name (car seq
)) "-add"
310 (mapcar #'(lambda (x) (format "%s" x
)) (cdr seq
))))))
312 (defvar mh-flists-results-folder
"sequence"
313 "Subfolder for `mh-index-folder' where flists output is placed.")
314 (defvar mh-flists-sequence
)
315 (defvar mh-flists-called-flag nil
)
317 (defun mh-index-generate-pretty-name (string)
318 "Given STRING generate a name which is suitable for use as a folder name.
319 White space from the beginning and end are removed. All spaces in the name are
320 replaced with underscores and all / are replaced with $. If STRING is longer
321 than 20 it is truncated too. STRING could be a list of strings in which case
322 they are concatenated to construct the base name."
326 (when (car string
) (insert (car string
)))
327 (dolist (s (cdr string
))
329 (setq string
(mh-replace-string "-lbrace" " "))
330 (setq string
(mh-replace-string "-rbrace" " "))
331 (subst-char-in-region (point-min) (point-max) ?
( ? t
)
332 (subst-char-in-region (point-min) (point-max) ?
) ? t
)
333 (subst-char-in-region (point-min) (point-max) ?- ? t
)
334 (goto-char (point-min))
335 (while (and (not (eobp)) (memq (char-after) '(? ?
\t ?
\n ?
\r ?_
)))
337 (goto-char (point-max))
338 (while (and (not (bobp)) (memq (char-before) '(? ?
\t ?
\n ?
\r ?_
)))
339 (delete-backward-char 1))
340 (subst-char-in-region (point-min) (point-max) ? ?_ t
)
341 (subst-char-in-region (point-min) (point-max) ?
\t ?_ t
)
342 (subst-char-in-region (point-min) (point-max) ?
\n ?_ t
)
343 (subst-char-in-region (point-min) (point-max) ?
\r ?_ t
)
344 (subst-char-in-region (point-min) (point-max) ?
/ ?$ t
)
345 (let ((out (truncate-string-to-width (buffer-string) 20)))
346 (cond ((eq mh-indexer
'flists
)
347 (format "%s/%s" mh-flists-results-folder mh-flists-sequence
))
348 ((equal out mh-flists-results-folder
) (concat out
"1"))
352 (defun* mh-index-search
(redo-search-flag folder search-regexp
353 &optional window-config
)
354 "Perform an indexed search in an MH mail folder.
355 Use a prefix argument to repeat the search, as in REDO-SEARCH-FLAG below.
357 If REDO-SEARCH-FLAG is non-nil and the current folder buffer was generated by a
358 index search, then the search is repeated. Otherwise, FOLDER is searched with
359 SEARCH-REGEXP and the results are presented in an MH-E folder. If FOLDER is
360 \"+\" then mail in all folders are searched. Optional argument WINDOW-CONFIG
361 stores the window configuration that will be restored after the user quits the
362 folder containing the index search results.
364 Four indexing programs are supported; if none of these are present, then grep
365 is used. This function picks the first program that is available on your
366 system. If you would prefer to use a different program, set the customization
367 variable `mh-index-program' accordingly.
369 The documentation for the following functions describes how to generate the
370 index for each program:
372 - `mh-swish++-execute-search'
373 - `mh-swish-execute-search'
374 - `mh-mairix-execute-search'
375 - `mh-namazu-execute-search'
377 If none of these programs are present then we use pick. If desired grep can be
378 used instead. Details about these methods can be found in:
380 - `mh-pick-execute-search'
381 - `mh-grep-execute-search'
383 This and related functions use an X-MHE-Checksum header to cache the MD5
384 checksum of a message. This means that already present X-MHE-Checksum headers
385 in the incoming email could result in messages not being found. The following
386 procmail recipe should avoid this:
389 | formail -R \"X-MHE-Checksum\" \"Old-X-MHE-Checksum\"
391 This has the effect of renaming already present X-MHE-Checksum headers."
393 (list current-prefix-arg
395 (unless mh-find-path-run
(mh-find-path))
396 (or (and current-prefix-arg mh-index-sequence-search-flag
)
397 (and current-prefix-arg
(car mh-index-previous-search
))
398 (mh-prompt-for-folder "Search" "+" nil
"all" t
)))
400 ;; Yes, we do want to call mh-index-choose every time in case the
401 ;; user has switched the indexer manually.
402 (unless (mh-index-choose) (error "No indexing program found"))
403 (or (and current-prefix-arg
(cadr mh-index-previous-search
))
404 mh-index-regexp-builder
405 (read-string (format "%s regexp: "
407 (symbol-name mh-indexer
))))))
409 (and current-prefix-arg
(cadr mh-index-previous-search
)))
410 mh-index-regexp-builder
)
411 (current-window-configuration)
413 ;; Redoing a sequence search?
414 (when (and redo-search-flag mh-index-data mh-index-sequence-search-flag
415 (not mh-flists-called-flag
))
416 (let ((mh-flists-called-flag t
))
417 (apply #'mh-index-sequenced-messages mh-index-previous-search
))
418 (return-from mh-index-search
))
419 ;; We have fancy query parsing
420 (when (symbolp search-regexp
)
421 (mh-search-folder folder window-config
)
422 (setq mh-searching-function
'mh-index-do-search
)
423 (return-from mh-index-search
))
425 (let ((result-count 0)
426 (old-window-config (or window-config mh-previous-window-config
))
427 (previous-search mh-index-previous-search
)
428 (index-folder (format "%s/%s" mh-index-folder
429 (mh-index-generate-pretty-name search-regexp
))))
430 ;; Create a new folder for the search results or recreate the old one...
431 (if (and redo-search-flag mh-index-previous-search
)
432 (let ((buffer-name (buffer-name (current-buffer))))
433 (mh-process-or-undo-commands buffer-name
)
434 (save-excursion (mh-exec-cmd-quiet nil
"rmf" buffer-name
))
435 (mh-exec-cmd-quiet nil
"folder" "-create" "-fast" buffer-name
)
436 (setq index-folder buffer-name
))
437 (setq index-folder
(mh-index-new-folder index-folder search-regexp
)))
439 (let ((folder-path (format "%s%s" mh-user-path
(substring folder
1)))
440 (folder-results-map (make-hash-table :test
#'equal
))
441 (origin-map (make-hash-table :test
#'equal
)))
442 ;; Run search program...
443 (message "Executing %s... " mh-indexer
)
444 (funcall mh-index-execute-search-function folder-path search-regexp
)
446 ;; Parse indexer output
447 (message "Processing %s output... " mh-indexer
)
448 (goto-char (point-min))
449 (loop for next-result
= (funcall mh-index-next-result-function
)
451 do
(unless (eq next-result
'error
)
452 (unless (gethash (car next-result
) folder-results-map
)
453 (setf (gethash (car next-result
) folder-results-map
)
454 (make-hash-table :test
#'equal
)))
455 (setf (gethash (cadr next-result
)
456 (gethash (car next-result
) folder-results-map
))
459 ;; Copy the search results over
460 (maphash #'(lambda (folder msgs
)
461 (let ((cur (car (mh-translate-range folder
"cur")))
462 (msgs (sort (loop for msg being the hash-keys of msgs
465 (mh-exec-cmd "refile" msgs
"-src" folder
466 "-link" index-folder
)
467 ;; Restore cur to old value, that refile changed
469 (mh-exec-cmd-quiet nil
"mark" folder
"-add" "-zero"
470 "-sequence" "cur" (format "%s" cur
)))
471 (loop for msg in msgs
472 do
(incf result-count
)
473 (setf (gethash result-count origin-map
)
474 (cons folder msg
)))))
477 ;; Vist the results folder
478 (mh-visit-folder index-folder
() (list folder-results-map origin-map
))
480 (goto-char (point-min))
482 (mh-update-sequences)
485 ;; Update the speedbar, if needed
486 (when (mh-speed-flists-active-p)
487 (mh-speed-flists t mh-current-folder
))
490 (when (or (and redo-search-flag previous-search
) window-config
)
491 (setq mh-previous-window-config old-window-config
))
492 (setq mh-index-previous-search
(list folder search-regexp
))
494 ;; Write out data to disk
495 (unless mh-flists-called-flag
(mh-index-write-data))
497 (message "%s found %s matches in %s folders"
498 (upcase-initials (symbol-name mh-indexer
))
499 (loop for msg-hash being hash-values of mh-index-data
500 sum
(hash-table-count msg-hash
))
501 (loop for msg-hash being hash-values of mh-index-data
502 count
(> (hash-table-count msg-hash
) 0))))))
506 ;;; Functions to serialize index data...
508 (defun mh-index-write-data ()
509 "Write index data to file."
511 (unless (eq major-mode
'mh-folder-mode
)
512 (error "Can't be called from folder in `%s'" major-mode
))
513 (let ((data mh-index-data
)
514 (msg-checksum-map mh-index-msg-checksum-map
)
515 (checksum-origin-map mh-index-checksum-origin-map
)
516 (previous-search mh-index-previous-search
)
517 (sequence-search-flag mh-index-sequence-search-flag
)
518 (outfile (concat buffer-file-name mh-index-data-file
))
521 (with-temp-file outfile
522 (mh-index-write-hashtable
523 data
(lambda (x) (loop for y being the hash-keys of x collect y
)))
524 (mh-index-write-hashtable msg-checksum-map
#'identity
)
525 (mh-index-write-hashtable checksum-origin-map
#'identity
)
526 (pp previous-search
(current-buffer)) (insert "\n")
527 (pp sequence-search-flag
(current-buffer)) (insert "\n")))))
530 (defun mh-index-read-data ()
531 "Read index data from file."
533 (unless (eq major-mode
'mh-folder-mode
)
534 (error "Can't be called from folder in `%s'" major-mode
))
535 (let ((infile (concat buffer-file-name mh-index-data-file
))
538 (insert-file-contents-literally infile
)
539 (goto-char (point-min))
540 (setq t1
(mh-index-read-hashtable
542 (loop with table
= (make-hash-table :test
#'equal
)
543 for x in data do
(setf (gethash x table
) t
)
544 finally return table
)))
545 t2
(mh-index-read-hashtable #'identity
)
546 t3
(mh-index-read-hashtable #'identity
)
547 t4
(read (current-buffer))
548 t5
(read (current-buffer))))
549 (setq mh-index-data t1
550 mh-index-msg-checksum-map t2
551 mh-index-checksum-origin-map t3
552 mh-index-previous-search t4
553 mh-index-sequence-search-flag t5
))))
555 (defun mh-index-write-hashtable (table proc
)
556 "Write TABLE to `current-buffer'.
557 PROC is used to serialize the values corresponding to the hash table keys."
558 (pp (loop for x being the hash-keys of table
559 collect
(cons x
(funcall proc
(gethash x table
))))
563 (defun mh-index-read-hashtable (proc)
564 "From BUFFER read a hash table serialized as a list.
565 PROC is used to convert the value to actual data."
566 (loop with table
= (make-hash-table :test
#'equal
)
567 for pair in
(read (current-buffer))
568 do
(setf (gethash (car pair
) table
) (funcall proc
(cdr pair
)))
569 finally return table
))
573 "Non-nil means that this folder was generated by an index search."
577 (defun mh-index-do-search ()
578 "Construct appropriate regexp and call `mh-index-search'."
580 (unless (mh-index-choose) (error "No indexing program found"))
581 (let* ((regexp-list (mh-pick-parse-search-buffer))
582 (pattern (funcall mh-index-regexp-builder regexp-list
)))
584 (mh-index-search nil mh-current-folder pattern
585 mh-previous-window-config
)
586 (error "No search terms"))))
589 (defun mh-index-parse-search-regexp (input-string)
590 "Construct parse tree for INPUT-STRING.
591 All occurrences of &, |, ! and ~ in INPUT-STRING are replaced by AND, OR and
592 NOT as appropriate. Then the resulting string is parsed."
595 (insert input-string
)
597 (mh-replace-string "\t" " ")
599 (mh-replace-string " AND " " and ")
600 (mh-replace-string "&" " and ")
601 (mh-replace-string " -and " " and ")
603 (mh-replace-string " OR " " or ")
604 (mh-replace-string "|" " or ")
605 (mh-replace-string " -or " " or ")
607 (mh-replace-string " NOT " " not ")
608 (mh-replace-string "!" " not ")
609 (mh-replace-string "~" " not ")
610 (mh-replace-string " -not " " not ")
611 ;; synonyms of left brace
612 (mh-replace-string "(" " ( ")
613 (mh-replace-string " -lbrace " " ( ")
614 ;; synonyms of right brace
615 (mh-replace-string ")" " ) ")
616 (mh-replace-string " -rbrace " " ) ")
617 ;; get the normalized input
618 (setq input
(format "( %s )" (buffer-substring (point-min) (point-max)))))
620 (let ((tokens (mh-index-add-implicit-ops (split-string input
)))
624 (dolist (token tokens
)
625 (cond ((equal token
"(") (push 'paren op-stack
))
626 ((equal token
"not") (push 'not op-stack
))
627 ((equal token
"or") (push 'or op-stack
))
628 ((equal token
"and") (push 'and op-stack
))
630 (multiple-value-setq (op-stack operand-stack
)
631 (mh-index-evaluate op-stack operand-stack
))
632 (when (eq (car op-stack
) 'not
)
633 (setq op-stack
(cdr op-stack
))
634 (push `(not ,(pop operand-stack
)) operand-stack
))
635 (when (eq (car op-stack
) 'and
)
636 (setq op-stack
(cdr op-stack
))
637 (setq oper1
(pop operand-stack
))
638 (push `(and ,(pop operand-stack
) ,oper1
) operand-stack
)))
639 ((eq (car op-stack
) 'not
)
640 (setq op-stack
(cdr op-stack
))
641 (push `(not ,token
) operand-stack
)
642 (when (eq (car op-stack
) 'and
)
643 (setq op-stack
(cdr op-stack
))
644 (setq oper1
(pop operand-stack
))
645 (push `(and ,(pop operand-stack
) ,oper1
) operand-stack
)))
646 ((eq (car op-stack
) 'and
)
647 (setq op-stack
(cdr op-stack
))
648 (push `(and ,(pop operand-stack
) ,token
) operand-stack
))
649 (t (push token operand-stack
))))
650 (prog1 (pop operand-stack
)
651 (when (or op-stack operand-stack
)
652 (error "Invalid regexp: %s" input
))))))
654 (defun mh-index-add-implicit-ops (tokens)
655 "Add implicit operators in the list TOKENS."
660 (setq current
(pop tokens
))
661 (cond ((or (equal current
")") (equal current
"and") (equal current
"or"))
662 (setq literal-seen nil
)
663 (push current result
))
666 (setq literal-seen nil
)
669 (push current result
)
670 (unless (or (equal current
"(") (equal current
"not"))
671 (setq literal-seen t
)))))
674 (defun mh-index-evaluate (op-stack operand-stack
)
675 "Read expression till starting paren based on OP-STACK and OPERAND-STACK."
676 (block mh-index-evaluate
679 (setq op
(pop op-stack
))
680 (cond ((eq op
'paren
)
681 (return-from mh-index-evaluate
(values op-stack operand-stack
)))
683 (push `(not ,(pop operand-stack
)) operand-stack
))
684 ((or (eq op
'and
) (eq op
'or
))
685 (setq oper1
(pop operand-stack
))
686 (push `(,op
,(pop operand-stack
) ,oper1
) operand-stack
))))
687 (error "Ran out of tokens"))))
690 (defun mh-index-next-folder (&optional backward-flag
)
691 "Jump to the next folder marker.
692 The function is only applicable to folders displaying index search results.
693 With non-nil optional argument BACKWARD-FLAG, jump to the previous group of
696 (if (null mh-index-data
)
697 (message "Only applicable in an MH-E index search buffer")
698 (let ((point (point)))
699 (forward-line (if backward-flag -
1 1))
700 (cond ((if backward-flag
701 (re-search-backward "^+" (point-min) t
)
702 (re-search-forward "^+" (point-max) t
))
704 ((and (if backward-flag
705 (goto-char (point-max))
706 (goto-char (point-min)))
709 (re-search-backward "^+" (point-min) t
)
710 (re-search-forward "^+" (point-max) t
))
712 (t (goto-char point
))))))
715 (defun mh-index-previous-folder ()
716 "Jump to the previous folder marker."
718 (mh-index-next-folder t
))
720 (defun mh-folder-exists-p (folder)
721 "Check if FOLDER exists."
722 (and (mh-folder-name-p folder
)
725 (mh-exec-cmd-output "folder" nil
"-fast" "-nocreate" folder
)
726 (goto-char (point-min))
729 (defun mh-msg-exists-p (msg folder
)
730 "Check if MSG exists in FOLDER."
731 (file-exists-p (format "%s%s/%s" mh-user-path
(substring folder
1) msg
)))
733 (defun mh-index-new-folder (name search-regexp
)
734 "Return a folder name based on NAME for search results of SEARCH-REGEXP.
736 If folder NAME already exists and was generated for the same SEARCH-REGEXP
739 Otherwise if the folder NAME was generated from a different search then check
740 if NAME<2> can be used. Otherwise try NAME<3>. This is repeated till we find a
743 If the folder returned doesn't exist then it is created."
744 (unless (mh-folder-name-p name
)
745 (error "The argument should be a valid MH folder name"))
748 for candidate
= (if (equal i
1) name
(format "%s<%s>" name i
))
749 when
(or (not (mh-folder-exists-p candidate
))
750 (equal (mh-index-folder-search-regexp candidate
)
753 ;; Do pending refiles/deletes...
754 (when (get-buffer chosen-name
)
755 (mh-process-or-undo-commands chosen-name
))
756 ;; Recreate folder...
757 (save-excursion (mh-exec-cmd-quiet nil
"rmf" chosen-name
))
758 (mh-exec-cmd-quiet nil
"folder" "-create" "-fast" chosen-name
)
759 (mh-remove-from-sub-folders-cache chosen-name
)
760 (when (boundp 'mh-speed-folder-map
)
761 (mh-speed-add-folder chosen-name
))
764 (defun mh-index-folder-search-regexp (folder)
765 "If FOLDER was created by a index search, return the search regexp.
766 Return nil if FOLDER doesn't exist or the .mhe_index file is garbled."
769 (insert-file-contents
770 (format "%s%s/%s" mh-user-path
(substring folder
1) mh-index-data-file
))
771 (goto-char (point-min))
773 (cadr (read (current-buffer))))))
776 (defun mh-index-insert-folder-headers ()
777 "Annotate the search results with original folder names."
778 (let ((cur-msg (mh-get-msg-num nil
))
779 (old-buffer-modified-flag (buffer-modified-p))
780 (buffer-read-only nil
)
781 current-folder last-folder
)
782 (goto-char (point-min))
784 (setq current-folder
(car (gethash (gethash (mh-get-msg-num nil
)
785 mh-index-msg-checksum-map
)
786 mh-index-checksum-origin-map
)))
787 (when (and current-folder
(not (equal current-folder last-folder
)))
788 (insert (if last-folder
"\n" "") current-folder
"\n")
789 (setq last-folder current-folder
))
793 (mh-goto-msg cur-msg t
))
794 (set-buffer-modified-p old-buffer-modified-flag
))
795 (mh-index-create-imenu-index))
798 (defun mh-index-create-imenu-index ()
799 "Create alist of folder names and positions in index folder buffers."
801 (setq which-func-mode t
)
803 (goto-char (point-min))
804 (while (re-search-forward "^+" nil t
)
807 (push (cons (buffer-substring-no-properties
808 (point) (line-end-position))
809 (set-marker (make-marker) (point)))
811 (setq imenu--index-alist
(nreverse alist
)))))
814 (defun mh-index-group-by-folder ()
815 "Partition the messages based on source folder.
816 Returns an alist with the the folder names in the car and the cdr being the
817 list of messages originally from that folder."
819 (goto-char (point-min))
820 (let ((result-table (make-hash-table :test
#'equal
)))
821 (loop for msg being hash-keys of mh-index-msg-checksum-map
822 do
(push msg
(gethash (car (gethash
823 (gethash msg mh-index-msg-checksum-map
)
824 mh-index-checksum-origin-map
))
826 (loop for x being the hash-keys of result-table
827 collect
(cons x
(nreverse (gethash x result-table
)))))))
830 (defun mh-index-delete-folder-headers ()
831 "Delete the folder headers."
832 (let ((cur-msg (mh-get-msg-num nil
))
833 (old-buffer-modified-flag (buffer-modified-p))
834 (buffer-read-only nil
))
835 (while (and (not cur-msg
) (not (eobp)))
837 (setq cur-msg
(mh-get-msg-num nil
)))
838 (goto-char (point-min))
840 (if (or (char-equal (char-after) ?
+) (char-equal (char-after) 10))
841 (delete-region (point) (progn (forward-line) (point)))
843 (when cur-msg
(mh-goto-msg cur-msg t t
))
844 (set-buffer-modified-p old-buffer-modified-flag
)))
847 (defun mh-index-visit-folder ()
848 "Visit original folder from where the message at point was found."
850 (unless mh-index-data
851 (error "Not in an index folder"))
854 (cond ((and (bolp) (eolp))
855 (ignore-errors (forward-line -
1))
856 (setq msg
(mh-get-msg-num t
)))
857 ((equal (char-after (line-beginning-position)) ?
+)
858 (setq folder
(buffer-substring-no-properties
859 (line-beginning-position) (line-end-position))))
860 (t (setq msg
(mh-get-msg-num t
)))))
862 (setq folder
(car (gethash (gethash msg mh-index-msg-checksum-map
)
863 mh-index-checksum-origin-map
))))
864 (when (or (not (get-buffer folder
))
865 (y-or-n-p (format "Reuse buffer displaying %s? " folder
)))
867 folder
(loop for x being the hash-keys of
(gethash folder mh-index-data
)
868 when
(mh-msg-exists-p x folder
) collect x
)))))
870 (defun mh-index-match-checksum (msg folder checksum
)
871 "Check if MSG in FOLDER has X-MHE-Checksum header value of CHECKSUM."
873 (mh-exec-cmd-output mh-scan-prog nil
"-width" "80"
874 "-format" "%{x-mhe-checksum}\n" folder msg
)
875 (goto-char (point-min))
876 (string-equal (buffer-substring-no-properties (point) (line-end-position))
879 (defun mh-index-matching-source-msgs (msgs &optional delete-from-index-data
)
880 "Return a table of original messages and folders for messages in MSGS.
881 If optional argument DELETE-FROM-INDEX-DATA is non-nil, then each of the
882 messages, whose counter-part is found in some source folder, is removed from
884 (let ((table (make-hash-table :test
#'equal
)))
886 (let* ((checksum (gethash msg mh-index-msg-checksum-map
))
887 (pair (gethash checksum mh-index-checksum-origin-map
)))
888 (when (and checksum
(car pair
) (cdr pair
)
889 (mh-index-match-checksum (cdr pair
) (car pair
) checksum
))
890 (push (cdr pair
) (gethash (car pair
) table
))
891 (when delete-from-index-data
892 (remhash (cdr pair
) (gethash (car pair
) mh-index-data
))))))
896 (defun mh-index-execute-commands ()
897 "Delete/refile the actual messages.
898 The copies in the searched folder are then deleted/refiled to get the desired
899 result. Before deleting the messages we make sure that the message being
900 deleted is identical to the one that the user has marked in the index buffer."
903 (mh-speed-flists-inhibit-flag t
))
905 (lambda (folder msgs
)
906 (push folder folders
)
907 (if (not (get-buffer folder
))
908 ;; If source folder not open, just delete the messages...
909 (apply #'mh-exec-cmd
"rmm" folder
(mh-coalesce-msg-list msgs
))
910 ;; Otherwise delete the messages in the source buffer...
913 (let ((old-refile-list mh-refile-list
)
914 (old-delete-list mh-delete-list
))
915 (setq mh-refile-list nil
917 (unwind-protect (mh-execute-commands)
921 (loop for y in
(cdr x
)
922 unless
(memq y msgs
) collect y
)))
925 (loop for x in old-delete-list
926 unless
(memq x msgs
) collect x
))
927 (mh-set-folder-modified-p (mh-outstanding-commands-p))
928 (when (mh-outstanding-commands-p)
929 (mh-notate-deleted-and-refiled)))))))
930 (mh-index-matching-source-msgs (append (loop for x in mh-refile-list
937 (defun mh-index-add-to-sequence (seq msgs
)
938 "Add to SEQ the messages in the list MSGS.
939 This function updates the source folder sequences. Also makes an attempt to
940 update the source folder buffer if we have it open."
941 ;; Don't need to do anything for cur
943 (when (and (not (memq seq
(mh-unpropagated-sequences)))
944 (mh-valid-seq-p seq
))
946 (mh-speed-flists-inhibit-flag t
))
947 (maphash (lambda (folder msgs
)
948 (push folder folders
)
949 ;; Add messages to sequence in source folder...
950 (apply #'mh-exec-cmd-quiet nil
"mark" folder
951 "-add" "-nozero" "-sequence" (symbol-name seq
)
952 (mapcar (lambda (x) (format "%s" x
))
953 (mh-coalesce-msg-list msgs
)))
954 ;; Update source folder buffer if we have it open...
955 (when (get-buffer folder
)
958 (mh-put-msg-in-seq msgs seq
))))
959 (mh-index-matching-source-msgs msgs
))
963 (defun mh-index-delete-from-sequence (seq msgs
)
964 "Delete from SEQ the messages in MSGS.
965 This function updates the source folder sequences. Also makes an attempt to
966 update the source folder buffer if present."
968 (when (and (not (memq seq
(mh-unpropagated-sequences)))
969 (mh-valid-seq-p seq
))
971 (mh-speed-flists-inhibit-flag t
))
972 (maphash (lambda (folder msgs
)
973 (push folder folders
)
974 ;; Remove messages from sequence in source folder...
975 (apply #'mh-exec-cmd-quiet nil
"mark" folder
976 "-del" "-nozero" "-sequence" (symbol-name seq
)
977 (mapcar (lambda (x) (format "%s" x
))
978 (mh-coalesce-msg-list msgs
)))
979 ;; Update source folder buffer if we have it open...
980 (when (get-buffer folder
)
983 (mh-delete-msg-from-seq msgs seq t
))))
984 (mh-index-matching-source-msgs msgs
))
991 (defvar mh-index-pick-folder
)
992 (defvar mh-pick-binary
"pick")
994 (defun mh-pick-execute-search (folder-path search-regexp
)
997 Unlike the other index search programs \"pick\" only searches messages present
998 in the folder itself and does not descend into any sub-folders that may be
1001 FOLDER-PATH is the directory containing the mails to be searched and
1002 SEARCH-REGEXP is the pattern that pick gets."
1003 (set-buffer (get-buffer-create mh-index-temp-buffer
))
1005 (setq mh-index-pick-folder
1006 (concat "+" (substring folder-path
(length mh-user-path
))))
1007 (apply #'call-process
(expand-file-name "pick" mh-progs
) nil
'(t nil
) nil
1008 mh-index-pick-folder
"-list" search-regexp
)
1009 (goto-char (point-min)))
1011 (defun mh-pick-next-result ()
1012 "Return the next pick search result."
1014 (when (eobp) (return nil
))
1015 (unless (re-search-forward "^[1-9][0-9]*$" (line-end-position) t
)
1017 (list mh-index-pick-folder
1018 (car (read-from-string (buffer-substring-no-properties
1019 (line-beginning-position)
1020 (line-end-position))))
1028 (defvar mh-grep-binary
(executable-find "grep"))
1030 (defun mh-grep-execute-search (folder-path search-regexp
)
1031 "Execute grep and read the results.
1032 FOLDER-PATH is the directory in which SEARCH-REGEXP is used to search."
1033 (set-buffer (get-buffer-create mh-index-temp-buffer
))
1035 (call-process mh-grep-binary nil
'(t nil
) nil
1036 "-i" "-r" search-regexp folder-path
)
1037 (goto-char (point-min)))
1039 (defun mh-grep-next-result ()
1040 "Read the next result.
1041 Parse it and return the message folder, message index and the match. If no
1042 other matches left then return nil. If the current record is invalid return
1048 (let ((eol-pos (line-end-position))
1049 (bol-pos (line-beginning-position))
1050 folder-start msg-end
)
1052 (unless (search-forward mh-user-path eol-pos t
)
1054 (setq folder-start
(point))
1055 (unless (search-forward ":" eol-pos t
)
1057 (let ((match (buffer-substring-no-properties (point) eol-pos
)))
1059 (setq msg-end
(point))
1060 (unless (search-backward "/" folder-start t
)
1062 (list (format "+%s" (buffer-substring-no-properties
1063 folder-start
(point)))
1064 (let ((val (ignore-errors (read-from-string
1065 (buffer-substring-no-properties
1066 (1+ (point)) msg-end
)))))
1067 (if (and (consp val
) (integerp (car val
)))
1077 (defvar mh-mairix-binary
(executable-find "mairix"))
1078 (defvar mh-mairix-directory
".mairix")
1079 (defvar mh-mairix-folder nil
)
1081 (defun mh-mairix-execute-search (folder-path search-regexp-list
)
1082 "Execute mairix and read the results.
1084 In the examples below replace /home/user/Mail with the path to your MH
1087 First create the directory /home/user/Mail/.mairix. Then create the file
1088 /home/user/Mail/.mairix/config with the following contents:
1090 # This should contain the same thing as your `mh-user-path'
1091 base=/home/user/Mail
1093 # List of folders that should be indexed. 3 dots at the end means there are
1094 # subfolders within the folder
1095 mh_folders=archive...:inbox:drafts:news:sent:trash
1098 database=/home/user/Mail/mairix/database
1100 Use the following command line to generate the mairix index. Run this daily
1103 mairix -f /home/user/Mail/.mairix/config
1105 FOLDER-PATH is the directory in which SEARCH-REGEXP-LIST is used to search."
1106 (set-buffer (get-buffer-create mh-index-temp-buffer
))
1108 (unless mh-mairix-binary
1109 (error "Set mh-mairix-binary appropriately"))
1110 (apply #'call-process mh-mairix-binary nil
'(t nil
) nil
1111 "-f" (format "%s%s/config" mh-user-path mh-mairix-directory
)
1113 (goto-char (point-min))
1114 (setq mh-mairix-folder
1115 (let ((last-char (substring folder-path
(1- (length folder-path
)))))
1116 (if (equal last-char
"/")
1118 (format "%s/" folder-path
)))))
1120 (defun mh-mairix-next-result ()
1121 "Return next result from mairix output."
1124 (when (or (eobp) (and (bolp) (eolp)))
1126 (unless (eq (char-after) ?
/)
1128 (let ((start (point))
1130 (setq end
(line-end-position))
1131 (unless (search-forward mh-mairix-folder end t
)
1133 (goto-char (match-beginning 0))
1134 (unless (equal (point) start
)
1137 (unless (search-backward "/" start t
)
1139 (setq msg-start
(1+ (point)))
1141 (unless (search-forward mh-user-path end t
)
1143 (list (format "+%s" (buffer-substring-no-properties
1144 (point) (1- msg-start
)))
1145 (car (read-from-string
1146 (buffer-substring-no-properties msg-start end
)))
1150 (defun mh-mairix-regexp-builder (regexp-list)
1151 "Generate query for mairix.
1152 REGEXP-LIST is an alist of fields and values."
1154 (dolist (pair regexp-list
)
1158 (cond ((eq (car pair
) 'to
) "t:")
1159 ((eq (car pair
) 'from
) "f:")
1160 ((eq (car pair
) 'cc
) "c:")
1161 ((eq (car pair
) 'subject
) "s:")
1162 ((eq (car pair
) 'date
) "d:")
1164 (let ((sop (cdr (mh-mairix-convert-to-sop* (cdr pair
))))
1166 (dolist (conjunct sop
)
1167 (let ((expr-list (cdr conjunct
))
1169 (dolist (e expr-list
)
1170 (setq expr-string
(concat expr-string
"+"
1171 (if (atom e
) "" "~")
1172 (if (atom e
) e
(cadr e
)))))
1173 (setq final
(concat final
"," (substring expr-string
1)))))
1174 (substring final
1)))
1178 (defun mh-mairix-convert-to-sop* (expr)
1179 "Convert EXPR to sum of product form."
1180 (cond ((atom expr
) `(or (and ,expr
)))
1181 ((eq (car expr
) 'or
)
1183 (loop for e in
(mapcar #'mh-mairix-convert-to-sop
* (cdr expr
))
1185 ((eq (car expr
) 'and
)
1186 (let ((conjuncts (mapcar #'mh-mairix-convert-to-sop
* (cdr expr
)))
1188 (setq result
(pop conjuncts
))
1190 (setq next-factor
(pop conjuncts
))
1191 (setq result
(let ((res ()))
1192 (dolist (t1 (cdr result
))
1193 (dolist (t2 (cdr next-factor
))
1194 (push `(and ,@(cdr t1
) ,@(cdr t2
)) res
)))
1197 ((atom (cadr expr
)) `(or (and ,expr
)))
1198 ((eq (caadr expr
) 'not
) (mh-mairix-convert-to-sop* (cadadr expr
)))
1199 ((eq (caadr expr
) 'and
) (mh-mairix-convert-to-sop*
1200 `(or ,@(mapcar #'(lambda (x) `(not ,x
))
1202 ((eq (caadr expr
) 'or
) (mh-mairix-convert-to-sop*
1203 `(and ,@(mapcar #'(lambda (x) `(not ,x
))
1205 (t (error "Unreachable: %s" expr
))))
1209 ;; Interface to unseen messages script
1211 (defvar mh-flists-search-folders
)
1213 ;; XXX: This should probably be in mh-utils.el and used in other places where
1214 ;; MH-E calls out to /bin/sh.
1215 (defun mh-index-quote-for-shell (string)
1216 "Quote STRING for /bin/sh."
1218 (loop for x across string
1219 concat
(format (if (memq x
'(?
\\ ?
` ?$
)) "\\%c" "%c") x
))
1222 (defun mh-flists-execute (&rest args
)
1224 Search for messages belonging to `mh-flists-sequence' in the folders
1225 specified by `mh-flists-search-folders'. If `mh-recursive-folders-flag' is t,
1226 then the folders are searched recursively. All parameters ARGS are ignored."
1227 (set-buffer (get-buffer-create mh-index-temp-buffer
))
1229 (unless (executable-find "sh")
1230 (error "Didn't find sh"))
1232 (let ((seq (symbol-name mh-flists-sequence
)))
1233 (insert "for folder in `" (expand-file-name "flists" mh-progs
) " "
1234 (cond ((eq mh-flists-search-folders t
)
1235 (mh-index-quote-for-shell mh-inbox
))
1236 ((eq mh-flists-search-folders nil
) "")
1237 ((listp mh-flists-search-folders
)
1238 (loop for folder in mh-flists-search-folders
1240 (concat " " (mh-index-quote-for-shell folder
)))))
1241 (if mh-recursive-folders-flag
" -recurse" "")
1242 " -sequence " seq
" -noshowzero -fast` ; do\n"
1243 (expand-file-name "mhpath" mh-progs
) " \"+$folder\" " seq
"\n"
1245 (call-process-region
1246 (point-min) (point-max) "sh" nil
(get-buffer mh-index-temp-buffer
))))
1249 (defun mh-index-sequenced-messages (folders sequence
)
1250 "Display messages from FOLDERS in SEQUENCE.
1251 All messages in the sequence you provide from the folders in
1252 `mh-index-new-messages-folders' are listed. With a prefix argument, enter a
1253 space-separated list of folders, or nothing to search all folders."
1255 (list (if current-prefix-arg
1256 (split-string (read-string "Search folder(s): [all] "))
1257 mh-index-new-messages-folders
)
1258 (mh-read-seq-default "Search" nil
)))
1259 (unless sequence
(setq sequence mh-unseen-seq
))
1260 (let* ((mh-flists-search-folders folders
)
1261 (mh-flists-sequence sequence
)
1262 (mh-flists-called-flag t
)
1263 (mh-indexer 'flists
)
1264 (mh-index-execute-search-function 'mh-flists-execute
)
1265 (mh-index-next-result-function 'mh-mairix-next-result
)
1266 (mh-mairix-folder mh-user-path
)
1267 (mh-index-regexp-builder nil
)
1268 (new-folder (format "%s/%s/%s" mh-index-folder
1269 mh-flists-results-folder sequence
))
1270 (window-config (if (equal new-folder mh-current-folder
)
1271 mh-previous-window-config
1272 (current-window-configuration)))
1275 (cond ((buffer-live-p (get-buffer new-folder
))
1276 ;; The destination folder is being visited. Trick `mh-index-search'
1277 ;; into thinking that the folder resulted from a previous search.
1278 (set-buffer new-folder
)
1279 (setq mh-index-previous-search
(list folders sequence
))
1281 ((mh-folder-exists-p new-folder
)
1282 ;; Folder exists but we don't have it open. That means they are
1283 ;; stale results from a old flists search. Clear it out.
1284 (mh-exec-cmd-quiet nil
"rmf" new-folder
)))
1285 (setq message
(mh-index-search redo-flag
"+" mh-flists-results-folder
1287 mh-index-sequence-search-flag t
1288 mh-index-previous-search
(list folders sequence
))
1289 (mh-index-write-data)
1290 (when (stringp message
) (message message
))))
1293 (defun mh-index-new-messages (folders)
1294 "Display unseen messages.
1295 If you use a program such as `procmail' to use `rcvstore' to file your
1296 incoming mail automatically, you can display new, unseen, messages using this
1297 command. All messages in the `unseen' sequence from the folders in
1298 `mh-index-new-messages-folders' are listed. With a prefix argument, enter a
1299 space-separated list of FOLDERS, or nothing to search all folders."
1301 (list (if current-prefix-arg
1302 (split-string (read-string "Search folder(s): [all] "))
1303 mh-index-new-messages-folders
)))
1304 (mh-index-sequenced-messages folders mh-unseen-seq
))
1307 (defun mh-index-ticked-messages (folders)
1308 "Display ticked messages.
1309 All messages in `mh-tick-seq' from the folders in
1310 `mh-index-ticked-messages-folders' are listed. With a prefix argument, enter a
1311 space-separated list of FOLDERS, or nothing to search all folders."
1313 (list (if current-prefix-arg
1314 (split-string (read-string "Search folder(s): [all] "))
1315 mh-index-ticked-messages-folders
)))
1316 (mh-index-sequenced-messages folders mh-tick-seq
))
1322 (defvar mh-swish-binary
(executable-find "swish-e"))
1323 (defvar mh-swish-directory
".swish")
1324 (defvar mh-swish-folder nil
)
1327 (defun mh-swish-execute-search (folder-path search-regexp
)
1328 "Execute swish-e and read the results.
1330 In the examples below, replace /home/user/Mail with the path to your MH
1333 First create the directory /home/user/Mail/.swish. Then create the file
1334 /home/user/Mail/.swish/config with the following contents:
1336 IndexDir /home/user/Mail
1337 IndexFile /home/user/Mail/.swish/index
1338 IndexName \"Mail Index\"
1339 IndexDescription \"Mail Index\"
1340 IndexPointer \"http://nowhere\"
1341 IndexAdmin \"nobody\"
1342 #MetaNames automatic
1346 IgnoreTotalWordCountWhenRanking yes
1347 WordCharacters abcdefghijklmnopqrstuvwxyz0123456789-
1348 BeginCharacters abcdefghijklmnopqrstuvwxyz
1349 EndCharacters abcdefghijklmnopqrstuvwxyz0123456789
1352 FileRules pathname contains /home/user/Mail/.swish
1353 FileRules pathname contains /home/user/Mail/mhe-index
1354 FileRules filename is index
1355 FileRules filename is \\..*
1356 FileRules filename is #.*
1357 FileRules filename is ,.*
1358 FileRules filename is .*~
1360 If there are any directories you would like to ignore, append lines like the
1361 following to config:
1363 FileRules pathname contains /home/user/Mail/scripts
1365 You do not want to index the folders that hold the results of your searches
1366 since they tend to be ephemeral and the original messages are indexed anyway.
1367 The configuration file above assumes that the results are found in sub-folders
1368 of `mh-index-folder' which is +mhe-index by default.
1370 Use the following command line to generate the swish index. Run this
1373 swish-e -c /home/user/Mail/.swish/config
1375 FOLDER-PATH is the directory in which SEARCH-REGEXP is used to search."
1376 (set-buffer (get-buffer-create mh-index-temp-buffer
))
1378 (unless mh-swish-binary
1379 (error "Set mh-swish-binary appropriately"))
1380 (call-process mh-swish-binary nil
'(t nil
) nil
1382 "-f" (format "%s%s/index" mh-user-path mh-swish-directory
))
1383 (goto-char (point-min))
1384 (setq mh-swish-folder
1385 (let ((last-char (substring folder-path
(1- (length folder-path
)))))
1386 (if (equal last-char
"/")
1388 (format "%s/" folder-path
)))))
1390 (defun mh-swish-next-result ()
1391 "Get the next result from swish output."
1394 (when (or (eobp) (equal (char-after (point)) ?.
))
1396 (when (equal (char-after (point)) ?
#)
1398 (let* ((start (search-forward " " (line-end-position) t
))
1399 (end (search-forward " " (line-end-position) t
)))
1400 (unless (and start end
)
1403 (unless (file-exists-p (buffer-substring-no-properties start end
))
1405 (unless (search-backward "/" start t
)
1407 (list (let* ((s (buffer-substring-no-properties start
(1+ (point)))))
1408 (unless (string-match mh-swish-folder s
)
1410 (if (string-match mh-user-path s
)
1412 (substring s
(match-end 0) (1- (length s
))))
1414 (let* ((s (buffer-substring-no-properties (1+ (point)) end
))
1415 (val (ignore-errors (read-from-string s
))))
1416 (if (and (consp val
) (numberp (car val
)))
1424 ;; Swish++ interface
1426 (defvar mh-swish
++-binary
(or (executable-find "search++")
1427 (executable-find "search")))
1428 (defvar mh-swish
++-directory
".swish++")
1431 (defun mh-swish++-execute-search
(folder-path search-regexp
)
1432 "Execute swish++ and read the results.
1434 In the examples below, replace /home/user/Mail with the path to your MH
1437 First create the directory /home/user/Mail/.swish++. Then create the file
1438 /home/user/Mail/.swish++/swish++.conf with the following contents:
1440 IncludeMeta Bcc Cc Comments Content-Description From Keywords
1441 IncludeMeta Newsgroups Resent-To Subject To
1442 IncludeMeta Message-Id References In-Reply-To
1444 IndexFile /home/user/Mail/.swish++/swish++.index
1446 Use the following command line to generate the swish index. Run this
1449 find /home/user/Mail -path /home/user/Mail/mhe-index -prune \\
1450 -o -path /home/user/Mail/.swish++ -prune \\
1451 -o -name \"[0-9]*\" -print \\
1452 | index -c /home/user/Mail/.swish++/swish++.conf /home/user/Mail
1454 You do not want to index the folders that hold the results of your searches
1455 since they tend to be ephemeral and the original messages are indexed anyway.
1456 The command above assumes that the results are found in sub-folders of
1457 `mh-index-folder' which is +mhe-index by default.
1459 On some systems (Debian GNU/Linux, for example), use index++ instead of index.
1461 FOLDER-PATH is the directory in which SEARCH-REGEXP is used to search."
1462 (set-buffer (get-buffer-create mh-index-temp-buffer
))
1464 (unless mh-swish
++-binary
1465 (error "Set mh-swish++-binary appropriately"))
1466 (call-process mh-swish
++-binary nil
'(t nil
) nil
1468 (format "-i%s%s/swish++.index"
1469 mh-user-path mh-swish
++-directory
)
1471 (goto-char (point-min))
1472 (setq mh-swish-folder
1473 (let ((last-char (substring folder-path
(1- (length folder-path
)))))
1474 (if (equal last-char
"/")
1476 (format "%s/" folder-path
)))))
1478 (defalias 'mh-swish
++-next-result
'mh-swish-next-result
)
1480 (defun mh-swish++-regexp-builder
(regexp-list)
1481 "Generate query for swish++.
1482 REGEXP-LIST is an alist of fields and values."
1484 (dolist (elem regexp-list
)
1486 (setq regexp
(concat regexp
" and "
1487 (if (car elem
) "(" "")
1488 (if (car elem
) (symbol-name (car elem
)) "")
1489 (if (car elem
) " = " "")
1490 (mh-swish++-print-regexp
(cdr elem
))
1491 (if (car elem
) ")" "")))))
1492 (substring regexp
4)))
1494 (defun mh-swish++-print-regexp
(expr)
1495 "Return infix expression corresponding to EXPR."
1496 (cond ((atom expr
) (format "%s" expr
))
1497 ((eq (car expr
) 'not
)
1498 (format "(not %s)" (mh-swish++-print-regexp
(cadr expr
))))
1499 (t (format "(%s %s %s)" (mh-swish++-print-regexp
(cadr expr
))
1500 (symbol-name (car expr
))
1501 (mh-swish++-print-regexp
(caddr expr
))))))
1507 (defvar mh-namazu-binary
(executable-find "namazu"))
1508 (defvar mh-namazu-directory
".namazu")
1509 (defvar mh-namazu-folder nil
)
1512 (defun mh-namazu-execute-search (folder-path search-regexp
)
1513 "Execute namazu and read the results.
1515 In the examples below, replace /home/user/Mail with the path to your MH
1518 First create the directory /home/user/Mail/.namazu. Then create the file
1519 /home/user/Mail/.namazu/mknmzrc with the following contents:
1521 package conf; # Don't remove this line!
1522 $ADDRESS = 'user@localhost';
1523 $ALLOW_FILE = \"[0-9]*\";
1524 $EXCLUDE_PATH = \"^/home/user/Mail/(mhe-index|spam)\";
1526 In the above example configuration, none of the mail files contained in the
1527 directories /home/user/Mail/mhe-index and /home/user/Mail/spam are indexed.
1529 You do not want to index the folders that hold the results of your searches
1530 since they tend to be ephemeral and the original messages are indexed anyway.
1531 The configuration file above assumes that the results are found in sub-folders
1532 of `mh-index-folder' which is +mhe-index by default.
1534 Use the following command line to generate the namazu index. Run this
1537 mknmz -f /home/user/Mail/.namazu/mknmzrc -O /home/user/Mail/.namazu \\
1540 FOLDER-PATH is the directory in which SEARCH-REGEXP is used to search."
1541 (let ((namazu-index-directory
1542 (format "%s%s" mh-user-path mh-namazu-directory
)))
1543 (unless (file-exists-p namazu-index-directory
)
1544 (error "Namazu directory %s not present" namazu-index-directory
))
1545 (unless (executable-find mh-namazu-binary
)
1546 (error "Set mh-namazu-binary appropriately"))
1547 (set-buffer (get-buffer-create mh-index-temp-buffer
))
1549 (call-process mh-namazu-binary nil
'(t nil
) nil
1550 "-alR" search-regexp namazu-index-directory
)
1551 (goto-char (point-min))
1552 (setq mh-namazu-folder
1553 (let ((last (substring folder-path
(1- (length folder-path
)))))
1554 (if (equal last
"/")
1556 (format "%s/" folder-path
))))))
1558 (defun mh-namazu-next-result ()
1559 "Get the next result from namazu output."
1562 (when (eobp) (return nil
))
1563 (let ((file-name (buffer-substring-no-properties
1564 (point) (line-end-position))))
1565 (unless (equal (string-match mh-namazu-folder file-name
) 0)
1567 (unless (file-exists-p file-name
)
1569 (string-match mh-user-path file-name
)
1570 (let* ((folder/msg
(substring file-name
(match-end 0)))
1571 (mark (mh-search-from-end ?
/ folder
/msg
)))
1572 (unless mark
(return 'error
))
1573 (list (format "+%s" (substring folder
/msg
0 mark
))
1574 (let ((n (ignore-errors (read-from-string
1575 (substring folder
/msg
(1+ mark
))))))
1576 (if (and (consp n
) (numberp (car n
)))
1585 (defun mh-index-choose ()
1586 "Choose an indexing function.
1587 The side-effects of this function are that the variables `mh-indexer',
1588 `mh-index-execute-search-function', and `mh-index-next-result-function' are
1589 set according to the first indexer in `mh-indexer-choices' present on the
1592 ;; The following favors the user's preference; otherwise, the last
1593 ;; automatically chosen indexer is used for efficiency rather than going
1594 ;; through the list.
1595 (let ((program-alist (cond (mh-index-program
1597 (assoc mh-index-program mh-indexer-choices
)))
1599 (list (assoc mh-indexer mh-indexer-choices
)))
1600 (t mh-indexer-choices
))))
1601 (while program-alist
1602 (let* ((current (pop program-alist
))
1603 (executable (symbol-value (cadr current
))))
1605 (setq mh-indexer
(car current
))
1606 (setq mh-index-execute-search-function
(nth 2 current
))
1607 (setq mh-index-next-result-function
(nth 3 current
))
1608 (setq mh-index-regexp-builder
(nth 4 current
))
1609 (return mh-indexer
))))
1616 ;;; Local Variables:
1617 ;;; indent-tabs-mode: nil
1618 ;;; sentence-end-double-space: nil
1621 ;;; arch-tag: 607762ad-0dff-4fe1-a27e-6c0dde0dcc47
1622 ;;; mh-index ends here