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