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