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