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