a8ecd67c9eba431d95fa9284bf3d28ca15dec72a
[gnus] / lisp / nndoc.el
1 ;;; nndoc.el --- single file access for Gnus
2 ;; Copyright (C) 1995,96,97 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
5 ;;      Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
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 'message)
31 (require 'nnmail)
32 (require 'nnoo)
33 (eval-when-compile (require 'cl))
34
35 (nnoo-declare nndoc)
36
37 (defvoo nndoc-article-type 'guess
38   "*Type of the file.
39 One of `mbox', `babyl', `digest', `news', `rnews', `mmdf', `forward',
40 `rfc934', `mime-digest', `standard-digest', `slack-digest',
41 `clari-briefs' or `guess'.")
42
43 (defvoo nndoc-post-type 'mail
44   "*Whether the nndoc group is `mail' or `post'.")
45
46 (defvar nndoc-type-alist 
47   `((mmdf 
48      (article-begin .  "^\^A\^A\^A\^A\n")
49      (body-end .  "^\^A\^A\^A\^A\n"))
50     (news
51      (article-begin . "^Path:"))
52     (rnews
53      (article-begin . "^#! *rnews +\\([0-9]+\\) *\n")
54      (body-end-function . nndoc-rnews-body-end))
55     (mbox 
56      (article-begin-function . nndoc-mbox-article-begin)
57      (body-end-function . nndoc-mbox-body-end))
58     (babyl 
59      (article-begin . "\^_\^L *\n")
60      (body-end . "\^_")
61      (body-begin-function . nndoc-babyl-body-begin)
62      (head-begin-function . nndoc-babyl-head-begin))
63     (forward
64      (article-begin . "^-+ Start of forwarded message -+\n+")
65      (body-end . "^-+ End of forwarded message -+$")
66      (prepare-body-function . nndoc-unquote-dashes))
67     (rfc934
68      (article-begin . "^--.*\n+")
69      (body-end . "^--.*$")
70      (prepare-body-function . nndoc-unquote-dashes))
71     (clari-briefs
72      (article-begin . "^ \\*")
73      (body-end . "^\t------*[ \t]^*\n^ \\*")
74      (body-begin . "^\t")
75      (head-end . "^\t")
76      (generate-head-function . nndoc-generate-clari-briefs-head)
77      (article-transform-function . nndoc-transform-clari-briefs))
78     (mime-digest
79      (article-begin . "")
80      (head-end . "^ ?$")
81      (body-end . "")
82      (file-end . "")
83      (subtype digest guess))
84     (standard-digest
85      (first-article . ,(concat "^" (make-string 70 ?-) "\n\n+"))
86      (article-begin . ,(concat "^\n" (make-string 30 ?-) "\n\n+"))
87      (prepare-body-function . nndoc-unquote-dashes)
88      (body-end-function . nndoc-digest-body-end)
89      (head-end . "^ ?$")
90      (body-begin . "^ ?\n")
91      (file-end . "^End of .*digest.*[0-9].*\n\\*\\*\\|^End of.*Digest *$")
92      (subtype digest guess))
93     (slack-digest
94      (article-begin . "^------------------------------*[\n \t]+")
95      (head-end . "^ ?$")
96      (body-end-function . nndoc-digest-body-end)
97      (body-begin . "^ ?$")
98      (file-end . "^End of")
99      (prepare-body-function . nndoc-unquote-dashes)
100      (subtype digest guess))
101     (lanl-gov-announce
102       (article-begin . "^\\\\\\\\\n")
103       (head-begin . "^Paper.*:")
104       (head-end   . "\\(^\\\\\\\\.*\n\\|-----------------\\)")
105       (body-begin . "")
106       (body-end   . "-------------------------------------------------")
107       (file-end   . "^Title: Recent Seminal")
108       (generate-head-function . nndoc-generate-lanl-gov-head)
109       (article-transform-function . nndoc-transform-lanl-gov-announce)
110       (subtype preprints guess))
111     (guess 
112      (guess . t)
113      (subtype nil))
114     (digest
115      (guess . t)
116      (subtype nil))
117     (preprints
118      (guess . t)
119      (subtype nil))
120     ))
121
122 \f
123
124 (defvoo nndoc-file-begin nil)
125 (defvoo nndoc-first-article nil)
126 (defvoo nndoc-article-end nil)
127 (defvoo nndoc-article-begin nil)
128 (defvoo nndoc-head-begin nil)
129 (defvoo nndoc-head-end nil)
130 (defvoo nndoc-file-end nil)
131 (defvoo nndoc-body-begin nil)
132 (defvoo nndoc-body-end-function nil)
133 (defvoo nndoc-body-begin-function nil)
134 (defvoo nndoc-head-begin-function nil)
135 (defvoo nndoc-body-end nil)
136 (defvoo nndoc-dissection-alist nil)
137 (defvoo nndoc-prepare-body-function nil)
138 (defvoo nndoc-generate-head-function nil)
139 (defvoo nndoc-article-transform-function nil)
140 (defvoo nndoc-article-begin-function nil)
141
142 (defvoo nndoc-status-string "")
143 (defvoo nndoc-group-alist nil)
144 (defvoo nndoc-current-buffer nil
145   "Current nndoc news buffer.")
146 (defvoo nndoc-address nil)
147
148 (defconst nndoc-version "nndoc 1.0"
149   "nndoc version.")
150
151 \f
152
153 ;;; Interface functions
154
155 (nnoo-define-basics nndoc)
156
157 (deffoo nndoc-retrieve-headers (articles &optional newsgroup server fetch-old)
158   (when (nndoc-possibly-change-buffer newsgroup server)
159     (save-excursion
160       (set-buffer nntp-server-buffer)
161       (erase-buffer)
162       (let (article entry)
163         (if (stringp (car articles))
164             'headers
165           (while articles
166             (when (setq entry (cdr (assq (setq article (pop articles))
167                                          nndoc-dissection-alist)))
168               (insert (format "221 %d Article retrieved.\n" article))
169               (if nndoc-generate-head-function
170                   (funcall nndoc-generate-head-function article)
171                 (insert-buffer-substring
172                  nndoc-current-buffer (car entry) (nth 1 entry)))
173               (goto-char (point-max))
174               (unless (= (char-after (1- (point))) ?\n)
175                 (insert "\n"))
176               (insert (format "Lines: %d\n" (nth 4 entry)))
177               (insert ".\n")))
178
179           (nnheader-fold-continuation-lines)
180           'headers)))))
181
182 (deffoo nndoc-request-article (article &optional newsgroup server buffer)
183   (nndoc-possibly-change-buffer newsgroup server)
184   (save-excursion
185     (let ((buffer (or buffer nntp-server-buffer))
186           (entry (cdr (assq article nndoc-dissection-alist)))
187           beg)
188       (set-buffer buffer)
189       (erase-buffer)
190       (when entry
191         (if (stringp article)
192             nil
193           (insert-buffer-substring 
194            nndoc-current-buffer (car entry) (nth 1 entry))
195           (insert "\n")
196           (setq beg (point))
197           (insert-buffer-substring 
198            nndoc-current-buffer (nth 2 entry) (nth 3 entry))
199           (goto-char beg)
200           (when nndoc-prepare-body-function
201             (funcall nndoc-prepare-body-function))
202           (when nndoc-article-transform-function
203             (funcall nndoc-article-transform-function article))
204           t)))))
205
206 (deffoo nndoc-request-group (group &optional server dont-check)
207   "Select news GROUP."
208   (let (number)
209     (cond 
210      ((not (nndoc-possibly-change-buffer group server))
211       (nnheader-report 'nndoc "No such file or buffer: %s"
212                        nndoc-address))
213      (dont-check
214       (nnheader-report 'nndoc "Selected group %s" group)
215       t)
216      ((zerop (setq number (length nndoc-dissection-alist)))
217       (nndoc-close-group group)
218       (nnheader-report 'nndoc "No articles in group %s" group))
219      (t
220       (nnheader-insert "211 %d %d %d %s\n" number 1 number group)))))
221
222 (deffoo nndoc-request-type (group &optional article)
223   (cond ((not article) 'unknown)
224         (nndoc-post-type nndoc-post-type)
225         (t 'unknown)))
226
227 (deffoo nndoc-close-group (group &optional server)
228   (nndoc-possibly-change-buffer group server)
229   (and nndoc-current-buffer
230        (buffer-name nndoc-current-buffer)
231        (kill-buffer nndoc-current-buffer))
232   (setq nndoc-group-alist (delq (assoc group nndoc-group-alist)
233                                 nndoc-group-alist))
234   (setq nndoc-current-buffer nil)
235   (nnoo-close-server 'nndoc server)
236   (setq nndoc-dissection-alist nil)
237   t)
238
239 (deffoo nndoc-request-list (&optional server)
240   nil)
241
242 (deffoo nndoc-request-newgroups (date &optional server)
243   nil)
244
245 (deffoo nndoc-request-list-newsgroups (&optional server)
246   nil)
247
248 \f
249 ;;; Internal functions.
250
251 (defun nndoc-possibly-change-buffer (group source)
252   (let (buf)
253     (cond 
254      ;; The current buffer is this group's buffer.
255      ((and nndoc-current-buffer
256            (buffer-name nndoc-current-buffer)
257            (eq nndoc-current-buffer 
258                (setq buf (cdr (assoc group nndoc-group-alist))))))
259      ;; We change buffers by taking an old from the group alist.
260      ;; `source' is either a string (a file name) or a buffer object. 
261      (buf
262       (setq nndoc-current-buffer buf))
263      ;; It's a totally new group.    
264      ((or (and (bufferp nndoc-address)
265                (buffer-name nndoc-address))
266           (and (stringp nndoc-address)
267                (file-exists-p nndoc-address)
268                (not (file-directory-p nndoc-address))))
269       (push (cons group (setq nndoc-current-buffer 
270                               (get-buffer-create 
271                                (concat " *nndoc " group "*"))))
272             nndoc-group-alist)
273       (setq nndoc-dissection-alist nil)
274       (save-excursion
275         (set-buffer nndoc-current-buffer)
276         (buffer-disable-undo (current-buffer))
277         (erase-buffer)
278         (if (stringp nndoc-address)
279             (nnheader-insert-file-contents nndoc-address)
280           (insert-buffer-substring nndoc-address)))))
281     ;; Initialize the nndoc structures according to this new document.
282     (when (and nndoc-current-buffer
283                (not nndoc-dissection-alist))
284       (save-excursion
285         (set-buffer nndoc-current-buffer)
286         (nndoc-set-delims)
287         (nndoc-dissect-buffer)))
288     (unless nndoc-current-buffer
289       (nndoc-close-server))
290     ;; Return whether we managed to select a file.
291     nndoc-current-buffer))
292
293 ;;;
294 ;;; Deciding what document type we have
295 ;;;
296
297 (defun nndoc-set-delims ()
298   "Set the nndoc delimiter variables according to the type of the document."
299   (let ((vars '(nndoc-file-begin 
300                 nndoc-first-article 
301                 nndoc-article-end nndoc-head-begin nndoc-head-end
302                 nndoc-file-end nndoc-article-begin
303                 nndoc-body-begin nndoc-body-end-function nndoc-body-end
304                 nndoc-prepare-body-function nndoc-article-transform-function
305                 nndoc-generate-head-function nndoc-body-begin-function
306                 nndoc-head-begin-function)))
307     (while vars
308       (set (pop vars) nil)))
309   (let (defs)
310     ;; Guess away until we find the real file type.
311     (while (assq 'guess (setq defs (cdr (assq nndoc-article-type 
312                                               nndoc-type-alist))))
313       (setq nndoc-article-type (nndoc-guess-type nndoc-article-type)))
314     ;; Set the nndoc variables.
315     (while defs
316       (set (intern (format "nndoc-%s" (caar defs)))
317            (cdr (pop defs))))))
318
319 (defun nndoc-guess-type (subtype)
320   (let ((alist nndoc-type-alist)
321         results result entry)
322     (while (and (not result)
323                 (setq entry (pop alist)))
324       (when (memq subtype (or (cdr (assq 'subtype entry)) '(guess)))
325         (goto-char (point-min))
326         (when (numberp (setq result (funcall (intern
327                                               (format "nndoc-%s-type-p" 
328                                                       (car entry))))))
329           (push (cons result entry) results)
330           (setq result nil))))
331     (unless (or result results)
332       (error "Document is not of any recognized type"))
333     (if result
334         (car entry)
335       (cadar (sort results (lambda (r1 r2) (< (car r1) (car r2))))))))
336
337 ;;; 
338 ;;; Built-in type predicates and functions
339 ;;;
340
341 (defun nndoc-mbox-type-p ()
342   (when (looking-at message-unix-mail-delimiter)
343     t))
344
345 (defun nndoc-mbox-article-begin ()
346   (when (re-search-forward (concat "^" message-unix-mail-delimiter) nil t)
347     (goto-char (match-beginning 0))))
348
349 (defun nndoc-mbox-body-end ()
350   (let ((beg (point))
351         len end)
352     (when
353         (save-excursion
354           (and (re-search-backward 
355                 (concat "^" message-unix-mail-delimiter) nil t)
356                (setq end (point))
357                (search-forward "\n\n" beg t)
358                (re-search-backward
359                 "^Content-Length:[ \t]*\\([0-9]+\\) *$" end t)
360                (setq len (string-to-int (match-string 1)))
361                (search-forward "\n\n" beg t)
362                (unless (= (setq len (+ (point) len)) (point-max))
363                  (and (< len (point-max))
364                       (goto-char len)
365                       (looking-at message-unix-mail-delimiter)))))
366       (goto-char len))))
367
368 (defun nndoc-mmdf-type-p ()
369   (when (looking-at "\^A\^A\^A\^A$")
370     t))
371
372 (defun nndoc-news-type-p ()
373   (when (looking-at "^Path:.*\n")
374     t))
375
376 (defun nndoc-rnews-type-p ()
377   (when (looking-at "#! *rnews")
378     t))
379
380 (defun nndoc-rnews-body-end ()
381   (and (re-search-backward nndoc-article-begin nil t)
382        (forward-line 1)
383        (goto-char (+ (point) (string-to-int (match-string 1))))))
384
385 (defun nndoc-babyl-type-p ()
386   (when (re-search-forward "\^_\^L *\n" nil t)
387     t))
388
389 (defun nndoc-babyl-body-begin ()
390   (re-search-forward "^\n" nil t)
391   (when (looking-at "\*\*\* EOOH \*\*\*")
392     (let ((next (or (save-excursion
393                       (re-search-forward nndoc-article-begin nil t))
394                     (point-max))))
395       (unless (re-search-forward "^\n" next t)
396         (goto-char next)
397         (forward-line -1)
398         (insert "\n")
399         (forward-line -1)))))
400
401 (defun nndoc-babyl-head-begin ()
402   (when (re-search-forward "^[0-9].*\n" nil t)
403     (when (looking-at "\*\*\* EOOH \*\*\*")
404       (forward-line 1))
405     t))
406
407 (defun nndoc-forward-type-p ()
408   (when (and (re-search-forward "^-+ Start of forwarded message -+\n+" nil t)
409              (not (re-search-forward "^Subject:.*digest" nil t))
410              (not (re-search-backward "^From:" nil t 2))
411              (not (re-search-forward "^From:" nil t 2)))
412     t))
413
414 (defun nndoc-rfc934-type-p ()
415   (when (and (re-search-forward "^-+ Start of forwarded.*\n+" nil t)
416              (not (re-search-forward "^Subject:.*digest" nil t))
417              (not (re-search-backward "^From:" nil t 2))
418              (not (re-search-forward "^From:" nil t 2)))
419     t))
420
421 (defun nndoc-clari-briefs-type-p ()
422   (when (let ((case-fold-search nil))
423           (re-search-forward "^\t[^a-z]+ ([^a-z]+) --" nil t))
424     t))
425
426 (defun nndoc-transform-clari-briefs (article)
427   (goto-char (point-min))
428   (when (looking-at " *\\*\\(.*\\)\n")
429     (replace-match "" t t))
430   (nndoc-generate-clari-briefs-head article))
431
432 (defun nndoc-generate-clari-briefs-head (article)
433   (let ((entry (cdr (assq article nndoc-dissection-alist)))
434         subject from)
435     (save-excursion
436       (set-buffer nndoc-current-buffer)
437       (save-restriction
438         (narrow-to-region (car entry) (nth 3 entry))
439         (goto-char (point-min))
440         (when (looking-at " *\\*\\(.*\\)$")
441           (setq subject (match-string 1))
442           (when (string-match "[ \t]+$" subject)
443             (setq subject (substring subject 0 (match-beginning 0)))))
444         (when
445             (let ((case-fold-search nil))
446               (re-search-forward
447                "^\t\\([^a-z]+\\(,[^(]+\\)? ([^a-z]+)\\) --" nil t))
448           (setq from (match-string 1)))))
449     (insert "From: " "clari@clari.net (" (or from "unknown") ")"
450             "\nSubject: " (or subject "(no subject)") "\n")))
451
452 (defun nndoc-mime-digest-type-p ()
453   (let ((case-fold-search t)
454         boundary-id b-delimiter entry)
455     (when (and
456            (re-search-forward
457             (concat "^Content-Type: *multipart/digest;[ \t\n]*[ \t]"
458                     "boundary=\"\\([^\"\n]*[^\" \t\n]\\)\"")
459             nil t)
460            (match-beginning 1))
461       (setq boundary-id (match-string 1)
462             b-delimiter (concat "\n--" boundary-id "[\n \t]+"))
463       (setq entry (assq 'mime-digest nndoc-type-alist))
464       (setcdr entry
465               (list
466                (cons 'head-end "^ ?$")
467                (cons 'body-begin "^ ?\n")
468                (cons 'article-begin b-delimiter)
469                (cons 'body-end-function 'nndoc-digest-body-end)
470                (cons 'file-end (concat "\n--" boundary-id "--[ \t]*$"))))
471       t)))
472
473 (defun nndoc-standard-digest-type-p ()
474   (when (and (re-search-forward (concat "^" (make-string 70 ?-) "\n\n") nil t)
475              (re-search-forward 
476               (concat "\n\n" (make-string 30 ?-) "\n\n") nil t))
477     t))
478
479 (defun nndoc-digest-body-end ()
480   (and (re-search-forward nndoc-article-begin nil t)
481        (goto-char (match-beginning 0))))
482
483 (defun nndoc-slack-digest-type-p ()
484   0)
485
486 (defun nndoc-lanl-gov-announce-type-p ()
487   (when (let ((case-fold-search nil))
488           (re-search-forward "^\\\\\\\\\nPaper: [a-z-]+/[0-9]+" nil t))
489     t))
490
491 (defun nndoc-transform-lanl-gov-announce (article)
492   (goto-char (point-max))
493   (when (re-search-backward "^\\\\\\\\ +(\\([^ ]*\\) , *\\([^ ]*\\))" nil t)
494     (replace-match "\n\nGet it at \\1 (\\2)" t nil))
495   ;;  (when (re-search-backward "^\\\\\\\\$" nil t)
496   ;;    (replace-match "" t t))
497   )
498  
499 (defun nndoc-generate-lanl-gov-head (article)
500   (let ((entry (cdr (assq article nndoc-dissection-alist)))
501         (e-mail "no address given")
502         subject from)
503     (save-excursion
504       (set-buffer nndoc-current-buffer)
505       (save-restriction
506         (narrow-to-region (car entry) (nth 1 entry))
507         (goto-char (point-min))
508         (when (looking-at "^Paper.*: \\([a-z-]+/[0-9]+\\)")
509           (setq subject (concat " (" (match-string 1) ")"))
510           (when (re-search-forward "^From: \\([^ ]+\\)" nil t)
511             (setq e-mail (match-string 1)))
512           (when (re-search-forward "^Title: \\([^\f]*\\)\nAuthors?: \\(.*\\)"
513                                    nil t)
514             (setq subject (concat (match-string 1) subject))
515             (setq from (concat (match-string 2) " <" e-mail ">"))))
516         ))
517     (while (and from (string-match "(\[^)\]*)" from))
518       (setq from (replace-match "" t t from)))
519     (insert "From: "  (or from "unknown")
520             "\nSubject: " (or subject "(no subject)") "\n")))
521  
522
523
524 ;;;
525 ;;; Functions for dissecting the documents
526 ;;;
527
528 (defun nndoc-search (regexp)
529   (prog1
530       (re-search-forward regexp nil t)
531     (beginning-of-line)))
532
533 (defun nndoc-dissect-buffer ()
534   "Go through the document and partition it into heads/bodies/articles."
535   (let ((i 0)
536         (first t)
537         head-begin head-end body-begin body-end)
538     (setq nndoc-dissection-alist nil)
539     (save-excursion
540       (set-buffer nndoc-current-buffer)
541       (goto-char (point-min))
542       ;; Find the beginning of the file.
543       (when nndoc-file-begin
544         (nndoc-search nndoc-file-begin))
545       ;; Go through the file.
546       (while (if (and first nndoc-first-article)
547                  (nndoc-search nndoc-first-article)
548                (nndoc-article-begin))
549         (setq first nil)
550         (cond (nndoc-head-begin-function
551                (funcall nndoc-head-begin-function))
552               (nndoc-head-begin 
553                (nndoc-search nndoc-head-begin)))
554         (if (or (>= (point) (point-max))
555                 (and nndoc-file-end
556                      (looking-at nndoc-file-end)))
557             (goto-char (point-max))
558           (setq head-begin (point))
559           (nndoc-search (or nndoc-head-end "^$"))
560           (setq head-end (point))
561           (if nndoc-body-begin-function
562               (funcall nndoc-body-begin-function)
563             (nndoc-search (or nndoc-body-begin "^\n")))
564           (setq body-begin (point))
565           (or (and nndoc-body-end-function
566                    (funcall nndoc-body-end-function))
567               (and nndoc-body-end
568                    (nndoc-search nndoc-body-end))
569               (nndoc-article-begin)
570               (progn
571                 (goto-char (point-max))
572                 (when nndoc-file-end
573                   (and (re-search-backward nndoc-file-end nil t)
574                        (beginning-of-line)))))
575           (setq body-end (point))
576           (push (list (incf i) head-begin head-end body-begin body-end
577                       (count-lines body-begin body-end))
578                 nndoc-dissection-alist))))))
579
580 (defun nndoc-article-begin ()
581   (if nndoc-article-begin-function
582       (funcall nndoc-article-begin-function)
583     (ignore-errors
584       (nndoc-search nndoc-article-begin))))
585
586 (defun nndoc-unquote-dashes ()
587   "Unquote quoted non-separators in digests."
588   (while (re-search-forward "^- -"nil t)
589     (replace-match "-" t t)))
590
591 ;;;###autoload
592 (defun nndoc-add-type (definition &optional position)
593   "Add document DEFINITION to the list of nndoc document definitions.
594 If POSITION is nil or `last', the definition will be added
595 as the last checked definition, if t or `first', add as the
596 first definition, and if any other symbol, add after that
597 symbol in the alist."
598   ;; First remove any old instances.
599   (setq nndoc-type-alist
600         (delq (assq (car definition) nndoc-type-alist)
601               nndoc-type-alist))
602   ;; Then enter the new definition in the proper place.
603   (cond
604    ((or (null position) (eq position 'last))
605     (setq nndoc-type-alist (nconc nndoc-type-alist (list definition))))
606    ((or (eq position t) (eq position 'first))
607     (push definition nndoc-type-alist))
608    (t
609     (let ((list (memq (assq position nndoc-type-alist)
610                       nndoc-type-alist)))
611       (unless list
612         (error "No such position: %s" position))
613       (setcdr list (cons definition (cdr list)))))))
614
615 (provide 'nndoc)
616
617 ;;; nndoc.el ends here