*** empty log message ***
[gnus] / lisp / nnml.el
1 ;;; nnml.el --- mail spool access for Gnus
2 ;; Copyright (C) 1995 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
22 ;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
23
24 ;;; Commentary:
25
26 ;; Based on nnspool.el by Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>.
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 'nnmail)
34 (eval-when-compile (require 'cl))
35
36 (defvar nnml-directory "~/Mail/"
37   "Mail spool directory.")
38
39 (defvar nnml-active-file (concat nnml-directory "active")
40   "Mail active file.")
41
42 (defvar nnml-newsgroups-file (concat nnml-directory "newsgroups")
43   "Mail newsgroups description file.")
44
45 (defvar nnml-get-new-mail t
46   "If non-nil, nnml will check the incoming mail file and split the mail.")
47
48 (defvar nnml-nov-is-evil nil
49   "If non-nil, Gnus will never generate and use nov databases for mail groups.
50 Using nov databases will speed up header fetching considerably.
51 This variable shouldn't be flipped much. If you have, for some reason,
52 set this to t, and want to set it to nil again, you should always run
53 the `nnml-generate-nov-databases' command. The function will go
54 through all nnml directories and generate nov databases for them
55 all. This may very well take some time.")
56
57 (defvar nnml-prepare-save-mail-hook nil
58   "Hook run narrowed to an article before saving.")
59
60 \f
61
62 (defconst nnml-version "nnml 1.0"
63   "nnml version.")
64
65 (defvar nnml-nov-file-name ".overview")
66
67 (defvar nnml-current-directory nil)
68 (defvar nnml-current-group nil)
69 (defvar nnml-status-string "")
70 (defvar nnml-nov-buffer-alist nil)
71 (defvar nnml-group-alist nil)
72 (defvar nnml-active-timestamp nil)
73
74 (defvar nnml-generate-active-function 'nnml-generate-active-info)
75
76 \f
77
78 ;; Server variables.
79
80 (defvar nnml-current-server nil)
81 (defvar nnml-server-alist nil)
82 (defvar nnml-server-variables 
83   (list 
84    (list 'nnml-directory nnml-directory)
85    (list 'nnml-active-file nnml-active-file)
86    (list 'nnml-newsgroups-file nnml-newsgroups-file)
87    (list 'nnml-get-new-mail nnml-get-new-mail)
88    (list 'nnml-nov-is-evil nnml-nov-is-evil)
89    (list 'nnml-nov-file-name nnml-nov-file-name)
90    '(nnml-current-directory nil)
91    '(nnml-current-group nil)
92    '(nnml-status-string "")
93    '(nnml-nov-buffer-alist nil)
94    '(nnml-group-alist nil)
95    '(nnml-active-timestamp nil)))
96
97 \f
98
99 ;;; Interface functions.
100
101 (defun nnml-retrieve-headers (sequence &optional newsgroup server fetch-old)
102   (save-excursion
103     (set-buffer nntp-server-buffer)
104     (erase-buffer)
105     (let ((file nil)
106           (number (length sequence))
107           (count 0)
108           beg article)
109       (if (stringp (car sequence))
110           'headers
111         (nnml-possibly-change-directory newsgroup)
112         (if (nnml-retrieve-headers-with-nov sequence fetch-old)
113             'nov
114           (while sequence
115             (setq article (car sequence))
116             (setq file
117                   (concat nnml-current-directory (int-to-string article)))
118             (if (and (file-exists-p file)
119                      (not (file-directory-p file)))
120                 (progn
121                   (insert (format "221 %d Article retrieved.\n" article))
122                   (setq beg (point))
123                   (nnheader-insert-head file)
124                   (goto-char beg)
125                   (if (search-forward "\n\n" nil t)
126                       (forward-char -1)
127                     (goto-char (point-max))
128                     (insert "\n\n"))
129                   (insert ".\n")
130                   (delete-region (point) (point-max))))
131             (setq sequence (cdr sequence))
132             (setq count (1+ count))
133             (and (numberp nnmail-large-newsgroup)
134                  (> number nnmail-large-newsgroup)
135                  (zerop (% count 20))
136                  gnus-verbose-backends
137                  (message "nnml: Receiving headers... %d%%"
138                           (/ (* count 100) number))))
139
140           (and (numberp nnmail-large-newsgroup)
141                (> number nnmail-large-newsgroup)
142                gnus-verbose-backends
143                (message "nnml: Receiving headers...done"))
144
145           ;; Fold continuation lines.
146           (goto-char (point-min))
147           (while (re-search-forward "\\(\r?\n[ \t]+\\)+" nil t)
148             (replace-match " " t t))
149           'headers)))))
150
151 (defun nnml-open-server (server &optional defs)
152   (nnheader-init-server-buffer)
153   (if (equal server nnml-current-server)
154       t
155     (if nnml-current-server
156         (setq nnml-server-alist 
157               (cons (list nnml-current-server
158                           (nnheader-save-variables nnml-server-variables))
159                     nnml-server-alist)))
160     (let ((state (assoc server nnml-server-alist)))
161       (if state 
162           (progn
163             (nnheader-restore-variables (nth 1 state))
164             (setq nnml-server-alist (delq state nnml-server-alist)))
165         (nnheader-set-init-variables nnml-server-variables defs)))
166     (setq nnml-current-server server)))
167
168 (defun nnml-close-server (&optional server)
169   (setq nnml-current-server nil)
170   t)
171
172 (defun nnml-server-opened (&optional server)
173   (and (equal server nnml-current-server)
174        nntp-server-buffer
175        (buffer-name nntp-server-buffer)))
176
177 (defun nnml-status-message (&optional server)
178   nnml-status-string)
179
180 (defun nnml-request-article (id &optional newsgroup server buffer)
181   (nnml-possibly-change-directory newsgroup)
182   (let* ((group-num (and (stringp id) (nnml-find-group-number id)))
183          (number (if (numberp id) id (cdr group-num)))
184          (file
185           (and number
186                (concat 
187                 (if (numberp id)
188                     nnml-current-directory
189                   (nnmail-group-pathname (car group-num) nnml-directory))
190                 (int-to-string number))))
191          (nntp-server-buffer (or buffer nntp-server-buffer)))
192     (and file
193          (file-exists-p file)
194          (not (file-directory-p file))
195          (save-excursion (nnmail-find-file file))
196          ;; We return the article number.
197          (cons newsgroup (string-to-int (file-name-nondirectory file))))))
198
199 (defun nnml-request-group (group &optional server dont-check)
200   (if (not (nnml-possibly-change-directory group))
201       (progn
202         (setq nnml-status-string "Invalid group (no such directory)")
203         nil)
204     (if dont-check 
205         t
206       (nnmail-activate 'nnml)
207       (let ((active (nth 1 (assoc group nnml-group-alist))))
208         (save-excursion
209           (set-buffer nntp-server-buffer)
210           (erase-buffer)
211           (if (not active)
212               ()
213             (insert (format "211 %d %d %d %s\n" 
214                             (max (1+ (- (cdr active) (car active))) 0)
215                             (car active) (cdr active) group))
216             t))))))
217
218 (defun nnml-request-scan (&optional group server)
219   (nnmail-get-new-mail 'nnml 'nnml-save-nov nnml-directory group))
220
221 (defun nnml-close-group (group &optional server)
222   t)
223
224 (defun nnml-request-close ()
225   (setq nnml-current-server nil)
226   (setq nnml-server-alist nil)
227   t)
228
229 (defun nnml-request-create-group (group &optional server) 
230   (nnmail-activate 'nnml)
231   (or (assoc group nnml-group-alist)
232       (let (active)
233         (setq nnml-group-alist (cons (list group (setq active (cons 1 0)))
234                                      nnml-group-alist))
235         (nnml-possibly-create-directory group)
236         (nnml-possibly-change-directory group)
237         (let ((articles 
238                (nnheader-directory-articles nnml-current-directory )))
239           (and articles
240                (progn
241                  (setcar active (apply 'min articles))
242                  (setcdr active (apply 'max articles)))))
243         (nnmail-save-active nnml-group-alist nnml-active-file)))
244   t)
245
246 (defun nnml-request-list (&optional server)
247   (save-excursion
248     (nnmail-find-file nnml-active-file)
249     (setq nnml-group-alist (nnmail-get-active))))
250
251 (defun nnml-request-newgroups (date &optional server)
252   (nnml-request-list server))
253
254 (defun nnml-request-list-newsgroups (&optional server)
255   (save-excursion
256     (nnmail-find-file nnml-newsgroups-file)))
257
258 (defun nnml-request-post (&optional server)
259   (mail-send-and-exit nil))
260
261 (defun nnml-request-expire-articles (articles newsgroup &optional server force)
262   (nnml-possibly-change-directory newsgroup)
263   (let* ((active-articles 
264           (nnheader-directory-articles nnml-current-directory))
265          (max-article (and active-articles (apply 'max active-articles)))
266          (is-old t)
267          article rest mod-time number)
268     (nnmail-activate 'nnml)
269
270     (while (and articles is-old)
271       (setq article (concat nnml-current-directory 
272                             (int-to-string 
273                              (setq number (pop articles)))))
274       (when (setq mod-time (nth 5 (file-attributes article)))
275         (if (and (nnml-deletable-article-p newsgroup number)
276                  (setq is-old 
277                        (nnmail-expired-article-p newsgroup mod-time force)))
278             (progn
279               (and gnus-verbose-backends 
280                    (message "Deleting article %s in %s..."
281                             article newsgroup))
282               (condition-case ()
283                   (funcall nnmail-delete-file-function article)
284                 (file-error
285                  (push number rest)))
286               (setq active-articles (delq number active-articles))
287               (nnml-nov-delete-article newsgroup number))
288           (push number rest))))
289     (let ((active (nth 1 (assoc newsgroup nnml-group-alist))))
290       (and active
291            (setcar active (or (and active-articles
292                                    (apply 'min active-articles))
293                               0)))
294       (nnmail-save-active nnml-group-alist nnml-active-file))
295     (nnml-save-nov)
296     (message "")
297     (nconc rest articles)))
298
299 (defun nnml-request-move-article 
300   (article group server accept-form &optional last)
301   (let ((buf (get-buffer-create " *nnml move*"))
302         result)
303     (and 
304      (nnml-deletable-article-p group article)
305      (nnml-request-article article group server)
306      (save-excursion
307        (set-buffer buf)
308        (insert-buffer-substring nntp-server-buffer)
309        (setq result (eval accept-form))
310        (kill-buffer (current-buffer))
311        result)
312      (progn
313        (condition-case ()
314            (funcall nnmail-delete-file-function
315                     (concat nnml-current-directory 
316                             (int-to-string article)))
317          (file-error nil))
318        (nnml-nov-delete-article group article)
319        (and last (nnml-save-nov))))
320     result))
321
322 (defun nnml-request-accept-article (group &optional last)
323   (let (result)
324     (if (stringp group)
325         (and 
326          (nnmail-activate 'nnml)
327          ;; We trick the choosing function into believing that only one
328          ;; group is availiable.  
329          (let ((nnmail-split-methods (list (list group ""))))
330            (setq result (car (nnml-save-mail))))
331          (progn
332            (nnmail-save-active nnml-group-alist nnml-active-file)
333            (and last (nnml-save-nov))))
334       (and
335        (nnmail-activate 'nnml)
336        (setq result (car (nnml-save-mail)))
337        (progn
338          (nnmail-save-active nnml-group-alist nnml-active-file)
339          (and last (nnml-save-nov)))))
340     result))
341
342 (defun nnml-request-replace-article (article group buffer)
343   (nnml-possibly-change-directory group)
344   (save-excursion
345     (set-buffer buffer)
346     (nnml-possibly-create-directory group)
347     (if (not (condition-case ()
348                  (progn
349                    (write-region (point-min) (point-max)
350                                  (concat nnml-current-directory 
351                                          (int-to-string article))
352                                  nil (if gnus-verbose-backends nil 'nomesg))
353                    t)
354                (error nil)))
355         ()
356       (let ((chars (nnmail-insert-lines))
357             (art (concat (int-to-string article) "\t"))
358             nov-line)
359         (setq nov-line (nnml-make-nov-line chars))
360         ;; Replace the NOV line in the NOV file.
361         (save-excursion 
362           (set-buffer (nnml-open-nov group))
363           (goto-char (point-min))
364           (if (or (looking-at art)
365                   (search-forward (concat "\n" art) nil t))
366               ;; Delete the old NOV line.
367               (delete-region (progn (beginning-of-line) (point))
368                              (progn (forward-line 1) (point)))
369             ;; The line isn't here, so we have to find out where
370             ;; we should insert it. (This situation should never
371             ;; occur, but one likes to make sure...)
372             (while (and (looking-at "[0-9]+\t")
373                         (< (string-to-int 
374                             (buffer-substring 
375                              (match-beginning 0) (match-end 0)))
376                            article)
377                         (zerop (forward-line 1)))))
378           (beginning-of-line)
379           (insert (int-to-string article) nov-line)
380           (nnml-save-nov)
381           t)))))
382
383 (defun nnml-request-delete-group (group &optional force server)
384   (nnml-possibly-change-directory group)
385   (when force
386     ;; Delete all articles in GROUP.
387     (let ((articles 
388            (directory-files 
389             nnml-current-directory t
390             (concat nnheader-numerical-short-files
391                     "\\|" (regexp-quote nnml-nov-file-name) "$")))
392           article)
393       (while articles 
394         (setq article (pop articles))
395         (when (file-writable-p article)
396           (when gnus-verbose-backends
397             (message "Deleting article %s in %s..." article group))
398           (funcall nnmail-delete-file-function article))))
399     ;; Try to delete the directory itself.
400     (condition-case ()
401         (delete-directory nnml-current-directory)
402       (error nil)))
403   ;; Remove the group from all structures.
404   (setq nnml-group-alist 
405         (delq (assoc group nnml-group-alist) nnml-group-alist)
406         nnml-current-group nil
407         nnml-current-directory nil)
408   ;; Save the active file.
409   (nnmail-save-active nnml-group-alist nnml-active-file)
410   t)
411
412 (defun nnml-request-rename-group (group new-name &optional server)
413   (nnml-possibly-change-directory group)
414   ;; Rename directory.
415   (and (file-writable-p nnml-current-directory)
416        (condition-case ()
417            (progn
418              (rename-file 
419               (directory-file-name nnml-current-directory)
420               (directory-file-name 
421                (nnmail-group-pathname new-name nnml-directory)))
422              t)
423          (error nil))
424        ;; That went ok, so we change the internal structures.
425        (let ((entry (assoc group nnml-group-alist)))
426          (and entry (setcar entry new-name))
427          (setq nnml-current-directory nil
428                nnml-current-group nil)
429          ;; Save the new group alist.
430          (nnmail-save-active nnml-group-alist nnml-active-file)
431          t)))
432
433 \f
434 ;;; Internal functions.
435
436 (defun nnml-deletable-article-p (group article)
437   "Say whether ARTICLE in GROUP can be deleted."
438   (or (not nnmail-keep-last-article)
439       (not (eq (cdr (nth 1 (assoc group nnml-group-alist))) article))))
440
441 ;; Find an article number in the current group given the Message-ID. 
442 (defun nnml-find-group-number (id)
443   (save-excursion
444     (set-buffer (get-buffer-create " *nnml id*"))
445     (buffer-disable-undo (current-buffer))
446     (let ((alist nnml-group-alist)
447           number)
448       ;; We want to look through all .overview files, but we want to
449       ;; start with the one in the current directory.  It seems most
450       ;; likely that the article we are looking for is in that group. 
451       (if (setq number (nnml-find-id nnml-current-group id))
452           (cons nnml-current-group number)
453         ;; It wasn't there, so we look through the other groups as well.
454         (while (and (not number)
455                     alist)
456           (or (string= (car (car alist)) nnml-current-group)
457               (setq number (nnml-find-id (car (car alist)) id)))
458           (or number
459               (setq alist (cdr alist))))
460         (and number
461              (cons (car (car alist)) number))))))
462
463 (defun nnml-find-id (group id)
464   (erase-buffer)
465   (insert-file-contents 
466    (concat (nnmail-group-pathname group nnml-directory)
467            nnml-nov-file-name))
468   (let (number found)
469     (while (and (not found) 
470                 (search-forward id nil t)) ; We find the ID.
471       ;; And the id is in the fourth field.
472       (if (search-backward 
473            "\t" (save-excursion (beginning-of-line) (point)) t 4)
474           (progn
475             (beginning-of-line)
476             (setq found t)
477             ;; We return the article number.
478             (setq number
479                   (condition-case ()
480                       (read (current-buffer))
481                     (error nil))))))
482     number))
483       
484
485 (defun nnml-retrieve-headers-with-nov (articles &optional fetch-old)
486   (if (or gnus-nov-is-evil nnml-nov-is-evil)
487       nil
488     (let ((first (car articles))
489           (last (progn (while (cdr articles) (setq articles (cdr articles)))
490                        (car articles)))
491           (nov (concat nnml-current-directory nnml-nov-file-name)))
492       (if (file-exists-p nov)
493           (save-excursion
494             (set-buffer nntp-server-buffer)
495             (erase-buffer)
496             (insert-file-contents nov)
497             (if (and fetch-old
498                      (not (numberp fetch-old)))
499                 t                       ; Don't remove anything.
500               (if fetch-old
501                   (setq first (max 1 (- first fetch-old))))
502               (goto-char (point-min))
503               (while (and (not (eobp)) (< first (read (current-buffer))))
504                 (forward-line 1))
505               (beginning-of-line)
506               (if (not (eobp)) (delete-region 1 (point)))
507               (while (and (not (eobp)) (>= last (read (current-buffer))))
508                 (forward-line 1))
509               (beginning-of-line)
510               (if (not (eobp)) (delete-region (point) (point-max)))
511               t))))))
512
513 (defun nnml-possibly-change-directory (newsgroup &optional force)
514   (if newsgroup
515       (let ((pathname (nnmail-group-pathname newsgroup nnml-directory)))
516         (and (or force (file-directory-p pathname))
517              (setq nnml-current-directory pathname
518                    nnml-current-group newsgroup)))
519     t))
520
521 (defun nnml-possibly-create-directory (group)
522   (let (dir dirs)
523     (setq dir (nnmail-group-pathname group nnml-directory))
524     (while (not (file-directory-p dir))
525       (setq dirs (cons dir dirs))
526       (setq dir (file-name-directory (directory-file-name dir))))
527     (while dirs
528       (make-directory (directory-file-name (car dirs)))
529       (and gnus-verbose-backends 
530            (message "Creating mail directory %s" (car dirs)))
531       (setq dirs (cdr dirs)))))
532              
533 (defun nnml-save-mail ()
534   "Called narrowed to an article."
535   (let ((group-art (nreverse (nnmail-article-group 'nnml-active-number)))
536         chars nov-line)
537     (setq chars (nnmail-insert-lines))
538     (nnmail-insert-xref group-art)
539     (run-hooks 'nnml-prepare-save-mail-hook)
540     (goto-char (point-min))
541     (while (looking-at "From ")
542       (replace-match "X-From-Line: ")
543       (forward-line 1))
544     ;; We save the article in all the newsgroups it belongs in.
545     (let ((ga group-art)
546           first)
547       (while ga
548         (nnml-possibly-create-directory (car (car ga)))
549         (let ((file (concat (nnmail-group-pathname 
550                              (car (car ga)) nnml-directory)
551                             (int-to-string (cdr (car ga))))))
552           (if first
553               ;; It was already saved, so we just make a hard link.
554               (add-name-to-file first file t)
555             ;; Save the article.
556             (write-region (point-min) (point-max) file nil 
557                           (if gnus-verbose-backends nil 'nomesg))
558             (setq first file)))
559         (setq ga (cdr ga))))
560     ;; Generate a nov line for this article. We generate the nov
561     ;; line after saving, because nov generation destroys the
562     ;; header. 
563     (setq nov-line (nnml-make-nov-line chars))
564     ;; Output the nov line to all nov databases that should have it.
565     (let ((ga group-art))
566       (while ga
567         (nnml-add-nov (car (car ga)) (cdr (car ga)) nov-line)
568         (setq ga (cdr ga))))
569     group-art))
570
571 (defun nnml-active-number (group)
572   "Compute the next article number in GROUP."
573   (let ((active (car (cdr (assoc group nnml-group-alist)))))
574     ;; The group wasn't known to nnml, so we just create an active
575     ;; entry for it.   
576     (or active
577         (progn
578           (setq active (cons 1 0))
579           (setq nnml-group-alist (cons (list group active) nnml-group-alist))))
580     (setcdr active (1+ (cdr active)))
581     (while (file-exists-p
582             (concat (nnmail-group-pathname group nnml-directory)
583                     (int-to-string (cdr active))))
584       (setcdr active (1+ (cdr active))))
585     (cdr active)))
586
587 (defun nnml-add-nov (group article line)
588   "Add a nov line for the GROUP base."
589   (save-excursion 
590     (set-buffer (nnml-open-nov group))
591     (goto-char (point-max))
592     (insert (int-to-string article) line)))
593
594 (defsubst nnml-header-value ()
595   (buffer-substring (match-end 0) (progn (end-of-line) (point))))
596
597 (defun nnml-make-nov-line (chars)
598   "Create a nov from the current headers."
599   (let ((case-fold-search t)
600         subject from date id references lines xref in-reply-to char)
601     (save-excursion
602       (save-restriction
603         (goto-char (point-min))
604         (narrow-to-region 
605          (point)
606          (1- (or (search-forward "\n\n" nil t) (point-max))))
607         ;; Fold continuation lines.
608         (goto-char (point-min))
609         (while (re-search-forward "\\(\r?\n[ \t]+\\)+" nil t)
610           (replace-match " " t t))
611         (subst-char-in-region (point-min) (point-max) ?\t ? )
612         ;; [number subject from date id references chars lines xref]
613         (save-excursion
614           (goto-char (point-min))
615           (while (re-search-forward "^\\(from\\|subject\\|message-id\\|date\\|lines\\|xref\\|references\\|in-reply-to\\): "
616                                     nil t)
617             (beginning-of-line)
618             (setq char (downcase (following-char))) 
619             (cond
620              ((eq char ?s)
621               (setq subject (nnml-header-value)))
622              ((eq char ?f)
623               (setq from (nnml-header-value)))
624              ((eq char ?x)
625               (setq xref (buffer-substring (match-beginning 0) 
626                                            (progn (end-of-line) (point)))))
627              ((eq char ?l)
628               (setq lines (nnml-header-value)))
629              ((eq char ?d)
630               (setq date (nnml-header-value)))
631              ((eq char ?m)
632               (setq id (setq id (nnml-header-value))))
633              ((eq char ?r)
634               (setq references (nnml-header-value)))
635              ((eq char ?i)
636               (setq in-reply-to (nnml-header-value))))
637             (forward-line 1))
638       
639           (and (not references)
640                in-reply-to
641                (string-match "<[^>]+>" in-reply-to)
642                (setq references
643                      (substring in-reply-to (match-beginning 0)
644                                 (match-end 0)))))
645         ;; [number subject from date id references chars lines xref]
646         (format "\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%s\t\n"
647                 (or subject "(none)")
648                 (or from "(nobody)") (or date "")
649                 (or id (concat "nnml-dummy-id-" 
650                                (mapconcat 
651                                 (lambda (time) (int-to-string time))
652                                 (current-time) "-")))
653                 (or references "")
654                 (or chars 0) (or lines "0") 
655                 (or xref ""))))))
656
657 (defun nnml-open-nov (group)
658   (or (cdr (assoc group nnml-nov-buffer-alist))
659       (let ((buffer (find-file-noselect 
660                      (concat (nnmail-group-pathname group nnml-directory)
661                              nnml-nov-file-name))))
662         (save-excursion
663           (set-buffer buffer)
664           (buffer-disable-undo (current-buffer)))
665         (setq nnml-nov-buffer-alist 
666               (cons (cons group buffer) nnml-nov-buffer-alist))
667         buffer)))
668
669 (defun nnml-save-nov ()
670   (save-excursion
671     (while nnml-nov-buffer-alist
672       (if (buffer-name (cdr (car nnml-nov-buffer-alist)))
673           (progn
674             (set-buffer (cdr (car nnml-nov-buffer-alist)))
675             (and (buffer-modified-p)
676                  (write-region 
677                   1 (point-max) (buffer-file-name) nil 'nomesg))
678             (set-buffer-modified-p nil)
679             (kill-buffer (current-buffer))))
680       (setq nnml-nov-buffer-alist (cdr nnml-nov-buffer-alist)))))
681
682 ;;;###autoload
683 (defun nnml-generate-nov-databases ()
684   "Generate nov databases in all nnml directories."
685   (interactive)
686   ;; Read the active file to make sure we don't re-use articles 
687   ;; numbers in empty groups.
688   (nnmail-activate 'nnml)
689   (nnml-open-server (or nnml-current-server ""))
690   (setq nnml-directory (expand-file-name nnml-directory))
691   ;; Recurse down the directories.
692   (nnml-generate-nov-databases-1 nnml-directory)
693   ;; Save the active file.
694   (nnmail-save-active nnml-group-alist nnml-active-file))
695
696 (defun nnml-generate-nov-databases-1 (dir)
697   (setq dir (file-name-as-directory dir))
698   ;; We descend recursively 
699   (let ((dirs (directory-files dir t nil t))
700         dir)
701     (while dirs 
702       (setq dir (pop dirs))
703       (when (and (not (string-match "/\\.\\.?$" dir))
704                  (file-directory-p dir))
705         (nnml-generate-nov-databases-1 dir))))
706   ;; Do this directory.
707   (let ((files (sort
708                 (mapcar
709                  (lambda (name) (string-to-int name))
710                  (directory-files dir nil "^[0-9]+$" t))
711                 '<)))
712     (when files
713       (funcall nnml-generate-active-function dir)
714       ;; Generate the nov file.
715       (nnml-generate-nov-file dir files))))
716
717 (defun nnml-generate-active-info (dir)
718   ;; Update the active info for this group.
719   (let ((group (nnmail-replace-chars-in-string 
720                 (substring dir (length nnml-directory))
721                 ?/ ?.)))
722     (setq nnml-group-alist
723           (delq (assoc group nnml-group-alist) nnml-group-alist))
724     (push (list group
725                 (cons (car files)
726                       (let ((f files))
727                         (while (cdr f) (setq f (cdr f)))
728                         (car f))))
729           nnml-group-alist)))
730
731 (defun nnml-generate-nov-file (dir files)
732   (let* ((dir (file-name-as-directory dir))
733          (nov (concat dir nnml-nov-file-name))
734          (nov-buffer (get-buffer-create " *nov*"))
735          nov-line chars)
736     (save-excursion
737       ;; Init the nov buffer.
738       (set-buffer nov-buffer)
739       (buffer-disable-undo (current-buffer))
740       (erase-buffer)
741       (set-buffer nntp-server-buffer)
742       ;; Delete the old NOV file.
743       (when (file-exists-p nov)
744         (funcall nnmail-delete-file-function nov))
745       (while files
746         (erase-buffer)
747         (insert-file-contents (concat dir (int-to-string (car files))))
748         (narrow-to-region 
749          (goto-char (point-min))
750          (progn
751            (search-forward "\n\n" nil t)
752            (setq chars (- (point-max) (point)))
753            (max 1 (1- (point)))))
754         (when (and (not (= 0 chars))    ; none of them empty files...
755                    (not (= (point-min) (point-max))))
756           (goto-char (point-min))
757           (setq nov-line (nnml-make-nov-line chars))
758           (save-excursion
759             (set-buffer nov-buffer)
760             (goto-char (point-max))
761             (insert (int-to-string (car files)) nov-line)))
762         (widen)
763         (setq files (cdr files)))
764       (save-excursion
765         (set-buffer nov-buffer)
766         (write-region 1 (point-max) (expand-file-name nov) nil
767                       'nomesg)
768         (kill-buffer (current-buffer))))))
769
770 (defun nnml-nov-delete-article (group article)
771   (save-excursion
772     (set-buffer (nnml-open-nov group))
773     (goto-char (point-min))
774     (if (re-search-forward (concat "^" (int-to-string article) "\t") nil t)
775         (delete-region (match-beginning 0) (progn (forward-line 1) (point))))
776     t))
777
778 (provide 'nnml)
779
780 ;;; nnml.el ends here