0f07bb14062f4de5689dafe19a915b82c9062c2b
[gnus] / lisp / nnir.el
1 ;;; nnir.el --- search mail with various search engines -*- coding: iso-8859-1 -*-
2
3 ;; Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,
4 ;;   2007, 2008, 2009, 2010 Free Software Foundation, Inc.
5
6 ;; Author: Kai Großjohann <grossjohann@ls6.cs.uni-dortmund.de>
7 ;; Swish-e and Swish++ backends by:
8 ;;   Christoph Conrad <christoph.conrad@gmx.de>.
9 ;; IMAP backend by: Simon Josefsson <jas@pdc.kth.se>.
10 ;; IMAP search by: Torsten Hilbrich <torsten.hilbrich <at> gmx.net>
11 ;; IMAP search improved by Daniel Pittman  <daniel@rimspace.net>.
12 ;; nnmaildir support for Swish++ and Namazu backends by:
13 ;;   Justus Piater <Justus <at> Piater.name>
14 ;; Keywords: news mail searching ir
15
16 ;; This file is part of GNU Emacs.
17
18 ;; GNU Emacs is free software: you can redistribute it and/or modify
19 ;; it under the terms of the GNU General Public License as published by
20 ;; the Free Software Foundation, either version 3 of the License, or
21 ;; (at your option) any later version.
22
23 ;; GNU Emacs is distributed in the hope that it will be useful,
24 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
25 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26 ;; GNU General Public License for more details.
27
28 ;; You should have received a copy of the GNU General Public License
29 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
30
31 ;;; Commentary:
32
33 ;; TODO: Documentation in the Gnus manual
34
35 ;; Where in the existing gnus manual would this fit best?
36
37 ;; What does it do?  Well, it allows you to search your mail using
38 ;; some search engine (imap, namazu, swish-e, gmane and others -- see
39 ;; later) by typing `G G' in the Group buffer.  You will then get a
40 ;; buffer which shows all articles matching the query, sorted by
41 ;; Retrieval Status Value (score).
42
43 ;; When looking at the retrieval result (in the Summary buffer) you
44 ;; can type `A W' (aka M-x gnus-warp-to-article RET) on an article.  You
45 ;; will be warped into the group this article came from. Typing `A T'
46 ;; (aka M-x gnus-summary-refer-thread RET) will warp to the group and
47 ;; also show the thread this article is part of.
48
49 ;; The Lisp setup may involve setting a few variables and setting up the
50 ;; search engine. You can define the variables in the server definition
51 ;; like this :
52 ;;   (setq gnus-secondary-select-methods '(
53 ;;       (nnimap "" (nnimap-address "localhost")
54 ;;                  (nnir-search-engine namazu)
55 ;;       )))
56 ;; The main variable to set is `nnir-search-engine'.  Choose one of
57 ;; the engines listed in `nnir-engines'.  (Actually `nnir-engines' is
58 ;; an alist, type `C-h v nnir-engines RET' for more information; this
59 ;; includes examples for setting `nnir-search-engine', too.)
60
61 ;; If you use one of the local indices (namazu, find-grep, swish) you
62 ;; must also set up a search engine backend.
63
64 ;; 1. Namazu
65 ;;
66 ;; The Namazu backend requires you to have one directory containing all
67 ;; index files, this is controlled by the `nnir-namazu-index-directory'
68 ;; variable.  To function the `nnir-namazu-remove-prefix' variable must
69 ;; also be correct, see the documentation for `nnir-namazu-remove-prefix'
70 ;; above.
71 ;;
72 ;; It is particularly important not to pass any any switches to namazu
73 ;; that will change the output format.  Good switches to use include
74 ;; `--sort', `--ascending', `--early' and `--late'.  Refer to the Namazu
75 ;; documentation for further information on valid switches.
76 ;;
77 ;; To index my mail with the `mknmz' program I use the following
78 ;; configuration file:
79 ;;
80 ;; ,----
81 ;; | package conf;  # Don't remove this line!
82 ;; |
83 ;; | # Paths which will not be indexed. Don't use `^' or `$' anchors.
84 ;; | $EXCLUDE_PATH = "spam|sent";
85 ;; |
86 ;; | # Header fields which should be searchable. case-insensitive
87 ;; | $REMAIN_HEADER = "from|date|message-id|subject";
88 ;; |
89 ;; | # Searchable fields. case-insensitive
90 ;; | $SEARCH_FIELD = "from|date|message-id|subject";
91 ;; |
92 ;; | # The max length of a word.
93 ;; | $WORD_LENG_MAX = 128;
94 ;; |
95 ;; | # The max length of a field.
96 ;; | $MAX_FIELD_LENGTH = 256;
97 ;; `----
98 ;;
99 ;; My mail is stored in the directories ~/Mail/mail/, ~/Mail/lists/ and
100 ;; ~/Mail/archive/, so to index them I go to the directory set in
101 ;; `nnir-namazu-index-directory' and issue the following command.
102 ;;
103 ;;      mknmz --mailnews ~/Mail/archive/ ~/Mail/mail/ ~/Mail/lists/
104 ;;
105 ;; For maximum searching efficiency I have a cron job set to run this
106 ;; command every four hours.
107
108 ;; 2. find-grep
109 ;;
110 ;; The find-grep engine simply runs find(1) to locate eligible
111 ;; articles and searches them with grep(1).  This, of course, is much
112 ;; slower than using a proper search engine but OTOH doesn't require
113 ;; maintenance of an index and is still faster than using any built-in
114 ;; means for searching.  The method specification of the server to
115 ;; search must include a directory for this engine to work (E.g.,
116 ;; `nnml-directory').  The tools must be POSIX compliant.  GNU Find
117 ;; prior to version 4.2.12 (4.2.26 on Linux due to incorrect ARG_MAX
118 ;; handling) does not work.
119 ;; ,----
120 ;; |    ;; find-grep configuration for searching the Gnus Cache
121 ;; |
122 ;; |    (nnml "cache"
123 ;; |          (nnml-get-new-mail nil)
124 ;; |          (nnir-search-engine find-grep)
125 ;; |          (nnml-directory "~/News/cache/")
126 ;; |          (nnml-active-file "~/News/cache/active"))
127 ;; `----
128
129 ;; Developer information:
130
131 ;; I have tried to make the code expandable.  Basically, it is divided
132 ;; into two layers.  The upper layer is somewhat like the `nnvirtual'
133 ;; backend: given a specification of what articles to show from
134 ;; another backend, it creates a group containing exactly those
135 ;; articles.  The lower layer issues a query to a search engine and
136 ;; produces such a specification of what articles to show from the
137 ;; other backend.
138
139 ;; The interface between the two layers consists of the single
140 ;; function `nnir-run-query', which just selects the appropriate
141 ;; function for the search engine one is using.  The input to
142 ;; `nnir-run-query' is a string, representing the query as input by
143 ;; the user.  The output of `nnir-run-query' is supposed to be a
144 ;; vector, each element of which should in turn be a three-element
145 ;; vector.  The first element should be full group name of the article,
146 ;; the second element should be the article number, and the third
147 ;; element should be the Retrieval Status Value (RSV) as returned from
148 ;; the search engine.  An RSV is the score assigned to the document by
149 ;; the search engine.  For Boolean search engines, the
150 ;; RSV is always 1000 (or 1 or 100, or whatever you like).
151
152 ;; The sorting order of the articles in the summary buffer created by
153 ;; nnir is based on the order of the articles in the above mentioned
154 ;; vector, so that's where you can do the sorting you'd like.  Maybe
155 ;; it would be nice to have a way of displaying the search result
156 ;; sorted differently?
157
158 ;; So what do you need to do when you want to add another search
159 ;; engine?  You write a function that executes the query.  Temporary
160 ;; data from the search engine can be put in `nnir-tmp-buffer'.  This
161 ;; function should return the list of articles as a vector, as
162 ;; described above.  Then, you need to register this backend in
163 ;; `nnir-engines'.  Then, users can choose the backend by setting
164 ;; `nnir-search-engine' as a server variable.
165
166 ;;; Code:
167
168 ;;; Setup:
169
170 ;; For Emacs <22.2 and XEmacs.
171 (eval-and-compile
172   (unless (fboundp 'declare-function) (defmacro declare-function (&rest r))))
173
174 (require 'nnoo)
175 (require 'gnus-group)
176 (require 'message)
177 (require 'gnus-util)
178 (eval-when-compile
179   (require 'cl))
180
181 ;;; Internal Variables:
182
183 (defvar nnir-current-query nil
184   "Internal: stores current query (= group name).")
185
186 (defvar nnir-current-server nil
187   "Internal: stores current server (does it ever change?).")
188
189 (defvar nnir-current-group-marked nil
190   "Internal: stores current list of process-marked groups.")
191
192 (defvar nnir-artlist nil
193   "Internal: stores search result.")
194
195 (defvar nnir-tmp-buffer " *nnir*"
196   "Internal: temporary buffer.")
197
198 (defvar nnir-search-history ()
199   "Internal: the history for querying search options in nnir")
200
201 (defvar nnir-extra-parms nil
202   "Internal: stores request for extra search parms")
203
204 ;; Imap variables
205
206 (defvar nnir-imap-search-arguments
207   '(("Whole message" . "TEXT")
208     ("Subject" . "SUBJECT")
209     ("To" . "TO")
210     ("From" . "FROM")
211     ("Imap" . ""))
212   "Mapping from user readable keys to IMAP search items for use in nnir")
213
214 (defvar nnir-imap-search-other "HEADER %S"
215   "The IMAP search item to use for anything other than
216   `nnir-imap-search-arguments'. By default this is the name of an
217   email header field")
218
219 (defvar nnir-imap-search-argument-history ()
220   "The history for querying search options in nnir")
221
222 ;;; Helper macros
223
224 ;; Data type article list.
225
226 (defmacro nnir-artlist-length (artlist)
227   "Returns number of articles in artlist."
228   `(length ,artlist))
229
230 (defmacro nnir-artlist-article (artlist n)
231   "Returns from ARTLIST the Nth artitem (counting starting at 1)."
232   `(when (> ,n 0)
233      (elt ,artlist (1- ,n))))
234
235 (defmacro nnir-artitem-group (artitem)
236   "Returns the group from the ARTITEM."
237   `(elt ,artitem 0))
238
239 (defmacro nnir-artitem-number (artitem)
240   "Returns the number from the ARTITEM."
241   `(elt ,artitem 1))
242
243 (defmacro nnir-artitem-rsv (artitem)
244   "Returns the Retrieval Status Value (RSV, score) from the ARTITEM."
245   `(elt ,artitem 2))
246
247 (defmacro nnir-article-group (article)
248   "Returns the group for ARTICLE"
249   `(nnir-artitem-group (nnir-artlist-article nnir-artlist ,article)))
250
251 (defmacro nnir-article-number (article)
252   "Returns the number for ARTICLE"
253   `(nnir-artitem-number (nnir-artlist-article nnir-artlist ,article)))
254
255 (defmacro nnir-article-rsv (article)
256   "Returns the rsv for ARTICLE"
257   `(nnir-artitem-rsv (nnir-artlist-article nnir-artlist ,article)))
258
259 (defsubst nnir-article-ids (article)
260   "Returns the pair `(nnir id . real id)' of ARTICLE"
261   (cons article (nnir-article-number article)))
262
263 (defmacro nnir-categorize (sequence keyfunc &optional valuefunc)
264   "Sorts a sequence into categories and returns a list of the form
265 `((key1 (element11 element12)) (key2 (element21 element22))'.
266 The category key for a member of the sequence is obtained
267 as `(keyfunc member)' and the corresponding element is just
268 `member'. If `valuefunc' is non-nil, the element of the list
269 is `(valuefunc member)'."
270   `(unless (null ,sequence)
271      (let (value)
272        (mapc
273         (lambda (member)
274           (let ((y (,keyfunc member))
275                 (x ,(if valuefunc
276                         `(,valuefunc member)
277                       'member)))
278             (if (assoc y value)
279                 (push x (cadr (assoc y value)))
280               (push (list y (list x)) value))))
281         ,sequence)
282        value)))
283
284 ;;; Finish setup:
285
286 (require 'gnus-sum)
287
288 (eval-when-compile
289   (autoload 'nnimap-buffer "nnimap")
290   (autoload 'nnimap-command "nnimap")
291   (autoload 'nnimap-possibly-change-group "nnimap")
292   (autoload 'gnus-registry-action "gnus-registry")
293   (defvar gnus-registry-install))
294
295
296 (nnoo-declare nnir)
297 (nnoo-define-basics nnir)
298
299 (gnus-declare-backend "nnir" 'mail)
300
301
302 ;;; User Customizable Variables:
303
304 (defgroup nnir nil
305   "Search groups in Gnus with assorted seach engines."
306   :group 'gnus)
307
308 (defcustom nnir-method-default-engines
309   '((nnimap . imap)
310     (nntp . gmane))
311   "*Alist of default search engines keyed by server method."
312   :type '(alist)
313   :group 'nnir)
314
315 (defcustom nnir-ignored-newsgroups ""
316   "*A regexp to match newsgroups in the active file that should
317   be skipped when searching."
318   :type '(regexp)
319   :group 'nnir)
320
321 (defcustom nnir-summary-line-format nil
322   "*The format specification of the lines in an nnir summary buffer.
323
324 All the items from `gnus-summary-line-format' are available, along
325 with three items unique to nnir summary buffers:
326
327 %Z    Search retrieval score value (integer)
328 %G    Article original full group name (string)
329 %g    Article original short group name (string)
330
331 If nil this will use `gnus-summary-line-format'."
332   :type '(regexp)
333   :group 'nnir)
334
335 (defcustom nnir-retrieve-headers-override-function nil
336   "*If non-nil, a function that accepts an article list and group
337 and populates the `nntp-server-buffer' with the retrieved
338 headers. Must return either 'nov or 'headers indicating the
339 retrieved header format.
340
341 If this variable is nil, or if the provided function returns nil for a search
342 result, `gnus-retrieve-headers' will be called instead."
343   :type '(function)
344   :group 'nnir)
345
346 (defcustom nnir-imap-default-search-key "Whole message"
347   "*The default IMAP search key for an nnir search. Must be one of
348   the keys in `nnir-imap-search-arguments'. To use raw imap queries
349   by default set this to \"Imap\"."
350   :type '(string)
351   :group 'nnir)
352
353 (defcustom nnir-swish++-configuration-file
354   (expand-file-name "~/Mail/swish++.conf")
355   "*Configuration file for swish++."
356   :type '(file)
357   :group 'nnir)
358
359 (defcustom nnir-swish++-program "search"
360   "*Name of swish++ search executable."
361   :type '(string)
362   :group 'nnir)
363
364 (defcustom nnir-swish++-additional-switches '()
365   "*A list of strings, to be given as additional arguments to swish++.
366
367 Note that this should be a list.  Ie, do NOT use the following:
368     (setq nnir-swish++-additional-switches \"-i -w\") ; wrong
369 Instead, use this:
370     (setq nnir-swish++-additional-switches '(\"-i\" \"-w\"))"
371   :type '(repeat (string))
372   :group 'nnir)
373
374 (defcustom nnir-swish++-remove-prefix (concat (getenv "HOME") "/Mail/")
375   "*The prefix to remove from each file name returned by swish++
376 in order to get a group name (albeit with / instead of .).  This is a
377 regular expression.
378
379 This variable is very similar to `nnir-namazu-remove-prefix', except
380 that it is for swish++, not Namazu."
381   :type '(regexp)
382   :group 'nnir)
383
384 ;; Swish-E.
385 ;; URL: http://swish-e.org/
386 ;; Variables `nnir-swish-e-index-file', `nnir-swish-e-program' and
387 ;; `nnir-swish-e-additional-switches'
388
389 (make-obsolete-variable 'nnir-swish-e-index-file
390                         'nnir-swish-e-index-files "Emacs 23.1")
391 (defcustom nnir-swish-e-index-file
392   (expand-file-name "~/Mail/index.swish-e")
393   "*Index file for swish-e.
394 This could be a server parameter.
395 It is never consulted once `nnir-swish-e-index-files', which should be
396 used instead, has been customized."
397   :type '(file)
398   :group 'nnir)
399
400 (defcustom nnir-swish-e-index-files
401   (list nnir-swish-e-index-file)
402   "*List of index files for swish-e.
403 This could be a server parameter."
404   :type '(repeat (file))
405   :group 'nnir)
406
407 (defcustom nnir-swish-e-program "swish-e"
408   "*Name of swish-e search executable.
409 This cannot be a server parameter."
410   :type '(string)
411   :group 'nnir)
412
413 (defcustom nnir-swish-e-additional-switches '()
414   "*A list of strings, to be given as additional arguments to swish-e.
415
416 Note that this should be a list.  Ie, do NOT use the following:
417     (setq nnir-swish-e-additional-switches \"-i -w\") ; wrong
418 Instead, use this:
419     (setq nnir-swish-e-additional-switches '(\"-i\" \"-w\"))
420
421 This could be a server parameter."
422   :type '(repeat (string))
423   :group 'nnir)
424
425 (defcustom nnir-swish-e-remove-prefix (concat (getenv "HOME") "/Mail/")
426   "*The prefix to remove from each file name returned by swish-e
427 in order to get a group name (albeit with / instead of .).  This is a
428 regular expression.
429
430 This variable is very similar to `nnir-namazu-remove-prefix', except
431 that it is for swish-e, not Namazu.
432
433 This could be a server parameter."
434   :type '(regexp)
435   :group 'nnir)
436
437 ;; HyREX engine, see <URL:http://ls6-www.cs.uni-dortmund.de/>
438
439 (defcustom nnir-hyrex-program "nnir-search"
440   "*Name of the nnir-search executable."
441   :type '(string)
442   :group 'nnir)
443
444 (defcustom nnir-hyrex-additional-switches '()
445   "*A list of strings, to be given as additional arguments for nnir-search.
446 Note that this should be a list. Ie, do NOT use the following:
447     (setq nnir-hyrex-additional-switches \"-ddl ddl.xml -c nnir\") ; wrong !
448 Instead, use this:
449     (setq nnir-hyrex-additional-switches '(\"-ddl\" \"ddl.xml\" \"-c\" \"nnir\"))"
450   :type '(repeat (string))
451   :group 'nnir)
452
453 (defcustom nnir-hyrex-index-directory (getenv "HOME")
454   "*Index directory for HyREX."
455   :type '(directory)
456   :group 'nnir)
457
458 (defcustom nnir-hyrex-remove-prefix (concat (getenv "HOME") "/Mail/")
459   "*The prefix to remove from each file name returned by HyREX
460 in order to get a group name (albeit with / instead of .).
461
462 For example, suppose that HyREX returns file names such as
463 \"/home/john/Mail/mail/misc/42\".  For this example, use the following
464 setting:  (setq nnir-hyrex-remove-prefix \"/home/john/Mail/\")
465 Note the trailing slash.  Removing this prefix gives \"mail/misc/42\".
466 `nnir' knows to remove the \"/42\" and to replace \"/\" with \".\" to
467 arrive at the correct group name, \"mail.misc\"."
468   :type '(directory)
469   :group 'nnir)
470
471 ;; Namazu engine, see <URL:http://www.namazu.org/>
472
473 (defcustom nnir-namazu-program "namazu"
474   "*Name of Namazu search executable."
475   :type '(string)
476   :group 'nnir)
477
478 (defcustom nnir-namazu-index-directory (expand-file-name "~/Mail/namazu/")
479   "*Index directory for Namazu."
480   :type '(directory)
481   :group 'nnir)
482
483 (defcustom nnir-namazu-additional-switches '()
484   "*A list of strings, to be given as additional arguments to namazu.
485 The switches `-q', `-a', and `-s' are always used, very few other switches
486 make any sense in this context.
487
488 Note that this should be a list.  Ie, do NOT use the following:
489     (setq nnir-namazu-additional-switches \"-i -w\") ; wrong
490 Instead, use this:
491     (setq nnir-namazu-additional-switches '(\"-i\" \"-w\"))"
492   :type '(repeat (string))
493   :group 'nnir)
494
495 (defcustom nnir-namazu-remove-prefix (concat (getenv "HOME") "/Mail/")
496   "*The prefix to remove from each file name returned by Namazu
497 in order to get a group name (albeit with / instead of .).
498
499 For example, suppose that Namazu returns file names such as
500 \"/home/john/Mail/mail/misc/42\".  For this example, use the following
501 setting:  (setq nnir-namazu-remove-prefix \"/home/john/Mail/\")
502 Note the trailing slash.  Removing this prefix gives \"mail/misc/42\".
503 `nnir' knows to remove the \"/42\" and to replace \"/\" with \".\" to
504 arrive at the correct group name, \"mail.misc\"."
505   :type '(directory)
506   :group 'nnir)
507
508 ;;; Developer Extension Variable:
509
510 (defvar nnir-engines
511   `((imap    nnir-run-imap
512              ((criteria
513                "Imap Search in"                   ; Prompt
514                ,(mapcar 'car nnir-imap-search-arguments) ; alist for completing
515                nil                                ; allow any user input
516                nil                                ; initial value
517                nnir-imap-search-argument-history  ; the history to use
518                ,nnir-imap-default-search-key      ; default
519                )))
520     (gmane   nnir-run-gmane
521              ((author . "Gmane Author: ")))
522     (swish++ nnir-run-swish++
523              ((group . "Swish++ Group spec: ")))
524     (swish-e nnir-run-swish-e
525              ((group . "Swish-e Group spec: ")))
526     (namazu  nnir-run-namazu
527              ())
528     (hyrex   nnir-run-hyrex
529              ((group . "Hyrex Group spec: ")))
530     (find-grep nnir-run-find-grep
531                ((grep-options . "Grep options: "))))
532   "Alist of supported search engines.
533 Each element in the alist is a three-element list (ENGINE FUNCTION ARGS).
534 ENGINE is a symbol designating the searching engine.  FUNCTION is also
535 a symbol, giving the function that does the search.  The third element
536 ARGS is a list of cons pairs (PARAM . PROMPT).  When issuing a query,
537 the FUNCTION will issue a query for each of the PARAMs, using PROMPT.
538
539 The value of `nnir-search-engine' must be one of the ENGINE symbols.
540 For example, for searching a server using namazu include
541     (nnir-search-engine namazu)
542 in the server definition.  Note that you have to set additional
543 variables for most backends.  For example, the `namazu' backend
544 needs the variables `nnir-namazu-program',
545 `nnir-namazu-index-directory' and `nnir-namazu-remove-prefix'.
546
547 Add an entry here when adding a new search engine.")
548
549
550 ;; Gnus glue.
551
552 (defun gnus-group-make-nnir-group (nnir-extra-parms)
553   "Create an nnir group.  Asks for query."
554   (interactive "P")
555   (setq nnir-current-query nil
556         nnir-current-server nil
557         nnir-current-group-marked nil
558         nnir-artlist nil)
559   (let* ((query (read-string "Query: " nil 'nnir-search-history))
560          (parms (list (cons 'query query)))
561          (srv (if (gnus-server-server-name)
562                   "all" "")))
563     (add-to-list 'parms (cons 'unique-id (message-unique-id)) t)
564     (gnus-group-read-ephemeral-group
565      (concat "nnir:" (prin1-to-string parms)) (list 'nnir srv) t
566      (cons (current-buffer) gnus-current-window-configuration)
567      nil)))
568
569
570 ;; Gnus backend interface functions.
571
572 (deffoo nnir-open-server (server &optional definitions)
573   ;; Just set the server variables appropriately.
574   (add-hook 'gnus-summary-mode-hook 'nnir-mode)
575   (nnoo-change-server 'nnir server definitions))
576
577 (deffoo nnir-request-group (group &optional server fast info)
578   "GROUP is the query string."
579   (nnir-possibly-change-server server)
580   ;; Check for cache and return that if appropriate.
581   (if (and (equal group nnir-current-query)
582            (equal gnus-group-marked nnir-current-group-marked)
583            (or (null server)
584                (equal server nnir-current-server)))
585       nnir-artlist
586     ;; Cache miss.
587     (setq nnir-artlist (nnir-run-query group server)))
588   (with-current-buffer nntp-server-buffer
589     (setq nnir-current-query group)
590     (when server (setq nnir-current-server server))
591     (setq nnir-current-group-marked gnus-group-marked)
592     (if (zerop (length nnir-artlist))
593         (nnheader-report 'nnir "Search produced empty results.")
594       ;; Remember data for cache.
595       (nnheader-insert "211 %d %d %d %s\n"
596                        (nnir-artlist-length nnir-artlist) ; total #
597                        1              ; first #
598                        (nnir-artlist-length nnir-artlist) ; last #
599                        group))))      ; group name
600
601 (deffoo nnir-retrieve-headers (articles &optional group server fetch-old)
602   (with-current-buffer nntp-server-buffer
603     (let ((gnus-inhibit-demon t)
604           (articles-by-group (nnir-categorize
605                               articles nnir-article-group nnir-article-ids))
606           headers)
607       (while (not (null articles-by-group))
608         (let* ((group-articles (pop articles-by-group))
609                (artgroup (car group-articles))
610                (articleids (cadr group-articles))
611                (artlist (sort (mapcar 'cdr articleids) '<))
612                (server (gnus-group-server artgroup))
613                (gnus-override-method (gnus-server-to-method server))
614                parsefunc)
615           ;; (or (numberp art)
616           ;;     (nnheader-report
617           ;;      'nnir
618           ;;      "nnir-retrieve-headers doesn't grok message ids: %s"
619           ;;      art))
620           (nnir-possibly-change-server server)
621           ;; is this needed?
622           (erase-buffer)
623           (case (setq gnus-headers-retrieved-by
624                       (or
625                        (and
626                         nnir-retrieve-headers-override-function
627                         (funcall nnir-retrieve-headers-override-function
628                                  artlist artgroup))
629                        (gnus-retrieve-headers artlist artgroup nil)))
630             (nov
631              (setq parsefunc 'nnheader-parse-nov))
632             (headers
633              (setq parsefunc 'nnheader-parse-head))
634             (t (error "Unknown header type %s while requesting articles \
635                     of group %s" gnus-headers-retrieved-by artgroup)))
636           (goto-char (point-min))
637           (while (not (eobp))
638             (let* ((novitem (funcall parsefunc))
639                    (artno (mail-header-number novitem))
640                    (art (car (rassq artno articleids))))
641               (when art
642                 (mail-header-set-number novitem art)
643                 (push novitem headers))
644               (forward-line 1)))))
645       (setq headers
646             (sort headers
647                   (lambda (x y)
648                     (< (mail-header-number x) (mail-header-number y)))))
649       (erase-buffer)
650       (mapc 'nnheader-insert-nov headers)
651       'nov)))
652
653 (deffoo nnir-request-article (article &optional group server to-buffer)
654   (if (stringp article)
655       (nnheader-report
656        'nnir
657        "nnir-retrieve-headers doesn't grok message ids: %s"
658        article)
659     (save-excursion
660       (let ((artfullgroup (nnir-article-group article))
661             (artno (nnir-article-number article)))
662         (message "Requesting article %d from group %s"
663                  artno artfullgroup)
664         (if to-buffer
665             (with-current-buffer to-buffer
666               (let ((gnus-article-decode-hook nil))
667                 (gnus-request-article-this-buffer artno artfullgroup)))
668           (gnus-request-article artno artfullgroup))
669         (cons artfullgroup artno)))))
670
671 (deffoo nnir-request-move-article (article group server accept-form
672                                            &optional last internal-move-group)
673   (let* ((artfullgroup (nnir-article-group article))
674          (artno (nnir-article-number article))
675          (to-newsgroup (nth 1 accept-form))
676          (to-method (gnus-find-method-for-group to-newsgroup))
677          (from-method (gnus-find-method-for-group artfullgroup))
678          (move-is-internal (gnus-server-equal from-method to-method)))
679     (unless (gnus-check-backend-function
680              'request-move-article artfullgroup)
681       (error "The group %s does not support article moving" artfullgroup))
682     (gnus-request-move-article
683      artno
684      artfullgroup
685      (nth 1 from-method)
686      accept-form
687      last
688      (and move-is-internal
689           to-newsgroup          ; Not respooling
690           (gnus-group-real-name to-newsgroup)))))
691
692 (deffoo nnir-request-expire-articles (articles group &optional server force)
693   (if force
694     (let ((articles-by-group (nnir-categorize
695                               articles nnir-article-group nnir-article-ids))
696           not-deleted)
697       (while (not (null articles-by-group))
698         (let* ((group-articles (pop articles-by-group))
699                (artgroup (car group-articles))
700                (articleids (cadr group-articles))
701                (artlist (sort (mapcar 'cdr articleids) '<)))
702           (unless (gnus-check-backend-function 'request-expire-articles
703                                                artgroup)
704             (error "The group %s does not support article deletion" artgroup))
705           (unless (gnus-check-server (gnus-find-method-for-group artgroup))
706             (error "Couldn't open server for group %s" artgroup))
707           (push (gnus-request-expire-articles
708                  artlist artgroup force)
709                 not-deleted)))
710       (sort (delq nil not-deleted) '<))
711     articles))
712
713 (deffoo nnir-warp-to-article ()
714   (let* ((cur (if (> (gnus-summary-article-number) 0)
715                   (gnus-summary-article-number)
716                 (error "This is not a real article.")))
717          (gnus-newsgroup-name (nnir-article-group cur))
718          (backend-number (nnir-article-number cur)))
719     (gnus-summary-read-group-1 gnus-newsgroup-name t t gnus-summary-buffer
720                                nil (list backend-number))))
721
722 (nnoo-define-skeleton nnir)
723
724
725 (defmacro nnir-add-result (dirnam artno score prefix server artlist)
726   "Ask `nnir-compose-result' to construct a result vector,
727 and if it is non-nil, add it to artlist."
728   `(let ((result (nnir-compose-result ,dirnam ,artno ,score ,prefix ,server)))
729      (when (not (null result))
730        (push result ,artlist))))
731
732 (autoload 'nnmaildir-base-name-to-article-number "nnmaildir")
733
734 ;; Helper function currently used by the Swish++ and Namazu backends;
735 ;; perhaps useful for other backends as well
736 (defun nnir-compose-result (dirnam article score prefix server)
737   "Extract the group from dirnam, and create a result vector
738 ready to be added to the list of search results."
739
740   ;; remove nnir-*-remove-prefix from beginning of dirnam filename
741   (when (string-match (concat "^" prefix) dirnam)
742     (setq dirnam (replace-match "" t t dirnam)))
743
744   (when (file-readable-p (concat prefix dirnam article))
745     ;; remove trailing slash and, for nnmaildir, cur/new/tmp
746     (setq dirnam
747           (substring dirnam 0
748                      (if (string= (gnus-group-server server) "nnmaildir")
749                          -5 -1)))
750
751     ;; Set group to dirnam without any leading dots or slashes,
752     ;; and with all subsequent slashes replaced by dots
753     (let ((group (gnus-replace-in-string
754                  (gnus-replace-in-string dirnam "^[./\\]" "" t)
755                  "[/\\]" "." t)))
756
757     (vector (gnus-group-full-name group server)
758             (if (string= (gnus-group-server server) "nnmaildir")
759                 (nnmaildir-base-name-to-article-number
760                  (substring article 0 (string-match ":" article))
761                  group nil)
762               (string-to-number article))
763             (string-to-number score)))))
764
765 ;;; Search Engine Interfaces:
766
767 ;; imap interface
768 (defun nnir-run-imap (query srv &optional groups)
769   "Run a search against an IMAP back-end server.
770 This uses a custom query language parser; see `nnir-imap-make-query' for
771 details on the language and supported extensions"
772   (save-excursion
773     (let ((qstring (cdr (assq 'query query)))
774           (server (cadr (gnus-server-to-method srv)))
775           (defs (caddr (gnus-server-to-method srv)))
776           (criteria (or (cdr (assq 'criteria query))
777                         (cdr (assoc nnir-imap-default-search-key
778                                     nnir-imap-search-arguments))))
779           (gnus-inhibit-demon t)
780           (groups (or groups (nnir-get-active srv))))
781       (message "Opening server %s" server)
782       (apply
783        'vconcat
784        (mapcar
785         (lambda (group)
786           (let (artlist)
787             (condition-case ()
788                 (when (nnimap-possibly-change-group
789                        (gnus-group-short-name group) server)
790                   (with-current-buffer (nnimap-buffer)
791                     (message "Searching %s..." group)
792                     (let ((arts 0)
793                           (result (nnimap-command "UID SEARCH %s"
794                                                   (if (string= criteria "")
795                                                       qstring
796                                                     (nnir-imap-make-query
797                                                      criteria qstring)))))
798                       (mapc
799                        (lambda (artnum)
800                          (let ((artn (string-to-number artnum)))
801                            (when (> artn 0)
802                              (push (vector group artn 100)
803                                    artlist)
804                              (setq arts (1+ arts)))))
805                        (and (car result) (cdr (assoc "SEARCH" (cdr result)))))
806                       (message "Searching %s... %d matches" group arts)))
807                   (message "Searching %s...done" group))
808               (quit nil))
809             (nreverse artlist)))
810         groups)))))
811
812 (defun nnir-imap-make-query (criteria qstring)
813   "Parse the query string and criteria into an appropriate IMAP search
814 expression, returning the string query to make.
815
816 This implements a little language designed to return the expected results
817 to an arbitrary query string to the end user.
818
819 The search is always case-insensitive, as defined by RFC2060, and supports
820 the following features (inspired by the Google search input language):
821
822 Automatic \"and\" queries
823     If you specify multiple words then they will be treated as an \"and\"
824     expression intended to match all components.
825
826 Phrase searches
827     If you wrap your query in double-quotes then it will be treated as a
828     literal string.
829
830 Negative terms
831     If you precede a term with \"-\" then it will negate that.
832
833 \"OR\" queries
834     If you include an upper-case \"OR\" in your search it will cause the
835     term before it and the term after it to be treated as alternatives.
836
837 In future the following will be added to the language:
838  * support for date matches
839  * support for location of text matching within the query
840  * from/to/etc headers
841  * additional search terms
842  * flag based searching
843  * anything else that the RFC supports, basically."
844   ;; Walk through the query and turn it into an IMAP query string.
845   (nnir-imap-query-to-imap criteria (nnir-imap-parse-query qstring)))
846
847
848 (defun nnir-imap-query-to-imap (criteria query)
849   "Turn a s-expression format query into IMAP."
850   (mapconcat
851    ;; Turn the expressions into IMAP text
852    (lambda (item)
853      (nnir-imap-expr-to-imap criteria item))
854    ;; The query, already in s-expr format.
855    query
856    ;; Append a space between each expression
857    " "))
858
859
860 (defun nnir-imap-expr-to-imap (criteria expr)
861   "Convert EXPR into an IMAP search expression on CRITERIA"
862   ;; What sort of expression is this, eh?
863   (cond
864    ;; Simple string term
865    ((stringp expr)
866     (format "%s %S" criteria expr))
867    ;; Trivial term: and
868    ((eq expr 'and) nil)
869    ;; Composite term: or expression
870    ((eq (car-safe expr) 'or)
871     (format "OR %s %s"
872             (nnir-imap-expr-to-imap criteria (second expr))
873             (nnir-imap-expr-to-imap criteria (third expr))))
874    ;; Composite term: just the fax, mam
875    ((eq (car-safe expr) 'not)
876     (format "NOT (%s)" (nnir-imap-query-to-imap criteria (rest expr))))
877    ;; Composite term: just expand it all.
878    ((and (not (null expr)) (listp expr))
879     (format "(%s)" (nnir-imap-query-to-imap criteria expr)))
880    ;; Complex value, give up for now.
881    (t (error "Unhandled input: %S" expr))))
882
883
884 (defun nnir-imap-parse-query (string)
885   "Turn STRING into an s-expression based query based on the IMAP
886 query language as defined in `nnir-imap-make-query'.
887
888 This involves turning individual tokens into higher level terms
889 that the search language can then understand and use."
890   (with-temp-buffer
891     ;; Set up the parsing environment.
892     (insert string)
893     (goto-char (point-min))
894     ;; Now, collect the output terms and return them.
895     (let (out)
896       (while (not (nnir-imap-end-of-input))
897         (push (nnir-imap-next-expr) out))
898       (reverse out))))
899
900
901 (defun nnir-imap-next-expr (&optional count)
902   "Return the next expression from the current buffer."
903   (let ((term (nnir-imap-next-term count))
904         (next (nnir-imap-peek-symbol)))
905     ;; Are we looking at an 'or' expression?
906     (cond
907      ;; Handle 'expr or expr'
908      ((eq next 'or)
909       (list 'or term (nnir-imap-next-expr 2)))
910      ;; Anything else
911      (t term))))
912
913
914 (defun nnir-imap-next-term (&optional count)
915   "Return the next TERM from the current buffer."
916   (let ((term (nnir-imap-next-symbol count)))
917     ;; What sort of term is this?
918     (cond
919      ;; and -- just ignore it
920      ((eq term 'and) 'and)
921      ;; negated term
922      ((eq term 'not) (list 'not (nnir-imap-next-expr)))
923      ;; generic term
924      (t term))))
925
926
927 (defun nnir-imap-peek-symbol ()
928   "Return the next symbol from the current buffer, but don't consume it."
929   (save-excursion
930     (nnir-imap-next-symbol)))
931
932 (defun nnir-imap-next-symbol (&optional count)
933   "Return the next symbol from the current buffer, or nil if we are
934 at the end of the buffer.  If supplied COUNT skips some symbols before
935 returning the one at the supplied position."
936   (when (and (numberp count) (> count 1))
937     (nnir-imap-next-symbol (1- count)))
938   (let ((case-fold-search t))
939     ;; end of input stream?
940     (unless (nnir-imap-end-of-input)
941       ;; No, return the next symbol from the stream.
942       (cond
943        ;; negated expression -- return it and advance one char.
944        ((looking-at "-") (forward-char 1) 'not)
945        ;; quoted string
946        ((looking-at "\"") (nnir-imap-delimited-string "\""))
947        ;; list expression -- we parse the content and return this as a list.
948        ((looking-at "(")
949         (nnir-imap-parse-query (nnir-imap-delimited-string ")")))
950        ;; keyword input -- return a symbol version
951        ((looking-at "\\band\\b") (forward-char 3) 'and)
952        ((looking-at "\\bor\\b")  (forward-char 2) 'or)
953        ((looking-at "\\bnot\\b") (forward-char 3) 'not)
954        ;; Simple, boring keyword
955        (t (let ((start (point))
956                 (end (if (search-forward-regexp "[[:blank:]]" nil t)
957                          (prog1
958                              (match-beginning 0)
959                            ;; unskip if we hit a non-blank terminal character.
960                            (when (string-match "[^[:blank:]]" (match-string 0))
961                              (backward-char 1)))
962                        (goto-char (point-max)))))
963             (buffer-substring start end)))))))
964
965 (defun nnir-imap-delimited-string (delimiter)
966   "Return a delimited string from the current buffer."
967   (let ((start (point)) end)
968     (forward-char 1)                    ; skip the first delimiter.
969     (while (not end)
970       (unless (search-forward delimiter nil t)
971         (error "Unmatched delimited input with %s in query" delimiter))
972       (let ((here (point)))
973         (unless (equal (buffer-substring (- here 2) (- here 1)) "\\")
974           (setq end (point)))))
975     (buffer-substring (1+ start) (1- end))))
976
977 (defun nnir-imap-end-of-input ()
978   "Are we at the end of input?"
979   (skip-chars-forward "[[:blank:]]")
980   (looking-at "$"))
981
982
983 ;; Swish++ interface.
984 ;; -cc- Todo
985 ;; Search by
986 ;; - group
987 ;; Sort by
988 ;; - rank (default)
989 ;; - article number
990 ;; - file size
991 ;; - group
992 (defun nnir-run-swish++ (query server &optional group)
993   "Run QUERY against swish++.
994 Returns a vector of (group name, file name) pairs (also vectors,
995 actually).
996
997 Tested with swish++ 4.7 on GNU/Linux and with swish++ 5.0b2 on
998 Windows NT 4.0."
999
1000   ;; (when group
1001   ;;   (error "The swish++ backend cannot search specific groups"))
1002
1003   (save-excursion
1004     (let ( (qstring (cdr (assq 'query query)))
1005            (groupspec (cdr (assq 'group query)))
1006            (prefix (nnir-read-server-parm 'nnir-swish++-remove-prefix server))
1007            artlist
1008            ;; nnml-use-compressed-files might be any string, but probably this
1009            ;; is sufficient.  Note that we can't only use the value of
1010            ;; nnml-use-compressed-files because old articles might have been
1011            ;; saved with a different value.
1012            (article-pattern (if (string= (gnus-group-server server) "nnmaildir")
1013                                 ":[0-9]+"
1014                               "^[0-9]+\\(\\.[a-z0-9]+\\)?$"))
1015            score artno dirnam filenam)
1016
1017       (when (equal "" qstring)
1018         (error "swish++: You didn't enter anything"))
1019
1020       (set-buffer (get-buffer-create nnir-tmp-buffer))
1021       (erase-buffer)
1022
1023       (if groupspec
1024           (message "Doing swish++ query %s on %s..." qstring groupspec)
1025         (message "Doing swish++ query %s..." qstring))
1026
1027       (let* ((cp-list `( ,nnir-swish++-program
1028                          nil            ; input from /dev/null
1029                          t              ; output
1030                          nil            ; don't redisplay
1031                          "--config-file" ,(nnir-read-server-parm 'nnir-swish++-configuration-file server)
1032                          ,@(nnir-read-server-parm 'nnir-swish++-additional-switches server)
1033                          ,qstring       ; the query, in swish++ format
1034                          ))
1035              (exitstatus
1036               (progn
1037                 (message "%s args: %s" nnir-swish++-program
1038                          (mapconcat 'identity (cddddr cp-list) " ")) ;; ???
1039                 (apply 'call-process cp-list))))
1040         (unless (or (null exitstatus)
1041                     (zerop exitstatus))
1042           (nnheader-report 'nnir "Couldn't run swish++: %s" exitstatus)
1043           ;; swish++ failure reason is in this buffer, show it if
1044           ;; the user wants it.
1045           (when (> gnus-verbose 6)
1046             (display-buffer nnir-tmp-buffer))))
1047
1048       ;; The results are output in the format of:
1049       ;; V 4.7 Linux
1050       ;; rank relative-path-name file-size file-title
1051       ;; V 5.0b2:
1052       ;; rank relative-path-name file-size topic??
1053       ;; where rank is an integer from 1 to 100.
1054       (goto-char (point-min))
1055       (while (re-search-forward
1056               "\\(^[0-9]+\\) \\([^ ]+\\) [0-9]+ \\(.*\\)$" nil t)
1057         (setq score (match-string 1)
1058               filenam (match-string 2)
1059               artno (file-name-nondirectory filenam)
1060               dirnam (file-name-directory filenam))
1061
1062         ;; don't match directories
1063         (when (string-match article-pattern artno)
1064           (when (not (null dirnam))
1065
1066             ;; maybe limit results to matching groups.
1067             (when (or (not groupspec)
1068                       (string-match groupspec dirnam))
1069               (nnir-add-result dirnam artno score prefix server artlist)))))
1070
1071       (message "Massaging swish++ output...done")
1072
1073       ;; Sort by score
1074       (apply 'vector
1075              (sort artlist
1076                    (function (lambda (x y)
1077                                (> (nnir-artitem-rsv x)
1078                                   (nnir-artitem-rsv y)))))))))
1079
1080 ;; Swish-E interface.
1081 (defun nnir-run-swish-e (query server &optional group)
1082   "Run given query against swish-e.
1083 Returns a vector of (group name, file name) pairs (also vectors,
1084 actually).
1085
1086 Tested with swish-e-2.0.1 on Windows NT 4.0."
1087
1088   ;; swish-e crashes with empty parameter to "-w" on commandline...
1089   ;; (when group
1090   ;;   (error "The swish-e backend cannot search specific groups"))
1091
1092   (save-excursion
1093     (let ((qstring (cdr (assq 'query query)))
1094           (prefix
1095            (or (nnir-read-server-parm 'nnir-swish-e-remove-prefix server)
1096                (error "Missing parameter `nnir-swish-e-remove-prefix'")))
1097           artlist score artno dirnam group )
1098
1099       (when (equal "" qstring)
1100         (error "swish-e: You didn't enter anything"))
1101
1102       (set-buffer (get-buffer-create nnir-tmp-buffer))
1103       (erase-buffer)
1104
1105       (message "Doing swish-e query %s..." query)
1106       (let* ((index-files
1107               (or (nnir-read-server-parm
1108                    'nnir-swish-e-index-files server)
1109                   (error "Missing parameter `nnir-swish-e-index-files'")))
1110              (additional-switches
1111               (nnir-read-server-parm
1112                'nnir-swish-e-additional-switches server))
1113              (cp-list `(,nnir-swish-e-program
1114                         nil             ; input from /dev/null
1115                         t               ; output
1116                         nil             ; don't redisplay
1117                         "-f" ,@index-files
1118                         ,@additional-switches
1119                         "-w"
1120                         ,qstring        ; the query, in swish-e format
1121                         ))
1122              (exitstatus
1123               (progn
1124                 (message "%s args: %s" nnir-swish-e-program
1125                          (mapconcat 'identity (cddddr cp-list) " "))
1126                 (apply 'call-process cp-list))))
1127         (unless (or (null exitstatus)
1128                     (zerop exitstatus))
1129           (nnheader-report 'nnir "Couldn't run swish-e: %s" exitstatus)
1130           ;; swish-e failure reason is in this buffer, show it if
1131           ;; the user wants it.
1132           (when (> gnus-verbose 6)
1133             (display-buffer nnir-tmp-buffer))))
1134
1135       ;; The results are output in the format of:
1136       ;; rank path-name file-title file-size
1137       (goto-char (point-min))
1138       (while (re-search-forward
1139               "\\(^[0-9]+\\) \\([^ ]+\\) \"\\([^\"]+\\)\" [0-9]+$" nil t)
1140         (setq score (match-string 1)
1141               artno (match-string 3)
1142               dirnam (file-name-directory (match-string 2)))
1143
1144         ;; don't match directories
1145         (when (string-match "^[0-9]+$" artno)
1146           (when (not (null dirnam))
1147
1148             ;; remove nnir-swish-e-remove-prefix from beginning of dirname
1149             (when (string-match (concat "^" prefix) dirnam)
1150               (setq dirnam (replace-match "" t t dirnam)))
1151
1152             (setq dirnam (substring dirnam 0 -1))
1153             ;; eliminate all ".", "/", "\" from beginning. Always matches.
1154             (string-match "^[./\\]*\\(.*\\)$" dirnam)
1155             ;; "/" -> "."
1156             (setq group (gnus-replace-in-string (match-string 1 dirnam) "/" "."))
1157             ;; Windows "\\" -> "."
1158             (setq group (gnus-replace-in-string group "\\\\" "."))
1159
1160             (push (vector (gnus-group-full-name group server)
1161                           (string-to-number artno)
1162                           (string-to-number score))
1163                   artlist))))
1164
1165       (message "Massaging swish-e output...done")
1166
1167       ;; Sort by score
1168       (apply 'vector
1169              (sort artlist
1170                    (function (lambda (x y)
1171                                (> (nnir-artitem-rsv x)
1172                                   (nnir-artitem-rsv y)))))))))
1173
1174 ;; HyREX interface
1175 (defun nnir-run-hyrex (query server &optional group)
1176   (save-excursion
1177     (let ((artlist nil)
1178           (groupspec (cdr (assq 'group query)))
1179           (qstring (cdr (assq 'query query)))
1180           (prefix (nnir-read-server-parm 'nnir-hyrex-remove-prefix server))
1181           score artno dirnam)
1182       (when (and (not groupspec) group)
1183         (setq groupspec
1184               (regexp-opt
1185                (mapcar (lambda (x) (gnus-group-real-name x)) group))))
1186       (set-buffer (get-buffer-create nnir-tmp-buffer))
1187       (erase-buffer)
1188       (message "Doing hyrex-search query %s..." query)
1189       (let* ((cp-list
1190               `( ,nnir-hyrex-program
1191                  nil                    ; input from /dev/null
1192                  t                      ; output
1193                  nil                    ; don't redisplay
1194                  "-i",(nnir-read-server-parm 'nnir-hyrex-index-directory server) ; index directory
1195                  ,@(nnir-read-server-parm 'nnir-hyrex-additional-switches server)
1196                  ,qstring          ; the query, in hyrex-search format
1197                  ))
1198              (exitstatus
1199               (progn
1200                 (message "%s args: %s" nnir-hyrex-program
1201                          (mapconcat 'identity (cddddr cp-list) " "))
1202                 (apply 'call-process cp-list))))
1203         (unless (or (null exitstatus)
1204                     (zerop exitstatus))
1205           (nnheader-report 'nnir "Couldn't run hyrex-search: %s" exitstatus)
1206           ;; nnir-search failure reason is in this buffer, show it if
1207           ;; the user wants it.
1208           (when (> gnus-verbose 6)
1209             (display-buffer nnir-tmp-buffer)))) ;; FIXME: Dont clear buffer !
1210       (message "Doing hyrex-search query \"%s\"...done" qstring)
1211       (sit-for 0)
1212       ;; nnir-search returns:
1213       ;;   for nnml/nnfolder: "filename mailid weigth"
1214       ;;   for nnimap:        "group mailid weigth"
1215       (goto-char (point-min))
1216       (delete-non-matching-lines "^\\S + [0-9]+ [0-9]+$")
1217       ;; HyREX doesn't search directly in groups -- so filter out here.
1218       (when groupspec
1219         (keep-lines groupspec))
1220       ;; extract data from result lines
1221       (goto-char (point-min))
1222       (while (re-search-forward
1223               "\\(\\S +\\) \\([0-9]+\\) \\([0-9]+\\)" nil t)
1224         (setq dirnam (match-string 1)
1225               artno (match-string 2)
1226               score (match-string 3))
1227         (when (string-match prefix dirnam)
1228           (setq dirnam (replace-match "" t t dirnam)))
1229         (push (vector (gnus-group-full-name
1230                        (gnus-replace-in-string dirnam "/" ".") server)
1231                       (string-to-number artno)
1232                       (string-to-number score))
1233               artlist))
1234       (message "Massaging hyrex-search output...done.")
1235       (apply 'vector
1236              (sort artlist
1237                    (function (lambda (x y)
1238                                (if (string-lessp (nnir-artitem-group x)
1239                                                  (nnir-artitem-group y))
1240                                    t
1241                                  (< (nnir-artitem-number x)
1242                                     (nnir-artitem-number y)))))))
1243       )))
1244
1245 ;; Namazu interface
1246 (defun nnir-run-namazu (query server &optional group)
1247   "Run given query against Namazu.  Returns a vector of (group name, file name)
1248 pairs (also vectors, actually).
1249
1250 Tested with Namazu 2.0.6 on a GNU/Linux system."
1251   ;; (when group
1252   ;;   (error "The Namazu backend cannot search specific groups"))
1253   (save-excursion
1254     (let ((article-pattern (if (string= (gnus-group-server server) "nnmaildir")
1255                                ":[0-9]+"
1256                              "^[0-9]+$"))
1257           artlist
1258           (qstring (cdr (assq 'query query)))
1259           (prefix (nnir-read-server-parm 'nnir-namazu-remove-prefix server))
1260           score group article
1261           (process-environment (copy-sequence process-environment)))
1262       (setenv "LC_MESSAGES" "C")
1263       (set-buffer (get-buffer-create nnir-tmp-buffer))
1264       (erase-buffer)
1265       (let* ((cp-list
1266               `( ,nnir-namazu-program
1267                  nil                    ; input from /dev/null
1268                  t                      ; output
1269                  nil                    ; don't redisplay
1270                  "-q"                   ; don't be verbose
1271                  "-a"                   ; show all matches
1272                  "-s"                   ; use short format
1273                  ,@(nnir-read-server-parm 'nnir-namazu-additional-switches server)
1274                  ,qstring               ; the query, in namazu format
1275                  ,(nnir-read-server-parm 'nnir-namazu-index-directory server) ; index directory
1276                  ))
1277              (exitstatus
1278               (progn
1279                 (message "%s args: %s" nnir-namazu-program
1280                          (mapconcat 'identity (cddddr cp-list) " "))
1281                 (apply 'call-process cp-list))))
1282         (unless (or (null exitstatus)
1283                     (zerop exitstatus))
1284           (nnheader-report 'nnir "Couldn't run namazu: %s" exitstatus)
1285           ;; Namazu failure reason is in this buffer, show it if
1286           ;; the user wants it.
1287           (when (> gnus-verbose 6)
1288             (display-buffer nnir-tmp-buffer))))
1289
1290       ;; Namazu output looks something like this:
1291       ;; 2. Re: Gnus agent expire broken (score: 55)
1292       ;; /home/henrik/Mail/mail/sent/1310 (4,138 bytes)
1293
1294       (goto-char (point-min))
1295       (while (re-search-forward
1296               "^\\([0-9]+\\.\\).*\\((score: \\([0-9]+\\)\\))\n\\([^ ]+\\)"
1297               nil t)
1298         (setq score (match-string 3)
1299               group (file-name-directory (match-string 4))
1300               article (file-name-nondirectory (match-string 4)))
1301
1302         ;; make sure article and group is sane
1303         (when (and (string-match article-pattern article)
1304                    (not (null group)))
1305           (nnir-add-result group article score prefix server artlist)))
1306
1307       ;; sort artlist by score
1308       (apply 'vector
1309              (sort artlist
1310                    (function (lambda (x y)
1311                                (> (nnir-artitem-rsv x)
1312                                   (nnir-artitem-rsv y)))))))))
1313
1314 (defun nnir-run-find-grep (query server &optional grouplist)
1315   "Run find and grep to obtain matching articles."
1316   (let* ((method (gnus-server-to-method server))
1317          (sym (intern
1318                (concat (symbol-name (car method)) "-directory")))
1319          (directory (cadr (assoc sym (cddr method))))
1320          (regexp (cdr (assoc 'query query)))
1321          (grep-options (cdr (assoc 'grep-options query)))
1322          (grouplist (or grouplist (nnir-get-active server)))
1323          artlist)
1324     (unless directory
1325       (error "No directory found in method specification of server %s"
1326              server))
1327     (apply
1328      'vconcat
1329      (mapcar (lambda (x)
1330                (let ((group x))
1331                  (message "Searching %s using find-grep..."
1332                           (or group server))
1333                  (save-window-excursion
1334                    (set-buffer (get-buffer-create nnir-tmp-buffer))
1335                    (erase-buffer)
1336                    (if (> gnus-verbose 6)
1337                        (pop-to-buffer (current-buffer)))
1338                    (cd directory) ; Using relative paths simplifies
1339                                   ; postprocessing.
1340                    (let ((group
1341                           (if (not group)
1342                               "."
1343                             ;; Try accessing the group literally as
1344                             ;; well as interpreting dots as directory
1345                             ;; separators so the engine works with
1346                             ;; plain nnml as well as the Gnus Cache.
1347                             (let ((group (gnus-group-real-name group)))
1348                               ;; Replace cl-func find-if.
1349                               (if (file-directory-p group)
1350                                   group
1351                                 (if (file-directory-p
1352                                      (setq group
1353                                            (gnus-replace-in-string
1354                                             group
1355                                             "\\." "/" t)))
1356                                     group))))))
1357                      (unless group
1358                        (error "Cannot locate directory for group"))
1359                      (save-excursion
1360                        (apply
1361                         'call-process "find" nil t
1362                         "find" group "-type" "f" "-name" "[0-9]*" "-exec"
1363                         "grep"
1364                         `("-l" ,@(and grep-options
1365                                       (split-string grep-options "\\s-" t))
1366                           "-e" ,regexp "{}" "+"))))
1367
1368                    ;; Translate relative paths to group names.
1369                    (while (not (eobp))
1370                      (let* ((path (split-string
1371                                    (buffer-substring
1372                                     (point)
1373                                     (line-end-position)) "/" t))
1374                             (art (string-to-number (car (last path)))))
1375                        (while (string= "." (car path))
1376                          (setq path (cdr path)))
1377                        (let ((group (mapconcat 'identity
1378                                                ;; Replace cl-func:
1379                                                ;; (subseq path 0 -1)
1380                                                (let ((end (1- (length path)))
1381                                                      res)
1382                                                  (while
1383                                                      (>= (setq end (1- end)) 0)
1384                                                    (push (pop path) res))
1385                                                  (nreverse res))
1386                                                ".")))
1387                          (push
1388                           (vector (gnus-group-full-name group server) art 0)
1389                           artlist))
1390                        (forward-line 1)))
1391                    (message "Searching %s using find-grep...done"
1392                             (or group server))
1393                    artlist)))
1394      grouplist))))
1395
1396 (declare-function mm-url-insert "mm-url" (url &optional follow-refresh))
1397 (declare-function mm-url-encode-www-form-urlencoded "mm-url" (pairs))
1398
1399 ;; gmane interface
1400 (defun nnir-run-gmane (query srv &optional groups)
1401   "Run a search against a gmane back-end server."
1402       (let* ((case-fold-search t)
1403              (qstring (cdr (assq 'query query)))
1404              (server (cadr (gnus-server-to-method srv)))
1405              (groupspec (mapconcat
1406                          (lambda (x)
1407                            (if (gnus-string-match-p "gmane" x)
1408                                (format "group:%s" (gnus-group-short-name x))
1409                              (error "Can't search non-gmane groups: %s" x)))
1410                            groups " "))
1411              (authorspec
1412               (if (assq 'author query)
1413                   (format "author:%s" (cdr (assq 'author query))) ""))
1414              (search (format "%s %s %s"
1415                              qstring groupspec authorspec))
1416              (gnus-inhibit-demon t)
1417              artlist)
1418         (require 'mm-url)
1419         (with-current-buffer (get-buffer-create nnir-tmp-buffer)
1420           (erase-buffer)
1421           (mm-url-insert
1422            (concat
1423             "http://search.gmane.org/nov.php"
1424             "?"
1425             (mm-url-encode-www-form-urlencoded
1426              `(("query" . ,search)
1427                ("HITSPERPAGE" . "999")))))
1428           (unless (featurep 'xemacs) (set-buffer-multibyte t))
1429           (mm-decode-coding-region (point-min) (point-max) 'utf-8)
1430           (goto-char (point-min))
1431           (forward-line 1)
1432           (while (not (eobp))
1433             (unless (or (eolp) (looking-at "\x0d"))
1434               (let ((header (nnheader-parse-nov)))
1435                 (let ((xref (mail-header-xref header))
1436                       (xscore (string-to-number (cdr (assoc 'X-Score
1437                                (mail-header-extra header))))))
1438                   (when (string-match " \\([^:]+\\)[:/]\\([0-9]+\\)" xref)
1439                     (push
1440                      (vector
1441                       (gnus-group-prefixed-name (match-string 1 xref) srv)
1442                       (string-to-number (match-string 2 xref)) xscore)
1443                      artlist)))))
1444             (forward-line 1)))
1445         (apply 'vector (nreverse (mm-delete-duplicates artlist)))))
1446
1447 ;;; Util Code:
1448
1449 (defun nnir-read-parms (query nnir-search-engine)
1450   "Reads additional search parameters according to `nnir-engines'."
1451   (let ((parmspec (caddr (assoc nnir-search-engine nnir-engines))))
1452     (append query
1453            (mapcar 'nnir-read-parm parmspec))))
1454
1455 (defun nnir-read-parm (parmspec)
1456   "Reads a single search parameter.
1457 `parmspec' is a cons cell, the car is a symbol, the cdr is a prompt."
1458   (let ((sym (car parmspec))
1459         (prompt (cdr parmspec)))
1460     (if (listp prompt)
1461         (let* ((result (apply 'gnus-completing-read prompt))
1462                (mapping (or (assoc result nnir-imap-search-arguments)
1463                             (cons nil nnir-imap-search-other))))
1464           (cons sym (format (cdr mapping) result)))
1465       (cons sym (read-string prompt)))))
1466
1467 (autoload 'gnus-group-topic-name "gnus-topic")
1468
1469 (defun nnir-run-query (query nserver)
1470   "Invoke appropriate search engine function (see `nnir-engines').
1471   If some groups were process-marked, run the query for each of the groups
1472   and concat the results."
1473   (let ((q (car (read-from-string query)))
1474         (groups (if (string= "all-ephemeral" nserver)
1475                     (with-current-buffer gnus-server-buffer
1476                       (list (list (gnus-server-server-name))))
1477                   (nnir-categorize
1478                    (or gnus-group-marked
1479                        (if (gnus-group-group-name)
1480                            (list (gnus-group-group-name))
1481                          (cdr (assoc (gnus-group-topic-name)
1482                                      gnus-topic-alist))))
1483                    gnus-group-server))))
1484     (apply 'vconcat
1485            (mapcar
1486             (lambda (x)
1487               (let* ((server (car x))
1488                      (nnir-search-engine
1489                       (or (nnir-read-server-parm 'nnir-search-engine
1490                                                  server)
1491                           (cdr (assoc (car
1492                                        (gnus-server-to-method server))
1493                                       nnir-method-default-engines))))
1494                      search-func)
1495                 (setq search-func (cadr (assoc nnir-search-engine
1496                                                nnir-engines)))
1497                 (if search-func
1498                     (funcall search-func
1499                              (if nnir-extra-parms
1500                                  (nnir-read-parms q nnir-search-engine)
1501                                q)
1502                              server (cadr x))
1503                   nil)))
1504             groups))))
1505
1506 (defun nnir-read-server-parm (key server)
1507   "Returns the parameter value of key for the given server, where
1508 server is of form 'backend:name'."
1509   (let ((method (gnus-server-to-method server)))
1510     (cond ((and method (assq key (cddr method)))
1511            (nth 1 (assq key (cddr method))))
1512           (t nil))))
1513
1514 (defun nnir-possibly-change-server (server)
1515   (unless (and server (nnir-server-opened server))
1516     (nnir-open-server server)))
1517
1518
1519
1520 ;; unused?
1521 (defun nnir-artlist-groups (artlist)
1522   "Returns a list of all groups in the given ARTLIST."
1523   (let ((res nil)
1524         (with-dups nil))
1525     ;; from each artitem, extract group component
1526     (setq with-dups (mapcar 'nnir-artitem-group artlist))
1527     ;; remove duplicates from above
1528     (mapc (function (lambda (x) (add-to-list 'res x)))
1529             with-dups)
1530     res))
1531
1532 (defun nnir-get-active (srv)
1533   (let ((method (gnus-server-to-method srv))
1534         groups)
1535     (gnus-request-list method)
1536     (with-current-buffer nntp-server-buffer
1537       (let ((cur (current-buffer))
1538             name)
1539         (goto-char (point-min))
1540         (unless (string= nnir-ignored-newsgroups "")
1541           (delete-matching-lines nnir-ignored-newsgroups))
1542         (if (eq (car method) 'nntp)
1543             (while (not (eobp))
1544               (ignore-errors
1545                 (push (mm-string-as-unibyte
1546                        (gnus-group-full-name
1547                         (buffer-substring
1548                          (point)
1549                          (progn
1550                            (skip-chars-forward "^ \t")
1551                            (point))) method))
1552                       groups))
1553               (forward-line))
1554           (while (not (eobp))
1555             (ignore-errors
1556               (push (mm-string-as-unibyte
1557                      (if (eq (char-after) ?\")
1558                          (gnus-group-full-name (read cur) method)
1559                        (let ((p (point)) (name ""))
1560                          (skip-chars-forward "^ \t\\\\")
1561                          (setq name (buffer-substring p (point)))
1562                          (while (eq (char-after) ?\\)
1563                            (setq p (1+ (point)))
1564                            (forward-char 2)
1565                            (skip-chars-forward "^ \t\\\\")
1566                            (setq name (concat name (buffer-substring
1567                                                     p (point)))))
1568                          (gnus-group-full-name name method))))
1569                     groups))
1570             (forward-line)))))
1571     groups))
1572
1573 (defun nnir-registry-action (action data-header from &optional to method)
1574   "Call `gnus-registry-action' with the original article group."
1575   (gnus-registry-action
1576    action
1577    data-header
1578    (nnir-article-group (mail-header-number data-header))
1579    to
1580    method))
1581
1582 (defun nnir-mode ()
1583   (when (eq (car (gnus-find-method-for-group gnus-newsgroup-name)) 'nnir)
1584     (setq gnus-summary-line-format
1585           (or nnir-summary-line-format gnus-summary-line-format))
1586     (when (eq gnus-registry-install t)
1587       (remove-hook 'gnus-summary-article-delete-hook 'gnus-registry-action t)
1588       (remove-hook 'gnus-summary-article-move-hook 'gnus-registry-action t)
1589       (remove-hook 'gnus-summary-article-expire-hook 'gnus-registry-action t)
1590       (add-hook 'gnus-summary-article-delete-hook 'nnir-registry-action t t)
1591       (add-hook 'gnus-summary-article-move-hook 'nnir-registry-action t t)
1592       (add-hook 'gnus-summary-article-expire-hook 'nnir-registry-action t t))))
1593
1594
1595
1596 ;; The end.
1597 (provide 'nnir)
1598
1599 ;;; nnir.el ends here