88155
|
1 ;;; mh-search --- MH-E search
|
|
2
|
|
3 ;; Copyright (C) 1993, 1995,
|
|
4 ;; 2001, 2002, 2003, 2004, 2005, 2006 Free Software Foundation, Inc.
|
|
5
|
|
6 ;; Author: Indexed search by Satyaki Das <satyaki@theforce.stanford.edu>
|
|
7 ;; Maintainer: Bill Wohler <wohler@newt.com>
|
|
8 ;; Keywords: mail
|
|
9 ;; See: mh-e.el
|
|
10
|
|
11 ;; This file is part of GNU Emacs.
|
|
12
|
|
13 ;; GNU Emacs is free software; you can redistribute it and/or modify
|
|
14 ;; it under the terms of the GNU General Public License as published by
|
|
15 ;; the Free Software Foundation; either version 2, or (at your option)
|
|
16 ;; any later version.
|
|
17
|
|
18 ;; GNU Emacs is distributed in the hope that it will be useful,
|
|
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
21 ;; GNU General Public License for more details.
|
|
22
|
|
23 ;; You should have received a copy of the GNU General Public License
|
|
24 ;; along with GNU Emacs; see the file COPYING. If not, write to the
|
|
25 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
26 ;; Boston, MA 02110-1301, USA.
|
|
27
|
|
28 ;;; Commentary:
|
|
29
|
|
30 ;; (1) The following search engines are supported:
|
|
31 ;; swish++
|
|
32 ;; swish-e
|
|
33 ;; mairix
|
|
34 ;; namazu
|
|
35 ;; pick
|
|
36 ;; grep
|
|
37 ;;
|
|
38 ;; (2) To use this package, you first have to build an index. Please
|
|
39 ;; read the documentation for `mh-search' to get started. That
|
|
40 ;; documentation will direct you to the specific instructions for
|
|
41 ;; your particular searcher.
|
|
42
|
|
43 ;;; Change Log:
|
|
44
|
|
45 ;;; Code:
|
|
46
|
|
47 ;;(message "> mh-search")
|
|
48 (eval-when-compile (require 'mh-acros))
|
|
49 (mh-require-cl)
|
|
50
|
|
51 (require 'gnus-util)
|
|
52 (require 'mh-buffers)
|
|
53 (require 'mh-e)
|
|
54 ;;(message "< mh-search")
|
|
55
|
|
56 (defvar mh-searcher nil
|
|
57 "Cached value of chosen search program.")
|
|
58
|
|
59 (defvar mh-search-function nil
|
|
60 "Function which executes the search program.")
|
|
61
|
|
62 (defvar mh-search-next-result-function nil
|
|
63 "Function to parse the next line of output.
|
|
64 Expected to return a list of three strings: name of the folder,
|
|
65 message number, and optionally the match.")
|
|
66
|
|
67 (defvar mh-search-regexp-builder nil
|
|
68 "Function used to construct search regexp.")
|
|
69
|
|
70 (defvar mh-index-folder "+mhe-index"
|
|
71 "Folder that contains the folders resulting from the index searches.")
|
|
72
|
|
73 (defvar mh-flists-results-folder "sequence"
|
|
74 "Subfolder for `mh-index-folder' where flists output is placed.")
|
|
75
|
|
76 (defvar mh-flists-sequence)
|
|
77
|
|
78 (defvar mh-flists-called-flag nil)
|
|
79
|
|
80
|
|
81
|
|
82 ;;; MH-Search mode
|
|
83
|
|
84 ;;;###mh-autoload
|
|
85 (defun* mh-search (folder search-regexp
|
|
86 &optional redo-search-flag window-config)
|
|
87 "Search your MH mail.
|
|
88
|
|
89 This command helps you find messages in your entire corpus of
|
|
90 mail. You can search for messages to or from a particular person
|
|
91 or about a particular subject. In fact, you can also search for
|
|
92 messages containing selected strings in any arbitrary header
|
|
93 field or any string found within the messages.
|
|
94
|
|
95 Out of the box, MH-E uses \"pick\" to find messages. With a
|
|
96 little extra effort, you can set an indexing program which
|
|
97 rewards you with extremely quick results. The drawback is that
|
|
98 sometimes the index does not contain the words you're looking
|
|
99 for. You can still use \"pick\" in these situations.
|
|
100
|
|
101 You are prompted for the FOLDER to search. This can be \"all\" to
|
|
102 search all folders. Note that the search works recursively on the
|
|
103 listed folder.
|
|
104
|
|
105 Next, an MH-Search buffer appears where you can enter search
|
|
106 criteria SEARCH-REGEXP.
|
|
107
|
|
108 From:
|
|
109 To:
|
|
110 Cc:
|
|
111 Date:
|
|
112 Subject:
|
|
113 --------
|
|
114
|
|
115 Edit this template by entering your search criteria in an
|
|
116 appropriate header field that is already there, or create a new
|
|
117 field yourself. If the string you're looking for could be
|
|
118 anywhere in a message, then place the string underneath the row
|
|
119 of dashes.
|
|
120
|
|
121 As an example, let's say that we want to find messages from
|
|
122 Ginnean about horseback riding in the Kosciusko National
|
|
123 Park (Australia) during January, 1994. Normally we would start
|
|
124 with a broad search and narrow it down if necessary to produce a
|
|
125 manageable amount of data, but we'll cut to the chase and create
|
|
126 a fairly restrictive set of criteria as follows:\\<mh-search-mode-map>
|
|
127
|
|
128 From: ginnean
|
|
129 To:
|
|
130 Cc:
|
|
131 Date: Jan 1994
|
|
132 Subject:
|
|
133 --------
|
|
134 horse
|
|
135 kosciusko
|
|
136
|
|
137 As with MH-Letter mode, MH-Search provides commands like
|
|
138 \\[mh-to-field] to help you fill in the blanks.\\<mh-folder-mode-map>
|
|
139
|
|
140 If you find that you do the same thing over and over when editing
|
|
141 the search template, you may wish to bind some shortcuts to keys.
|
|
142 This can be done with the variable `mh-search-mode-hook', which is
|
|
143 called when \\[mh-search] is run on a new pattern.\\<mh-search-mode-map>
|
|
144
|
|
145 To perform the search, type \\[mh-index-do-search].
|
|
146
|
|
147 Sometimes you're searching for text that is either not indexed,
|
|
148 or hasn't been indexed yet. In this case you can override the
|
|
149 default method with the pick method by running the command
|
|
150 \\[mh-pick-do-search].
|
|
151
|
|
152 The messages that are found are put in a temporary sub-folder of
|
|
153 \"+mhe-index\" and are displayed in an MH-Folder buffer. This
|
|
154 buffer is special because it displays messages from multiple
|
|
155 folders; each set of messages from a given folder has a heading
|
|
156 with the folder name.\\<mh-folder-mode-map>
|
|
157
|
|
158 The appearance of the heading can be modified by customizing the
|
|
159 face `mh-search-folder'. You can jump back and forth between the
|
|
160 headings using the commands \\[mh-index-next-folder] and
|
|
161 \\[mh-index-previous-folder].
|
|
162
|
|
163 In addition, the command \\[mh-index-visit-folder] can be used to
|
|
164 visit the folder of the message at point. Initially, only the
|
|
165 messages that matched the search criteria are displayed in the
|
|
166 folder. While the temporary buffer has its own set of message
|
|
167 numbers, the actual messages numbers are shown in the visited
|
|
168 folder. Thus, the command \\[mh-index-visit-folder] is useful to
|
|
169 find the actual message number of an interesting message, or to
|
|
170 view surrounding messages with the command \\[mh-rescan-folder].
|
|
171
|
|
172 Because this folder is temporary, you'll probably get in the
|
|
173 habit of killing it when you're done with \\[mh-kill-folder].
|
|
174
|
|
175 You can regenerate the results by running this command with a
|
|
176 prefix argument REDO-SEARCH-FLAG.
|
|
177
|
|
178 Note: This command uses an \"X-MHE-Checksum:\" header field to
|
|
179 cache the MD5 checksum of a message. This means that if an
|
|
180 incoming message already contains an \"X-MHE-Checksum:\" field,
|
|
181 that message might not be found by this command. The following
|
|
182 \"procmail\" recipe avoids this problem by renaming the existing
|
|
183 header field:
|
|
184
|
|
185 :0 wf
|
|
186 | formail -R \"X-MHE-Checksum\" \"X-Old-MHE-Checksum\"
|
|
187
|
|
188 Configuring Indexed Searches
|
|
189
|
|
190 The command \\[mh-search] runs the command defined by the option
|
|
191 `mh-search-program'. The default value is \"Auto-detect\" which
|
|
192 means that MH-E will automatically choose one of \"swish++\",
|
|
193 \"swish-e\", \"mairix\", \"namazu\", \"pick\" and \"grep\" in
|
|
194 that order. If, for example, you have both \"swish++\" and
|
|
195 \"mairix\" installed and you want to use \"mairix\", then you can
|
|
196 set this option to \"mairix\".
|
|
197
|
|
198 The documentation for the following commands describe how to set
|
|
199 up the various indexing programs to use with MH-E.
|
|
200
|
|
201 - `mh-swish++-execute-search'
|
|
202 - `mh-swish-execute-search'
|
|
203 - `mh-mairix-execute-search'
|
|
204 - `mh-namazu-execute-search'
|
|
205 - `mh-pick-execute-search'
|
|
206 - `mh-grep-execute-search'
|
|
207
|
|
208 In a program, if FOLDER is \"+\" or nil, then mail in all folders
|
|
209 are searched. Optional argument WINDOW-CONFIG stores the window
|
|
210 configuration that will be restored after the user quits the
|
|
211 folder containing the index search results."
|
|
212 (interactive
|
|
213 (list (progn
|
|
214 (mh-find-path)
|
|
215 ;; Yes, we do want to call mh-search-choose every time in case the
|
|
216 ;; user has switched the searcher manually.
|
|
217 (unless (mh-search-choose (and current-prefix-arg
|
|
218 mh-index-previous-search
|
|
219 (cadr mh-index-previous-search)))
|
|
220 (error "No search program found"))
|
|
221 (or (and current-prefix-arg mh-index-sequence-search-flag)
|
|
222 (and current-prefix-arg (car mh-index-previous-search))
|
|
223 (mh-prompt-for-folder "Search" "+" nil "all" t)))
|
|
224 (or (and current-prefix-arg (caddr mh-index-previous-search))
|
|
225 mh-search-regexp-builder
|
|
226 (read-string (format "%s regexp: "
|
|
227 (upcase-initials (symbol-name mh-searcher)))))
|
|
228 current-prefix-arg
|
|
229 (if (and (not (and current-prefix-arg
|
|
230 (caddr mh-index-previous-search)))
|
|
231 mh-search-regexp-builder)
|
|
232 (current-window-configuration)
|
|
233 nil)))
|
|
234 ;; Redoing a sequence search?
|
|
235 (when (and redo-search-flag mh-index-data mh-index-sequence-search-flag
|
|
236 (not mh-flists-called-flag))
|
|
237 (let ((mh-flists-called-flag t))
|
|
238 (apply #'mh-index-sequenced-messages mh-index-previous-search))
|
|
239 (return-from mh-search))
|
|
240 ;; We have fancy query parsing.
|
|
241 (when (symbolp search-regexp)
|
|
242 (mh-search-folder folder window-config)
|
|
243 (return-from mh-search))
|
|
244 ;; Begin search proper.
|
|
245 (mh-checksum-choose)
|
|
246 (let ((result-count 0)
|
|
247 (old-window-config (or window-config mh-previous-window-config))
|
|
248 (previous-search mh-index-previous-search)
|
|
249 (index-folder (format "%s/%s" mh-index-folder
|
|
250 (mh-index-generate-pretty-name search-regexp))))
|
|
251 ;; Create a new folder for the search results or recreate the old one...
|
|
252 (if (and redo-search-flag mh-index-previous-search)
|
|
253 (let ((buffer-name (buffer-name (current-buffer))))
|
|
254 (mh-process-or-undo-commands buffer-name)
|
|
255 (save-excursion (mh-exec-cmd-quiet nil "rmf" buffer-name))
|
|
256 (mh-exec-cmd-quiet nil "folder" "-create" "-fast" buffer-name)
|
|
257 (setq index-folder buffer-name))
|
|
258 (setq index-folder (mh-index-new-folder index-folder search-regexp)))
|
|
259
|
|
260 (let ((folder-path (format "%s%s" mh-user-path (substring folder 1)))
|
|
261 (folder-results-map (make-hash-table :test #'equal))
|
|
262 (origin-map (make-hash-table :test #'equal)))
|
|
263 ;; Run search program...
|
|
264 (message "Executing %s... " mh-searcher)
|
|
265 (funcall mh-search-function folder-path search-regexp)
|
|
266
|
|
267 ;; Parse searcher output.
|
|
268 (message "Processing %s output... " mh-searcher)
|
|
269 (goto-char (point-min))
|
|
270 (loop for next-result = (funcall mh-search-next-result-function)
|
|
271 while next-result
|
|
272 do (unless (eq next-result 'error)
|
|
273 (unless (gethash (car next-result) folder-results-map)
|
|
274 (setf (gethash (car next-result) folder-results-map)
|
|
275 (make-hash-table :test #'equal)))
|
|
276 (setf (gethash (cadr next-result)
|
|
277 (gethash (car next-result) folder-results-map))
|
|
278 t)))
|
|
279
|
|
280 ;; Copy the search results over.
|
|
281 (maphash #'(lambda (folder msgs)
|
|
282 (let ((cur (car (mh-translate-range folder "cur")))
|
|
283 (msgs (sort (loop for msg being the hash-keys of msgs
|
|
284 collect msg)
|
|
285 #'<)))
|
|
286 (mh-exec-cmd "refile" msgs "-src" folder
|
|
287 "-link" index-folder)
|
|
288 ;; Restore cur to old value, that refile changed
|
|
289 (when cur
|
|
290 (mh-exec-cmd-quiet nil "mark" folder "-add" "-zero"
|
|
291 "-sequence" "cur" (format "%s" cur)))
|
|
292 (loop for msg in msgs
|
|
293 do (incf result-count)
|
|
294 (setf (gethash result-count origin-map)
|
|
295 (cons folder msg)))))
|
|
296 folder-results-map)
|
|
297
|
|
298 ;; Vist the results folder.
|
|
299 (mh-visit-folder index-folder () (list folder-results-map origin-map))
|
|
300
|
|
301 (goto-char (point-min))
|
|
302 (forward-line)
|
|
303 (mh-update-sequences)
|
|
304 (mh-recenter nil)
|
|
305
|
|
306 ;; Update the speedbar, if needed.
|
|
307 (when (mh-speed-flists-active-p)
|
|
308 (mh-speed-flists t mh-current-folder))
|
|
309
|
|
310 ;; Maintain history.
|
|
311 (when (or (and redo-search-flag previous-search) window-config)
|
|
312 (setq mh-previous-window-config old-window-config))
|
|
313 (setq mh-index-previous-search (list folder mh-searcher search-regexp))
|
|
314
|
|
315 ;; Write out data to disk.
|
|
316 (unless mh-flists-called-flag (mh-index-write-data))
|
|
317
|
|
318 (message "%s found %s matches in %s folders"
|
|
319 (upcase-initials (symbol-name mh-searcher))
|
|
320 (loop for msg-hash being hash-values of mh-index-data
|
|
321 sum (hash-table-count msg-hash))
|
|
322 (loop for msg-hash being hash-values of mh-index-data
|
|
323 count (> (hash-table-count msg-hash) 0))))))
|
|
324
|
|
325 (defun mh-search-folder (folder window-config)
|
|
326 "Search FOLDER for messages matching a pattern.
|
|
327
|
|
328 In a program, argument WINDOW-CONFIG is the current window
|
|
329 configuration and is used when the search folder is dismissed."
|
|
330 (interactive (list (mh-prompt-for-folder "Search" mh-current-folder nil nil t)
|
|
331 (current-window-configuration)))
|
|
332 (let ((pick-folder (if (equal folder "+") mh-current-folder folder)))
|
|
333 (switch-to-buffer-other-window "search-pattern")
|
|
334 (if (or (zerop (buffer-size))
|
|
335 (not (y-or-n-p "Reuse pattern? ")))
|
|
336 (mh-make-pick-template)
|
|
337 (message ""))
|
|
338 (mh-make-local-vars 'mh-current-folder folder
|
|
339 'mh-previous-window-config window-config)
|
|
340 (message "%s" (substitute-command-keys
|
|
341 (concat "Type \\[mh-index-do-search] to search messages, "
|
|
342 "\\[mh-pick-do-search] to use pick, "
|
|
343 "\\[mh-help] for help")))))
|
|
344
|
|
345 (defun mh-make-pick-template ()
|
|
346 "Initialize the current buffer with a template for a pick pattern."
|
|
347 (let ((inhibit-read-only t)) (erase-buffer))
|
|
348 (insert "From: \n"
|
|
349 "To: \n"
|
|
350 "Cc: \n"
|
|
351 "Date: \n"
|
|
352 "Subject: \n"
|
|
353 "---------\n")
|
|
354 (mh-search-mode)
|
|
355 (goto-char (point-min))
|
|
356 (dotimes (i 5)
|
|
357 (add-text-properties (point) (1+ (point)) '(front-sticky t))
|
|
358 (add-text-properties (- (line-end-position) 2) (1- (line-end-position))
|
|
359 '(rear-nonsticky t))
|
|
360 (add-text-properties (point) (1- (line-end-position)) '(read-only t))
|
|
361 (forward-line))
|
|
362 (add-text-properties (point) (1+ (point)) '(front-sticky t))
|
|
363 (add-text-properties (point) (1- (line-end-position)) '(read-only t))
|
|
364 (goto-char (point-max)))
|
|
365
|
|
366 ;;;###mh-autoload
|
|
367 (defvar mh-search-mode-map (make-sparse-keymap)
|
|
368 "Keymap for searching folder.")
|
|
369
|
|
370 ;;;###mh-autoload
|
|
371 ;; If this changes, modify mh-search-mode-help-messages accordingly, below.
|
|
372 (gnus-define-keys mh-search-mode-map
|
|
373 "\C-c?" mh-help
|
|
374 "\C-c\C-c" mh-index-do-search
|
|
375 "\C-c\C-p" mh-pick-do-search
|
|
376 "\C-c\C-f\C-b" mh-to-field
|
|
377 "\C-c\C-f\C-c" mh-to-field
|
|
378 "\C-c\C-f\C-d" mh-to-field
|
|
379 "\C-c\C-f\C-f" mh-to-field
|
|
380 "\C-c\C-f\C-r" mh-to-field
|
|
381 "\C-c\C-f\C-s" mh-to-field
|
|
382 "\C-c\C-f\C-t" mh-to-field
|
|
383 "\C-c\C-fb" mh-to-field
|
|
384 "\C-c\C-fc" mh-to-field
|
|
385 "\C-c\C-fd" mh-to-field
|
|
386 "\C-c\C-ff" mh-to-field
|
|
387 "\C-c\C-fr" mh-to-field
|
|
388 "\C-c\C-fs" mh-to-field
|
|
389 "\C-c\C-ft" mh-to-field)
|
|
390
|
|
391 (easy-menu-define
|
|
392 mh-pick-menu mh-search-mode-map "Menu for MH-E Search"
|
|
393 '("Search"
|
|
394 ["Perform Search" mh-index-do-search t]
|
|
395 ["Search with pick" mh-pick-do-search t]))
|
|
396
|
|
397 ;; Group messages logically, more or less.
|
|
398 (defvar mh-search-mode-help-messages
|
|
399 '((nil
|
|
400 "Perform search: \\[mh-index-do-search]\n"
|
|
401 "Search with pick: \\[mh-pick-do-search]\n"
|
|
402 "Move to a field by typing C-c C-f C-<field>\n"
|
|
403 "where <field> is the first letter of the desired field\n"
|
|
404 "(except for From: which uses \"m\")."))
|
|
405 "Key binding cheat sheet.
|
|
406
|
|
407 This is an associative array which is used to show the most common
|
|
408 commands. The key is a prefix char. The value is one or more strings
|
|
409 which are concatenated together and displayed in the minibuffer if ?
|
|
410 is pressed after the prefix character. The special key nil is used to
|
|
411 display the non-prefixed commands.
|
|
412
|
|
413 The substitutions described in `substitute-command-keys' are performed
|
|
414 as well.")
|
|
415
|
|
416 (put 'mh-search-mode 'mode-class 'special)
|
|
417
|
|
418 (define-derived-mode mh-search-mode fundamental-mode "MH-Search"
|
|
419 "Mode for creating search templates in MH-E.\\<mh-search-mode-map>
|
|
420
|
|
421 Edit this template by entering your search criteria in an
|
|
422 appropriate header field that is already there, or create a new
|
|
423 field yourself. If the string you're looking for could be
|
|
424 anywhere in a message, then place the string underneath the row
|
|
425 of dashes.
|
|
426
|
|
427 To perform the search, type \\[mh-index-do-search].
|
|
428
|
|
429 Sometimes you're searching for text that is either not indexed,
|
|
430 or hasn't been indexed yet. In this case you can override the
|
|
431 default method with the pick method by running the command
|
|
432 \\[mh-pick-do-search].
|
|
433
|
|
434 The hook `mh-search-mode-hook' is called upon entry to this mode.
|
|
435
|
|
436 \\{mh-search-mode-map}"
|
|
437
|
|
438 (make-local-variable 'mh-help-messages)
|
|
439 (easy-menu-add mh-pick-menu)
|
|
440 (setq mh-help-messages mh-search-mode-help-messages))
|
|
441
|
|
442 ;;;###mh-autoload
|
|
443 (defun mh-index-do-search (&optional searcher)
|
|
444 "Find messages using `mh-search-program'.
|
|
445 If optional argument SEARCHER is present, use it instead of
|
|
446 `mh-search-program'."
|
|
447 (interactive)
|
|
448 (unless (mh-search-choose searcher) (error "No search program found"))
|
|
449 (let* ((regexp-list (mh-pick-parse-search-buffer))
|
|
450 (pattern (funcall mh-search-regexp-builder regexp-list)))
|
|
451 (if pattern
|
|
452 (mh-search mh-current-folder pattern nil mh-previous-window-config)
|
|
453 (error "No search terms"))))
|
|
454
|
|
455 ;;;###mh-autoload
|
|
456 (defun mh-pick-do-search ()
|
|
457 "Find messages using \"pick\".
|
|
458
|
|
459 Uses the pick method described in `mh-pick-execute-search'."
|
|
460 (interactive)
|
|
461 (mh-index-do-search 'pick))
|
|
462
|
|
463 (defun mh-pick-parse-search-buffer ()
|
|
464 "Parse the search buffer contents.
|
|
465 The function returns a alist. The car of each element is either
|
|
466 the header name to search in or nil to search the whole message.
|
|
467 The cdr of the element is the pattern to search."
|
|
468 (save-excursion
|
|
469 (let ((pattern-list ())
|
|
470 (in-body-flag nil)
|
|
471 start begin)
|
|
472 (goto-char (point-min))
|
|
473 (while (not (eobp))
|
|
474 (if (search-forward "--------" (line-end-position) t)
|
|
475 (setq in-body-flag t)
|
|
476 (beginning-of-line)
|
|
477 (setq begin (point))
|
|
478 (setq start (if in-body-flag
|
|
479 (point)
|
|
480 (search-forward ":" (line-end-position) t)
|
|
481 (point)))
|
|
482 (push (cons (and (not in-body-flag)
|
|
483 (intern (downcase
|
|
484 (buffer-substring-no-properties
|
|
485 begin (1- start)))))
|
|
486 (mh-index-parse-search-regexp
|
|
487 (buffer-substring-no-properties
|
|
488 start (line-end-position))))
|
|
489 pattern-list))
|
|
490 (forward-line))
|
|
491 pattern-list)))
|
|
492
|
|
493 ;;;###mh-autoload
|
|
494 (defun mh-index-parse-search-regexp (input-string)
|
|
495 "Construct parse tree for INPUT-STRING.
|
|
496 All occurrences of &, |, ! and ~ in INPUT-STRING are replaced by
|
|
497 AND, OR and NOT as appropriate. Then the resulting string is
|
|
498 parsed."
|
|
499 (let (input)
|
|
500 (with-temp-buffer
|
|
501 (insert input-string)
|
|
502 ;; replace tabs
|
|
503 (mh-replace-string "\t" " ")
|
|
504 ;; synonyms of AND
|
|
505 (mh-replace-string " AND " " and ")
|
|
506 (mh-replace-string "&" " and ")
|
|
507 (mh-replace-string " -and " " and ")
|
|
508 ;; synonyms of OR
|
|
509 (mh-replace-string " OR " " or ")
|
|
510 (mh-replace-string "|" " or ")
|
|
511 (mh-replace-string " -or " " or ")
|
|
512 ;; synonyms of NOT
|
|
513 (mh-replace-string " NOT " " not ")
|
|
514 (mh-replace-string "!" " not ")
|
|
515 (mh-replace-string "~" " not ")
|
|
516 (mh-replace-string " -not " " not ")
|
|
517 ;; synonyms of left brace
|
|
518 (mh-replace-string "(" " ( ")
|
|
519 (mh-replace-string " -lbrace " " ( ")
|
|
520 ;; synonyms of right brace
|
|
521 (mh-replace-string ")" " ) ")
|
|
522 (mh-replace-string " -rbrace " " ) ")
|
|
523 ;; get the normalized input
|
|
524 (setq input (format "( %s )" (buffer-substring (point-min) (point-max)))))
|
|
525
|
|
526 (let ((tokens (mh-index-add-implicit-ops (split-string input)))
|
|
527 (op-stack ())
|
|
528 (operand-stack ())
|
|
529 oper1)
|
|
530 (dolist (token tokens)
|
|
531 (cond ((equal token "(") (push 'paren op-stack))
|
|
532 ((equal token "not") (push 'not op-stack))
|
|
533 ((equal token "or") (push 'or op-stack))
|
|
534 ((equal token "and") (push 'and op-stack))
|
|
535 ((equal token ")")
|
|
536 (multiple-value-setq (op-stack operand-stack)
|
|
537 (mh-index-evaluate op-stack operand-stack))
|
|
538 (when (eq (car op-stack) 'not)
|
|
539 (setq op-stack (cdr op-stack))
|
|
540 (push `(not ,(pop operand-stack)) operand-stack))
|
|
541 (when (eq (car op-stack) 'and)
|
|
542 (setq op-stack (cdr op-stack))
|
|
543 (setq oper1 (pop operand-stack))
|
|
544 (push `(and ,(pop operand-stack) ,oper1) operand-stack)))
|
|
545 ((eq (car op-stack) 'not)
|
|
546 (setq op-stack (cdr op-stack))
|
|
547 (push `(not ,token) operand-stack)
|
|
548 (when (eq (car op-stack) 'and)
|
|
549 (setq op-stack (cdr op-stack))
|
|
550 (setq oper1 (pop operand-stack))
|
|
551 (push `(and ,(pop operand-stack) ,oper1) operand-stack)))
|
|
552 ((eq (car op-stack) 'and)
|
|
553 (setq op-stack (cdr op-stack))
|
|
554 (push `(and ,(pop operand-stack) ,token) operand-stack))
|
|
555 (t (push token operand-stack))))
|
|
556 (prog1 (pop operand-stack)
|
|
557 (when (or op-stack operand-stack)
|
|
558 (error "Invalid regexp: %s" input))))))
|
|
559
|
|
560 (defun mh-index-add-implicit-ops (tokens)
|
|
561 "Add implicit operators in the list TOKENS."
|
|
562 (let ((result ())
|
|
563 (literal-seen nil)
|
|
564 current)
|
|
565 (while tokens
|
|
566 (setq current (pop tokens))
|
|
567 (cond ((or (equal current ")") (equal current "and") (equal current "or"))
|
|
568 (setq literal-seen nil)
|
|
569 (push current result))
|
|
570 ((and literal-seen
|
|
571 (push "and" result)
|
|
572 (setq literal-seen nil)
|
|
573 nil))
|
|
574 (t
|
|
575 (push current result)
|
|
576 (unless (or (equal current "(") (equal current "not"))
|
|
577 (setq literal-seen t)))))
|
|
578 (nreverse result)))
|
|
579
|
|
580 (defun mh-index-evaluate (op-stack operand-stack)
|
|
581 "Read expression till starting paren based on OP-STACK and OPERAND-STACK."
|
|
582 (block mh-index-evaluate
|
|
583 (let (op oper1)
|
|
584 (while op-stack
|
|
585 (setq op (pop op-stack))
|
|
586 (cond ((eq op 'paren)
|
|
587 (return-from mh-index-evaluate (values op-stack operand-stack)))
|
|
588 ((eq op 'not)
|
|
589 (push `(not ,(pop operand-stack)) operand-stack))
|
|
590 ((or (eq op 'and) (eq op 'or))
|
|
591 (setq oper1 (pop operand-stack))
|
|
592 (push `(,op ,(pop operand-stack) ,oper1) operand-stack))))
|
|
593 (error "Ran out of tokens"))))
|
|
594
|
|
595
|
|
596
|
|
597 ;;; Sequence browsing
|
|
598
|
|
599 ;;;###mh-autoload
|
|
600 (defun mh-index-new-messages (folders)
|
|
601 "Display unseen messages.
|
|
602
|
|
603 If you use a program such as \"procmail\" to use \"rcvstore\" to file
|
|
604 your incoming mail automatically, you can display new, unseen,
|
|
605 messages using this command. All messages in the \"unseen\"
|
|
606 sequence from the folders in `mh-new-messages-folders' are
|
|
607 listed.
|
|
608
|
|
609 With a prefix argument, enter a space-separated list of FOLDERS,
|
|
610 or nothing to search all folders."
|
|
611 (interactive
|
|
612 (list (if current-prefix-arg
|
|
613 (split-string (read-string "Search folder(s) (default all): "))
|
|
614 mh-new-messages-folders)))
|
|
615 (mh-index-sequenced-messages folders mh-unseen-seq))
|
|
616
|
|
617 ;;;###mh-autoload
|
|
618 (defun mh-index-ticked-messages (folders)
|
|
619 "Display ticked messages.
|
|
620
|
|
621 All messages in `mh-tick-seq' from the folders in
|
|
622 `mh-ticked-messages-folders' are listed.
|
|
623
|
|
624 With a prefix argument, enter a space-separated list of FOLDERS,
|
|
625 or nothing to search all folders."
|
|
626 (interactive
|
|
627 (list (if current-prefix-arg
|
|
628 (split-string (read-string "Search folder(s) (default all): "))
|
|
629 mh-ticked-messages-folders)))
|
|
630 (mh-index-sequenced-messages folders mh-tick-seq))
|
|
631
|
|
632 ;;;###mh-autoload
|
|
633 (defun mh-index-sequenced-messages (folders sequence)
|
|
634 "Display messages in any sequence.
|
|
635
|
|
636 All messages from the FOLDERS in `mh-new-messages-folders' in the
|
|
637 SEQUENCE you provide are listed. With a prefix argument, enter a
|
|
638 space-separated list of folders at the prompt, or nothing to
|
|
639 search all folders."
|
|
640 (interactive
|
|
641 (list (if current-prefix-arg
|
|
642 (split-string (read-string "Search folder(s) (default all): "))
|
|
643 mh-new-messages-folders)
|
|
644 (mh-read-seq-default "Search" nil)))
|
|
645 (unless sequence (setq sequence mh-unseen-seq))
|
|
646 (let* ((mh-flists-search-folders folders)
|
|
647 (mh-flists-sequence sequence)
|
|
648 (mh-flists-called-flag t)
|
|
649 (mh-searcher 'flists)
|
|
650 (mh-search-function 'mh-flists-execute)
|
|
651 (mh-search-next-result-function 'mh-mairix-next-result)
|
|
652 (mh-mairix-folder mh-user-path)
|
|
653 (mh-search-regexp-builder nil)
|
|
654 (new-folder (format "%s/%s/%s" mh-index-folder
|
|
655 mh-flists-results-folder sequence))
|
|
656 (window-config (if (equal new-folder mh-current-folder)
|
|
657 mh-previous-window-config
|
|
658 (current-window-configuration)))
|
|
659 (redo-flag nil)
|
|
660 message)
|
|
661 (cond ((buffer-live-p (get-buffer new-folder))
|
|
662 ;; The destination folder is being visited. Trick `mh-search'
|
|
663 ;; into thinking that the folder resulted from a previous search.
|
|
664 (set-buffer new-folder)
|
|
665 (setq mh-index-previous-search (list folders mh-searcher sequence))
|
|
666 (setq redo-flag t))
|
|
667 ((mh-folder-exists-p new-folder)
|
|
668 ;; Folder exists but we don't have it open. That means they are
|
|
669 ;; stale results from a old flists search. Clear it out.
|
|
670 (mh-exec-cmd-quiet nil "rmf" new-folder)))
|
|
671 (setq message (mh-search "+" mh-flists-results-folder
|
|
672 redo-flag window-config)
|
|
673 mh-index-sequence-search-flag t
|
|
674 mh-index-previous-search (list folders mh-searcher sequence))
|
|
675 (mh-index-write-data)
|
|
676 (when (stringp message) (message "%s" message))))
|
|
677
|
|
678 (defvar mh-flists-search-folders)
|
|
679
|
|
680 (defun mh-flists-execute (&rest args)
|
|
681 "Execute flists.
|
|
682 Search for messages belonging to `mh-flists-sequence' in the
|
|
683 folders specified by `mh-flists-search-folders'. If
|
|
684 `mh-recursive-folders-flag' is t, then the folders are searched
|
|
685 recursively. All parameters ARGS are ignored."
|
|
686 (set-buffer (get-buffer-create mh-temp-index-buffer))
|
|
687 (erase-buffer)
|
|
688 (unless (executable-find "sh")
|
|
689 (error "Didn't find sh"))
|
|
690 (with-temp-buffer
|
|
691 (let ((seq (symbol-name mh-flists-sequence)))
|
|
692 (insert "for folder in `" (expand-file-name "flists" mh-progs) " "
|
|
693 (cond ((eq mh-flists-search-folders t)
|
|
694 (mh-quote-for-shell mh-inbox))
|
|
695 ((eq mh-flists-search-folders nil) "")
|
|
696 ((listp mh-flists-search-folders)
|
|
697 (loop for folder in mh-flists-search-folders
|
|
698 concat
|
|
699 (concat " " (mh-quote-for-shell folder)))))
|
|
700 (if mh-recursive-folders-flag " -recurse" "")
|
|
701 " -sequence " seq " -noshowzero -fast` ; do\n"
|
|
702 (expand-file-name "mhpath" mh-progs) " \"+$folder\" " seq "\n"
|
|
703 "done\n"))
|
|
704 (call-process-region
|
|
705 (point-min) (point-max) "sh" nil (get-buffer mh-temp-index-buffer))))
|
|
706
|
|
707
|
|
708
|
|
709 ;;; Folder navigation and utilities
|
|
710
|
|
711 ;;;###mh-autoload
|
|
712 (defun mh-index-group-by-folder ()
|
|
713 "Partition the messages based on source folder.
|
|
714 Returns an alist with the the folder names in the car and the cdr
|
|
715 being the list of messages originally from that folder."
|
|
716 (save-excursion
|
|
717 (goto-char (point-min))
|
|
718 (let ((result-table (make-hash-table :test #'equal)))
|
|
719 (loop for msg being hash-keys of mh-index-msg-checksum-map
|
|
720 do (push msg (gethash (car (gethash
|
|
721 (gethash msg mh-index-msg-checksum-map)
|
|
722 mh-index-checksum-origin-map))
|
|
723 result-table)))
|
|
724 (loop for x being the hash-keys of result-table
|
|
725 collect (cons x (nreverse (gethash x result-table)))))))
|
|
726
|
|
727 ;;;###mh-autoload
|
|
728 (defun mh-index-insert-folder-headers ()
|
|
729 "Annotate the search results with original folder names."
|
|
730 (let ((cur-msg (mh-get-msg-num nil))
|
|
731 (old-buffer-modified-flag (buffer-modified-p))
|
|
732 (buffer-read-only nil)
|
|
733 current-folder last-folder)
|
|
734 (goto-char (point-min))
|
|
735 (while (not (eobp))
|
|
736 (setq current-folder (car (gethash (gethash (mh-get-msg-num nil)
|
|
737 mh-index-msg-checksum-map)
|
|
738 mh-index-checksum-origin-map)))
|
|
739 (when (and current-folder (not (equal current-folder last-folder)))
|
|
740 (insert (if last-folder "\n" "") current-folder "\n")
|
|
741 (setq last-folder current-folder))
|
|
742 (forward-line))
|
|
743 (when cur-msg
|
|
744 (mh-notate-cur)
|
|
745 (mh-goto-msg cur-msg t))
|
|
746 (set-buffer-modified-p old-buffer-modified-flag))
|
|
747 (mh-index-create-imenu-index))
|
|
748
|
|
749 ;;;###mh-autoload
|
|
750 (defun mh-index-delete-folder-headers ()
|
|
751 "Delete the folder headers."
|
|
752 (let ((cur-msg (mh-get-msg-num nil))
|
|
753 (old-buffer-modified-flag (buffer-modified-p))
|
|
754 (buffer-read-only nil))
|
|
755 (while (and (not cur-msg) (not (eobp)))
|
|
756 (forward-line)
|
|
757 (setq cur-msg (mh-get-msg-num nil)))
|
|
758 (goto-char (point-min))
|
|
759 (while (not (eobp))
|
|
760 (if (or (char-equal (char-after) ?+) (char-equal (char-after) 10))
|
|
761 (delete-region (point) (progn (forward-line) (point)))
|
|
762 (forward-line)))
|
|
763 (when cur-msg (mh-goto-msg cur-msg t t))
|
|
764 (set-buffer-modified-p old-buffer-modified-flag)))
|
|
765
|
|
766 ;;;###mh-autoload
|
|
767 (defun mh-index-create-imenu-index ()
|
|
768 "Create alist of folder names and positions in index folder buffers."
|
|
769 (save-excursion
|
|
770 (setq which-func-mode t)
|
|
771 (let ((alist ()))
|
|
772 (goto-char (point-min))
|
|
773 (while (re-search-forward "^+" nil t)
|
|
774 (save-excursion
|
|
775 (beginning-of-line)
|
|
776 (push (cons (buffer-substring-no-properties
|
|
777 (point) (line-end-position))
|
|
778 (set-marker (make-marker) (point)))
|
|
779 alist)))
|
|
780 (setq imenu--index-alist (nreverse alist)))))
|
|
781
|
|
782 ;;;###mh-autoload
|
|
783 (defun mh-index-next-folder (&optional backward-flag)
|
|
784 "Jump to the next folder marker.
|
|
785
|
|
786 With non-nil optional argument BACKWARD-FLAG, jump to the previous
|
|
787 group of results."
|
|
788 (interactive "P")
|
|
789 (if (null mh-index-data)
|
|
790 (message "Only applicable in an MH-E index search buffer")
|
|
791 (let ((point (point)))
|
|
792 (forward-line (if backward-flag 0 1))
|
|
793 (cond ((if backward-flag
|
|
794 (re-search-backward "^+" (point-min) t)
|
|
795 (re-search-forward "^+" (point-max) t))
|
|
796 (beginning-of-line))
|
|
797 ((and (if backward-flag
|
|
798 (goto-char (point-max))
|
|
799 (goto-char (point-min)))
|
|
800 nil))
|
|
801 ((if backward-flag
|
|
802 (re-search-backward "^+" (point-min) t)
|
|
803 (re-search-forward "^+" (point-max) t))
|
|
804 (beginning-of-line))
|
|
805 (t (goto-char point))))))
|
|
806
|
|
807 ;;;###mh-autoload
|
|
808 (defun mh-index-previous-folder ()
|
|
809 "Jump to the previous folder marker."
|
|
810 (interactive)
|
|
811 (mh-index-next-folder t))
|
|
812
|
|
813 ;;;###mh-autoload
|
|
814 (defun mh-index-visit-folder ()
|
|
815 "Visit original folder from where the message at point was found."
|
|
816 (interactive)
|
|
817 (unless mh-index-data
|
|
818 (error "Not in an index folder"))
|
|
819 (let (folder msg)
|
|
820 (save-excursion
|
|
821 (cond ((and (bolp) (eolp))
|
|
822 (ignore-errors (forward-line -1))
|
|
823 (setq msg (mh-get-msg-num t)))
|
|
824 ((equal (char-after (line-beginning-position)) ?+)
|
|
825 (setq folder (buffer-substring-no-properties
|
|
826 (line-beginning-position) (line-end-position))))
|
|
827 (t (setq msg (mh-get-msg-num t)))))
|
|
828 (when (not folder)
|
|
829 (setq folder (car (gethash (gethash msg mh-index-msg-checksum-map)
|
|
830 mh-index-checksum-origin-map))))
|
|
831 (when (or (not (get-buffer folder))
|
|
832 (y-or-n-p (format "Reuse buffer displaying %s? " folder)))
|
|
833 (mh-visit-folder
|
|
834 folder (loop for x being the hash-keys of (gethash folder mh-index-data)
|
|
835 when (mh-msg-exists-p x folder) collect x)))))
|
|
836
|
|
837 ;;;###mh-autoload
|
|
838 (defun mh-search-p ()
|
|
839 "Non-nil means that this folder was generated by searching."
|
|
840 mh-index-data)
|
|
841
|
|
842 ;;;###mh-autoload
|
|
843 (defun mh-index-execute-commands ()
|
|
844 "Delete/refile the actual messages.
|
|
845 The copies in the searched folder are then deleted/refiled to get
|
|
846 the desired result. Before deleting the messages we make sure
|
|
847 that the message being deleted is identical to the one that the
|
|
848 user has marked in the index buffer."
|
|
849 (save-excursion
|
|
850 (let ((folders ())
|
|
851 (mh-speed-flists-inhibit-flag t))
|
|
852 (maphash
|
|
853 (lambda (folder msgs)
|
|
854 (push folder folders)
|
|
855 (if (not (get-buffer folder))
|
|
856 ;; If source folder not open, just delete the messages...
|
|
857 (apply #'mh-exec-cmd "rmm" folder (mh-coalesce-msg-list msgs))
|
|
858 ;; Otherwise delete the messages in the source buffer...
|
|
859 (save-excursion
|
|
860 (set-buffer folder)
|
|
861 (let ((old-refile-list mh-refile-list)
|
|
862 (old-delete-list mh-delete-list))
|
|
863 (setq mh-refile-list nil
|
|
864 mh-delete-list msgs)
|
|
865 (unwind-protect (mh-execute-commands)
|
|
866 (setq mh-refile-list
|
|
867 (mapcar (lambda (x)
|
|
868 (cons (car x)
|
|
869 (loop for y in (cdr x)
|
|
870 unless (memq y msgs) collect y)))
|
|
871 old-refile-list)
|
|
872 mh-delete-list
|
|
873 (loop for x in old-delete-list
|
|
874 unless (memq x msgs) collect x))
|
|
875 (mh-set-folder-modified-p (mh-outstanding-commands-p))
|
|
876 (when (mh-outstanding-commands-p)
|
|
877 (mh-notate-deleted-and-refiled)))))))
|
|
878 (mh-index-matching-source-msgs (append (loop for x in mh-refile-list
|
|
879 append (cdr x))
|
|
880 mh-delete-list)
|
|
881 t))
|
|
882 folders)))
|
|
883
|
|
884
|
|
885
|
|
886 ;;; Indexing functions
|
|
887
|
|
888 ;; Support different search programs
|
|
889 (defvar mh-search-choices
|
|
890 '((swish++
|
|
891 mh-swish++-binary mh-swish++-execute-search mh-swish++-next-result
|
|
892 mh-swish++-regexp-builder)
|
|
893 (swish
|
|
894 mh-swish-binary mh-swish-execute-search mh-swish-next-result nil)
|
|
895 (mairix
|
|
896 mh-mairix-binary mh-mairix-execute-search mh-mairix-next-result
|
|
897 mh-mairix-regexp-builder)
|
|
898 (namazu
|
|
899 mh-namazu-binary mh-namazu-execute-search mh-namazu-next-result nil)
|
|
900 (pick
|
|
901 mh-pick-binary mh-pick-execute-search mh-pick-next-result
|
|
902 mh-pick-regexp-builder)
|
|
903 (grep
|
|
904 mh-grep-binary mh-grep-execute-search mh-grep-next-result nil))
|
|
905 "List of possible searcher choices.")
|
|
906
|
|
907 (defun mh-search-choose (&optional searcher)
|
|
908 "Choose a searching function.
|
|
909 The side-effects of this function are that the variables
|
|
910 `mh-searcher', `mh-search-function', and
|
|
911 `mh-search-next-result-function' are set according to the first
|
|
912 searcher in `mh-search-choices' present on the system. If
|
|
913 optional argument SEARCHER is present, use it instead of
|
|
914 `mh-search-program'."
|
|
915 (block nil
|
|
916 (let ((program-alist (cond (searcher
|
|
917 (list (assoc searcher mh-search-choices)))
|
|
918 (mh-search-program
|
|
919 (list
|
|
920 (assoc mh-search-program mh-search-choices)))
|
|
921 (t mh-search-choices))))
|
|
922 (while program-alist
|
|
923 (let* ((current (pop program-alist))
|
|
924 (executable (symbol-value (cadr current))))
|
|
925 (when executable
|
|
926 (setq mh-searcher (car current))
|
|
927 (setq mh-search-function (nth 2 current))
|
|
928 (setq mh-search-next-result-function (nth 3 current))
|
|
929 (setq mh-search-regexp-builder (nth 4 current))
|
|
930 (return mh-searcher))))
|
|
931 nil)))
|
|
932
|
|
933 ;;; Swish++ interface
|
|
934
|
|
935 (defvar mh-swish++-binary (or (executable-find "search++")
|
|
936 (executable-find "search")))
|
|
937 (defvar mh-swish++-directory ".swish++")
|
|
938 (defvar mh-swish-folder nil)
|
|
939
|
|
940 ;;;###mh-autoload
|
|
941 (defun mh-swish++-execute-search (folder-path search-regexp)
|
|
942 "Execute swish++.
|
|
943
|
|
944 In the examples below, replace \"/home/user/Mail\" with the path to
|
|
945 your MH directory.
|
|
946
|
|
947 First create the directory \"/home/user/Mail/.swish++\". Then create
|
|
948 the file \"/home/user/Mail/.swish++/swish++.conf\" with the following
|
|
949 contents:
|
|
950
|
|
951 IncludeMeta Bcc Cc Comments Content-Description From Keywords
|
|
952 IncludeMeta Newsgroups Resent-To Subject To
|
|
953 IncludeMeta Message-Id References In-Reply-To
|
|
954 IncludeFile Mail *
|
|
955 IndexFile /home/user/Mail/.swish++/swish++.index
|
|
956
|
|
957 Use the following command line to generate the swish index. Run
|
|
958 this daily from cron:
|
|
959
|
|
960 find /home/user/Mail -path /home/user/Mail/mhe-index -prune \\
|
|
961 -o -path /home/user/Mail/.swish++ -prune \\
|
|
962 -o -name \"[0-9]*\" -print \\
|
|
963 | index -c /home/user/Mail/.swish++/swish++.conf -
|
|
964
|
|
965 This command does not index the folders that hold the results of your
|
|
966 searches in \"+mhe-index\" since they tend to be ephemeral and the
|
|
967 original messages are indexed anyway.
|
|
968
|
|
969 On some systems (Debian GNU/Linux, for example), use \"index++\"
|
|
970 instead of \"index\".
|
|
971
|
|
972 In a program, FOLDER-PATH is the directory in which SEARCH-REGEXP is
|
|
973 used to search."
|
|
974 (set-buffer (get-buffer-create mh-temp-index-buffer))
|
|
975 (erase-buffer)
|
|
976 (unless mh-swish++-binary
|
|
977 (error "Set `mh-swish++-binary' appropriately"))
|
|
978 (call-process mh-swish++-binary nil '(t nil) nil
|
|
979 "-m" "10000"
|
|
980 (format "-i%s%s/swish++.index"
|
|
981 mh-user-path mh-swish++-directory)
|
|
982 search-regexp)
|
|
983 (goto-char (point-min))
|
|
984 (setq mh-swish-folder
|
|
985 (let ((last-char (substring folder-path (1- (length folder-path)))))
|
|
986 (if (equal last-char "/")
|
|
987 folder-path
|
|
988 (format "%s/" folder-path)))))
|
|
989
|
|
990 (defalias 'mh-swish++-next-result 'mh-swish-next-result)
|
|
991
|
|
992 (defun mh-swish++-regexp-builder (regexp-list)
|
|
993 "Generate query for swish++.
|
|
994 REGEXP-LIST is an alist of fields and values."
|
|
995 (let ((regexp ""))
|
|
996 (dolist (elem regexp-list)
|
|
997 (when (cdr elem)
|
|
998 (setq regexp (concat regexp " and "
|
|
999 (if (car elem) "(" "")
|
|
1000 (if (car elem) (symbol-name (car elem)) "")
|
|
1001 (if (car elem) " = " "")
|
|
1002 (mh-swish++-print-regexp (cdr elem))
|
|
1003 (if (car elem) ")" "")))))
|
|
1004 (substring regexp 4)))
|
|
1005
|
|
1006 (defun mh-swish++-print-regexp (expr)
|
|
1007 "Return infix expression corresponding to EXPR."
|
|
1008 (cond ((atom expr) (format "%s" expr))
|
|
1009 ((eq (car expr) 'not)
|
|
1010 (format "(not %s)" (mh-swish++-print-regexp (cadr expr))))
|
|
1011 (t (format "(%s %s %s)" (mh-swish++-print-regexp (cadr expr))
|
|
1012 (symbol-name (car expr))
|
|
1013 (mh-swish++-print-regexp (caddr expr))))))
|
|
1014
|
|
1015 ;;; Swish interface
|
|
1016
|
|
1017 (defvar mh-swish-binary (executable-find "swish-e"))
|
|
1018 (defvar mh-swish-directory ".swish")
|
|
1019
|
|
1020 ;;;###mh-autoload
|
|
1021 (defun mh-swish-execute-search (folder-path search-regexp)
|
|
1022 "Execute swish-e.
|
|
1023
|
|
1024 In the examples below, replace \"/home/user/Mail\" with the path
|
|
1025 to your MH directory.
|
|
1026
|
|
1027 First create the directory \"/home/user/Mail/.swish\". Then
|
|
1028 create the file \"/home/user/Mail/.swish/config\" with the
|
|
1029 following contents:
|
|
1030
|
|
1031 DefaultContents TXT*
|
|
1032 IndexDir /home/user/Mail
|
|
1033 IndexFile /home/user/Mail/.swish/index
|
|
1034 IndexName \"Mail Index\"
|
|
1035 IndexDescription \"Mail Index\"
|
|
1036 IndexPointer \"http://nowhere\"
|
|
1037 IndexAdmin \"nobody\"
|
|
1038 #MetaNames automatic
|
|
1039 IndexReport 3
|
|
1040 FollowSymLinks no
|
|
1041 UseStemming no
|
|
1042 IgnoreTotalWordCountWhenRanking yes
|
|
1043 WordCharacters abcdefghijklmnopqrstuvwxyz0123456789-
|
|
1044 BeginCharacters abcdefghijklmnopqrstuvwxyz
|
|
1045 EndCharacters abcdefghijklmnopqrstuvwxyz0123456789
|
|
1046 IgnoreLimit 50 1000
|
|
1047 IndexComments 0
|
|
1048 FileRules filename contains \\D
|
|
1049 FileRules pathname contains /home/user/Mail/.swish
|
|
1050 FileRules pathname contains /home/user/Mail/mhe-index
|
|
1051
|
|
1052 This configuration does not index the folders that hold the
|
|
1053 results of your searches in \"+mhe-index\" since they tend to be
|
|
1054 ephemeral and the original messages are indexed anyway.
|
|
1055
|
|
1056 If there are any directories you would like to ignore, append
|
|
1057 lines like the following to \"config\":
|
|
1058
|
|
1059 FileRules pathname contains /home/user/Mail/scripts
|
|
1060
|
|
1061 Use the following command line to generate the swish index. Run
|
|
1062 this daily from cron:
|
|
1063
|
|
1064 swish-e -c /home/user/Mail/.swish/config
|
|
1065
|
|
1066 In a program, FOLDER-PATH is the directory in which SEARCH-REGEXP
|
|
1067 is used to search."
|
|
1068 (set-buffer (get-buffer-create mh-temp-index-buffer))
|
|
1069 (erase-buffer)
|
|
1070 (unless mh-swish-binary
|
|
1071 (error "Set `mh-swish-binary' appropriately"))
|
|
1072 (call-process mh-swish-binary nil '(t nil) nil
|
|
1073 "-w" search-regexp
|
|
1074 "-f" (format "%s%s/index" mh-user-path mh-swish-directory))
|
|
1075 (goto-char (point-min))
|
|
1076 (setq mh-swish-folder
|
|
1077 (let ((last-char (substring folder-path (1- (length folder-path)))))
|
|
1078 (if (equal last-char "/")
|
|
1079 folder-path
|
|
1080 (format "%s/" folder-path)))))
|
|
1081
|
|
1082 (defun mh-swish-next-result ()
|
|
1083 "Get the next result from swish output."
|
|
1084 (prog1
|
|
1085 (block nil
|
|
1086 (when (or (eobp) (equal (char-after (point)) ?.))
|
|
1087 (return nil))
|
|
1088 (when (equal (char-after (point)) ?#)
|
|
1089 (return 'error))
|
|
1090 (let* ((start (search-forward " " (line-end-position) t))
|
|
1091 (end (search-forward " " (line-end-position) t)))
|
|
1092 (unless (and start end)
|
|
1093 (return 'error))
|
|
1094 (setq end (1- end))
|
|
1095 (unless (file-exists-p (buffer-substring-no-properties start end))
|
|
1096 (return 'error))
|
|
1097 (unless (search-backward "/" start t)
|
|
1098 (return 'error))
|
|
1099 (list (let* ((s (buffer-substring-no-properties start (1+ (point)))))
|
|
1100 (unless (string-match mh-swish-folder s)
|
|
1101 (return 'error))
|
|
1102 (if (and (string-match mh-user-path s)
|
|
1103 (< (match-end 0) (1- (length s))))
|
|
1104 (format "+%s"
|
|
1105 (substring s (match-end 0) (1- (length s))))
|
|
1106 (return 'error)))
|
|
1107 (let* ((s (buffer-substring-no-properties (1+ (point)) end))
|
|
1108 (val (ignore-errors (read-from-string s))))
|
|
1109 (if (and (consp val) (numberp (car val)))
|
|
1110 (car val)
|
|
1111 (return 'error)))
|
|
1112 nil)))
|
|
1113 (forward-line)))
|
|
1114
|
|
1115 ;;; Mairix interface
|
|
1116
|
|
1117 (defvar mh-mairix-binary (executable-find "mairix"))
|
|
1118 (defvar mh-mairix-directory ".mairix")
|
|
1119 (defvar mh-mairix-folder nil)
|
|
1120
|
|
1121 ;;;###mh-autoload
|
|
1122 (defun mh-mairix-execute-search (folder-path search-regexp-list)
|
|
1123 "Execute mairix.
|
|
1124
|
|
1125 In the examples below, replace \"/home/user/Mail\" with the path
|
|
1126 to your MH directory.
|
|
1127
|
|
1128 First create the directory \"/home/user/Mail/.mairix\". Then
|
|
1129 create the file \"/home/user/Mail/.mairix/config\" with the
|
|
1130 following contents:
|
|
1131
|
|
1132 base=/home/user/Mail
|
|
1133
|
|
1134 # List of folders that should be indexed. 3 dots at the end means there
|
|
1135 # are subfolders within the folder
|
|
1136 mh=archive...:inbox:drafts:news:sent:trash
|
|
1137
|
|
1138 vfolder_format=raw
|
|
1139 database=/home/user/Mail/mairix/database
|
|
1140
|
|
1141 Use the following command line to generate the mairix index. Run
|
|
1142 this daily from cron:
|
|
1143
|
|
1144 mairix -f /home/user/Mail/.mairix/config
|
|
1145
|
|
1146 In a program, FOLDER-PATH is the directory in which
|
|
1147 SEARCH-REGEXP-LIST is used to search."
|
|
1148 (set-buffer (get-buffer-create mh-temp-index-buffer))
|
|
1149 (erase-buffer)
|
|
1150 (unless mh-mairix-binary
|
|
1151 (error "Set `mh-mairix-binary' appropriately"))
|
|
1152 (apply #'call-process mh-mairix-binary nil '(t nil) nil
|
|
1153 "-r" "-f" (format "%s%s/config" mh-user-path mh-mairix-directory)
|
|
1154 search-regexp-list)
|
|
1155 (goto-char (point-min))
|
|
1156 (setq mh-mairix-folder
|
|
1157 (let ((last-char (substring folder-path (1- (length folder-path)))))
|
|
1158 (if (equal last-char "/")
|
|
1159 folder-path
|
|
1160 (format "%s/" folder-path)))))
|
|
1161
|
|
1162 (defun mh-mairix-next-result ()
|
|
1163 "Return next result from mairix output."
|
|
1164 (prog1
|
|
1165 (block nil
|
|
1166 (when (or (eobp) (and (bolp) (eolp)))
|
|
1167 (return nil))
|
|
1168 (unless (eq (char-after) ?/)
|
|
1169 (return 'error))
|
|
1170 (let ((start (point))
|
|
1171 end msg-start)
|
|
1172 (setq end (line-end-position))
|
|
1173 (unless (search-forward mh-mairix-folder end t)
|
|
1174 (return 'error))
|
|
1175 (goto-char (match-beginning 0))
|
|
1176 (unless (equal (point) start)
|
|
1177 (return 'error))
|
|
1178 (goto-char end)
|
|
1179 (unless (search-backward "/" start t)
|
|
1180 (return 'error))
|
|
1181 (setq msg-start (1+ (point)))
|
|
1182 (goto-char start)
|
|
1183 (unless (search-forward mh-user-path end t)
|
|
1184 (return 'error))
|
|
1185 (list (format "+%s" (buffer-substring-no-properties
|
|
1186 (point) (1- msg-start)))
|
|
1187 (car (read-from-string
|
|
1188 (buffer-substring-no-properties msg-start end)))
|
|
1189 nil)))
|
|
1190 (forward-line)))
|
|
1191
|
|
1192 (defun mh-mairix-regexp-builder (regexp-list)
|
|
1193 "Generate query for mairix.
|
|
1194 REGEXP-LIST is an alist of fields and values."
|
|
1195 (let ((result ()))
|
|
1196 (dolist (pair regexp-list)
|
|
1197 (when (cdr pair)
|
|
1198 (push
|
|
1199 (concat
|
|
1200 (cond ((eq (car pair) 'to) "t:")
|
|
1201 ((eq (car pair) 'from) "f:")
|
|
1202 ((eq (car pair) 'cc) "c:")
|
|
1203 ((eq (car pair) 'subject) "s:")
|
|
1204 ((eq (car pair) 'date) "d:")
|
|
1205 (t ""))
|
|
1206 (let ((sop (cdr (mh-mairix-convert-to-sop* (cdr pair))))
|
|
1207 (final ""))
|
|
1208 (dolist (conjunct sop)
|
|
1209 (let ((expr-list (cdr conjunct))
|
|
1210 (expr-string ""))
|
|
1211 (dolist (e expr-list)
|
|
1212 (setq expr-string (concat expr-string ","
|
|
1213 (if (atom e) "" "~")
|
|
1214 (if (atom e) e (cadr e)))))
|
|
1215 (setq final (concat final "/" (substring expr-string 1)))))
|
|
1216 (substring final 1)))
|
|
1217 result)))
|
|
1218 result))
|
|
1219
|
|
1220 (defun mh-mairix-convert-to-sop* (expr)
|
|
1221 "Convert EXPR to sum of product form."
|
|
1222 (cond ((atom expr) `(or (and ,expr)))
|
|
1223 ((eq (car expr) 'or)
|
|
1224 (cons 'or
|
|
1225 (loop for e in (mapcar #'mh-mairix-convert-to-sop* (cdr expr))
|
|
1226 append (cdr e))))
|
|
1227 ((eq (car expr) 'and)
|
|
1228 (let ((conjuncts (mapcar #'mh-mairix-convert-to-sop* (cdr expr)))
|
|
1229 result next-factor)
|
|
1230 (setq result (pop conjuncts))
|
|
1231 (while conjuncts
|
|
1232 (setq next-factor (pop conjuncts))
|
|
1233 (setq result (let ((res ()))
|
|
1234 (dolist (t1 (cdr result))
|
|
1235 (dolist (t2 (cdr next-factor))
|
|
1236 (push `(and ,@(cdr t1) ,@(cdr t2)) res)))
|
|
1237 (cons 'or res))))
|
|
1238 result))
|
|
1239 ((atom (cadr expr)) `(or (and ,expr)))
|
|
1240 ((eq (caadr expr) 'not) (mh-mairix-convert-to-sop* (cadadr expr)))
|
|
1241 ((eq (caadr expr) 'and) (mh-mairix-convert-to-sop*
|
|
1242 `(or ,@(mapcar #'(lambda (x) `(not ,x))
|
|
1243 (cdadr expr)))))
|
|
1244 ((eq (caadr expr) 'or) (mh-mairix-convert-to-sop*
|
|
1245 `(and ,@(mapcar #'(lambda (x) `(not ,x))
|
|
1246 (cdadr expr)))))
|
|
1247 (t (error "Unreachable: %s" expr))))
|
|
1248
|
|
1249 ;;; Namazu interface
|
|
1250
|
|
1251 (defvar mh-namazu-binary (executable-find "namazu"))
|
|
1252 (defvar mh-namazu-directory ".namazu")
|
|
1253 (defvar mh-namazu-folder nil)
|
|
1254
|
|
1255 ;;;###mh-autoload
|
|
1256 (defun mh-namazu-execute-search (folder-path search-regexp)
|
|
1257 "Execute namazu.
|
|
1258
|
|
1259 In the examples below, replace \"/home/user/Mail\" with the path to
|
|
1260 your MH directory.
|
|
1261
|
|
1262 First create the directory \"/home/user/Mail/.namazu\". Then create
|
|
1263 the file \"/home/user/Mail/.namazu/mknmzrc\" with the following
|
|
1264 contents:
|
|
1265
|
|
1266 package conf; # Don't remove this line!
|
|
1267 $ADDRESS = 'user@localhost';
|
|
1268 $ALLOW_FILE = \"[0-9]*\";
|
|
1269 $EXCLUDE_PATH = \"^/home/user/Mail/(mhe-index|spam)\";
|
|
1270
|
|
1271 This configuration does not index the folders that hold the results of
|
|
1272 your searches in \"+mhe-index\" since they tend to be ephemeral and
|
|
1273 the original messages are indexed anyway.
|
|
1274
|
|
1275 Use the following command line to generate the namazu index. Run this
|
|
1276 daily from cron:
|
|
1277
|
|
1278 mknmz -f /home/user/Mail/.namazu/mknmzrc -O /home/user/Mail/.namazu \\
|
|
1279 /home/user/Mail
|
|
1280
|
|
1281 In a program, FOLDER-PATH is the directory in which SEARCH-REGEXP
|
|
1282 is used to search."
|
|
1283 (let ((namazu-index-directory
|
|
1284 (format "%s%s" mh-user-path mh-namazu-directory)))
|
|
1285 (unless (file-exists-p namazu-index-directory)
|
|
1286 (error "Namazu directory %s not present" namazu-index-directory))
|
|
1287 (unless (executable-find mh-namazu-binary)
|
|
1288 (error "Set `mh-namazu-binary' appropriately"))
|
|
1289 (set-buffer (get-buffer-create mh-temp-index-buffer))
|
|
1290 (erase-buffer)
|
|
1291 (call-process mh-namazu-binary nil '(t nil) nil
|
|
1292 "-alR" search-regexp namazu-index-directory)
|
|
1293 (goto-char (point-min))
|
|
1294 (setq mh-namazu-folder
|
|
1295 (let ((last (substring folder-path (1- (length folder-path)))))
|
|
1296 (if (equal last "/")
|
|
1297 folder-path
|
|
1298 (format "%s/" folder-path))))))
|
|
1299
|
|
1300 (defun mh-namazu-next-result ()
|
|
1301 "Get the next result from namazu output."
|
|
1302 (prog1
|
|
1303 (block nil
|
|
1304 (when (eobp) (return nil))
|
|
1305 (let ((file-name (buffer-substring-no-properties
|
|
1306 (point) (line-end-position))))
|
|
1307 (unless (equal (string-match mh-namazu-folder file-name) 0)
|
|
1308 (return 'error))
|
|
1309 (unless (file-exists-p file-name)
|
|
1310 (return 'error))
|
|
1311 (string-match mh-user-path file-name)
|
|
1312 (let* ((folder/msg (substring file-name (match-end 0)))
|
|
1313 (mark (mh-search-from-end ?/ folder/msg)))
|
|
1314 (unless mark (return 'error))
|
|
1315 (list (format "+%s" (substring folder/msg 0 mark))
|
|
1316 (let ((n (ignore-errors (read-from-string
|
|
1317 (substring folder/msg (1+ mark))))))
|
|
1318 (if (and (consp n) (numberp (car n)))
|
|
1319 (car n)
|
|
1320 (return 'error)))
|
|
1321 nil))))
|
|
1322 (forward-line)))
|
|
1323
|
|
1324 ;;; Pick interface
|
|
1325
|
|
1326 (defvar mh-index-pick-folder)
|
|
1327 (defvar mh-pick-binary "pick")
|
|
1328 (defconst mh-pick-single-dash '(cc date from subject to)
|
|
1329 "Search components that are supported by single-dash option in pick.")
|
|
1330
|
|
1331 ;;;###mh-autoload
|
|
1332 (defun mh-pick-execute-search (folder-path search-regexp)
|
|
1333 "Execute pick.
|
|
1334
|
|
1335 Read \"pick(1)\" or the section Finding Messages with pick in the
|
|
1336 MH book to find out more about how to enter the criteria (see URL
|
|
1337 `http://www.ics.uci.edu/~mh/book/mh/finpic.htm').
|
|
1338
|
|
1339 In a program, FOLDER-PATH is the directory in which SEARCH-REGEXP
|
|
1340 is used to search."
|
|
1341 (set-buffer (get-buffer-create mh-temp-index-buffer))
|
|
1342 (erase-buffer)
|
|
1343 (let ((folders
|
|
1344 (mh-folder-list (substring folder-path (length mh-user-path)))))
|
|
1345 (loop for folder in folders do
|
|
1346 (setq folder (concat "+" folder))
|
|
1347 (insert folder "\n")
|
|
1348 (apply #'call-process (expand-file-name "pick" mh-progs)
|
|
1349 nil '(t nil) nil folder "-list" search-regexp)))
|
|
1350 (goto-char (point-min)))
|
|
1351
|
|
1352 (defun mh-pick-next-result ()
|
|
1353 "Return the next pick search result."
|
|
1354 (prog1
|
|
1355 (block nil
|
|
1356 (when (eobp) (return nil))
|
|
1357 (when (search-forward-regexp "^\+" (line-end-position) t)
|
|
1358 (setq mh-index-pick-folder
|
|
1359 (buffer-substring-no-properties (line-beginning-position)
|
|
1360 (line-end-position)))
|
|
1361 (return 'error))
|
|
1362 (unless (search-forward-regexp "^[1-9][0-9]*$" (line-end-position) t)
|
|
1363 (return 'error))
|
|
1364 (list mh-index-pick-folder
|
|
1365 (string-to-number
|
|
1366 (buffer-substring-no-properties (line-beginning-position)
|
|
1367 (line-end-position)))
|
|
1368 nil))
|
|
1369 (forward-line)))
|
|
1370
|
|
1371 ;; All implementations of pick have special options -cc, -date, -from and
|
|
1372 ;; -subject that allow to search for corresponding components. Any other
|
|
1373 ;; component is searched using option --COMPNAME, for example: `pick
|
|
1374 ;; --x-mailer mh-e'. Mailutils "pick" supports this option using a certain
|
|
1375 ;; kludge, but it prefers the following syntax for this purpose:
|
|
1376 ;; "--component=COMPNAME --pattern=PATTERN".
|
|
1377 ;; -- Sergey Poznyakoff, Aug 2003
|
|
1378 (defun mh-pick-regexp-builder (pattern-list)
|
|
1379 "Generate pick search expression from PATTERN-LIST."
|
|
1380 (let ((result ()))
|
|
1381 (dolist (pattern pattern-list)
|
|
1382 (when (cdr pattern)
|
|
1383 (setq result `(,@result "-and" "-lbrace"
|
|
1384 ,@(mh-pick-construct-regexp
|
|
1385 (if (and (mh-variant-p 'mu-mh) (car pattern))
|
|
1386 (format "--pattern=%s" (cdr pattern))
|
|
1387 (cdr pattern))
|
|
1388 (if (car pattern)
|
|
1389 (cond
|
|
1390 ((mh-variant-p 'mu-mh)
|
|
1391 (format "--component=%s" (car pattern)))
|
|
1392 ((member (car pattern) mh-pick-single-dash)
|
|
1393 (format "-%s" (car pattern)))
|
|
1394 (t
|
|
1395 (format "--%s" (car pattern))))
|
|
1396 "-search"))
|
|
1397 "-rbrace"))))
|
|
1398 (cdr result)))
|
|
1399
|
|
1400 (defun mh-pick-construct-regexp (expr component)
|
|
1401 "Construct pick compatible expression corresponding to EXPR.
|
|
1402 COMPONENT is the component to search."
|
|
1403 (cond ((atom expr) (list component expr))
|
|
1404 ((eq (car expr) 'and)
|
|
1405 `("-lbrace" ,@(mh-pick-construct-regexp (cadr expr) component) "-and"
|
|
1406 ,@(mh-pick-construct-regexp (caddr expr) component) "-rbrace"))
|
|
1407 ((eq (car expr) 'or)
|
|
1408 `("-lbrace" ,@(mh-pick-construct-regexp (cadr expr) component) "-or"
|
|
1409 ,@(mh-pick-construct-regexp (caddr expr) component) "-rbrace"))
|
|
1410 ((eq (car expr) 'not)
|
|
1411 `("-lbrace" "-not" ,@(mh-pick-construct-regexp (cadr expr) component)
|
|
1412 "-rbrace"))
|
|
1413 (t (error "Unknown operator %s seen" (car expr)))))
|
|
1414
|
|
1415 ;;; Grep interface
|
|
1416
|
|
1417 (defvar mh-grep-binary (executable-find "grep"))
|
|
1418
|
|
1419 ;;;###mh-autoload
|
|
1420 (defun mh-grep-execute-search (folder-path search-regexp)
|
|
1421 "Execute grep.
|
|
1422
|
|
1423 Unlike the other search methods, this method does not use the
|
|
1424 MH-Search buffer. Instead, you simply enter a regular expression
|
|
1425 in the minibuffer. For help in constructing regular expressions,
|
|
1426 see your man page for \"grep\".
|
|
1427
|
|
1428 In a program, FOLDER-PATH is the directory in which SEARCH-REGEXP
|
|
1429 is used to search."
|
|
1430 (set-buffer (get-buffer-create mh-temp-index-buffer))
|
|
1431 (erase-buffer)
|
|
1432 (call-process mh-grep-binary nil '(t nil) nil
|
|
1433 "-i" "-r" search-regexp folder-path)
|
|
1434 (goto-char (point-min)))
|
|
1435
|
|
1436 (defun mh-grep-next-result ()
|
|
1437 "Read the next result.
|
|
1438 Parse it and return the message folder, message index and the
|
|
1439 match. If no other matches left then return nil. If the current
|
|
1440 record is invalid return 'error."
|
|
1441 (prog1
|
|
1442 (block nil
|
|
1443 (when (eobp)
|
|
1444 (return nil))
|
|
1445 (let ((eol-pos (line-end-position))
|
|
1446 (bol-pos (line-beginning-position))
|
|
1447 folder-start msg-end)
|
|
1448 (goto-char bol-pos)
|
|
1449 (unless (search-forward mh-user-path eol-pos t)
|
|
1450 (return 'error))
|
|
1451 (setq folder-start (point))
|
|
1452 (unless (search-forward ":" eol-pos t)
|
|
1453 (return 'error))
|
|
1454 (let ((match (buffer-substring-no-properties (point) eol-pos)))
|
|
1455 (forward-char -1)
|
|
1456 (setq msg-end (point))
|
|
1457 (unless (search-backward "/" folder-start t)
|
|
1458 (return 'error))
|
|
1459 (list (format "+%s" (buffer-substring-no-properties
|
|
1460 folder-start (point)))
|
|
1461 (let ((val (ignore-errors (read-from-string
|
|
1462 (buffer-substring-no-properties
|
|
1463 (1+ (point)) msg-end)))))
|
|
1464 (if (and (consp val) (integerp (car val)))
|
|
1465 (car val)
|
|
1466 (return 'error)))
|
|
1467 match))))
|
|
1468 (forward-line)))
|
|
1469
|
|
1470
|
|
1471
|
|
1472 ;;; Folder support
|
|
1473
|
|
1474 (defun mh-index-generate-pretty-name (string)
|
|
1475 "Given STRING generate a name which is suitable for use as a folder name.
|
|
1476 White space from the beginning and end are removed. All spaces in
|
|
1477 the name are replaced with underscores and all / are replaced
|
|
1478 with $. If STRING is longer than 20 it is truncated too. STRING
|
|
1479 could be a list of strings in which case they are concatenated to
|
|
1480 construct the base name."
|
|
1481 (with-temp-buffer
|
|
1482 (if (stringp string)
|
|
1483 (insert string)
|
|
1484 (when (car string) (insert (car string)))
|
|
1485 (dolist (s (cdr string))
|
|
1486 (insert "_" s)))
|
|
1487 (setq string (mh-replace-string "-lbrace" " "))
|
|
1488 (setq string (mh-replace-string "-rbrace" " "))
|
|
1489 (setq string (mh-replace-string "-search" " "))
|
|
1490 (subst-char-in-region (point-min) (point-max) ?( ? t)
|
|
1491 (subst-char-in-region (point-min) (point-max) ?) ? t)
|
|
1492 (subst-char-in-region (point-min) (point-max) ?- ? t)
|
|
1493 (goto-char (point-min))
|
|
1494 (while (and (not (eobp)) (memq (char-after) '(? ?\t ?\n ?\r ?_)))
|
|
1495 (delete-char 1))
|
|
1496 (goto-char (point-max))
|
|
1497 (while (and (not (bobp)) (memq (char-before) '(? ?\t ?\n ?\r ?_)))
|
|
1498 (delete-backward-char 1))
|
|
1499 (subst-char-in-region (point-min) (point-max) ? ?_ t)
|
|
1500 (subst-char-in-region (point-min) (point-max) ?\t ?_ t)
|
|
1501 (subst-char-in-region (point-min) (point-max) ?\n ?_ t)
|
|
1502 (subst-char-in-region (point-min) (point-max) ?\r ?_ t)
|
|
1503 (subst-char-in-region (point-min) (point-max) ?/ ?$ t)
|
|
1504 (let ((out (truncate-string-to-width (buffer-string) 20)))
|
|
1505 (cond ((eq mh-searcher 'flists)
|
|
1506 (format "%s/%s" mh-flists-results-folder mh-flists-sequence))
|
|
1507 ((equal out mh-flists-results-folder) (concat out "1"))
|
|
1508 (t out)))))
|
|
1509
|
|
1510 (defun mh-folder-exists-p (folder)
|
|
1511 "Check if FOLDER exists."
|
|
1512 (and (mh-folder-name-p folder)
|
|
1513 (save-excursion
|
|
1514 (with-temp-buffer
|
|
1515 (mh-exec-cmd-output "folder" nil "-fast" "-nocreate" folder)
|
|
1516 (goto-char (point-min))
|
|
1517 (not (eobp))))))
|
|
1518
|
|
1519 (defun mh-msg-exists-p (msg folder)
|
|
1520 "Check if MSG exists in FOLDER."
|
|
1521 (file-exists-p (format "%s%s/%s" mh-user-path (substring folder 1) msg)))
|
|
1522
|
|
1523 (defun mh-index-new-folder (name search-regexp)
|
|
1524 "Return a folder name based on NAME for search results of SEARCH-REGEXP.
|
|
1525
|
|
1526 If folder NAME already exists and was generated for the same
|
|
1527 SEARCH-REGEXP then it is reused.
|
|
1528
|
|
1529 Otherwise if the folder NAME was generated from a different
|
|
1530 search then check if NAME<2> can be used. Otherwise try NAME<3>.
|
|
1531 This is repeated till we find a new folder name.
|
|
1532
|
|
1533 If the folder returned doesn't exist then it is created."
|
|
1534 (unless (mh-folder-name-p name)
|
|
1535 (error "The argument should be a valid MH folder name"))
|
|
1536 (let ((chosen-name
|
|
1537 (loop for i from 1
|
|
1538 for candidate = (if (equal i 1) name (format "%s<%s>" name i))
|
|
1539 when (or (not (mh-folder-exists-p candidate))
|
|
1540 (equal (mh-index-folder-search-regexp candidate)
|
|
1541 search-regexp))
|
|
1542 return candidate)))
|
|
1543 ;; Do pending refiles/deletes...
|
|
1544 (when (get-buffer chosen-name)
|
|
1545 (mh-process-or-undo-commands chosen-name))
|
|
1546 ;; Recreate folder...
|
|
1547 (save-excursion (mh-exec-cmd-quiet nil "rmf" chosen-name))
|
|
1548 (mh-exec-cmd-quiet nil "folder" "-create" "-fast" chosen-name)
|
|
1549 (mh-remove-from-sub-folders-cache chosen-name)
|
|
1550 (when (boundp 'mh-speed-folder-map)
|
|
1551 (mh-speed-add-folder chosen-name))
|
|
1552 chosen-name))
|
|
1553
|
|
1554 (defun mh-index-folder-search-regexp (folder)
|
|
1555 "If FOLDER was created by a index search, return the search regexp.
|
|
1556 Return nil if FOLDER doesn't exist or the .mhe_index file is
|
|
1557 garbled."
|
|
1558 (ignore-errors
|
|
1559 (with-temp-buffer
|
|
1560 (insert-file-contents
|
|
1561 (format "%s%s/%s" mh-user-path (substring folder 1) mh-index-data-file))
|
|
1562 (goto-char (point-min))
|
|
1563 (forward-list 3)
|
|
1564 (cadr (read (current-buffer))))))
|
|
1565
|
|
1566
|
|
1567
|
|
1568 ;;; Sequence support
|
|
1569
|
|
1570 ;;;###mh-autoload
|
|
1571 (defun mh-index-create-sequences ()
|
|
1572 "Mirror sequences present in source folders in index folder."
|
|
1573 (let ((seq-hash (make-hash-table :test #'equal))
|
|
1574 (seq-list ()))
|
|
1575 (loop for folder being the hash-keys of mh-index-data
|
|
1576 do (setf (gethash folder seq-hash)
|
|
1577 (mh-create-sequence-map
|
|
1578 (mh-read-folder-sequences folder nil))))
|
|
1579 (dolist (msg (mh-translate-range mh-current-folder "all"))
|
|
1580 (let* ((checksum (gethash msg mh-index-msg-checksum-map))
|
|
1581 (pair (gethash checksum mh-index-checksum-origin-map))
|
|
1582 (ofolder (car pair))
|
|
1583 (omsg (cdr pair)))
|
|
1584 (loop for seq in (ignore-errors
|
|
1585 (gethash omsg (gethash ofolder seq-hash)))
|
|
1586 do (if (assoc seq seq-list)
|
|
1587 (push msg (cdr (assoc seq seq-list)))
|
|
1588 (push (list seq msg) seq-list)))))
|
|
1589 (loop for seq in seq-list
|
|
1590 do (apply #'mh-exec-cmd "mark" mh-current-folder
|
|
1591 "-sequence" (symbol-name (car seq)) "-add"
|
|
1592 (mapcar #'(lambda (x) (format "%s" x)) (cdr seq))))))
|
|
1593
|
|
1594 ;;;###mh-autoload
|
|
1595 (defun mh-create-sequence-map (seq-list)
|
|
1596 "Return a map from msg number to list of sequences in which it is present.
|
|
1597 SEQ-LIST is an assoc list whose keys are sequence names and whose
|
|
1598 cdr is the list of messages in that sequence."
|
|
1599 (loop with map = (make-hash-table)
|
|
1600 for seq in seq-list
|
|
1601 when (and (not (memq (car seq) (mh-unpropagated-sequences)))
|
|
1602 (mh-valid-seq-p (car seq)))
|
|
1603 do (loop for msg in (cdr seq)
|
|
1604 do (push (car seq) (gethash msg map)))
|
|
1605 finally return map))
|
|
1606
|
|
1607 ;;;###mh-autoload
|
|
1608 (defun mh-index-add-to-sequence (seq msgs)
|
|
1609 "Add to SEQ the messages in the list MSGS.
|
|
1610 This function updates the source folder sequences. Also makes an
|
|
1611 attempt to update the source folder buffer if we have it open."
|
|
1612 ;; Don't need to do anything for cur
|
|
1613 (save-excursion
|
|
1614 (when (and (not (memq seq (mh-unpropagated-sequences)))
|
|
1615 (mh-valid-seq-p seq))
|
|
1616 (let ((folders ())
|
|
1617 (mh-speed-flists-inhibit-flag t))
|
|
1618 (maphash (lambda (folder msgs)
|
|
1619 (push folder folders)
|
|
1620 ;; Add messages to sequence in source folder...
|
|
1621 (apply #'mh-exec-cmd-quiet nil "mark" folder
|
|
1622 "-add" "-nozero" "-sequence" (symbol-name seq)
|
|
1623 (mapcar (lambda (x) (format "%s" x))
|
|
1624 (mh-coalesce-msg-list msgs)))
|
|
1625 ;; Update source folder buffer if we have it open...
|
|
1626 (when (get-buffer folder)
|
|
1627 (save-excursion
|
|
1628 (set-buffer folder)
|
|
1629 (mh-put-msg-in-seq msgs seq))))
|
|
1630 (mh-index-matching-source-msgs msgs))
|
|
1631 folders))))
|
|
1632
|
|
1633 ;;;###mh-autoload
|
|
1634 (defun mh-index-delete-from-sequence (seq msgs)
|
|
1635 "Delete from SEQ the messages in MSGS.
|
|
1636 This function updates the source folder sequences. Also makes an
|
|
1637 attempt to update the source folder buffer if present."
|
|
1638 (save-excursion
|
|
1639 (when (and (not (memq seq (mh-unpropagated-sequences)))
|
|
1640 (mh-valid-seq-p seq))
|
|
1641 (let ((folders ())
|
|
1642 (mh-speed-flists-inhibit-flag t))
|
|
1643 (maphash (lambda (folder msgs)
|
|
1644 (push folder folders)
|
|
1645 ;; Remove messages from sequence in source folder...
|
|
1646 (apply #'mh-exec-cmd-quiet nil "mark" folder
|
|
1647 "-del" "-nozero" "-sequence" (symbol-name seq)
|
|
1648 (mapcar (lambda (x) (format "%s" x))
|
|
1649 (mh-coalesce-msg-list msgs)))
|
|
1650 ;; Update source folder buffer if we have it open...
|
|
1651 (when (get-buffer folder)
|
|
1652 (save-excursion
|
|
1653 (set-buffer folder)
|
|
1654 (mh-delete-msg-from-seq msgs seq t))))
|
|
1655 (mh-index-matching-source-msgs msgs))
|
|
1656 folders))))
|
|
1657
|
|
1658 (defvar mh-unpropagated-sequences '(cur range subject search)
|
|
1659 "List of sequences that aren't preserved.")
|
|
1660
|
|
1661 (defun mh-unpropagated-sequences ()
|
|
1662 "Return a list of sequences that aren't propagated to the source folders.
|
|
1663 It is just the sequences in the variable
|
|
1664 `mh-unpropagated-sequences' in addition to the
|
|
1665 Previous-Sequence (see mh-profile 5)."
|
|
1666 (if mh-previous-seq
|
|
1667 (cons mh-previous-seq mh-unpropagated-sequences)
|
|
1668 mh-unpropagated-sequences))
|
|
1669
|
|
1670 (defun mh-index-matching-source-msgs (msgs &optional delete-from-index-data)
|
|
1671 "Return a table of original messages and folders for messages in MSGS.
|
|
1672 If optional argument DELETE-FROM-INDEX-DATA is non-nil, then each
|
|
1673 of the messages, whose counter-part is found in some source
|
|
1674 folder, is removed from `mh-index-data'."
|
|
1675 (let ((table (make-hash-table :test #'equal)))
|
|
1676 (dolist (msg msgs)
|
|
1677 (let* ((checksum (gethash msg mh-index-msg-checksum-map))
|
|
1678 (pair (gethash checksum mh-index-checksum-origin-map)))
|
|
1679 (when (and checksum (car pair) (cdr pair)
|
|
1680 (mh-index-match-checksum (cdr pair) (car pair) checksum))
|
|
1681 (push (cdr pair) (gethash (car pair) table))
|
|
1682 (when delete-from-index-data
|
|
1683 (remhash (cdr pair) (gethash (car pair) mh-index-data))))))
|
|
1684 table))
|
|
1685
|
|
1686 (defun mh-index-match-checksum (msg folder checksum)
|
|
1687 "Check if MSG in FOLDER has X-MHE-Checksum header value of CHECKSUM."
|
|
1688 (with-temp-buffer
|
|
1689 (mh-exec-cmd-output mh-scan-prog nil "-width" "80"
|
|
1690 "-format" "%{x-mhe-checksum}\n" folder msg)
|
|
1691 (goto-char (point-min))
|
|
1692 (string-equal (buffer-substring-no-properties (point) (line-end-position))
|
|
1693 checksum)))
|
|
1694
|
|
1695
|
|
1696
|
|
1697 ;;; Serialization of index data
|
|
1698
|
|
1699 (defun mh-index-write-data ()
|
|
1700 "Write index data to file."
|
|
1701 (ignore-errors
|
|
1702 (unless (eq major-mode 'mh-folder-mode)
|
|
1703 (error "Can't be called from folder in \"%s\"" major-mode))
|
|
1704 (let ((data mh-index-data)
|
|
1705 (msg-checksum-map mh-index-msg-checksum-map)
|
|
1706 (checksum-origin-map mh-index-checksum-origin-map)
|
|
1707 (previous-search mh-index-previous-search)
|
|
1708 (sequence-search-flag mh-index-sequence-search-flag)
|
|
1709 (outfile (concat buffer-file-name mh-index-data-file))
|
|
1710 (print-length nil)
|
|
1711 (print-level nil))
|
|
1712 (with-temp-file outfile
|
|
1713 (mh-index-write-hashtable
|
|
1714 data (lambda (x) (loop for y being the hash-keys of x collect y)))
|
|
1715 (mh-index-write-hashtable msg-checksum-map #'identity)
|
|
1716 (mh-index-write-hashtable checksum-origin-map #'identity)
|
|
1717 (pp previous-search (current-buffer)) (insert "\n")
|
|
1718 (pp sequence-search-flag (current-buffer)) (insert "\n")))))
|
|
1719
|
|
1720 (defun mh-index-write-hashtable (table proc)
|
|
1721 "Write TABLE to `current-buffer'.
|
|
1722 PROC is used to serialize the values corresponding to the hash
|
|
1723 table keys."
|
|
1724 (pp (loop for x being the hash-keys of table
|
|
1725 collect (cons x (funcall proc (gethash x table))))
|
|
1726 (current-buffer))
|
|
1727 (insert "\n"))
|
|
1728
|
|
1729 ;;;###mh-autoload
|
|
1730 (defun mh-index-read-data ()
|
|
1731 "Read index data from file."
|
|
1732 (ignore-errors
|
|
1733 (unless (eq major-mode 'mh-folder-mode)
|
|
1734 (error "Can't be called from folder in \"%s\"" major-mode))
|
|
1735 (let ((infile (concat buffer-file-name mh-index-data-file))
|
|
1736 t1 t2 t3 t4 t5)
|
|
1737 (with-temp-buffer
|
|
1738 (insert-file-contents-literally infile)
|
|
1739 (goto-char (point-min))
|
|
1740 (setq t1 (mh-index-read-hashtable
|
|
1741 (lambda (data)
|
|
1742 (loop with table = (make-hash-table :test #'equal)
|
|
1743 for x in data do (setf (gethash x table) t)
|
|
1744 finally return table)))
|
|
1745 t2 (mh-index-read-hashtable #'identity)
|
|
1746 t3 (mh-index-read-hashtable #'identity)
|
|
1747 t4 (read (current-buffer))
|
|
1748 t5 (read (current-buffer))))
|
|
1749 (setq mh-index-data t1
|
|
1750 mh-index-msg-checksum-map t2
|
|
1751 mh-index-checksum-origin-map t3
|
|
1752 mh-index-previous-search t4
|
|
1753 mh-index-sequence-search-flag t5))))
|
|
1754
|
|
1755 (defun mh-index-read-hashtable (proc)
|
|
1756 "From BUFFER read a hash table serialized as a list.
|
|
1757 PROC is used to convert the value to actual data."
|
|
1758 (loop with table = (make-hash-table :test #'equal)
|
|
1759 for pair in (read (current-buffer))
|
|
1760 do (setf (gethash (car pair) table) (funcall proc (cdr pair)))
|
|
1761 finally return table))
|
|
1762
|
|
1763
|
|
1764
|
|
1765 ;;; Checksum routines
|
|
1766
|
|
1767 ;; A few different checksum programs are supported. The supported programs
|
|
1768 ;; are:
|
|
1769 ;;
|
|
1770 ;; 1. md5sum
|
|
1771 ;; 2. md5
|
|
1772 ;; 3. openssl
|
|
1773 ;;
|
|
1774 ;; To add support for your favorite checksum program add a clause to the cond
|
|
1775 ;; statement in mh-checksum-choose. This should set the variable
|
|
1776 ;; mh-checksum-cmd to the command line needed to run the checsum program and
|
|
1777 ;; should set mh-checksum-parser to a function which returns a cons cell
|
|
1778 ;; containing the message number and checksum string.
|
|
1779
|
|
1780 (defvar mh-checksum-cmd)
|
|
1781 (defvar mh-checksum-parser)
|
|
1782
|
|
1783 (defun mh-checksum-choose ()
|
|
1784 "Check if a program to create a checksum is present."
|
|
1785 (unless (boundp 'mh-checksum-cmd)
|
|
1786 (let ((exec-path (append '("/sbin" "/usr/sbin") exec-path)))
|
|
1787 (cond ((executable-find "md5sum")
|
|
1788 (setq mh-checksum-cmd (list (executable-find "md5sum")))
|
|
1789 (setq mh-checksum-parser #'mh-md5sum-parser))
|
|
1790 ((executable-find "openssl")
|
|
1791 (setq mh-checksum-cmd (list (executable-find "openssl") "md5"))
|
|
1792 (setq mh-checksum-parser #'mh-openssl-parser))
|
|
1793 ((executable-find "md5")
|
|
1794 (setq mh-checksum-cmd (list (executable-find "md5")))
|
|
1795 (setq mh-checksum-parser #'mh-md5-parser))
|
|
1796 (t (error "No suitable checksum program"))))))
|
|
1797
|
|
1798 (defun mh-md5sum-parser ()
|
|
1799 "Parse md5sum output."
|
|
1800 (let ((begin (line-beginning-position))
|
|
1801 (end (line-end-position))
|
|
1802 first-space last-slash)
|
|
1803 (setq first-space (search-forward " " end t))
|
|
1804 (goto-char end)
|
|
1805 (setq last-slash (search-backward "/" begin t))
|
|
1806 (cond ((and first-space last-slash)
|
|
1807 (cons (car (read-from-string (buffer-substring-no-properties
|
|
1808 (1+ last-slash) end)))
|
|
1809 (buffer-substring-no-properties begin (1- first-space))))
|
|
1810 (t (cons nil nil)))))
|
|
1811
|
|
1812 (defun mh-openssl-parser ()
|
|
1813 "Parse openssl output."
|
|
1814 (let ((begin (line-beginning-position))
|
|
1815 (end (line-end-position))
|
|
1816 last-space last-slash)
|
|
1817 (goto-char end)
|
|
1818 (setq last-space (search-backward " " begin t))
|
|
1819 (setq last-slash (search-backward "/" begin t))
|
|
1820 (cond ((and last-slash last-space)
|
|
1821 (cons (car (read-from-string (buffer-substring-no-properties
|
|
1822 (1+ last-slash) (1- last-space))))
|
|
1823 (buffer-substring-no-properties (1+ last-space) end))))))
|
|
1824
|
|
1825 (defalias 'mh-md5-parser 'mh-openssl-parser)
|
|
1826
|
|
1827 ;;;###mh-autoload
|
|
1828 (defun mh-index-update-maps (folder &optional origin-map)
|
|
1829 "Annotate all as yet unannotated messages in FOLDER with their MD5 hash.
|
|
1830 As a side effect msg -> checksum map is updated. Optional
|
|
1831 argument ORIGIN-MAP is a hashtable which maps each message in the
|
|
1832 index folder to the original folder and message from whence it
|
|
1833 was copied. If present the checksum -> (origin-folder,
|
|
1834 origin-index) map is updated too."
|
|
1835 (clrhash mh-index-msg-checksum-map)
|
|
1836 (save-excursion
|
|
1837 ;; Clear temp buffer
|
|
1838 (set-buffer (get-buffer-create mh-temp-checksum-buffer))
|
|
1839 (erase-buffer)
|
|
1840 ;; Run scan to check if any messages needs MD5 annotations at all
|
|
1841 (with-temp-buffer
|
|
1842 (mh-exec-cmd-output mh-scan-prog nil "-width" "80"
|
|
1843 "-format" "%(msg)\n%{x-mhe-checksum}\n"
|
|
1844 folder "all")
|
|
1845 (goto-char (point-min))
|
|
1846 (let (msg checksum)
|
|
1847 (while (not (eobp))
|
|
1848 (setq msg (buffer-substring-no-properties
|
|
1849 (point) (line-end-position)))
|
|
1850 (forward-line)
|
|
1851 (save-excursion
|
|
1852 (cond ((not (string-match "^[0-9]*$" msg)))
|
|
1853 ((eolp)
|
|
1854 ;; need to compute checksum
|
|
1855 (set-buffer mh-temp-checksum-buffer)
|
|
1856 (insert mh-user-path (substring folder 1) "/" msg "\n"))
|
|
1857 (t
|
|
1858 ;; update maps
|
|
1859 (setq checksum (buffer-substring-no-properties
|
|
1860 (point) (line-end-position)))
|
|
1861 (let ((msg (car (read-from-string msg))))
|
|
1862 (set-buffer folder)
|
|
1863 (mh-index-update-single-msg msg checksum origin-map)))))
|
|
1864 (forward-line))))
|
|
1865 ;; Run checksum program if needed
|
|
1866 (unless (and (eobp) (bobp))
|
|
1867 (apply #'mh-xargs mh-checksum-cmd)
|
|
1868 (goto-char (point-min))
|
|
1869 (while (not (eobp))
|
|
1870 (let* ((intermediate (funcall mh-checksum-parser))
|
|
1871 (msg (car intermediate))
|
|
1872 (checksum (cdr intermediate)))
|
|
1873 (when msg
|
|
1874 ;; annotate
|
|
1875 (mh-exec-cmd "anno" folder msg "-component" "X-MHE-Checksum"
|
|
1876 "-nodate" "-text" checksum "-inplace")
|
|
1877 ;; update maps
|
|
1878 (save-excursion
|
|
1879 (set-buffer folder)
|
|
1880 (mh-index-update-single-msg msg checksum origin-map)))
|
|
1881 (forward-line)))))
|
|
1882 (mh-index-write-data))
|
|
1883
|
|
1884 (defun mh-index-update-single-msg (msg checksum origin-map)
|
|
1885 "Update various maps for one message.
|
|
1886 MSG is a index folder message, CHECKSUM its MD5 hash and
|
|
1887 ORIGIN-MAP, if non-nil, a hashtable containing which maps each
|
|
1888 message in the index folder to the folder and message that it was
|
|
1889 copied from. The function updates the hash tables
|
|
1890 `mh-index-msg-checksum-map' and `mh-index-checksum-origin-map'.
|
|
1891
|
|
1892 This function should only be called in the appropriate index
|
|
1893 folder buffer."
|
|
1894 (cond ((and origin-map (gethash checksum mh-index-checksum-origin-map))
|
|
1895 (let* ((intermediate (gethash msg origin-map))
|
|
1896 (ofolder (car intermediate))
|
|
1897 (omsg (cdr intermediate)))
|
|
1898 ;; This is most probably a duplicate. So eliminate it.
|
|
1899 (call-process "rm" nil nil nil
|
|
1900 (format "%s%s/%s" mh-user-path
|
|
1901 (substring mh-current-folder 1) msg))
|
|
1902 (when (gethash ofolder mh-index-data)
|
|
1903 (remhash omsg (gethash ofolder mh-index-data)))))
|
|
1904 (t
|
|
1905 (setf (gethash msg mh-index-msg-checksum-map) checksum)
|
|
1906 (when origin-map
|
|
1907 (setf (gethash checksum mh-index-checksum-origin-map)
|
|
1908 (gethash msg origin-map))))))
|
|
1909
|
|
1910 (provide 'mh-search)
|
|
1911
|
|
1912 ;; Local Variables:
|
|
1913 ;; indent-tabs-mode: nil
|
|
1914 ;; sentence-end-double-space: nil
|
|
1915 ;; End:
|
|
1916
|
|
1917 ;; arch-tag: 607762ad-0dff-4fe1-a27e-6c0dde0dcc47
|
|
1918 ;;; mh-search ends here
|