*** empty log message ***
[gnus] / lisp / nnspool.el
1 ;;; nnspool.el --- spool access for GNU Emacs
2
3 ;; Copyright (C) 1988, 89, 90, 93, 94, 95 Free Software Foundation, Inc.
4
5 ;; Author: Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
6 ;;      Lars Ingebrigtsen <larsi@ifi.uio.no>
7 ;; Keywords: news
8
9 ;; This file is part of GNU Emacs.
10
11 ;; GNU Emacs is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation; either version 2, or (at your option)
14 ;; any later version.
15
16 ;; GNU Emacs is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 ;; GNU General Public License for more details.
20
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs; see the file COPYING.  If not, write to
23 ;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
24
25 ;;; Commentary:
26
27 ;; All the Gnus backends have the same interface, and should return
28 ;; data in a similar format. Below is and overview of what functions
29 ;; these packages must supply and what result they should return.
30 ;;
31 ;; Variables:
32 ;;
33 ;; `nntp-server-buffer' - All data should be returned to Gnus in this
34 ;; buffer. 
35 ;;
36 ;; Functions for the imaginary backend `choke':
37 ;;
38 ;; `choke-retrieve-headers ARTICLES &optional GROUP SERVER'
39 ;; Should return all headers for all ARTICLES, or return NOV lines for
40 ;; the same.
41 ;;
42 ;; `choke-request-group GROUP &optional SERVER DISCARD'
43 ;; Switch to GROUP. If DISCARD is nil, active information on the group
44 ;; must be returned.
45 ;;
46 ;; `choke-close-group GROUP &optional SERVER'
47 ;; Close group. Most backends won't have to do anything with this
48 ;; call, but it is an opportunity to clean up, if that is needed. It
49 ;; is called when Gnus exits a group.
50 ;;
51 ;; `choke-request-article ARTICLE &optional GROUP SERVER'
52 ;; Return ARTICLE, which is either an article number or id.
53 ;;
54 ;; `choke-request-list SERVER'
55 ;; Return a list of all active newsgroups on SERVER.
56 ;;
57 ;; `choke-request-list-newsgroups SERVER'
58 ;; Return a list of descriptions of all newsgroups on SERVER.
59 ;;
60 ;; `choke-request-post-buffer METHOD HEADER ARTICLE-BUFFER GROUP INFO'
61 ;; Should return a buffer that is suitable for "posting". nnspool and
62 ;; nntp return a `*post-buffer*', and nnmail return a `*mail*'
63 ;; buffer. This function should fill out the appropriate header
64 ;; fields. 
65 ;;
66 ;; `choke-request-post &optional SERVER'
67 ;; Function that will be called from a buffer to be posted. 
68 ;;
69 ;; `choke-open-server SERVER &optional ARGUMENT'
70 ;; Open a connection to SERVER.
71 ;;
72 ;; `choke-close-server &optional SERVER'
73 ;; Close the connection to server.
74 ;;
75 ;; `choke-server-opened &optional SERVER'
76 ;; Whether the server is opened or not.
77 ;;
78 ;; `choke-server-status &optional SERVER'
79 ;; Should return a status string (not in nntp buffer, but as the
80 ;; result of the function).
81 ;;
82 ;; `choke-request-expire-articles ARTICLES &optional NEWSGROUP SERVER'
83 ;; Should expire (according to some aging scheme) all ARTICLES. Most
84 ;; backends will not be able to expire articles. Should return a list
85 ;; of all articles that were not expired.
86 ;;
87 ;; All these functions must return nil if they couldn't service the
88 ;; request. If the optional arguments are not supplied, some "current"
89 ;; or "default" values should be used. In short, one should emulate an
90 ;; NNTP server, in a way. All results should be returned in the NNTP
91 ;; format. (See RFC977).
92
93 ;;; Code:
94
95 (require 'nnheader)
96 (require 'nntp)
97
98 (defvar nnspool-inews-program news-inews-program
99   "Program to post news.")
100
101 (defvar nnspool-inews-switches '("-h")
102   "Switches for nnspool-request-post to pass to `inews' for posting news.")
103
104 (defvar nnspool-spool-directory news-path
105   "Local news spool directory.")
106
107 (defvar nnspool-lib-dir "/usr/lib/news/"
108   "Where the local news library files are stored.")
109
110 (defvar nnspool-active-file (concat nnspool-lib-dir "active")
111   "Local news active file.")
112
113 (defvar nnspool-newsgroups-file (concat nnspool-lib-dir "newsgroups")
114   "Local news newsgroups file.")
115
116 (defvar nnspool-distributions-file (concat nnspool-lib-dir "distributions")
117   "Local news distributions file.")
118
119 (defvar nnspool-history-file (concat nnspool-lib-dir "history")
120   "Local news history file.")
121
122 (defvar nnspool-active-times-file (concat nnspool-lib-dir "active.times")
123   "Local news active date file.")
124
125 (defvar nnspool-large-newsgroup 50
126   "The number of the articles which indicates a large newsgroup.
127 If the number of the articles is greater than the value, verbose
128 messages will be shown to indicate the current status.")
129
130 (defvar nnspool-nov-is-evil nil
131   "Non-nil means that nnspool will never return NOV lines instead of headers.")
132
133 \f
134
135 (defconst nnspool-version "nnspool 2.0"
136   "Version numbers of this version of NNSPOOL.")
137
138 (defvar nnspool-current-directory nil
139   "Current news group directory.")
140
141 (defvar nnspool-status-string "")
142
143 \f
144
145 ;;; Interface functions.
146
147 (defun nnspool-retrieve-headers (sequence &optional newsgroup server)
148   "Retrieve the headers for the articles in SEQUENCE.
149 Newsgroup must be selected before calling this function."
150   (save-excursion
151     (set-buffer nntp-server-buffer)
152     (erase-buffer)
153     (let* ((number (length sequence))
154            (count 0)
155            (do-message (and (numberp nnspool-large-newsgroup)
156                             (> number nnspool-large-newsgroup)))
157            file beg article)
158       (nnspool-possibly-change-directory newsgroup)
159       (if (nnspool-retrieve-headers-with-nov sequence)
160           'nov
161         (while sequence
162           (setq article (car sequence))
163           (setq file (concat nnspool-current-directory 
164                              (int-to-string article)))
165           (and (file-exists-p file)
166                (progn
167                  (insert (format "221 %d Article retrieved.\n" article))
168                  (setq beg (point))
169                  (insert-file-contents file)
170                  (goto-char beg)
171                  (search-forward "\n\n" nil t)
172                  (forward-char -1)
173                  (insert ".\n")
174                  (delete-region (point) (point-max))))
175           (setq sequence (cdr sequence))
176
177           (and do-message
178                (zerop (% (setq count (1+ count)) 20))
179                (message "NNSPOOL: Receiving headers... %d%%"
180                         (/ (* count 100) number))))
181
182         (and do-message (message "NNSPOOL: Receiving headers... done"))
183
184         ;; Fold continuation lines.
185         (goto-char 1)
186         (while (re-search-forward "\\(\r?\n[ \t]+\\)+" nil t)
187           (replace-match " " t t))
188         'headers))))
189
190 (defun nnspool-open-server (host &optional service)
191   "Open local spool."
192   (setq nnspool-status-string "")
193   (cond ((and (file-directory-p nnspool-spool-directory)
194               (file-exists-p nnspool-active-file))
195          (nnspool-open-server-internal host service))
196         (t
197          (setq nnspool-status-string
198                (format "NNSPOOL: cannot talk to %s." host))
199          nil)))
200
201 (defun nnspool-close-server (&optional server)
202   "Close news server."
203   (nnspool-close-server-internal))
204
205 (fset 'nnspool-request-quit (symbol-function 'nnspool-close-server))
206
207 (defun nnspool-server-opened (&optional server)
208   "Return server process status, T or NIL.
209 If the stream is opened, return T, otherwise return NIL."
210   (and nntp-server-buffer
211        (get-buffer nntp-server-buffer)))
212
213 (defun nnspool-status-message ()
214   "Return server status response as string."
215   nnspool-status-string)
216
217 (defun nnspool-request-article (id &optional newsgroup server buffer)
218   "Select article by message ID (or number)."
219   (nnspool-possibly-change-directory newsgroup)
220   (let ((file (if (stringp id)
221                   (nnspool-find-article-by-message-id id)
222                 (concat nnspool-current-directory (prin1-to-string id))))
223         (nntp-server-buffer (or buffer nntp-server-buffer)))
224     (if (and (stringp file)
225              (file-exists-p file)
226              (not (file-directory-p file)))
227         (save-excursion
228           (nnspool-find-file file)))))
229
230 (defun nnspool-request-body (id &optional newsgroup server)
231   "Select article body by message ID (or number)."
232   (nnspool-possibly-change-directory newsgroup)
233   (if (nnspool-request-article id)
234       (save-excursion
235         (set-buffer nntp-server-buffer)
236         (goto-char (point-min))
237         (if (search-forward "\n\n" nil t)
238             (delete-region (point-min) (point)))
239         t)))
240
241 (defun nnspool-request-head (id &optional newsgroup server)
242   "Select article head by message ID (or number)."
243   (nnspool-possibly-change-directory newsgroup)
244   (if (nnspool-request-article id)
245       (save-excursion
246         (set-buffer nntp-server-buffer)
247         (goto-char (point-min))
248         (if (search-forward "\n\n" nil t)
249             (delete-region (1- (point)) (point-max)))
250         t)))
251
252 (defun nnspool-request-group (group &optional server dont-check)
253   "Select news GROUP."
254   (let ((pathname (nnspool-article-pathname
255                    (nnspool-replace-chars-in-string group ?. ?/)))
256         dir)
257     (if (file-directory-p pathname)
258         (progn
259           (setq nnspool-current-directory pathname)
260           (if (not dont-check)
261               (progn
262                 (setq dir (directory-files pathname nil "^[0-9]+$" t))
263                 ;; yes, completely empty spool directories *are* possible
264                 ;; Fix by Sudish Joseph <joseph@cis.ohio-state.edu>
265                 (and dir
266                     (setq dir
267                           (sort 
268                            (mapcar
269                             (function
270                              (lambda (name)
271                                (string-to-int name)))
272                             dir)
273                            '<)))
274                 (save-excursion
275                   (set-buffer nntp-server-buffer)
276                   (erase-buffer)
277                   (if dir
278                       (insert
279                        (format "211 %d %d %d %s\n" (length dir) (car dir)
280                                (progn (while (cdr dir) (setq dir (cdr dir)))
281                                       (car dir))
282                                group))
283                     (insert (format "211 0 0 0 %s\n" group))))))
284           t))))
285
286 (defun nnspool-close-group (group &optional server)
287   t)
288
289 (defun nnspool-request-list (&optional server)
290   "List active newsgoups."
291   (save-excursion
292     (nnspool-find-file nnspool-active-file)))
293
294 (defun nnspool-request-list-newsgroups (&optional server)
295   "List newsgroups (defined in NNTP2)."
296   (save-excursion
297     (nnspool-find-file nnspool-newsgroups-file)))
298
299 (defun nnspool-request-list-distributions (&optional server)
300   "List distributions (defined in NNTP2)."
301   (save-excursion
302     (nnspool-find-file nnspool-distributions-file)))
303
304 (defun nnspool-request-newgroups (date &optional server)
305   "List groups created after DATE."
306   (save-excursion
307     (nnspool-find-file nnspool-active-times-file)
308     (setq nnspool-status-string "NEWGROUPS is not supported.")
309     nil))
310
311 (defun nnspool-request-post (&optional server)
312   "Post a new news in current buffer."
313   (save-excursion
314     ;; We have to work in the server buffer because of NEmacs hack.
315     (copy-to-buffer nntp-server-buffer (point-min) (point-max))
316     (set-buffer nntp-server-buffer)
317     (apply (function call-process-region)
318            (point-min) (point-max)
319            nnspool-inews-program 'delete t nil nnspool-inews-switches)
320     (prog1
321         (or (zerop (buffer-size))
322             ;; If inews returns strings, it must be error message 
323             ;;  unless SPOOLNEWS is defined.  
324             ;; This condition is very weak, but there is no good rule 
325             ;;  identifying errors when SPOOLNEWS is defined.  
326             ;; Suggested by ohm@kaba.junet.
327             (string-match "spooled" (buffer-string)))
328       ;; Make status message by unfolding lines.
329       (subst-char-in-region (point-min) (point-max) ?\n ?\\ 'noundo)
330       (setq nnspool-status-string (buffer-string))
331       (erase-buffer))))
332
333 (fset 'nnspool-request-post-buffer 'nntp-request-post-buffer)
334
335 \f
336 ;;; Internal functions.
337
338 (defun nnspool-retrieve-headers-with-nov (articles)
339   (if (or gnus-nov-is-evil nnspool-nov-is-evil)
340       nil
341     (let ((nov (concat nnspool-current-directory ".nov"))
342           article)
343       (if (file-exists-p nov)
344           (save-excursion
345             (set-buffer nntp-server-buffer)
346             (erase-buffer)
347             (insert-file-contents nov)
348             ;; First we find the first wanted line. We issue a number
349             ;; of search-forwards - the first article we are lookign
350             ;; for may be expired, so we have to go on searching until
351             ;; we find one of the articles we want.
352             (while (and articles
353                         (setq article (concat (int-to-string 
354                                                (car articles) "\t")))
355                         (not (or (looking-at article)
356                                  (search-forward (concat "\n" article) 
357                                                  nil t))))
358               (setq articles (cdr articles)))
359             (if (not articles)
360                 ()
361               (beginning-of-line)
362               (delete-region (point-min) (point))
363               ;; Then we find the last wanted line. We go to the end
364               ;; of the buffer and search backward much the same way
365               ;; we did to find the first article.
366               ;; !!! Perhaps it would be better just to do a (last articles), 
367               ;; and go forward successively over each line and
368               ;; compare to avoid this (reverse), like this:
369               ;; (while (and (>= last (read nntp-server-buffer)))
370               ;;             (zerop (forward-line 1))))
371               (setq articles (reverse articles))
372               (goto-char (point-max))
373               (while (and articles
374                           (not (search-backward 
375                                 (concat "\n" (int-to-string (car articles))
376                                         "\t") nil t)))
377                 (setq articles (cdr articles)))
378               (if articles
379                   (progn
380                     (forward-line 2)
381                     (delete-region (point) (point-max)))))
382             (or articles (progn (erase-buffer) nil)))))))
383
384 (defun nnspool-open-server-internal (host &optional service)
385   (save-excursion
386     ;; Initialize communication buffer.
387     (setq nntp-server-buffer (get-buffer-create " *nntpd*"))
388     (set-buffer nntp-server-buffer)
389     (buffer-disable-undo (current-buffer))
390     (erase-buffer)
391     (kill-all-local-variables)
392     (setq case-fold-search t)           ;Should ignore case.
393     t))
394
395 (defun nnspool-close-server-internal ()
396   "Close connection to news server."
397   )
398
399 (defun nnspool-find-article-by-message-id (id)
400   "Return full pathname of an article identified by message-ID."
401   (save-excursion
402     (set-buffer nntp-server-buffer)
403     (erase-buffer)
404     (call-process "grep" nil t nil id nnspool-history-file)
405     (goto-char (point-min))
406     (if (looking-at "<[^>]+>[ \t]+[-0-9~]+[ \t]+\\(.*\\)$")
407         (concat nnspool-spool-directory
408                 (nnspool-replace-chars-in-string 
409                  (buffer-substring (match-beginning 1) (match-end 1)) 
410                  ?. ?/)))))
411
412 (defun nnspool-find-file (file)
413   "Insert FILE in server buffer safely."
414   (set-buffer nntp-server-buffer)
415   (erase-buffer)
416   (condition-case ()
417       (progn (insert-file-contents file) t)
418     (file-error nil)))
419
420 (defun nnspool-possibly-change-directory (newsgroup)
421   (if newsgroup
422       (let ((pathname (nnspool-article-pathname
423                        (nnspool-replace-chars-in-string newsgroup ?. ?/))))
424         (if (file-directory-p pathname)
425             (setq nnspool-current-directory pathname)
426           (error "No such newsgroup: %s" newsgroup)))))
427
428 (defun nnspool-article-pathname (group)
429   "Make pathname for GROUP."
430   (concat (file-name-as-directory nnspool-spool-directory) group "/"))
431
432 (defun nnspool-replace-chars-in-string (string from to)
433   "Replace characters in STRING from FROM to TO."
434   (let ((string (substring string 0))   ;Copy string.
435         (len (length string))
436         (idx 0))
437     ;; Replace all occurrences of FROM with TO.
438     (while (< idx len)
439       (if (= (aref string idx) from)
440           (aset string idx to))
441       (setq idx (1+ idx)))
442     string))
443
444 (provide 'nnspool)
445
446 ;;; nnspool.el ends here