2005-02-11 Hiroya Murata <lapis-lazuli@pop06.odn.ne.jp>
[elisp/wanderlust.git] / elmo / elmo-maildir.el
1 ;;; elmo-maildir.el --- Maildir interface for ELMO.
2
3 ;; Copyright (C) 1998,1999,2000 Yuuichi Teranishi <teranisi@gohome.org>
4
5 ;; Author: Yuuichi Teranishi <teranisi@gohome.org>
6 ;; Keywords: mail, net news
7
8 ;; This file is part of ELMO (Elisp Library for Message Orchestration).
9
10 ;; This program 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 ;; This program 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
26 ;;; Commentary:
27 ;;
28
29 ;;; Code:
30 ;;
31
32 (eval-when-compile (require 'cl))
33
34 (require 'elmo-util)
35 (require 'elmo)
36 (require 'elmo-map)
37
38 (defcustom elmo-maildir-folder-path "~/Maildir"
39   "*Maildir folder path."
40   :type 'directory
41   :group 'elmo)
42
43 (defconst elmo-maildir-flag-specs '((important ?F)
44                                     (read ?S)
45                                     (unread ?S 'remove)
46                                     (answered ?R)))
47
48 ;; Decided at compile time.
49 (defcustom elmo-maildir-separator
50   (if (memq system-type '(windows-nt)) ?\- ?:)
51   "Character separating the id section from the flags section.
52 According to the maildir specification, this should be a colon (?:),
53 but some file systems don't support colons in filenames."
54   :type 'character
55   :group 'elmo)
56
57 (defmacro elmo-maildir-adjust-separator (string)
58   `(if (= elmo-maildir-separator ?:)
59        ,string
60      (elmo-replace-in-string
61       ,string ":" (char-to-string elmo-maildir-separator))))
62
63 ;;; ELMO Maildir folder
64 (eval-and-compile
65   (luna-define-class elmo-maildir-folder
66                      (elmo-map-folder)
67                      (directory unread-locations
68                                 flagged-locations
69                                 answered-locations))
70   (luna-define-internal-accessors 'elmo-maildir-folder))
71
72 (luna-define-method elmo-folder-initialize ((folder
73                                              elmo-maildir-folder)
74                                             name)
75   (if (file-name-absolute-p name)
76       (elmo-maildir-folder-set-directory-internal
77        folder
78        (expand-file-name name))
79     (elmo-maildir-folder-set-directory-internal
80      folder
81      (expand-file-name
82       name
83       elmo-maildir-folder-path)))
84   folder)
85
86 (luna-define-method elmo-folder-expand-msgdb-path ((folder
87                                                     elmo-maildir-folder))
88   (expand-file-name
89    (elmo-replace-string-as-filename
90     (elmo-maildir-folder-directory-internal folder))
91    (expand-file-name
92     "maildir"
93     elmo-msgdb-directory)))
94
95 (defun elmo-maildir-message-file-name (folder location)
96   "Get a file name of the message from FOLDER which corresponded to
97 LOCATION."
98   (let ((file (file-name-completion
99                location
100                (expand-file-name
101                 "cur"
102                 (elmo-maildir-folder-directory-internal folder)))))
103     (if file
104         (expand-file-name
105          (if (eq file t) location file)
106          (expand-file-name
107           "cur"
108           (elmo-maildir-folder-directory-internal folder))))))
109
110 (defsubst elmo-maildir-list-location (dir &optional child-dir)
111   (let* ((cur-dir (expand-file-name (or child-dir "cur") dir))
112          (cur (directory-files cur-dir
113                                nil "^[^.].*$" t))
114          unread-locations flagged-locations answered-locations
115          sym locations flag-list)
116     (setq locations
117           (mapcar
118            (lambda (x)
119              (if (string-match
120                   (elmo-maildir-adjust-separator "^\\([^:]+\\):\\([^:]+\\)$")
121                   x)
122                  (progn
123                    (setq sym (elmo-match-string 1 x)
124                          flag-list (string-to-char-list
125                                     (elmo-match-string 2 x)))
126                    (when (memq ?F flag-list)
127                      (setq flagged-locations
128                            (cons sym flagged-locations)))
129                    (when (memq ?R flag-list)
130                      (setq answered-locations
131                            (cons sym answered-locations)))
132                    (unless (memq ?S flag-list)
133                      (setq unread-locations
134                            (cons sym unread-locations)))
135                    sym)
136                x))
137            cur))
138     (list locations unread-locations flagged-locations answered-locations)))
139
140 (luna-define-method elmo-map-folder-list-message-locations
141   ((folder elmo-maildir-folder))
142   (elmo-maildir-update-current folder)
143   (let ((locs (elmo-maildir-list-location
144                (elmo-maildir-folder-directory-internal folder))))
145     ;; 0: locations, 1: unread-locs, 2: flagged-locs 3: answered-locs
146     (elmo-maildir-folder-set-unread-locations-internal folder (nth 1 locs))
147     (elmo-maildir-folder-set-flagged-locations-internal folder (nth 2 locs))
148     (elmo-maildir-folder-set-answered-locations-internal folder (nth 3 locs))
149     (nth 0 locs)))
150
151 (luna-define-method elmo-map-folder-list-flagged ((folder elmo-maildir-folder)
152                                                   flag)
153   (case flag
154     (unread
155      (elmo-maildir-folder-unread-locations-internal folder))
156     (important
157      (elmo-maildir-folder-flagged-locations-internal folder))
158     (answered
159      (elmo-maildir-folder-answered-locations-internal folder))
160     (otherwise
161      t)))
162
163 (luna-define-method elmo-folder-msgdb-create ((folder elmo-maildir-folder)
164                                               numbers flag-table)
165   (let* ((unread-list (elmo-maildir-folder-unread-locations-internal folder))
166          (flagged-list (elmo-maildir-folder-flagged-locations-internal folder))
167          (answered-list (elmo-maildir-folder-answered-locations-internal
168                          folder))
169          (len (length numbers))
170          (new-msgdb (elmo-make-msgdb))
171          (i 0)
172          entity message-id flags location)
173     (message "Creating msgdb...")
174     (dolist (number numbers)
175       (setq location (elmo-map-message-location folder number))
176       (setq entity
177             (elmo-msgdb-create-message-entity-from-file
178              (elmo-msgdb-message-entity-handler new-msgdb)
179              number
180              (elmo-maildir-message-file-name folder location)))
181       (when entity
182         (setq message-id (elmo-message-entity-field entity 'message-id)
183               ;; Precede flag-table to file-info.
184               flags (copy-sequence
185                      (elmo-flag-table-get flag-table message-id)))
186
187         ;; Already flagged on filename (precede it to flag-table).
188         (when (member location flagged-list)
189           (or (memq 'important flags)
190               (setq flags (cons 'important flags))))
191         (when (member location answered-list)
192           (or (memq 'answered flags)
193               (setq flags (cons 'answered flags))))
194         (unless (member location unread-list)
195           (and (memq 'unread flags)
196                (setq flags (delq 'unread flags))))
197
198         ;; Update filename's info portion according to the flag-table.
199         (when (and (memq 'important flags)
200                    (not (member location flagged-list)))
201           (elmo-maildir-set-mark
202            (elmo-maildir-message-file-name folder location)
203            ?F)
204           ;; Append to flagged location list.
205           (elmo-maildir-folder-set-flagged-locations-internal
206            folder
207            (cons location
208                  (elmo-maildir-folder-flagged-locations-internal
209                   folder)))
210           (setq flags (delq 'unread flags)))
211         (when (and (memq 'answered flags)
212                    (not (member location answered-list)))
213           (elmo-maildir-set-mark
214            (elmo-maildir-message-file-name folder location)
215            ?R)
216           ;; Append to answered location list.
217           (elmo-maildir-folder-set-answered-locations-internal
218            folder
219            (cons location
220                  (elmo-maildir-folder-answered-locations-internal folder)))
221           (setq flags (delq 'unread flags)))
222         (when (and (not (memq 'unread flags))
223                    (member location unread-list))
224           (elmo-maildir-set-mark
225            (elmo-maildir-message-file-name folder location)
226            ?S)
227           ;; Delete from unread locations.
228           (elmo-maildir-folder-set-unread-locations-internal
229            folder
230            (delete location
231                    (elmo-maildir-folder-unread-locations-internal
232                     folder))))
233         (unless (memq 'unread flags)
234           (setq flags (delq 'new flags)))
235         (elmo-global-flags-set flags folder number message-id)
236         (elmo-msgdb-append-entity new-msgdb entity flags)
237         (when (> len elmo-display-progress-threshold)
238           (setq i (1+ i))
239           (elmo-display-progress
240            'elmo-maildir-msgdb-create "Creating msgdb..."
241            (/ (* i 100) len)))))
242     (message "Creating msgdb...done")
243     (elmo-msgdb-sort-by-date new-msgdb)))
244
245 (defun elmo-maildir-cleanup-temporal (dir)
246   ;; Delete files in the tmp dir which are not accessed
247   ;; for more than 36 hours.
248   (let ((cur-time (current-time))
249         (count 0)
250         last-accessed)
251     (mapcar (function
252              (lambda (file)
253                (setq last-accessed (nth 4 (file-attributes file)))
254                (when (or (> (- (car cur-time)(car last-accessed)) 1)
255                          (and (eq (- (car cur-time)(car last-accessed)) 1)
256                               (> (- (cadr cur-time)(cadr last-accessed))
257                                  64064))) ; 36 hours.
258                  (message "Maildir: %d tmp file(s) are cleared."
259                           (setq count (1+ count)))
260                  (delete-file file))))
261             (directory-files (expand-file-name "tmp" dir)
262                              t ; full
263                              "^[^.].*$" t))))
264
265 (defun elmo-maildir-update-current (folder)
266   "Move all new msgs to cur in the maildir."
267   (let* ((maildir (elmo-maildir-folder-directory-internal folder))
268          (news (directory-files (expand-file-name "new"
269                                                   maildir)
270                                 nil
271                                 "^[^.].*$" t)))
272     ;; cleanup tmp directory.
273     (elmo-maildir-cleanup-temporal maildir)
274     ;; move new msgs to cur directory.
275     (while news
276       (rename-file
277        (expand-file-name (car news) (expand-file-name "new" maildir))
278        (expand-file-name (concat
279                           (car news)
280                           (unless (string-match
281                                    (elmo-maildir-adjust-separator ":2,[A-Z]*$")
282                                    (car news))
283                             (elmo-maildir-adjust-separator  ":2,")))
284                          (expand-file-name "cur" maildir)))
285       (setq news (cdr news)))))
286
287 (defun elmo-maildir-set-mark (filename mark)
288   "Mark the FILENAME file in the maildir.  MARK is a character."
289   (if (string-match
290        (elmo-maildir-adjust-separator "^\\([^:]+:[12],\\)\\(.*\\)$")
291        filename)
292       (let ((flaglist (string-to-char-list (elmo-match-string
293                                             2 filename))))
294         (unless (memq mark flaglist)
295           (setq flaglist (sort (cons mark flaglist) '<))
296           (rename-file filename
297                        (concat (elmo-match-string 1 filename)
298                                (char-list-to-string flaglist)))))
299     ;; Rescue no info file in maildir.
300     (rename-file filename
301                  (concat filename
302                          (elmo-maildir-adjust-separator ":2,")
303                          (char-to-string mark))))
304   t)
305
306 (defun elmo-maildir-delete-mark (filename mark)
307   "Mark the FILENAME file in the maildir.  MARK is a character."
308   (if (string-match (elmo-maildir-adjust-separator "^\\([^:]+:2,\\)\\(.*\\)$")
309                     filename)
310       (let ((flaglist (string-to-char-list (elmo-match-string
311                                             2 filename))))
312         (when (memq mark flaglist)
313           (setq flaglist (delq mark flaglist))
314           (rename-file filename
315                        (concat (elmo-match-string 1 filename)
316                                (if flaglist
317                                    (char-list-to-string flaglist))))))))
318
319 (defsubst elmo-maildir-set-mark-msgs (folder locs mark)
320   (dolist (loc locs)
321     (elmo-maildir-set-mark
322      (elmo-maildir-message-file-name folder loc)
323      mark))
324   t)
325
326 (defsubst elmo-maildir-delete-mark-msgs (folder locs mark)
327   (dolist (loc locs)
328     (elmo-maildir-delete-mark
329      (elmo-maildir-message-file-name folder loc)
330      mark))
331   t)
332
333 (defsubst elmo-maildir-set-mark-messages (folder locations mark remove)
334   (when mark
335     (if remove
336         (elmo-maildir-delete-mark-msgs folder locations mark)
337       (elmo-maildir-set-mark-msgs folder locations mark))))
338
339 (luna-define-method elmo-map-folder-set-flag ((folder elmo-maildir-folder)
340                                               locations flag)
341   (let ((spec (cdr (assq flag elmo-maildir-flag-specs))))
342     (when spec
343       (elmo-maildir-set-mark-messages folder locations
344                                       (car spec) (nth 1 spec)))))
345
346 (luna-define-method elmo-map-folder-unset-flag ((folder elmo-maildir-folder)
347                                                 locations flag)
348   (let ((spec (cdr (assq flag elmo-maildir-flag-specs))))
349     (when spec
350       (elmo-maildir-set-mark-messages folder locations
351                                       (car spec) (not (nth 1 spec))))))
352
353 (luna-define-method elmo-folder-list-subfolders
354   ((folder elmo-maildir-folder) &optional one-level)
355   (let ((prefix (concat (elmo-folder-name-internal folder)
356                         (unless (string= (elmo-folder-prefix-internal folder)
357                                          (elmo-folder-name-internal folder))
358                           elmo-path-sep)))
359         (elmo-list-subdirectories-ignore-regexp
360          "^\\(\\.\\.?\\|cur\\|tmp\\|new\\)$")
361         elmo-have-link-count)
362     (append
363      (list (elmo-folder-name-internal folder))
364      (elmo-mapcar-list-of-list
365       (function (lambda (x) (concat prefix x)))
366       (elmo-list-subdirectories
367        (elmo-maildir-folder-directory-internal folder)
368        ""
369        one-level)))))
370
371 (defvar elmo-maildir-sequence-number-internal 0)
372
373 (static-cond
374  ((>= emacs-major-version 19)
375   (defun elmo-maildir-make-unique-string ()
376     "This function generates a string that can be used as a unique
377 file name for maildir directories."
378      (let ((cur-time (current-time)))
379        (format "%.0f.%d_%d.%s"
380               (+ (* (car cur-time)
381                     (float 65536)) (cadr cur-time))
382               (emacs-pid)
383               (incf elmo-maildir-sequence-number-internal)
384               (system-name)))))
385  ((eq emacs-major-version 18)
386   ;; A fake function for v18
387   (defun elmo-maildir-make-unique-string ()
388     "This function generates a string that can be used as a unique
389 file name for maildir directories."
390     (unless (fboundp 'float-to-string)
391       (load-library "float"))
392     (let ((time (current-time)))
393       (format "%s%d.%d.%s"
394               (substring
395                (float-to-string
396                 (f+ (f* (f (car time))
397                         (f 65536))
398                     (f (cadr time))))
399                0 5)
400               (cadr time)
401               (% (abs (random t)) 10000); dummy pid
402               (system-name))))))
403
404 (defun elmo-maildir-temporal-filename (basedir)
405   (let ((filename (expand-file-name
406                    (concat "tmp/" (elmo-maildir-make-unique-string))
407                    basedir)))
408     (unless (file-exists-p (file-name-directory filename))
409       (make-directory (file-name-directory filename)))
410     (while (file-exists-p filename)
411 ;;; I don't want to wait.
412 ;;;   (sleep-for 2)
413       (setq filename
414             (expand-file-name
415              (concat "tmp/" (elmo-maildir-make-unique-string))
416              basedir)))
417     filename))
418
419 (defun elmo-maildir-move-file (src dst)
420   (or (condition-case nil
421           (progn
422             ;; 1. Try add-link-to-file, then delete the original.
423             ;;    This is safe on NFS.
424             (add-name-to-file src dst)
425             (ignore-errors
426               ;; It's ok if the delete-file fails;
427               ;; elmo-maildir-cleanup-temporal will catch it later.
428               (delete-file src))
429             t)
430         (error))
431       ;; 2. Even on systems with hardlinks, some filesystems (like AFS)
432       ;;    might not support them, so fall back on rename-file. This is
433       ;;    our best shot at atomic when add-name-to-file fails.
434       (rename-file src dst)))
435
436 (luna-define-method elmo-folder-append-buffer ((folder elmo-maildir-folder)
437                                                &optional flags number)
438   (let ((basedir (elmo-maildir-folder-directory-internal folder))
439         (src-buf (current-buffer))
440         dst-buf filename)
441     (condition-case nil
442         (with-temp-buffer
443           (setq filename (elmo-maildir-temporal-filename basedir))
444           (setq dst-buf (current-buffer))
445           (with-current-buffer src-buf
446             (copy-to-buffer dst-buf (point-min) (point-max)))
447           (as-binary-output-file
448            (write-region (point-min) (point-max) filename nil 'no-msg))
449           (elmo-maildir-move-file
450            filename
451            (expand-file-name
452             (concat "new/" (file-name-nondirectory filename))
453             basedir))
454           (elmo-folder-preserve-flags
455            folder (elmo-msgdb-get-message-id-from-buffer) flags)
456           t)
457       ;; If an error occured, return nil.
458       (error))))
459
460 (luna-define-method elmo-folder-message-file-p ((folder elmo-maildir-folder))
461   t)
462
463 (luna-define-method elmo-message-file-name ((folder elmo-maildir-folder)
464                                             number)
465   (elmo-maildir-message-file-name
466    folder
467    (elmo-map-message-location folder number)))
468
469 (luna-define-method elmo-folder-message-make-temp-file-p
470   ((folder elmo-maildir-folder))
471   t)
472
473 (luna-define-method elmo-folder-message-make-temp-files ((folder
474                                                           elmo-maildir-folder)
475                                                          numbers
476                                                          &optional
477                                                          start-number)
478   (let ((temp-dir (elmo-folder-make-temporary-directory folder))
479         (cur-number (if start-number 0)))
480     (dolist (number numbers)
481       (elmo-copy-file
482        (elmo-message-file-name folder number)
483        (expand-file-name
484         (int-to-string (if start-number (incf cur-number) number))
485         temp-dir)))
486     temp-dir))
487
488 (luna-define-method elmo-folder-append-messages :around
489   ((folder elmo-maildir-folder)
490    src-folder numbers &optional same-number)
491   (if (elmo-folder-message-file-p src-folder)
492       (let ((src-msgdb-exists (not (zerop (elmo-folder-length src-folder))))
493             (dir (elmo-maildir-folder-directory-internal folder))
494             (table (elmo-folder-flag-table folder))
495             (succeeds numbers)
496             filename flags id)
497         (dolist (number numbers)
498           (setq flags (elmo-message-flags src-folder (car numbers))
499                 filename (elmo-maildir-temporal-filename dir))
500           (elmo-copy-file
501            (elmo-message-file-name src-folder number)
502            filename)
503           (elmo-maildir-move-file
504            filename
505            (expand-file-name
506             (concat "new/" (file-name-nondirectory filename))
507             dir))
508           ;; src folder's msgdb is loaded.
509           (when (setq id (and src-msgdb-exists
510                               (elmo-message-field src-folder (car numbers)
511                                                   'message-id)))
512             (elmo-flag-table-set table id flags))
513           (elmo-progress-notify 'elmo-folder-move-messages))
514         (when (elmo-folder-persistent-p folder)
515           (elmo-folder-close-flag-table folder))
516         succeeds)
517     (luna-call-next-method)))
518
519 (luna-define-method elmo-map-folder-delete-messages
520   ((folder elmo-maildir-folder) locations)
521   (let (file)
522     (dolist (location locations)
523       (setq file (elmo-maildir-message-file-name folder location))
524       (if (and file
525                (file-writable-p file)
526                (not (file-directory-p file)))
527           (delete-file file))))
528   t)
529
530 (luna-define-method elmo-map-message-fetch ((folder elmo-maildir-folder)
531                                             location strategy
532                                             &optional section unseen)
533   (let ((file (elmo-maildir-message-file-name folder location)))
534     (when (file-exists-p file)
535       (insert-file-contents-as-binary file)
536       (unless unseen
537         (elmo-map-folder-set-flag folder (list location) 'read))
538       t)))
539
540 (luna-define-method elmo-folder-exists-p ((folder elmo-maildir-folder))
541   (let ((basedir (elmo-maildir-folder-directory-internal folder)))
542     (and (file-directory-p (expand-file-name "new" basedir))
543          (file-directory-p (expand-file-name "cur" basedir))
544          (file-directory-p (expand-file-name "tmp" basedir)))))
545
546 (luna-define-method elmo-folder-diff ((folder elmo-maildir-folder))
547   (let* ((dir (elmo-maildir-folder-directory-internal folder))
548          (new-len (length (car (elmo-maildir-list-location dir "new"))))
549          (cur-len (length (car (elmo-maildir-list-location dir "cur")))))
550     (cons new-len (+ new-len cur-len))))
551
552 (luna-define-method elmo-folder-creatable-p ((folder elmo-maildir-folder))
553   t)
554
555 (luna-define-method elmo-folder-writable-p ((folder elmo-maildir-folder))
556   t)
557
558 (luna-define-method elmo-folder-create ((folder elmo-maildir-folder))
559   (let ((basedir (elmo-maildir-folder-directory-internal folder)))
560     (condition-case nil
561         (progn
562           (dolist (dir '("." "new" "cur" "tmp"))
563             (setq dir (expand-file-name dir basedir))
564             (or (file-directory-p dir)
565                 (progn
566                   (elmo-make-directory dir)
567                   (set-file-modes dir 448))))
568           t)
569       (error))))
570
571 (luna-define-method elmo-folder-delete ((folder elmo-maildir-folder))
572   (let ((msgs (and (elmo-folder-exists-p folder)
573                    (elmo-folder-list-messages folder))))
574     (when (yes-or-no-p (format "%sDelete msgdb and substance of \"%s\"? "
575                                (if (> (length msgs) 0)
576                                    (format "%d msg(s) exists. " (length msgs))
577                                  "")
578                                (elmo-folder-name-internal folder)))
579       (let ((basedir (elmo-maildir-folder-directory-internal folder)))
580         (condition-case nil
581             (let ((tmp-files (directory-files
582                               (expand-file-name "tmp" basedir)
583                               t "[^.].*")))
584               ;; Delete files in tmp.
585               (dolist (file tmp-files)
586                 (delete-file file))
587               (dolist (dir '("new" "cur" "tmp" "."))
588                 (setq dir (expand-file-name dir basedir))
589                 (if (not (file-directory-p dir))
590                     (error nil)
591                   (elmo-delete-directory dir t))))
592           (error nil)))
593       (elmo-msgdb-delete-path folder)
594       t)))
595
596 (luna-define-method elmo-folder-rename-internal ((folder elmo-maildir-folder)
597                                                  new-folder)
598   (let* ((old (elmo-maildir-folder-directory-internal folder))
599          (new (elmo-maildir-folder-directory-internal new-folder))
600          (new-dir (directory-file-name (file-name-directory new))))
601     (unless (file-directory-p old)
602       (error "No such directory: %s" old))
603     (when (file-exists-p new)
604       (error "Already exists directory: %s" new))
605     (unless (file-directory-p new-dir)
606       (elmo-make-directory new-dir))
607     (rename-file old new)
608     t))
609
610 (require 'product)
611 (product-provide (provide 'elmo-maildir) (require 'elmo-version))
612
613 ;;; elmo-maildir.el ends here