*** empty log message ***
[gnus] / lisp / nnmh.el
1 ;;; nnmh.el --- mail spool access for Gnus (mhspool)
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 'rmail)
34 (require 'nnmail)
35 (require 'gnus)
36
37 (defvar nnmh-directory "~/Mail/"
38   "Mail directory.")
39
40 (defvar nnmh-get-new-mail t
41   "If non-nil, nnmh will check the incoming mail file and split the mail.")
42
43 \f
44
45 (defconst nnmh-version "nnmh 0.1"
46   "nnmh version.")
47
48 (defvar nnmh-current-directory nil
49   "Current news group directory.")
50
51 (defvar nnmh-status-string "")
52 (defvar nnmh-group-alist nil)
53
54 \f
55
56 (defvar nnmh-current-server nil)
57 (defvar nnmh-server-alist nil)
58 (defvar nnmh-server-variables 
59   (list
60    (list 'nnmh-directory nnmh-directory)
61    (list 'nnmh-get-new-mail nnmh-get-new-mail)
62    '(nnmh-current-directory nil)
63    '(nnmh-status-string "")
64    '(nnmh-group-alist)))
65
66 \f
67
68 ;;; Interface functions.
69
70 (defun nnmh-retrieve-headers (sequence &optional newsgroup server)
71   (save-excursion
72     (set-buffer nntp-server-buffer)
73     (erase-buffer)
74     (let ((file nil)
75           (number (length sequence))
76           (count 0)
77           beg article)
78       (nnmh-possibly-change-directory newsgroup)
79       (while sequence
80         (setq article (car sequence))
81         (setq file
82               (concat nnmh-current-directory (prin1-to-string article)))
83         (if (and (file-exists-p file)
84                  (not (file-directory-p file)))
85             (progn
86               (insert (format "221 %d Article retrieved.\n" article))
87               (setq beg (point))
88               (insert-file-contents file)
89               (goto-char beg)
90               (if (search-forward "\n\n" nil t)
91                   (forward-char -1)
92                 (goto-char (point-max))
93                 (insert "\n\n"))
94               (insert (format "Lines: %d\n" (count-lines (point) (point-max))))
95               (insert ".\n")
96               (delete-region (point) (point-max))))
97         (setq sequence (cdr sequence))
98         (setq count (1+ count))
99         (and (numberp nnmail-large-newsgroup)
100              (> number nnmail-large-newsgroup)
101              (zerop (% count 20))
102              (message "nnmh: Receiving headers... %d%%"
103                       (/ (* count 100) number))))
104
105       (and (numberp nnmail-large-newsgroup)
106            (> number nnmail-large-newsgroup)
107            (message "nnmh: Receiving headers... done"))
108
109       ;; Fold continuation lines.
110       (goto-char 1)
111       (while (re-search-forward "\\(\r?\n[ \t]+\\)+" nil t)
112         (replace-match " " t t))
113       'headers)))
114
115 (defun nnmh-open-server (server &optional defs)
116   (nnheader-init-server-buffer)
117   (if (equal server nnmh-current-server)
118       t
119     (if nnmh-current-server
120         (setq nnmh-server-alist 
121               (cons (list nnmh-current-server
122                           (nnheader-save-variables nnmh-server-variables))
123                     nnmh-server-alist)))
124     (let ((state (assoc server nnmh-server-alist)))
125       (if state 
126           (progn
127             (nnheader-restore-variables (nth 1 state))
128             (setq nnmh-server-alist (delq state nnmh-server-alist)))
129         (nnheader-set-init-variables nnmh-server-variables defs)))
130     (setq nnmh-current-server server)))
131
132 (defun nnmh-close-server (&optional server)
133   t)
134
135 (defun nnmh-server-opened (&optional server)
136   (and (equal server nnmh-current-server)
137        nntp-server-buffer
138        (buffer-name nntp-server-buffer)))
139
140 (defun nnmh-status-message (&optional server)
141   nnmh-status-string)
142
143 (defun nnmh-request-article (id &optional newsgroup server buffer)
144   (nnmh-possibly-change-directory newsgroup)
145   (let ((file (if (stringp id)
146                   nil
147                 (concat nnmh-current-directory (prin1-to-string id))))
148         (nntp-server-buffer (or buffer nntp-server-buffer)))
149     (if (and (stringp file)
150              (file-exists-p file)
151              (not (file-directory-p file)))
152         (save-excursion
153           (nnmail-find-file file)))))
154
155 (defun nnmh-request-group (group &optional server dont-check)
156   (and nnmh-get-new-mail (or dont-check (nnmh-get-new-mail)))
157   (let ((pathname (nnmh-article-pathname group nnmh-directory))
158         dir)
159     (if (file-directory-p pathname)
160         (progn
161           (setq nnmh-current-directory pathname)
162           (and nnmh-get-new-mail (nnmh-update-gnus-unreads group))
163           (or dont-check
164               (progn
165                 (setq dir 
166                       (sort
167                        (mapcar
168                         (function
169                          (lambda (name)
170                            (string-to-int name)))
171                         (directory-files pathname nil "^[0-9]+$" t))
172                        '<))
173                 (save-excursion
174                   (set-buffer nntp-server-buffer)
175                   (erase-buffer)
176                   (if dir
177                       (insert (format "211 %d %d %d %s\n" (length dir) 
178                                       (car dir)
179                                       (progn (while (cdr dir)
180                                                (setq dir (cdr dir)))
181                                              (car dir))
182                                       group))
183                     (insert (format "211 0 1 0 %s\n" group))))))
184           t))))
185
186 (defun nnmh-request-list (&optional server dir)
187   (and server nnmh-get-new-mail (nnmh-get-new-mail))
188   (or dir
189       (save-excursion
190         (set-buffer nntp-server-buffer)
191         (erase-buffer)
192         (setq dir nnmh-directory)))
193   (setq dir (expand-file-name dir))
194   ;; Recurse down all directories.
195   (let ((dirs (and (file-readable-p dir)
196                    (directory-files dir t nil t))))
197     (while dirs 
198       (if (and (not (string-match "/\\.\\.$" (car dirs)))
199                (not (string-match "/\\.$" (car dirs)))
200                (file-directory-p (car dirs))
201                (file-readable-p (car dirs)))
202           (nnmh-request-list server (car dirs)))
203       (setq dirs (cdr dirs))))
204   ;; For each directory, generate an active file line.
205   (if (not (string= (expand-file-name nnmh-directory) dir))
206       (let ((files (mapcar
207                     (lambda (name) (string-to-int name))
208                     (directory-files dir nil "^[0-9]+$" t))))
209         (if (null files)
210             ()
211           (save-excursion
212             (set-buffer nntp-server-buffer)
213             (insert 
214              (format 
215               "%s %d %d y\n" 
216               (progn
217                 (string-match (expand-file-name nnmh-directory) dir)
218                 (nnmail-replace-chars-in-string
219                  (substring dir (match-end 0)) ?/ ?.))
220               (apply (function max) files) 
221               (apply (function min) files)))))))
222   t)
223
224 (defun nnmh-request-newgroups (date &optional server)
225   (nnmh-request-list server))
226
227 (defun nnmh-request-post (&optional server)
228   (mail-send-and-exit nil))
229
230 (fset 'nnmh-request-post-buffer 'nnmail-request-post-buffer)
231
232 (defun nnmh-request-expire-articles (articles newsgroup &optional server force)
233   (nnmh-possibly-change-directory newsgroup)
234   (let* ((days (or (and nnmail-expiry-wait-function
235                         (funcall nnmail-expiry-wait-function newsgroup))
236                    nnmail-expiry-wait))
237          article rest mod-time)
238     (if nnmail-keep-last-article
239         (progn
240           (setq articles (sort articles '>))
241           (setq rest (cons (car articles) rest))
242           (setq articles (cdr articles))))
243     (while articles
244       (setq article (concat nnmh-current-directory (int-to-string
245                                                     (car articles))))
246       (if (setq mod-time (nth 5 (file-attributes article)))
247           (if (or force
248                   (> (nnmail-days-between
249                       (current-time-string)
250                       (current-time-string mod-time))
251                      days))
252               (progn
253                 (message "Deleting %s..." article)
254                 (condition-case ()
255                     (delete-file article)
256                   (file-error nil)))
257             (setq rest (cons (car articles) rest))))
258       (setq articles (cdr articles)))
259     rest))
260
261 (defun nnmh-close-group (group &optional server)
262   t)
263
264 (defun nnmh-request-move-article 
265   (article group server accept-form &optional last)
266   (let ((buf (get-buffer-create " *nnmh move*"))
267         result)
268     (and 
269      (nnmh-request-article article group server)
270      (save-excursion
271        (set-buffer buf)
272        (insert-buffer-substring nntp-server-buffer)
273        (setq result (eval accept-form))
274        (kill-buffer (current-buffer))
275        result)
276      (condition-case ()
277          (delete-file (concat nnmh-current-directory 
278                               (int-to-string article)))
279        (file-error nil)))
280  result))
281
282 (defun nnmh-request-accept-article (group &optional last)
283   (if (stringp group)
284       (and 
285        (nnmh-request-list)
286        (setq nnmh-group-alist (nnmail-get-active))
287        ;; We trick the choosing function into believing that only one
288        ;; group is availiable.  
289        (let ((nnmail-split-methods '(group "")))
290          (cons group (nnmh-save-mail))))
291     (and
292      (nnmh-request-list)
293      (setq nnmh-group-alist (nnmail-get-active))
294      (nnmh-save-mail))))
295
296 (defun nnmh-request-replace-article (article group buffer)
297   (nnmh-possibly-change-directory group)
298   (save-excursion
299     (set-buffer buffer)
300     (nnmh-possibly-create-directory group)
301     (condition-case ()
302         (progn
303           (write-region (point-min) (point-max)
304                         (concat nnmh-current-directory (int-to-string article))
305                         nil (if gnus-verbose-backends nil 'nomesg))
306           t)
307       (error nil))))
308
309 \f
310 ;;; Internal functions.
311
312 (defun nnmh-possibly-change-directory (newsgroup)
313   (if newsgroup
314       (let ((pathname (nnmh-article-pathname newsgroup nnmh-directory)))
315         (if (file-directory-p pathname)
316             (setq nnmh-current-directory pathname)
317           (error "No such newsgroup: %s" newsgroup)))))
318
319 (defun nnmh-possibly-create-directory (group)
320   (let (dir dirs)
321     (setq dir (nnmail-article-pathname group nnmh-directory))
322     (while (not (file-directory-p dir))
323       (setq dirs (cons dir dirs))
324       (setq dir (file-name-directory (directory-file-name dir))))
325     (while dirs
326       (if (make-directory (directory-file-name (car dirs)))
327           (error "Could not create directory %s" (car dirs)))
328       (and gnus-verbose-backends 
329            (message "Creating mail directory %s" (car dirs)))
330       (setq dirs (cdr dirs)))))
331              
332 (defun nnmh-save-mail ()
333   "Called narrowed to an article."
334   (let ((group-art (nreverse (nnmail-article-group 'nnmh-active-number)))
335         chars nov-line lines hbeg hend)
336     (setq chars (nnmail-insert-lines))
337     (nnmail-insert-xref group-art)
338     (goto-char (point-min))
339     (while (looking-at "From ")
340       (replace-match "X-From-Line: ")
341       (forward-line 1))
342     ;; We save the article in all the newsgroups it belongs in.
343     (let ((ga group-art)
344           first)
345       (while ga
346         (nnmh-possibly-create-directory (car (car ga)))
347         (let ((file (concat (nnmh-article-pathname 
348                              (car (car ga)) nnmh-directory) 
349                             (int-to-string (cdr (car ga))))))
350           (if first
351               ;; It was already saved, so we just make a hard link.
352               (add-name-to-file first file t)
353             ;; Save the article.
354             (write-region (point-min) (point-max) file nil nil)
355             (setq first file)))
356         (setq ga (cdr ga))))
357     group-art))
358
359 (defun nnmh-active-number (group)
360   "Compute the next article number in GROUP."
361   (let ((active (car (cdr (assoc group nnmh-group-alist)))))
362     (setcdr active (1+ (cdr active)))
363     (let (file)
364       (while (file-exists-p
365               (setq file (concat (nnmh-article-pathname 
366                                   group nnmh-directory)
367                                  (int-to-string (cdr active)))))
368         (setcdr active (1+ (cdr active)))))
369     (cdr active)))
370
371 (defun nnmh-article-pathname (group mail-dir)
372   "Make pathname for GROUP."
373   (let ((mail-dir (file-name-as-directory (expand-file-name mail-dir))))
374     (if (file-directory-p (concat mail-dir group))
375         (concat mail-dir group "/")
376       (concat mail-dir (nnmail-replace-chars-in-string group ?. ?/) "/"))))
377
378 (defun nnmh-get-new-mail ()
379   "Read new incoming mail."
380   (let (incoming)
381     (if (and nnmh-get-new-mail nnmail-spool-file
382              (file-exists-p nnmail-spool-file)
383              (> (nth 7 (file-attributes nnmail-spool-file)) 0))
384         (progn
385           (message "nnmh: Reading incoming mail...")
386           (setq incoming 
387                 (nnmail-move-inbox nnmail-spool-file
388                                    (concat nnmh-directory "Incoming")))
389           (nnmh-request-list)
390           (setq nnmh-group-alist (nnmail-get-active))
391           (nnmail-split-incoming incoming 'nnmh-save-mail)
392           (run-hooks 'nnmail-read-incoming-hook)
393 ;;         (delete-file incoming)
394           (message "nnmh: Reading incoming mail...done")))))
395
396 (defun nnmh-update-gnus-unreads (group)
397   ;; Go through the .nnmh-articles file and compare with the actual
398   ;; articles in this folder. The articles that are "new" will be
399   ;; marked as unread by Gnus.
400   (let* ((dir nnmh-current-directory)
401          (files (sort (mapcar (function (lambda (name) (string-to-int name)))
402                               (directory-files nnmh-current-directory 
403                                                nil "^[0-9]+$" t)) '<))
404          (nnmh-file (concat dir ".nnmh-articles"))
405          new articles)
406     ;; Load the .nnmh-articles file.
407     (if (file-exists-p nnmh-file)
408         (setq articles 
409               (let (nnmh-newsgroup-articles)
410                 (condition-case nil (load nnmh-file nil t t) (error nil))
411                 nnmh-newsgroup-articles)))
412     ;; Add all new articles to the `new' list.
413     (let ((art files))
414       (while art
415         (if (not (assq (car art) articles)) (setq new (cons (car art) new)))
416         (setq art (cdr art))))
417     ;; Remove all deleted articles.
418     (let ((art articles))
419       (while art
420         (if (not (memq (car (car art)) files))
421             (setq articles (delq (car art) articles)))
422         (setq art (cdr art))))
423     ;; Check whether the highest-numbered articles really are the ones
424     ;; that Gnus thinks they are by looking at the time-stamps.
425     (let ((art articles))
426       (while (and art 
427                   (not (equal 
428                         (nth 5 (file-attributes 
429                                 (concat dir (int-to-string (car (car art))))))
430                         (cdr (car art)))))
431         (setq articles (delq (car art) articles))
432         (setq new (cons (car (car art)) new))
433         (setq art (cdr art))))
434     ;; Go through all the new articles and add them, and their
435     ;; time-stamps to the list.
436     (let ((n new))
437       (while n
438         (setq articles 
439               (cons (cons 
440                      (car n)
441                      (nth 5 (file-attributes 
442                              (concat dir (int-to-string (car n))))))
443                     articles))
444         (setq n (cdr n))))
445     ;; Make Gnus mark all new articles as unread.
446     (or (zerop (length new))
447         (gnus-make-articles-unread 
448          (gnus-group-prefixed-name group (list 'nnmh ""))
449          (setq new (sort new '<))))
450     ;; Sort the article list with highest numbers first.
451     (setq articles (sort articles (lambda (art1 art2) 
452                                     (> (car art1) (car art2)))))
453     ;; Finally write this list back to the .nnmh-articles file.
454     (save-excursion
455       (set-buffer (get-buffer-create "*nnmh out*"))
456       (insert ";; Gnus article active file for " group "\n\n")
457       (insert "(setq nnmh-newsgroup-articles '")
458       (insert (prin1-to-string articles) ")\n")
459       (write-region (point-min) (point-max) nnmh-file nil 'nomesg)
460       (kill-buffer (current-buffer)))))
461
462 (provide 'nnmh)
463
464 ;;; nnmh.el ends here