44eb8a277ecc1634d5e863cdaacd73c9cde25840
[gnus] / lisp / nnbabyl.el
1 ;;; nnbabyl.el --- rmail mbox access for Gnus
2 ;; Copyright (C) 1995,96 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, mail
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 ;; For an overview of what the interface functions do, please see the
28 ;; Gnus sources.  
29
30 ;;; Code:
31
32 (require 'nnheader)
33 (require 'rmail)
34 (require 'nnmail)
35 (eval-when-compile (require 'cl))
36
37 (defvar nnbabyl-mbox-file (expand-file-name "~/RMAIL")
38   "The name of the rmail box file in the users home directory.")
39
40 (defvar nnbabyl-active-file (expand-file-name "~/.rmail-active")
41   "The name of the active file for the rmail box.")
42
43 (defvar nnbabyl-get-new-mail t
44   "If non-nil, nnbabyl will check the incoming mail file and split the mail.")
45
46 (defvar nnbabyl-prepare-save-mail-hook nil
47   "Hook run narrowed to an article before saving.")
48
49 \f
50
51 (defvar nnbabyl-mail-delimiter "\^_")
52
53 (defconst nnbabyl-version "nnbabyl 1.0"
54   "nnbabyl version.")
55
56 (defvar nnbabyl-mbox-buffer nil)
57 (defvar nnbabyl-current-group nil)
58 (defvar nnbabyl-status-string "")
59 (defvar nnbabyl-group-alist nil)
60 (defvar nnbabyl-active-timestamp nil)
61
62 (defvar nnbabyl-previous-buffer-mode nil)
63
64 \f
65
66 (defvar nnbabyl-current-server nil)
67 (defvar nnbabyl-server-alist nil)
68 (defvar nnbabyl-server-variables 
69   `((nnbabyl-mbox-file ,nnbabyl-mbox-file)
70     (nnbabyl-active-file ,nnbabyl-active-file)
71     (nnbabyl-get-new-mail ,nnbabyl-get-new-mail)
72     (nnbabyl-current-group nil)
73     (nnbabyl-status-string "")
74     (nnbabyl-previous-buffer-mode nil)
75     (nnbabyl-group-alist nil)))
76
77 \f
78
79 ;;; Interface functions
80
81 (defun nnbabyl-retrieve-headers (sequence &optional newsgroup server fetch-old)
82   (save-excursion
83     (set-buffer nntp-server-buffer)
84     (erase-buffer)
85     (let ((number (length sequence))
86           (count 0)
87           article art-string start stop)
88       (nnbabyl-possibly-change-newsgroup newsgroup)
89       (while sequence
90         (setq article (car sequence))
91         (setq art-string (nnbabyl-article-string article))
92         (set-buffer nnbabyl-mbox-buffer)
93         (if (or (search-forward art-string nil t)
94                 (search-backward art-string nil t))
95             (progn
96               (re-search-backward (concat "^" nnbabyl-mail-delimiter) nil t)
97               (while (and (not (looking-at ".+:"))
98                           (zerop (forward-line 1))))
99               (setq start (point))
100               (search-forward "\n\n" nil t)
101               (setq stop (1- (point)))
102               (set-buffer nntp-server-buffer)
103               (insert "221 " (int-to-string article) " Article retrieved.\n")
104               (insert-buffer-substring nnbabyl-mbox-buffer start stop)
105               (goto-char (point-max))
106               (insert ".\n")))
107         (setq sequence (cdr sequence))
108         (setq count (1+ count))
109         (and (numberp nnmail-large-newsgroup)
110              (> number nnmail-large-newsgroup)
111              (zerop (% count 20))
112              (nnheader-message 5 "nnbabyl: Receiving headers... %d%%"
113                                (/ (* count 100) number))))
114
115       (and (numberp nnmail-large-newsgroup)
116            (> number nnmail-large-newsgroup)
117            (nnheader-message 5 "nnbabyl: Receiving headers...done"))
118
119       (set-buffer nntp-server-buffer)
120       (nnheader-fold-continuation-lines)
121       'headers)))
122
123 (defun nnbabyl-open-server (server &optional defs)
124   (nnheader-change-server 'nnbabyl server defs)
125   (cond 
126    ((not (file-exists-p nnbabyl-mbox-file))
127     (nnbabyl-close-server)
128     (nnheader-report 'nnbabyl "No such file: %s" nnbabyl-mbox-file))
129    ((file-directory-p nnbabyl-mbox-file)
130     (nnbabyl-close-server)
131     (nnheader-report 'nnbabyl "Not a regular file: %s" nnbabyl-mbox-file))
132    (t
133     (nnheader-report 'nnbabyl "Opened server %s using mbox %s" server
134                      nnbabyl-mbox-file)
135     t)))
136
137 (defun nnbabyl-close-server (&optional server)
138   ;; Restore buffer mode.
139   (when (and (nnbabyl-server-opened)
140              nnbabyl-previous-buffer-mode)
141     (save-excursion
142       (set-buffer nnbabyl-mbox-buffer)
143       (narrow-to-region
144        (car (car nnbabyl-previous-buffer-mode))
145        (cdr (car nnbabyl-previous-buffer-mode)))
146       (funcall (cdr nnbabyl-previous-buffer-mode))))
147   (setq nnbabyl-current-server nil
148         nnbabyl-mbox-buffer nil)
149   t)
150
151 (defun nnbabyl-server-opened (&optional server)
152   (and (equal server nnbabyl-current-server)
153        nnbabyl-mbox-buffer
154        (buffer-name nnbabyl-mbox-buffer)
155        nntp-server-buffer
156        (buffer-name nntp-server-buffer)))
157
158 (defun nnbabyl-status-message (&optional server)
159   nnbabyl-status-string)
160
161 (defun nnbabyl-request-article (article &optional newsgroup server buffer)
162   (nnbabyl-possibly-change-newsgroup newsgroup)
163   (save-excursion
164     (set-buffer nnbabyl-mbox-buffer)
165     (goto-char (point-min))
166     (if (search-forward (nnbabyl-article-string article) nil t)
167         (let (start stop summary-line)
168           (re-search-backward (concat "^" nnbabyl-mail-delimiter) nil t)
169           (while (and (not (looking-at ".+:"))
170                       (zerop (forward-line 1))))
171           (setq start (point))
172           (or (and (re-search-forward 
173                     (concat "^" nnbabyl-mail-delimiter) nil t)
174                    (forward-line -1))
175               (goto-char (point-max)))
176           (setq stop (point))
177           (let ((nntp-server-buffer (or buffer nntp-server-buffer)))
178             (set-buffer nntp-server-buffer)
179             (erase-buffer)
180             (insert-buffer-substring nnbabyl-mbox-buffer start stop)
181             (goto-char (point-min))
182             ;; If there is an EOOH header, then we have to remove some
183             ;; duplicated headers. 
184             (setq summary-line (looking-at "Summary-line:"))
185             (when (search-forward "\n*** EOOH ***" nil t)
186               (if summary-line
187                   ;; The headers to be deleted are located before the
188                   ;; EOOH line...
189                   (delete-region (point-min) (progn (forward-line 1)
190                   (point)))
191                 ;; ...or after.
192                 (delete-region (progn (beginning-of-line) (point))
193                                (or (search-forward "\n\n" nil t)
194                                    (point)))))
195             (if (numberp article) 
196                 (cons nnbabyl-current-group article)
197               (nnbabyl-article-group-number)))))))
198
199 (defun nnbabyl-request-group (group &optional server dont-check)
200   (let ((active (cadr (assoc group nnbabyl-group-alist))))
201     (save-excursion
202       (cond 
203        ((null active)
204         (nnheader-report 'nnbabyl "No such group: %s" group))
205        ((null (nnbabyl-possibly-change-newsgroup group))
206         (nnheader-report 'nnbabyl "No such group: %s" group))
207        (dont-check
208         (nnheader-report 'nnbabyl "Selected group %s" group)
209         t)
210        (t
211         (nnheader-report 'nnbabyl "Selected group %s" group)
212         (nnheader-insert "211 %d %d %d %s\n" 
213                          (1+ (- (cdr active) (car active)))
214                          (car active) (cdr active) group)
215         t)))))
216
217 (defun nnbabyl-request-scan (&optional group server)
218   (nnbabyl-read-mbox)
219   (nnmail-get-new-mail 
220    'nnbabyl 
221    (lambda ()
222      (save-excursion
223        (set-buffer nnbabyl-mbox-buffer)
224        (save-buffer)))
225    nnbabyl-mbox-file group
226    (lambda ()
227      (save-excursion
228        (let ((in-buf (current-buffer)))
229          (goto-char (point-min))
230          (while (search-forward "\n\^_\n" nil t)
231            (delete-char -1))
232          (set-buffer nnbabyl-mbox-buffer)
233          (goto-char (point-max))
234          (search-backward "\n\^_" nil t)
235          (goto-char (match-end 0))
236          (insert-buffer-substring in-buf))))))
237
238 (defun nnbabyl-close-group (group &optional server)
239   t)
240
241 (defun nnbabyl-request-create-group (group &optional server) 
242   (nnmail-activate 'nnbabyl)
243   (or (assoc group nnbabyl-group-alist)
244       (let (active)
245         (setq nnbabyl-group-alist (cons (list group (setq active (cons 1 0)))
246                                         nnbabyl-group-alist))
247         (nnmail-save-active nnbabyl-group-alist nnbabyl-active-file)))
248   t)
249
250 (defun nnbabyl-request-list (&optional server)
251   (save-excursion
252     (or (nnmail-find-file nnbabyl-active-file)
253         (progn
254           (setq nnbabyl-group-alist (nnmail-get-active))
255           (nnmail-save-active nnbabyl-group-alist nnbabyl-active-file)
256           (nnmail-find-file nnbabyl-active-file)))))
257
258 (defun nnbabyl-request-newgroups (date &optional server)
259   (nnbabyl-request-list server))
260
261 (defun nnbabyl-request-list-newsgroups (&optional server)
262   (setq nnbabyl-status-string "nnbabyl: LIST NEWSGROUPS is not implemented.")
263   nil)
264
265 (defun nnbabyl-request-post (&optional server)
266   (mail-send-and-exit nil))
267
268 (defun nnbabyl-request-expire-articles
269   (articles newsgroup &optional server force)
270   (nnbabyl-possibly-change-newsgroup newsgroup)
271   (let* ((is-old t)
272          rest)
273     (nnmail-activate 'nnbabyl)
274
275     (save-excursion 
276       (set-buffer nnbabyl-mbox-buffer)
277       (set-text-properties (point-min) (point-max) nil)
278       (while (and articles is-old)
279         (goto-char (point-min))
280         (if (search-forward (nnbabyl-article-string (car articles)) nil t)
281             (if (setq is-old
282                       (nnmail-expired-article-p
283                        newsgroup
284                        (buffer-substring 
285                         (point) (progn (end-of-line) (point))) force))
286                 (progn
287                   (nnheader-message 5 "Deleting article %d in %s..." 
288                                     (car articles) newsgroup)
289                   (nnbabyl-delete-mail))
290               (setq rest (cons (car articles) rest))))
291         (setq articles (cdr articles)))
292       (save-buffer)
293       ;; Find the lowest active article in this group.
294       (let ((active (nth 1 (assoc newsgroup nnbabyl-group-alist))))
295         (goto-char (point-min))
296         (while (and (not (search-forward
297                           (nnbabyl-article-string (car active)) nil t))
298                     (<= (car active) (cdr active)))
299           (setcar active (1+ (car active)))
300           (goto-char (point-min))))
301       (nnmail-save-active nnbabyl-group-alist nnbabyl-active-file)
302       (nconc rest articles))))
303
304 (defun nnbabyl-request-move-article 
305   (article group server accept-form &optional last)
306   (nnbabyl-possibly-change-newsgroup group)
307   (let ((buf (get-buffer-create " *nnbabyl move*"))
308         result)
309     (and 
310      (nnbabyl-request-article article group server)
311      (save-excursion
312        (set-buffer buf)
313        (insert-buffer-substring nntp-server-buffer)
314        (goto-char (point-min))
315        (if (re-search-forward 
316             "^X-Gnus-Newsgroup:" 
317             (save-excursion (search-forward "\n\n" nil t) (point)) t)
318            (delete-region (progn (beginning-of-line) (point))
319                           (progn (forward-line 1) (point))))
320        (setq result (eval accept-form))
321        (kill-buffer (current-buffer))
322        result)
323      (save-excursion
324        (set-buffer nnbabyl-mbox-buffer)
325        (goto-char (point-min))
326        (if (search-forward (nnbabyl-article-string article) nil t)
327            (nnbabyl-delete-mail))
328        (and last (save-buffer))))
329     result))
330
331 (defun nnbabyl-request-accept-article (group &optional last)
332   (let ((buf (current-buffer))
333         result beg)
334     (and 
335      (nnmail-activate 'nnbabyl)
336      (save-excursion
337        (goto-char (point-min))
338        (search-forward "\n\n" nil t)
339        (forward-line -1)
340        (save-excursion
341          (while (re-search-backward "^X-Gnus-Newsgroup: " beg t)
342            (delete-region (point) (progn (forward-line 1) (point)))))
343        (let ((nnmail-split-methods
344               (if (stringp group) (list (list group "")) 
345                 nnmail-split-methods)))
346          (setq result (car (nnbabyl-save-mail))))
347        (set-buffer nnbabyl-mbox-buffer)
348        (goto-char (point-max))
349        (search-backward "\n\^_")
350        (goto-char (match-end 0))
351        (insert-buffer-substring buf)
352        (and last (progn 
353                    (save-buffer)
354                    (nnmail-save-active
355                     nnbabyl-group-alist nnbabyl-active-file)))
356        result))))
357
358 (defun nnbabyl-request-replace-article (article group buffer)
359   (nnbabyl-possibly-change-newsgroup group)
360   (save-excursion
361     (set-buffer nnbabyl-mbox-buffer)
362     (goto-char (point-min))
363     (if (not (search-forward (nnbabyl-article-string article) nil t))
364         nil
365       (nnbabyl-delete-mail t t)
366       (insert-buffer-substring buffer)
367       (save-buffer)
368       t)))
369
370 (defun nnbabyl-request-delete-group (group &optional force server)
371   (nnbabyl-possibly-change-newsgroup group)
372   ;; Delete all articles in GROUP.
373   (if (not force)
374       ()                                ; Don't delete the articles.
375     (save-excursion
376       (set-buffer nnbabyl-mbox-buffer)
377       (goto-char (point-min))
378       ;; Delete all articles in this group.
379       (let ((ident (concat "\nX-Gnus-Newsgroup: " nnbabyl-current-group ":"))
380             found)
381         (while (search-forward ident nil t)
382           (setq found t)
383           (nnbabyl-delete-mail))
384         (and found (save-buffer)))))
385   ;; Remove the group from all structures.
386   (setq nnbabyl-group-alist 
387         (delq (assoc group nnbabyl-group-alist) nnbabyl-group-alist)
388         nnbabyl-current-group nil)
389   ;; Save the active file.
390   (nnmail-save-active nnbabyl-group-alist nnbabyl-active-file)
391   t)
392
393 (defun nnbabyl-request-rename-group (group new-name &optional server)
394   (nnbabyl-possibly-change-newsgroup group)
395   (save-excursion
396     (set-buffer nnbabyl-mbox-buffer)
397     (goto-char (point-min))
398     (let ((ident (concat "\nX-Gnus-Newsgroup: " nnbabyl-current-group ":"))
399           (new-ident (concat "\nX-Gnus-Newsgroup: " new-name ":"))
400           found)
401       (while (search-forward ident nil t)
402         (replace-match new-ident t t)
403         (setq found t))
404       (and found (save-buffer))))
405   (let ((entry (assoc group nnbabyl-group-alist)))
406     (and entry (setcar entry new-name))
407     (setq nnbabyl-current-group nil)
408     ;; Save the new group alist.
409     (nnmail-save-active nnbabyl-group-alist nnbabyl-active-file)
410     t))
411
412 \f
413 ;;; Internal functions.
414
415 ;; If FORCE, delete article no matter how many X-Gnus-Newsgroup
416 ;; headers there are. If LEAVE-DELIM, don't delete the Unix mbox
417 ;; delimiter line.
418 (defun nnbabyl-delete-mail (&optional force leave-delim)
419   ;; Delete the current X-Gnus-Newsgroup line.
420   (or force
421       (delete-region
422        (progn (beginning-of-line) (point))
423        (progn (forward-line 1) (point))))
424   ;; Beginning of the article.
425   (save-excursion
426     (save-restriction
427       (widen)
428       (narrow-to-region
429        (save-excursion
430          (re-search-backward (concat "^" nnbabyl-mail-delimiter) nil t)
431          (if leave-delim (progn (forward-line 1) (point))
432            (match-beginning 0)))
433        (progn
434          (forward-line 1)
435          (or (and (re-search-forward (concat "^" nnbabyl-mail-delimiter) 
436                                      nil t)
437                   (if (and (not (bobp)) leave-delim)
438                       (progn (forward-line -2) (point))
439                     (match-beginning 0)))
440              (point-max))))
441       (goto-char (point-min))
442       ;; Only delete the article if no other groups owns it as well.
443       (if (or force (not (re-search-forward "^X-Gnus-Newsgroup: " nil t)))
444           (delete-region (point-min) (point-max))))))
445
446 (defun nnbabyl-possibly-change-newsgroup (newsgroup)
447   (if (or (not nnbabyl-mbox-buffer)
448           (not (buffer-name nnbabyl-mbox-buffer)))
449       (save-excursion (nnbabyl-read-mbox)))
450   (or nnbabyl-group-alist
451       (nnmail-activate 'nnbabyl))
452   (if newsgroup
453       (if (assoc newsgroup nnbabyl-group-alist)
454           (setq nnbabyl-current-group newsgroup)
455         (setq nnbabyl-status-string "No such group in file")
456         nil)
457     t))
458
459 (defun nnbabyl-article-string (article)
460   (if (numberp article)
461       (concat "\nX-Gnus-Newsgroup: " nnbabyl-current-group ":" 
462               (int-to-string article) " ")
463     (concat "\nMessage-ID: " article)))
464
465 (defun nnbabyl-article-group-number ()
466   (save-excursion
467     (goto-char (point-min))
468     (and (re-search-forward "^X-Gnus-Newsgroup: +\\([^:]+\\):\\([0-9]+\\) "
469                             nil t)
470          (cons (buffer-substring (match-beginning 1) (match-end 1))
471                (string-to-int
472                 (buffer-substring (match-beginning 2) (match-end 2)))))))
473
474 (defun nnbabyl-insert-lines ()
475   "Insert how many lines and chars there are in the body of the mail."
476   (let (lines chars)
477     (save-excursion
478       (goto-char (point-min))
479       (if (search-forward "\n\n" nil t) 
480           (progn
481             ;; There may be an EOOH line here...
482             (if (looking-at "\\*\\*\\* EOOH \\*\\*\\*")
483                 (search-forward "\n\n" nil t))
484             (setq chars (- (point-max) (point)))
485             (setq lines (- (count-lines (point) (point-max)) 1))
486             ;; Move back to the end of the headers. 
487             (goto-char (point-min))
488             (search-forward "\n\n" nil t)
489             (forward-char -1)
490             (save-excursion
491               (if (re-search-backward "^Lines: " nil t)
492                   (delete-region (point) (progn (forward-line 1) (point)))))
493             (insert (format "Lines: %d\n" lines))
494             chars)))))
495
496 (defun nnbabyl-save-mail ()
497   ;; Called narrowed to an article.
498   (let ((group-art (nreverse (nnmail-article-group 'nnbabyl-active-number))))
499     (nnbabyl-insert-lines)
500     (nnmail-insert-xref group-art)
501     (nnbabyl-insert-newsgroup-line group-art)
502     (run-hooks 'nnbabyl-prepare-save-mail-hook)
503     group-art))
504
505 (defun nnbabyl-insert-newsgroup-line (group-art)
506   (save-excursion
507     (goto-char (point-min))
508     (while (looking-at "From ")
509       (replace-match "Mail-from: From " t t)
510       (forward-line 1))
511     ;; If there is a C-l at the beginning of the narrowed region, this
512     ;; isn't really a "save", but rather a "scan".
513     (goto-char (point-min))
514     (or (looking-at "\^L")
515         (save-excursion
516           (insert "\^L\n0, unseen,,\n*** EOOH ***\n")
517           (goto-char (point-max))
518           (insert "\^_\n")))
519     (if (search-forward "\n\n" nil t)
520         (progn
521           (forward-char -1)
522           (while group-art
523             (insert (format "X-Gnus-Newsgroup: %s:%d   %s\n" 
524                             (car (car group-art)) (cdr (car group-art))
525                             (current-time-string)))
526             (setq group-art (cdr group-art)))))
527     t))
528
529 (defun nnbabyl-active-number (group)
530   ;; Find the next article number in GROUP.
531   (let ((active (car (cdr (assoc group nnbabyl-group-alist)))))
532     (if active
533         (setcdr active (1+ (cdr active)))
534       ;; This group is new, so we create a new entry for it.
535       ;; This might be a bit naughty... creating groups on the drop of
536       ;; a hat, but I don't know...
537       (setq nnbabyl-group-alist (cons (list group (setq active (cons 1 1)))
538                                       nnbabyl-group-alist)))
539     (cdr active)))
540
541 (defun nnbabyl-read-mbox ()
542   (nnmail-activate 'nnbabyl)
543   (or (file-exists-p nnbabyl-mbox-file)
544       (save-excursion
545         (set-buffer (setq nnbabyl-mbox-buffer
546                           (create-file-buffer nnbabyl-mbox-file)))
547         (setq buffer-file-name nnbabyl-mbox-file)
548         (insert "BABYL OPTIONS:\n\n\^_")
549         (write-region (point-min) (point-max) nnbabyl-mbox-file t 'nomesg)))
550
551   (if (and nnbabyl-mbox-buffer
552            (buffer-name nnbabyl-mbox-buffer)
553            (save-excursion
554              (set-buffer nnbabyl-mbox-buffer)
555              (= (buffer-size) (nth 7 (file-attributes nnbabyl-mbox-file)))))
556       ()
557     (save-excursion
558       (let ((delim (concat "^" nnbabyl-mail-delimiter))
559             start end)
560         (set-buffer (setq nnbabyl-mbox-buffer 
561                           (nnheader-find-file-noselect 
562                            nnbabyl-mbox-file nil 'raw)))
563         ;; Save buffer mode.
564         (setq nnbabyl-previous-buffer-mode 
565               (cons (cons (point-min) (point-max))
566                     major-mode))
567
568         (buffer-disable-undo (current-buffer))
569         (widen)
570         (setq buffer-read-only nil)
571         (fundamental-mode)
572         
573         (goto-char (point-min))
574         (re-search-forward delim nil t)
575         (setq start (match-end 0))
576         (while (re-search-forward delim nil t)
577           (setq end (match-end 0))
578           (or (search-backward "\nX-Gnus-Newsgroup: " start t)
579               (progn
580                 (goto-char end)
581                 (save-excursion
582                   (save-restriction
583                     (goto-char start)
584                     (narrow-to-region start end)
585                     (nnbabyl-save-mail)
586                     (setq end (point-max))))))
587           (goto-char (setq start end)))
588         (and (buffer-modified-p (current-buffer)) (save-buffer))
589         (nnmail-save-active nnbabyl-group-alist nnbabyl-active-file)))))
590
591 (defun nnbabyl-remove-incoming-delims ()
592   (goto-char (point-min))
593   (while (search-forward "\^_" nil t)
594     (replace-match "?" t t)))
595
596 (provide 'nnbabyl)
597
598 ;;; nnbabyl.el ends here