diff lisp/mh-e/mh-index.el @ 49578:b35587af8747

Upgraded to MH-E version 7.2. See etc/MH-E-NEWS and lisp/mh-e/ChangeLog for details.
author Bill Wohler <wohler@newt.com>
date Mon, 03 Feb 2003 20:55:30 +0000
parents 06b77df47802
children 0d8b17d428b5
line wrap: on
line diff
--- a/lisp/mh-e/mh-index.el	Mon Feb 03 16:39:05 2003 +0000
+++ b/lisp/mh-e/mh-index.el	Mon Feb 03 20:55:30 2003 +0000
@@ -40,13 +40,14 @@
 
 ;;; Change Log:
 
-;; $Id: mh-index.el,v 1.2 2003/01/08 23:21:16 wohler Exp $
+;; $Id: mh-index.el,v 1.83 2003/01/27 04:16:47 wohler Exp $
 
 ;;; Code:
 
 (require 'cl)
 (require 'mh-e)
 (require 'mh-mime)
+(require 'mh-pick)
 
 (autoload 'gnus-local-map-property "gnus-util")
 (autoload 'gnus-eval-format "gnus-spec")
@@ -56,15 +57,22 @@
 ;; Support different indexing programs
 (defvar mh-indexer-choices
   '((swish++
-     mh-swish++-binary mh-swish++-execute-search mh-swish++-next-result)
+     mh-swish++-binary mh-swish++-execute-search mh-swish++-next-result
+     mh-swish++-regexp-builder)
     (swish
-     mh-swish-binary mh-swish-execute-search mh-swish-next-result)
+     mh-swish-binary mh-swish-execute-search mh-swish-next-result nil)
+    (mairix
+     mh-mairix-binary mh-mairix-execute-search mh-mairix-next-result
+     mh-mairix-regexp-builder)
     (namazu
-     mh-namazu-binary mh-namazu-execute-search mh-namazu-next-result)
+     mh-namazu-binary mh-namazu-execute-search mh-namazu-next-result nil)
     (glimpse
-     mh-glimpse-binary mh-glimpse-execute-search mh-glimpse-next-result)
+     mh-glimpse-binary mh-glimpse-execute-search mh-glimpse-next-result nil)
+    (pick
+     mh-pick-binary mh-pick-execute-search mh-pick-next-result
+     mh-pick-regexp-builder)
     (grep
-     mh-grep-binary mh-grep-execute-search mh-grep-next-result))
+     mh-grep-binary mh-grep-execute-search mh-grep-next-result nil))
   "List of possible indexer choices.")
 (defvar mh-indexer nil
   "Chosen index program.")
@@ -72,6 +80,8 @@
   "Function which executes the search program.")
 (defvar mh-index-next-result-function nil
   "Function to parse the next line of output.")
+(defvar mh-index-regexp-builder nil
+  "Function used to construct search regexp.")
 
 ;; FIXME: This should be a defcustom...
 (defvar mh-index-folder "+mhe-index"
@@ -254,16 +264,26 @@
   "Given STRING generate a name which is suitable for use as a folder name.
 White space from the beginning and end are removed. All spaces in the name are
 replaced with underscores and all / are replaced with $. If STRING is longer
-than 20 it is truncated too."
+than 20 it is truncated too. STRING could be a list of strings in which case
+they are concatenated to construct the base name."
   (with-temp-buffer
-    (insert string)
+    (if (stringp string)
+        (insert string)
+      (when (car string) (insert (car string)))
+      (dolist (s (cdr string))
+        (insert "_" s)))
+    (setq string (mh-replace-string "-lbrace" " "))
+    (setq string (mh-replace-string "-rbrace" " "))
+    (subst-char-in-region (point-min) (point-max) ?( ?  t)
+    (subst-char-in-region (point-min) (point-max) ?) ?  t)
+    (subst-char-in-region (point-min) (point-max) ?- ?  t)
     (goto-char (point-min))
-    (while (and (not (eobp)) (memq (char-after) '(?  ?\t ?\n ?\r)))
+    (while (and (not (eobp)) (memq (char-after) '(?  ?\t ?\n ?\r ?_)))
       (delete-char 1))
     (goto-char (point-max))
-    (while (and (not (bobp)) (memq (char-before) '(?  ?\t ?\n ?\r)))
+    (while (and (not (bobp)) (memq (char-before) '(?  ?\t ?\n ?\r ?_)))
       (delete-backward-char 1))
-    (subst-char-in-region (point-min) (point-max) ? ?_ t)
+    (subst-char-in-region (point-min) (point-max) ?  ?_ t)
     (subst-char-in-region (point-min) (point-max) ?\t ?_ t)
     (subst-char-in-region (point-min) (point-max) ?\n ?_ t)
     (subst-char-in-region (point-min) (point-max) ?\r ?_ t)
@@ -271,13 +291,16 @@
     (truncate-string-to-width (buffer-substring (point-min) (point-max)) 20)))
 
 ;;;###mh-autoload
-(defun mh-index-search (redo-search-flag folder search-regexp)
+(defun* mh-index-search (redo-search-flag folder search-regexp
+                        &optional window-config)
   "Perform an indexed search in an MH mail folder.
 
 If REDO-SEARCH-FLAG is non-nil and the current folder buffer was generated by a
 index search, then the search is repeated. Otherwise, FOLDER is searched with
 SEARCH-REGEXP and the results are presented in an MH-E folder. If FOLDER is
-\"+\" then mail in all folders are searched.
+\"+\" then mail in all folders are searched. Optional argument WINDOW-CONFIG
+stores the window configuration that will be restored after the user quits the
+folder containing the index search results.
 
 Four indexing programs are supported; if none of these are present, then grep
 is used. This function picks the first program that is available on your
@@ -289,9 +312,16 @@
 
     - `mh-swish++-execute-search'
     - `mh-swish-execute-search'
+    - `mh-mairix-execute-search'
     - `mh-namazu-execute-search'
     - `mh-glimpse-execute-search'
 
+If none of these programs are present then we use pick. If desired grep can be
+used instead. Details about these methods can be found in:
+
+    - `mh-pick-execute-search'
+    - `mh-grep-execute-search'
+
 This and related functions use an X-MHE-Checksum header to cache the MD5
 checksum of a message. This means that already present X-MHE-Checksum headers
 in the incoming email could result in messages not being found. The following
@@ -306,18 +336,28 @@
          (progn
            (unless mh-find-path-run (mh-find-path))
            (or (and current-prefix-arg (car mh-index-previous-search))
-               (mh-prompt-for-folder "Search" "+" nil "all")))
+               (mh-prompt-for-folder "Search" "+" nil "all" t)))
          (progn
            ;; Yes, we do want to call mh-index-choose every time in case the
            ;; user has switched the indexer manually.
            (unless (mh-index-choose) (error "No indexing program found"))
            (or (and current-prefix-arg (cadr mh-index-previous-search))
+               mh-index-regexp-builder
                (read-string (format "%s regexp: "
                                     (upcase-initials
-                                     (symbol-name mh-indexer))))))))
+                                     (symbol-name mh-indexer))))))
+         (if (and (not
+                   (and current-prefix-arg (cadr mh-index-previous-search)))
+                  mh-index-regexp-builder)
+             (current-window-configuration)
+           nil)))
+  (when (symbolp search-regexp)
+    (mh-search-folder folder window-config)
+    (setq mh-searching-function 'mh-index-do-search)
+    (return-from mh-index-search))
   (mh-checksum-choose)
   (let ((result-count 0)
-        (old-window-config mh-previous-window-config)
+        (old-window-config (or window-config mh-previous-window-config))
         (previous-search mh-index-previous-search)
         (index-folder (format "%s/%s" mh-index-folder
                               (mh-index-generate-pretty-name search-regexp))))
@@ -373,7 +413,7 @@
       (mh-recenter nil)
 
       ;; Maintain history
-      (when (and redo-search-flag previous-search)
+      (when (or (and redo-search-flag previous-search) window-config)
         (setq mh-previous-window-config old-window-config))
       (setq mh-index-previous-search (list folder search-regexp))
 
@@ -385,6 +425,123 @@
                      count (> (hash-table-count msg-hash) 0))))))
 
 ;;;###mh-autoload
+(defun mh-index-do-search ()
+  "Construct appropriate regexp and call `mh-index-search'."
+  (interactive)
+  (unless (mh-index-choose) (error "No indexing program found"))
+  (let* ((regexp-list (mh-pick-parse-search-buffer))
+         (pattern (funcall mh-index-regexp-builder regexp-list)))
+    (if pattern
+        (mh-index-search nil mh-current-folder pattern
+                         mh-previous-window-config)
+      (error "No search terms"))))
+
+(defun mh-replace-string (old new)
+  "Replace all occurrences of OLD with NEW in the current buffer."
+  (goto-char (point-min))
+  (while (search-forward old nil t)
+    (replace-match new)))
+
+;;;###mh-autoload
+(defun mh-index-parse-search-regexp (input-string)
+  "Construct parse tree for INPUT-STRING.
+All occurrences of &, |, ! and ~ in INPUT-STRING are replaced by AND, OR and
+NOT as appropriate. Then the resulting string is parsed."
+  (let (input)
+    (with-temp-buffer
+      (insert input-string)
+      (downcase-region (point-min) (point-max))
+      ;; replace tabs
+      (mh-replace-string "\t" " ")
+      ;; synonyms of AND
+      (mh-replace-string "&" " and ")
+      (mh-replace-string " -and " " and ")
+      ;; synonyms of OR
+      (mh-replace-string "|" " or ")
+      (mh-replace-string " -or " " or ")
+      ;; synonyms of NOT
+      (mh-replace-string "!" " not ")
+      (mh-replace-string "~" " not ")
+      (mh-replace-string " -not " " not ")
+      ;; synonyms of left brace
+      (mh-replace-string "(" " ( ")
+      (mh-replace-string " -lbrace " " ( ")
+      ;; synonyms of right brace
+      (mh-replace-string ")" " ) ")
+      (mh-replace-string " -rbrace " " ) ")
+      ;; get the normalized input
+      (setq input (format "( %s )" (buffer-substring (point-min) (point-max)))))
+
+    (let ((tokens (mh-index-add-implicit-ops (split-string input)))
+          (op-stack ())
+          (operand-stack ())
+          oper1)
+      (dolist (token tokens)
+        (cond ((equal token "(") (push 'paren op-stack))
+              ((equal token "not") (push 'not op-stack))
+              ((equal token "or") (push 'or op-stack))
+              ((equal token "and") (push 'and op-stack))
+              ((equal token ")")
+               (multiple-value-setq (op-stack operand-stack)
+                 (mh-index-evaluate op-stack operand-stack))
+               (when (eq (car op-stack) 'not)
+                 (pop op-stack)
+                 (push `(not ,(pop operand-stack)) operand-stack))
+               (when (eq (car op-stack) 'and)
+                 (pop op-stack)
+                 (setq oper1 (pop operand-stack))
+                 (push `(and ,(pop operand-stack) ,oper1) operand-stack)))
+              ((eq (car op-stack) 'not)
+               (pop op-stack)
+               (push `(not ,token) operand-stack)
+               (when (eq (car op-stack) 'and)
+                 (pop op-stack)
+                 (setq oper1 (pop operand-stack))
+                 (push `(and ,(pop operand-stack) ,oper1) operand-stack)))
+              ((eq (car op-stack) 'and)
+               (pop op-stack)
+               (push `(and ,(pop operand-stack) ,token) operand-stack))
+              (t (push token operand-stack))))
+      (prog1 (pop operand-stack)
+        (when (or op-stack operand-stack)
+          (error "Invalid regexp: %s" input))))))
+
+(defun mh-index-add-implicit-ops (tokens)
+  "Add implicit operators in the list TOKENS."
+  (let ((result ())
+        (literal-seen nil)
+        current)
+    (while tokens
+      (setq current (pop tokens))
+      (cond ((or (equal current ")") (equal current "and") (equal current "or"))
+             (setq literal-seen nil)
+             (push current result))
+            ((and literal-seen
+                  (push "and" result)
+                  (setq literal-seen nil)
+                  nil))
+            (t
+             (push current result)
+             (unless (or (equal current "(") (equal current "not"))
+               (setq literal-seen t)))))
+    (nreverse result)))
+
+(defun mh-index-evaluate (op-stack operand-stack)
+  "Read expression till starting paren based on OP-STACK and OPERAND-STACK."
+  (block mh-index-evaluate
+    (let (op oper1)
+      (while op-stack
+        (setq op (pop op-stack))
+        (cond ((eq op 'paren)
+               (return-from mh-index-evaluate (values op-stack operand-stack)))
+              ((eq op 'not)
+               (push `(not ,(pop operand-stack)) operand-stack))
+              ((or (eq op 'and) (eq op 'or))
+               (setq oper1 (pop operand-stack))
+               (push `(,op ,(pop operand-stack) ,oper1) operand-stack))))
+      (error "Ran out of tokens"))))
+
+;;;###mh-autoload
 (defun mh-index-next-folder (&optional backward-flag)
   "Jump to the next folder marker.
 The function is only applicable to folders displaying index search results.
@@ -446,9 +603,9 @@
                    (setq chosen-name new-name)
                    (return-from unique-name)))))
     (mh-exec-cmd-quiet nil "folder" "-create" "-fast" chosen-name)
+    (mh-remove-from-sub-folders-cache chosen-name)
     (when (boundp 'mh-speed-folder-map)
       (mh-speed-add-folder chosen-name))
-    (push (list chosen-name) mh-folder-list)
     chosen-name))
 
 ;;;###mh-autoload
@@ -476,6 +633,9 @@
   (let ((cur-msg (mh-get-msg-num nil))
         (old-buffer-modified-flag (buffer-modified-p))
         (buffer-read-only nil))
+    (while (and (not cur-msg) (not (eobp)))
+      (forward-line)
+      (setq cur-msg (mh-get-msg-num nil)))
     (goto-char (point-min))
     (while (not (eobp))
       (if (or (char-equal (char-after) ?+) (char-equal (char-after) 10))
@@ -620,6 +780,43 @@
 
 
 
+;; Pick interface
+
+(defvar mh-index-pick-folder)
+(defvar mh-pick-binary "pick")
+
+(defun mh-pick-execute-search (folder-path search-regexp)
+  "Execute pick.
+
+Unlike the other index search programs \"pick\" only searches messages present
+in the folder itself and does not descend into any sub-folders that may be
+present.
+
+FOLDER-PATH is the directory containing the mails to be searched and
+SEARCH-REGEXP is the pattern that pick gets."
+  (set-buffer (get-buffer-create mh-index-temp-buffer))
+  (erase-buffer)
+  (setq mh-index-pick-folder
+        (concat "+" (substring folder-path (length mh-user-path))))
+  (apply #'call-process (expand-file-name "pick" mh-progs) nil '(t nil) nil
+         mh-index-pick-folder "-list" search-regexp)
+  (goto-char (point-min)))
+
+(defun mh-pick-next-result ()
+  "Return the next pick search result."
+  (prog1 (block nil
+           (when (eobp) (return nil))
+           (unless (re-search-forward "^[1-9][0-9]*$" (line-end-position) t)
+             (return 'error))
+           (list mh-index-pick-folder
+                 (car (read-from-string (buffer-substring-no-properties
+                                         (line-beginning-position)
+                                         (line-end-position))))
+                 nil))
+    (forward-line)))
+
+
+
 ;; Grep interface
 
 (defvar mh-grep-binary (executable-find "grep"))
@@ -669,6 +866,140 @@
 
 
 
+;; Mairix interface
+
+(defvar mh-mairix-binary (executable-find "mairix"))
+(defvar mh-mairix-directory ".mairix")
+(defvar mh-mairix-folder nil)
+
+(defun mh-mairix-execute-search (folder-path search-regexp-list)
+  "Execute mairix and read the results.
+
+In the examples below replace /home/user/Mail with the path to your MH
+directory.
+
+First create the directory /home/user/Mail/.mairix. Then create the file
+/home/user/Mail/.mairix/config with the following contents:
+
+   # This should contain the same thing as your `mh-user-path'
+   base=/home/user/Mail
+   
+   # List of folders that should be indexed. 3 dots at the end means there are
+   # subfolders within the folder
+   mh_folders=archive...:inbox:drafts:news:sent:trash
+   
+   vfolder_format=raw
+   database=/home/user/Mail/mairix/database
+
+Use the following command line to generate the mairix index. Run this daily
+from cron:
+
+    mairix -f /home/user/Mail/.mairix/config
+
+FOLDER-PATH is the directory in which SEARCH-REGEXP-LIST is used to search."
+  (set-buffer (get-buffer-create mh-index-temp-buffer))
+  (erase-buffer)
+  (unless mh-mairix-binary
+    (error "Set mh-mairix-binary appropriately"))
+  (apply #'call-process mh-mairix-binary nil '(t nil) nil
+         "-f" (format "%s%s/config" mh-user-path mh-mairix-directory)
+         search-regexp-list)
+  (goto-char (point-min))
+  (setq mh-mairix-folder
+        (let ((last-char (substring folder-path (1- (length folder-path)))))
+          (if (equal last-char "/")
+              folder-path
+            (format "%s/" folder-path)))))
+
+(defun mh-mairix-next-result ()
+  "Return next result from mairix output."
+  (prog1
+      (block nil
+        (when (or (eobp) (and (bolp) (eolp)))
+          (return nil))
+        (unless (eq (char-after) ?/)
+          (return error))
+        (let ((start (point))
+              end msg-start)
+          (setq end (line-end-position))
+          (unless (search-forward mh-mairix-folder end t)
+            (return 'error))
+          (goto-char (match-beginning 0))
+          (unless (equal (point) start)
+            (return 'error))
+          (goto-char end)
+          (unless (search-backward "/" start t)
+            (return 'error))
+          (setq msg-start (1+ (point)))
+          (goto-char start)
+          (unless (search-forward mh-user-path end t)
+            (return 'error))
+          (list (format "+%s" (buffer-substring-no-properties
+                               (point) (1- msg-start)))
+                (car (read-from-string
+                      (buffer-substring-no-properties msg-start end)))
+                ())))
+    (forward-line)))
+
+(defun mh-mairix-regexp-builder (regexp-list)
+  "Generate query for mairix.
+REGEXP-LIST is an alist of fields and values."
+  (let ((result ()))
+    (dolist (pair regexp-list)
+      (when (cdr pair)
+        (push
+         (concat
+          (cond ((eq (car pair) 'to) "t:")
+                ((eq (car pair) 'from) "f:")
+                ((eq (car pair) 'cc) "c:")
+                ((eq (car pair) 'subject) "s:")
+                ((eq (car pair) 'date) "d:")
+                (t ""))
+          (let ((sop (cdr (mh-mairix-convert-to-sop* (cdr pair))))
+                (final ""))
+            (dolist (conjunct sop)
+              (let ((expr-list (cdr conjunct))
+                    (expr-string ""))
+                (dolist (e expr-list)
+                  (setq expr-string (concat expr-string "+"
+                                            (if (atom e) "" "~")
+                                            (if (atom e) e (cadr e)))))
+                (setq final (concat final "," (substring expr-string 1)))))
+            (substring final 1)))
+         result)))
+    result))
+
+(defun mh-mairix-convert-to-sop* (expr)
+  "Convert EXPR to sum of product form."
+  (cond ((atom expr) `(or (and ,expr)))
+        ((eq (car expr) 'or)
+         (cons 'or
+               (loop for e in (mapcar #'mh-mairix-convert-to-sop* (cdr expr))
+                     append (cdr e))))
+        ((eq (car expr) 'and)
+         (let ((conjuncts (mapcar #'mh-mairix-convert-to-sop* (cdr expr)))
+               result next-factor)
+           (setq result (pop conjuncts))
+           (while conjuncts
+             (setq next-factor (pop conjuncts))
+             (setq result (let ((res ()))
+                            (dolist (t1 (cdr result))
+                              (dolist (t2 (cdr next-factor))
+                                (push `(and ,@(cdr t1) ,@(cdr t2)) res)))
+                            (cons 'or res))))
+           result))
+        ((atom (cadr expr)) `(or (and ,expr)))
+        ((eq (caadr expr) 'not) (mh-mairix-convert-to-sop* (cadadr expr)))
+        ((eq (caadr expr) 'and) (mh-mairix-convert-to-sop*
+                                 `(or ,@(mapcar #'(lambda (x) `(not ,x))
+                                                (cdadr expr)))))
+        ((eq (caadr expr) 'or) (mh-mairix-convert-to-sop*
+                                `(and ,@(mapcar #'(lambda (x) `(not ,x))
+                                                (cdadr expr)))))
+        (t (error "Unreachable: %s" expr))))
+
+
+
 ;; Swish interface
 
 (defvar mh-swish-binary (executable-find "swish-e"))
@@ -704,7 +1035,7 @@
     FileRules pathname contains /home/user/Mail/.swish
     FileRules pathname contains /home/user/Mail/mhe-index
     FileRules filename is index
-    FileRules filename is \..*
+    FileRules filename is \\..*
     FileRules filename is #.*
     FileRules filename is ,.*
     FileRules filename is .*~
@@ -829,6 +1160,29 @@
 
 (defalias 'mh-swish++-next-result 'mh-swish-next-result)
 
+(defun mh-swish++-regexp-builder (regexp-list)
+  "Generate query for swish++.
+REGEXP-LIST is an alist of fields and values."
+  (let ((regexp "") meta)
+    (dolist (elem regexp-list)
+      (when (cdr elem)
+        (setq regexp (concat regexp " and "
+                             (if (car elem) "(" "")
+                             (if (car elem) (symbol-name (car elem)) "")
+                             (if (car elem) " = " "")
+                             (mh-swish++-print-regexp (cdr elem))
+                             (if (car elem) ")" "")))))
+    (substring regexp 4)))
+
+(defun mh-swish++-print-regexp (expr)
+  "Return infix expression corresponding to EXPR."
+  (cond ((atom expr) (format "%s" expr))
+        ((eq (car expr) 'not)
+         (format "(not %s)" (mh-swish++-print-regexp (cadr expr))))
+        (t (format "(%s %s %s)" (mh-swish++-print-regexp (cadr expr))
+                   (symbol-name (car expr))
+                   (mh-swish++-print-regexp (caddr expr))))))
+
 
 
 ;; Namazu interface
@@ -931,8 +1285,9 @@
                (executable (symbol-value (cadr current))))
           (when executable
             (setq mh-indexer (car current))
-            (setq mh-index-execute-search-function (caddr current))
-            (setq mh-index-next-result-function (cadddr current))
+            (setq mh-index-execute-search-function (nth 2 current))
+            (setq mh-index-next-result-function (nth 3 current))
+            (setq mh-index-regexp-builder (nth 4 current))
             (return mh-indexer))))
       nil)))