*** empty log message ***
[gnus] / lisp / nnspool.el
1 ;;; nnspool.el --- spool access for GNU Emacs
2 ;; Copyright (C) 1988,89,90,93,94,95,96 Free Software Foundation, Inc.
3
4 ;; Author: Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
5 ;;      Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
6 ;; Keywords: news
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
24
25 ;;; Commentary:
26
27 ;;; Code:
28
29 (require 'nnheader)
30 (require 'nntp)
31 (require 'timezone)
32 (require 'nnoo)
33 (eval-when-compile (require 'cl))
34
35 (nnoo-declare nnspool)
36
37 (defvoo nnspool-inews-program news-inews-program
38   "Program to post news.
39 This is most commonly `inews' or `injnews'.")
40
41 (defvoo nnspool-inews-switches '("-h" "-S")
42   "Switches for nnspool-request-post to pass to `inews' for posting news.
43 If you are using Cnews, you probably should set this variable to nil.")
44
45 (defvoo nnspool-spool-directory (file-name-as-directory news-path)
46   "Local news spool directory.")
47
48 (defvoo nnspool-nov-directory (concat nnspool-spool-directory "over.view/")
49   "Local news nov directory.")
50
51 (defvoo nnspool-lib-dir "/usr/lib/news/"
52   "Where the local news library files are stored.")
53
54 (defvoo nnspool-active-file (concat nnspool-lib-dir "active")
55   "Local news active file.")
56
57 (defvoo nnspool-newsgroups-file (concat nnspool-lib-dir "newsgroups")
58   "Local news newsgroups file.")
59
60 (defvoo nnspool-distributions-file (concat nnspool-lib-dir "distributions")
61   "Local news distributions file.")
62
63 (defvoo nnspool-history-file (concat nnspool-lib-dir "history")
64   "Local news history file.")
65
66 (defvoo nnspool-active-times-file (concat nnspool-lib-dir "active.times")
67   "Local news active date file.")
68
69 (defvoo nnspool-large-newsgroup 50
70   "The number of the articles which indicates a large newsgroup.
71 If the number of the articles is greater than the value, verbose
72 messages will be shown to indicate the current status.")
73
74 (defvoo nnspool-nov-is-evil nil
75   "Non-nil means that nnspool will never return NOV lines instead of headers.")
76
77 (defconst nnspool-sift-nov-with-sed nil
78   "If non-nil, use sed to get the relevant portion from the overview file.
79 If nil, nnspool will load the entire file into a buffer and process it
80 there.")
81
82 (defvoo nnspool-rejected-article-hook nil
83   "*A hook that will be run when an article has been rejected by the server.")
84
85 \f
86
87 (defconst nnspool-version "nnspool 2.0"
88   "Version numbers of this version of NNSPOOL.")
89
90 (defvoo nnspool-current-directory nil
91   "Current news group directory.")
92
93 (defvoo nnspool-current-group nil)
94 (defvoo nnspool-status-string "")
95
96 \f
97 ;;; Interface functions.
98
99 (nnoo-define-basics nnspool)
100
101 (deffoo nnspool-retrieve-headers (articles &optional group server fetch-old)
102   "Retrieve the headers of ARTICLES."
103   (save-excursion
104     (set-buffer nntp-server-buffer)
105     (erase-buffer)
106     (when (nnspool-possibly-change-directory group)
107       (let* ((number (length articles))
108              (count 0)
109              (default-directory nnspool-current-directory)
110              (do-message (and (numberp nnspool-large-newsgroup)
111                               (> number nnspool-large-newsgroup)))
112              file beg article ag)
113         (if (and (numberp (car articles))
114                  (nnspool-retrieve-headers-with-nov articles fetch-old))
115             ;; We successfully retrieved the NOV headers.
116             'nov
117           ;; No NOV headers here, so we do it the hard way.
118           (while (setq article (pop articles))
119             (if (stringp article)
120                 ;; This is a Message-ID.
121                 (setq ag (nnspool-find-id article)
122                       file (and ag (nnspool-article-pathname 
123                                     (car ag) (cdr ag)))
124                       article (cdr ag))
125               ;; This is an article in the current group.
126               (setq file (int-to-string article)))
127             ;; Insert the head of the article.
128             (when (and file
129                        (file-exists-p file))
130               (insert "221 ")
131               (princ article (current-buffer))
132               (insert " Article retrieved.\n")
133               (setq beg (point))
134               (inline (nnheader-insert-head file))
135               (goto-char beg)
136               (search-forward "\n\n" nil t)
137               (forward-char -1)
138               (insert ".\n")
139               (delete-region (point) (point-max)))
140             
141             (and do-message
142                  (zerop (% (incf count) 20))
143                  (message "nnspool: Receiving headers... %d%%"
144                           (/ (* count 100) number))))
145           
146           (and do-message
147                (message "nnspool: Receiving headers...done"))
148           
149           ;; Fold continuation lines.
150           (nnheader-fold-continuation-lines)
151           'headers)))))
152
153 (deffoo nnspool-open-server (server &optional defs)
154   (nnoo-change-server 'nnspool server defs)
155   (cond 
156    ((not (file-exists-p nnspool-spool-directory))
157     (nnspool-close-server)
158     (nnheader-report 'nnspool "Spool directory doesn't exist: %s"
159                      nnspool-spool-directory))
160    ((not (file-directory-p (file-truename nnspool-spool-directory)))
161     (nnspool-close-server)
162     (nnheader-report 'nnspool "Not a directory: %s" nnspool-spool-directory))
163    (t
164     (nnheader-report 'nnspool "Opened server %s using directory %s"
165                      server nnspool-spool-directory)
166     t)))
167
168 (deffoo nnspool-request-article (id &optional group server buffer)
169   "Select article by message ID (or number)."
170   (nnspool-possibly-change-directory group)
171   (let ((nntp-server-buffer (or buffer nntp-server-buffer))
172         file ag)
173     (if (stringp id)
174         ;; This is a Message-ID.        
175         (when (setq ag (nnspool-find-id id))
176           (setq file (nnspool-article-pathname (car ag) (cdr ag))))
177       (setq file (nnspool-article-pathname nnspool-current-group id)))
178     (and file
179          (file-exists-p file)
180          (not (file-directory-p file))
181          (save-excursion (nnspool-find-file file))
182          ;; We return the article number and group name.
183          (if (numberp id)
184              (cons nnspool-current-group id)
185            ag))))
186             
187 (deffoo nnspool-request-body (id &optional group server)
188   "Select article body by message ID (or number)."
189   (nnspool-possibly-change-directory group)
190   (let ((res (nnspool-request-article id)))
191     (when res
192       (save-excursion
193         (set-buffer nntp-server-buffer)
194         (goto-char (point-min))
195         (when (search-forward "\n\n" nil t)
196           (delete-region (point-min) (point)))
197         res))))
198
199 (deffoo nnspool-request-head (id &optional group server)
200   "Select article head by message ID (or number)."
201   (nnspool-possibly-change-directory group)
202   (let ((res (nnspool-request-article id)))
203     (when res
204       (save-excursion
205         (set-buffer nntp-server-buffer)
206         (goto-char (point-min))
207         (when (search-forward "\n\n" nil t)
208           (delete-region (1- (point)) (point-max)))))
209     res))
210
211 (deffoo nnspool-request-group (group &optional server dont-check)
212   "Select news GROUP."
213   (let ((pathname (nnspool-article-pathname group))
214         dir)
215     (if (not (file-directory-p pathname))
216         (nnheader-report 
217          'nnspool "Invalid group name (no such directory): %s" group)
218       (setq nnspool-current-directory pathname)
219       (nnheader-report 'nnspool "Selected group %s" group)
220       (if dont-check
221           (progn
222             (nnheader-report 'nnspool "Selected group %s" group)
223             t)
224         ;; Yes, completely empty spool directories *are* possible.
225         ;; Fix by Sudish Joseph <joseph@cis.ohio-state.edu>
226         (when (setq dir (directory-files pathname nil "^[0-9]+$" t))
227           (setq dir 
228                 (sort (mapcar (lambda (name) (string-to-int name)) dir) '<)))
229         (if dir
230             (nnheader-insert
231              "211 %d %d %d %s\n" (length dir) (car dir)
232              (progn (while (cdr dir) (setq dir (cdr dir))) (car dir))
233              group)
234           (nnheader-report 'nnspool "Empty group %s" group)
235           (nnheader-insert "211 0 0 0 %s\n" group))))))
236
237 (deffoo nnspool-request-type (group &optional article)
238   'news)
239
240 (deffoo nnspool-close-group (group &optional server)
241   t)
242
243 (deffoo nnspool-request-list (&optional server)
244   "List active newsgroups."
245   (save-excursion
246     (or (nnspool-find-file nnspool-active-file)
247         (nnheader-report 'nnspool (nnheader-file-error nnspool-active-file)))))
248
249 (deffoo nnspool-request-list-newsgroups (&optional server)
250   "List newsgroups (defined in NNTP2)."
251   (save-excursion
252     (or (nnspool-find-file nnspool-newsgroups-file)
253         (nnheader-report 'nnspool (nnheader-file-error 
254                                    nnspool-newsgroups-file)))))
255
256 (deffoo nnspool-request-list-distributions (&optional server)
257   "List distributions (defined in NNTP2)."
258   (save-excursion
259     (or (nnspool-find-file nnspool-distributions-file)
260         (nnheader-report 'nnspool (nnheader-file-error 
261                                    nnspool-distributions-file)))))
262
263 ;; Suggested by Hallvard B Furuseth <h.b.furuseth@usit.uio.no>.
264 (deffoo nnspool-request-newgroups (date &optional server)
265   "List groups created after DATE."
266   (if (nnspool-find-file nnspool-active-times-file)
267       (save-excursion
268         ;; Find the last valid line.
269         (goto-char (point-max))
270         (while (and (not (looking-at 
271                           "\\([^ ]+\\) +\\([0-9]+\\)[0-9][0-9][0-9] "))
272                     (zerop (forward-line -1))))
273         (let ((seconds (nnspool-seconds-since-epoch date))
274               groups)
275           ;; Go through lines and add the latest groups to a list.
276           (while (and (looking-at "\\([^ ]+\\) +[0-9]+ ")
277                       (progn
278                         ;; We insert a .0 to make the list reader
279                         ;; interpret the number as a float. It is far
280                         ;; too big to be stored in a lisp integer. 
281                         (goto-char (1- (match-end 0)))
282                         (insert ".0")
283                         (> (progn
284                              (goto-char (match-end 1))
285                              (read (current-buffer)))
286                            seconds))
287                       (setq groups (cons (buffer-substring
288                                           (match-beginning 1) (match-end 1))
289                                          groups))
290                       (zerop (forward-line -1))))
291           (erase-buffer)
292           (while groups
293             (insert (car groups) " 0 0 y\n")
294             (setq groups (cdr groups))))
295         t)
296     nil))
297
298 (deffoo nnspool-request-post (&optional server)
299   "Post a new news in current buffer."
300   (save-excursion
301     (let* ((process-connection-type nil) ; t bugs out on Solaris
302            (inews-buffer (generate-new-buffer " *nnspool post*"))
303            (proc 
304             (condition-case err
305                 (apply 'start-process "*nnspool inews*" inews-buffer
306                        nnspool-inews-program nnspool-inews-switches)
307               (error
308                (nnheader-report 'nnspool "inews error: %S" err)))))
309       (if (not proc)
310           ;; The inews program failed.
311           ()
312         (nnheader-report 'nnspool "")
313         (set-process-sentinel proc 'nnspool-inews-sentinel)
314         (process-send-region proc (point-min) (point-max))
315         ;; We slap a condition-case around this, because the process may
316         ;; have exited already...
317         (condition-case nil
318             (process-send-eof proc)
319           (error nil))
320         t))))
321
322
323 \f
324 ;;; Internal functions.
325
326 (defun nnspool-inews-sentinel (proc status)
327   (save-excursion
328     (set-buffer (process-buffer proc))
329     (goto-char (point-min))
330     (if (or (zerop (buffer-size))
331             (search-forward "spooled" nil t))
332         (kill-buffer (current-buffer))
333       ;; Make status message by folding lines.
334       (while (re-search-forward "[ \t\n]+" nil t)
335         (replace-match " " t t))
336       (nnheader-report 'nnspool "%s" (buffer-string))
337       (message "nnspool: %s" nnspool-status-string)
338       (ding)
339       (run-hooks 'nnspool-rejected-article-hook))))
340
341 (defun nnspool-retrieve-headers-with-nov (articles &optional fetch-old)
342   (if (or gnus-nov-is-evil nnspool-nov-is-evil)
343       nil
344     (let ((nov (nnheader-group-pathname 
345                 nnspool-current-group nnspool-nov-directory ".overview"))
346           (arts articles)
347           last)
348       (if (not (file-exists-p nov))
349           ()
350         (save-excursion
351           (set-buffer nntp-server-buffer)
352           (erase-buffer)
353           (if nnspool-sift-nov-with-sed
354               (nnspool-sift-nov-with-sed articles nov)
355             (insert-file-contents nov)
356             (if (and fetch-old
357                      (not (numberp fetch-old)))
358                 t                       ; We want all the headers.
359               ;; First we find the first wanted line.
360               (nnspool-find-nov-line
361                (if fetch-old (max 1 (- (car articles) fetch-old))
362                  (car articles)))
363               (delete-region (point-min) (point))
364               ;; Then we find the last wanted line. 
365               (if (nnspool-find-nov-line 
366                    (progn (while (cdr articles) (setq articles (cdr articles)))
367                           (car articles)))
368                   (forward-line 1))
369               (delete-region (point) (point-max))
370               ;; If the buffer is empty, this wasn't very successful.
371               (unless (zerop (buffer-size))
372                 ;; We check what the last article number was.  The NOV file
373                 ;; may be out of sync with the articles in the group.
374                 (forward-line -1)
375                 (setq last (read (current-buffer)))
376                 (if (= last (car articles))
377                     ;; Yup, it's all there.
378                     t
379                   ;; Perhaps not.  We try to find the missing articles.
380                   (while (and arts
381                               (<= last (car arts)))
382                     (pop arts))
383                   ;; The articles in `arts' are missing from the buffer.
384                   (while arts
385                     (nnspool-insert-nov-head (pop arts)))
386                   t)))))))))
387
388 (defun nnspool-insert-nov-head (article)
389   "Read the head of ARTICLE, convert to NOV headers, and insert."
390   (save-excursion
391     (let ((cur (current-buffer))
392           buf)
393       (setq buf (nnheader-set-temp-buffer " *nnspool head*"))
394       (when (nnheader-insert-head
395              (nnspool-article-pathname nnspool-current-group article))
396         (nnheader-insert-article-line article)
397         (let ((headers (nnheader-parse-head)))
398           (set-buffer cur)
399           (goto-char (point-max))
400           (nnheader-insert-nov headers)))
401       (kill-buffer buf))))
402
403 (defun nnspool-find-nov-line (article)
404   (let ((max (point-max))
405         (min (goto-char (point-min)))
406         (cur (current-buffer))
407         (prev (point-min))
408         num found)
409     (while (not found)
410       (goto-char (/ (+ max min) 2))
411       (beginning-of-line)
412       (if (or (= (point) prev)
413               (eobp))
414           (setq found t)
415         (setq prev (point))
416         (cond ((> (setq num (read cur)) article)
417                (setq max (point)))
418               ((< num article)
419                (setq min (point)))
420               (t
421                (setq found 'yes)))))
422     ;; Now we may have found the article we're looking for, or we
423     ;; may be somewhere near it.
424     (when (and (not (eq found 'yes))
425                (not (eq num article)))
426       (setq found (point))
427       (while (and (< (point) max)
428                   (or (not (numberp num))
429                       (< num article)))
430         (forward-line 1)
431         (setq found (point))
432         (or (eobp)
433             (= (setq num (read cur)) article)))
434       (unless (eq num article)
435         (goto-char found)))
436     (beginning-of-line)
437     (eq num article)))
438     
439 (defun nnspool-sift-nov-with-sed (articles file)
440   (let ((first (car articles))
441         (last (progn (while (cdr articles) (setq articles (cdr articles)))
442                      (car articles))))
443     (call-process "awk" nil t nil 
444                   (format "BEGIN {firstmsg=%d; lastmsg=%d;}\n $1 >= firstmsg && $1 <= lastmsg {print;}"
445                           (1- first) (1+ last))
446                   file)))
447
448 ;; Fixed by fdc@cliwe.ping.de (Frank D. Cringle). 
449 ;; Find out what group an article identified by a Message-ID is in.
450 (defun nnspool-find-id (id)
451   (save-excursion
452     (set-buffer (get-buffer-create " *nnspool work*"))
453     (buffer-disable-undo (current-buffer))
454     (erase-buffer)
455     (condition-case ()
456         (call-process "grep" nil t nil id nnspool-history-file)
457       (error nil))
458     (goto-char (point-min))
459     (prog1
460         (if (looking-at "<[^>]+>[ \t]+[-0-9~]+[ \t]+\\([^ /\t\n]+\\)/\\([0-9]+\\)[ \t\n]")
461             (cons (match-string 1) (string-to-int (match-string 2))))
462       (kill-buffer (current-buffer)))))
463
464 (defun nnspool-find-file (file)
465   "Insert FILE in server buffer safely."
466   (set-buffer nntp-server-buffer)
467   (erase-buffer)
468   (condition-case ()
469       (progn (nnheader-insert-raw-file-contents file) t)
470     (file-error nil)))
471
472 (defun nnspool-possibly-change-directory (group)
473   (if (not group)
474       t
475     (let ((pathname (nnspool-article-pathname group)))
476       (if (file-directory-p pathname)
477           (setq nnspool-current-directory pathname
478                 nnspool-current-group group)
479         (nnheader-report 'nnspool "No such newsgroup: %s" group)))))
480
481 (defun nnspool-article-pathname (group &optional article)
482   "Find the path for GROUP."
483   (nnheader-group-pathname group nnspool-spool-directory article))
484
485 (defun nnspool-seconds-since-epoch (date)
486   (let* ((tdate (mapcar (lambda (ti) (and ti (string-to-int ti)))
487                         (timezone-parse-date date)))
488          (ttime (mapcar (lambda (ti) (and ti (string-to-int ti)))
489                         (timezone-parse-time
490                          (aref (timezone-parse-date date) 3))))
491          (unix (encode-time (nth 2 ttime) (nth 1 ttime) (nth 0 ttime)
492                             (nth 2 tdate) (nth 1 tdate) (nth 0 tdate) 
493                             (nth 4 tdate))))
494     (+ (* (car unix) 65536.0)
495        (cadr unix))))
496
497 (provide 'nnspool)
498
499 ;;; nnspool.el ends here