1 ;;; nnspool.el --- spool access for GNU Emacs
3 ;; Copyright (C) 1988, 89, 90, 93, 94, 95 Free Software Foundation, Inc.
5 ;; Author: Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
6 ;; Lars Ingebrigtsen <larsi@ifi.uio.no>
9 ;; This file is part of GNU Emacs.
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)
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.
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.
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.
33 ;; `nntp-server-buffer' - All data should be returned to Gnus in this
36 ;; Functions for the imaginary backend `choke':
38 ;; `choke-retrieve-headers ARTICLES &optional GROUP SERVER'
39 ;; Should return all headers for all ARTICLES, or return NOV lines for
42 ;; `choke-request-group GROUP &optional SERVER DISCARD'
43 ;; Switch to GROUP. If DISCARD is nil, active information on the group
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.
51 ;; `choke-request-article ARTICLE &optional GROUP SERVER'
52 ;; Return ARTICLE, which is either an article number or id.
54 ;; `choke-request-list SERVER'
55 ;; Return a list of all active newsgroups on SERVER.
57 ;; `choke-request-list-newsgroups SERVER'
58 ;; Return a list of descriptions of all newsgroups on SERVER.
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
66 ;; `choke-request-post &optional SERVER'
67 ;; Function that will be called from a buffer to be posted.
69 ;; `choke-open-server SERVER &optional ARGUMENT'
70 ;; Open a connection to SERVER.
72 ;; `choke-close-server &optional SERVER'
73 ;; Close the connection to server.
75 ;; `choke-server-opened &optional SERVER'
76 ;; Whether the server is opened or not.
78 ;; `choke-server-status &optional SERVER'
79 ;; Should return a status string (not in nntp buffer, but as the
80 ;; result of the function).
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.
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).
98 (defvar nnspool-inews-program news-inews-program
99 "Program to post news.")
101 (defvar nnspool-inews-switches '("-h")
102 "Switches for nnspool-request-post to pass to `inews' for posting news.")
104 (defvar nnspool-spool-directory news-path
105 "Local news spool directory.")
107 (defvar nnspool-lib-dir "/usr/lib/news/"
108 "Where the local news library files are stored.")
110 (defvar nnspool-active-file (concat nnspool-lib-dir "active")
111 "Local news active file.")
113 (defvar nnspool-newsgroups-file (concat nnspool-lib-dir "newsgroups")
114 "Local news newsgroups file.")
116 (defvar nnspool-distributions-file (concat nnspool-lib-dir "distributions")
117 "Local news distributions file.")
119 (defvar nnspool-history-file (concat nnspool-lib-dir "history")
120 "Local news history file.")
122 (defvar nnspool-active-times-file (concat nnspool-lib-dir "active.times")
123 "Local news active date file.")
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.")
130 (defvar nnspool-nov-is-evil nil
131 "Non-nil means that nnspool will never return NOV lines instead of headers.")
135 (defconst nnspool-version "nnspool 2.0"
136 "Version numbers of this version of NNSPOOL.")
138 (defvar nnspool-current-directory nil
139 "Current news group directory.")
141 (defvar nnspool-status-string "")
145 ;;; Interface functions.
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."
151 (set-buffer nntp-server-buffer)
153 (let* ((number (length sequence))
155 (do-message (and (numberp nnspool-large-newsgroup)
156 (> number nnspool-large-newsgroup)))
158 (nnspool-possibly-change-directory newsgroup)
159 (if (nnspool-retrieve-headers-with-nov sequence)
162 (setq article (car sequence))
163 (setq file (concat nnspool-current-directory
164 (int-to-string article)))
165 (and (file-exists-p file)
167 (insert (format "221 %d Article retrieved.\n" article))
169 (insert-file-contents file)
171 (search-forward "\n\n" nil t)
174 (delete-region (point) (point-max))))
175 (setq sequence (cdr sequence))
178 (zerop (% (setq count (1+ count)) 20))
179 (message "NNSPOOL: Receiving headers... %d%%"
180 (/ (* count 100) number))))
182 (and do-message (message "NNSPOOL: Receiving headers... done"))
184 ;; Fold continuation lines.
186 (while (re-search-forward "\\(\r?\n[ \t]+\\)+" nil t)
187 (replace-match " " t t))
190 (defun nnspool-open-server (host &optional service)
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))
197 (setq nnspool-status-string
198 (format "NNSPOOL: cannot talk to %s." host))
201 (defun nnspool-close-server (&optional server)
203 (nnspool-close-server-internal))
205 (fset 'nnspool-request-quit (symbol-function 'nnspool-close-server))
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)))
213 (defun nnspool-status-message ()
214 "Return server status response as string."
215 nnspool-status-string)
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)
226 (not (file-directory-p file)))
228 (nnspool-find-file file)))))
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)
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)))
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)
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)))
252 (defun nnspool-request-group (group &optional server dont-check)
254 (let ((pathname (nnspool-article-pathname
255 (nnspool-replace-chars-in-string group ?. ?/)))
257 (if (file-directory-p pathname)
259 (setq nnspool-current-directory pathname)
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>
271 (string-to-int name)))
275 (set-buffer nntp-server-buffer)
279 (format "211 %d %d %d %s\n" (length dir) (car dir)
280 (progn (while (cdr dir) (setq dir (cdr dir)))
283 (insert (format "211 0 0 0 %s\n" group))))))
286 (defun nnspool-close-group (group &optional server)
289 (defun nnspool-request-list (&optional server)
290 "List active newsgoups."
292 (nnspool-find-file nnspool-active-file)))
294 (defun nnspool-request-list-newsgroups (&optional server)
295 "List newsgroups (defined in NNTP2)."
297 (nnspool-find-file nnspool-newsgroups-file)))
299 (defun nnspool-request-list-distributions (&optional server)
300 "List distributions (defined in NNTP2)."
302 (nnspool-find-file nnspool-distributions-file)))
304 (defun nnspool-request-newgroups (date &optional server)
305 "List groups created after DATE."
307 (nnspool-find-file nnspool-active-times-file)
308 (setq nnspool-status-string "NEWGROUPS is not supported.")
311 (defun nnspool-request-post (&optional server)
312 "Post a new news in current buffer."
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)
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))
333 (fset 'nnspool-request-post-buffer 'nntp-request-post-buffer)
336 ;;; Internal functions.
338 (defun nnspool-retrieve-headers-with-nov (articles)
339 (if (or gnus-nov-is-evil nnspool-nov-is-evil)
341 (let ((nov (concat nnspool-current-directory ".nov"))
343 (if (file-exists-p nov)
345 (set-buffer nntp-server-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.
353 (setq article (concat (int-to-string
354 (car articles) "\t")))
355 (not (or (looking-at article)
356 (search-forward (concat "\n" article)
358 (setq articles (cdr articles)))
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))
374 (not (search-backward
375 (concat "\n" (int-to-string (car articles))
377 (setq articles (cdr articles)))
381 (delete-region (point) (point-max)))))
382 (or articles (progn (erase-buffer) nil)))))))
384 (defun nnspool-open-server-internal (host &optional service)
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))
391 (kill-all-local-variables)
392 (setq case-fold-search t) ;Should ignore case.
395 (defun nnspool-close-server-internal ()
396 "Close connection to news server."
399 (defun nnspool-find-article-by-message-id (id)
400 "Return full pathname of an article identified by message-ID."
402 (set-buffer nntp-server-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))
412 (defun nnspool-find-file (file)
413 "Insert FILE in server buffer safely."
414 (set-buffer nntp-server-buffer)
417 (progn (insert-file-contents file) t)
420 (defun nnspool-possibly-change-directory (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)))))
428 (defun nnspool-article-pathname (group)
429 "Make pathname for GROUP."
430 (concat (file-name-as-directory nnspool-spool-directory) group "/"))
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))
437 ;; Replace all occurrences of FROM with TO.
439 (if (= (aref string idx) from)
440 (aset string idx to))
446 ;;; nnspool.el ends here