(elmo-maildir-sequence-number): New function.
[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 x-time y-time)
117     (setq cur (sort cur
118                     (lambda (x y)
119                       (setq x-time (elmo-get-last-modification-time
120                                     (expand-file-name x cur-dir))
121                             y-time (elmo-get-last-modification-time
122                                     (expand-file-name y cur-dir)))
123                       (cond
124                        ((< x-time y-time)
125                         t)
126                        ((eq x-time y-time)
127                         (< (elmo-maildir-sequence-number x)
128                            (elmo-maildir-sequence-number y)))))))
129     (setq locations
130           (mapcar
131            (lambda (x)
132              (if (string-match
133                   (elmo-maildir-adjust-separator "^\\([^:]+\\):\\([^:]+\\)$")
134                   x)
135                  (progn
136                    (setq sym (elmo-match-string 1 x)
137                          flag-list (string-to-char-list
138                                     (elmo-match-string 2 x)))
139                    (when (memq ?F flag-list)
140                      (setq flagged-locations
141                            (cons sym flagged-locations)))
142                    (when (memq ?R flag-list)
143                      (setq answered-locations
144                            (cons sym answered-locations)))
145                    (unless (memq ?S flag-list)
146                      (setq unread-locations
147                            (cons sym unread-locations)))
148                    sym)
149                x))
150            cur))
151     (list locations unread-locations flagged-locations answered-locations)))
152
153 (luna-define-method elmo-map-folder-list-message-locations
154   ((folder elmo-maildir-folder))
155   (elmo-maildir-update-current folder)
156   (let ((locs (elmo-maildir-list-location
157                (elmo-maildir-folder-directory-internal folder))))
158     ;; 0: locations, 1: unread-locs, 2: flagged-locs 3: answered-locs
159     (elmo-maildir-folder-set-unread-locations-internal folder (nth 1 locs))
160     (elmo-maildir-folder-set-flagged-locations-internal folder (nth 2 locs))
161     (elmo-maildir-folder-set-answered-locations-internal folder (nth 3 locs))
162     (nth 0 locs)))
163
164 (luna-define-method elmo-map-folder-list-flagged ((folder elmo-maildir-folder)
165                                                   flag)
166   (case flag
167     (unread
168      (elmo-maildir-folder-unread-locations-internal folder))
169     (important
170      (elmo-maildir-folder-flagged-locations-internal folder))
171     (answered
172      (elmo-maildir-folder-answered-locations-internal folder))
173     (otherwise
174      t)))
175
176 (luna-define-method elmo-folder-msgdb-create ((folder elmo-maildir-folder)
177                                               numbers flag-table)
178   (let* ((unread-list (elmo-maildir-folder-unread-locations-internal folder))
179          (flagged-list (elmo-maildir-folder-flagged-locations-internal folder))
180          (answered-list (elmo-maildir-folder-answered-locations-internal
181                          folder))
182          (len (length numbers))
183          (new-msgdb (elmo-make-msgdb))
184          (i 0)
185          entity message-id flags location)
186     (message "Creating msgdb...")
187     (dolist (number numbers)
188       (setq location (elmo-map-message-location folder number))
189       (setq entity
190             (elmo-msgdb-create-message-entity-from-file
191              (elmo-msgdb-message-entity-handler new-msgdb)
192              number
193              (elmo-maildir-message-file-name folder location)))
194       (when entity
195         (setq message-id (elmo-message-entity-field entity 'message-id)
196               ;; Precede flag-table to file-info.
197               flags (copy-sequence
198                      (elmo-flag-table-get flag-table message-id)))
199
200         ;; Already flagged on filename (precede it to flag-table).
201         (when (member location flagged-list)
202           (or (memq 'important flags)
203               (setq flags (cons 'important flags))))
204         (when (member location answered-list)
205           (or (memq 'answered flags)
206               (setq flags (cons 'answered flags))))
207         (unless (member location unread-list)
208           (and (memq 'unread flags)
209                (setq flags (delq 'unread flags))))
210
211         ;; Update filename's info portion according to the flag-table.
212         (when (and (memq 'important flags)
213                    (not (member location flagged-list)))
214           (elmo-maildir-set-mark
215            (elmo-maildir-message-file-name folder location)
216            ?F)
217           ;; Append to flagged location list.
218           (elmo-maildir-folder-set-flagged-locations-internal
219            folder
220            (cons location
221                  (elmo-maildir-folder-flagged-locations-internal
222                   folder)))
223           (setq flags (delq 'unread flags)))
224         (when (and (memq 'answered flags)
225                    (not (member location answered-list)))
226           (elmo-maildir-set-mark
227            (elmo-maildir-message-file-name folder location)
228            ?R)
229           ;; Append to answered location list.
230           (elmo-maildir-folder-set-answered-locations-internal
231            folder
232            (cons location
233                  (elmo-maildir-folder-answered-locations-internal folder)))
234           (setq flags (delq 'unread flags)))
235         (when (and (not (memq 'unread flags))
236                    (member location unread-list))
237           (elmo-maildir-set-mark
238            (elmo-maildir-message-file-name folder location)
239            ?S)
240           ;; Delete from unread locations.
241           (elmo-maildir-folder-set-unread-locations-internal
242            folder
243            (delete location
244                    (elmo-maildir-folder-unread-locations-internal
245                     folder))))
246         (unless (memq 'unread flags)
247           (setq flags (delq 'new flags)))
248         (elmo-global-flags-set flags folder number message-id)
249         (elmo-msgdb-append-entity new-msgdb entity flags)
250         (when (> len elmo-display-progress-threshold)
251           (setq i (1+ i))
252           (elmo-display-progress
253            'elmo-maildir-msgdb-create "Creating msgdb..."
254            (/ (* i 100) len)))))
255     (message "Creating msgdb...done")
256     new-msgdb))
257
258 (defun elmo-maildir-cleanup-temporal (dir)
259   ;; Delete files in the tmp dir which are not accessed
260   ;; for more than 36 hours.
261   (let ((cur-time (current-time))
262         (count 0)
263         last-accessed)
264     (mapcar (function
265              (lambda (file)
266                (setq last-accessed (nth 4 (file-attributes file)))
267                (when (or (> (- (car cur-time)(car last-accessed)) 1)
268                          (and (eq (- (car cur-time)(car last-accessed)) 1)
269                               (> (- (cadr cur-time)(cadr last-accessed))
270                                  64064))) ; 36 hours.
271                  (message "Maildir: %d tmp file(s) are cleared."
272                           (setq count (1+ count)))
273                  (delete-file file))))
274             (directory-files (expand-file-name "tmp" dir)
275                              t ; full
276                              "^[^.].*$" t))))
277
278 (defun elmo-maildir-update-current (folder)
279   "Move all new msgs to cur in the maildir."
280   (let* ((maildir (elmo-maildir-folder-directory-internal folder))
281          (news (directory-files (expand-file-name "new"
282                                                   maildir)
283                                 nil
284                                 "^[^.].*$" t)))
285     ;; cleanup tmp directory.
286     (elmo-maildir-cleanup-temporal maildir)
287     ;; move new msgs to cur directory.
288     (while news
289       (rename-file
290        (expand-file-name (car news) (expand-file-name "new" maildir))
291        (expand-file-name (concat
292                           (car news)
293                           (unless (string-match
294                                    (elmo-maildir-adjust-separator ":2,[A-Z]*$")
295                                    (car news))
296                             (elmo-maildir-adjust-separator  ":2,")))
297                          (expand-file-name "cur" maildir)))
298       (setq news (cdr news)))))
299
300 (defun elmo-maildir-set-mark (filename mark)
301   "Mark the FILENAME file in the maildir.  MARK is a character."
302   (if (string-match
303        (elmo-maildir-adjust-separator "^\\([^:]+:[12],\\)\\(.*\\)$")
304        filename)
305       (let ((flaglist (string-to-char-list (elmo-match-string
306                                             2 filename))))
307         (unless (memq mark flaglist)
308           (setq flaglist (sort (cons mark flaglist) '<))
309           (rename-file filename
310                        (concat (elmo-match-string 1 filename)
311                                (char-list-to-string flaglist)))))
312     ;; Rescue no info file in maildir.
313     (rename-file filename
314                  (concat filename
315                          (elmo-maildir-adjust-separator ":2,")
316                          (char-to-string mark))))
317   t)
318
319 (defun elmo-maildir-delete-mark (filename mark)
320   "Mark the FILENAME file in the maildir.  MARK is a character."
321   (if (string-match (elmo-maildir-adjust-separator "^\\([^:]+:2,\\)\\(.*\\)$")
322                     filename)
323       (let ((flaglist (string-to-char-list (elmo-match-string
324                                             2 filename))))
325         (when (memq mark flaglist)
326           (setq flaglist (delq mark flaglist))
327           (rename-file filename
328                        (concat (elmo-match-string 1 filename)
329                                (if flaglist
330                                    (char-list-to-string flaglist))))))))
331
332 (defsubst elmo-maildir-set-mark-msgs (folder locs mark)
333   (dolist (loc locs)
334     (elmo-maildir-set-mark
335      (elmo-maildir-message-file-name folder loc)
336      mark))
337   t)
338
339 (defsubst elmo-maildir-delete-mark-msgs (folder locs mark)
340   (dolist (loc locs)
341     (elmo-maildir-delete-mark
342      (elmo-maildir-message-file-name folder loc)
343      mark))
344   t)
345
346 (defsubst elmo-maildir-set-mark-messages (folder locations mark remove)
347   (when mark
348     (if remove
349         (elmo-maildir-delete-mark-msgs folder locations mark)
350       (elmo-maildir-set-mark-msgs folder locations mark))))
351
352 (luna-define-method elmo-map-folder-set-flag ((folder elmo-maildir-folder)
353                                               locations flag)
354   (let ((spec (cdr (assq flag elmo-maildir-flag-specs))))
355     (when spec
356       (elmo-maildir-set-mark-messages folder locations
357                                       (car spec) (nth 1 spec)))))
358
359 (luna-define-method elmo-map-folder-unset-flag ((folder elmo-maildir-folder)
360                                                 locations flag)
361   (let ((spec (cdr (assq flag elmo-maildir-flag-specs))))
362     (when spec
363       (elmo-maildir-set-mark-messages folder locations
364                                       (car spec) (not (nth 1 spec))))))
365
366 (luna-define-method elmo-folder-list-subfolders
367   ((folder elmo-maildir-folder) &optional one-level)
368   (let ((prefix (concat (elmo-folder-name-internal folder)
369                         (unless (string= (elmo-folder-prefix-internal folder)
370                                          (elmo-folder-name-internal folder))
371                           elmo-path-sep)))
372         (elmo-list-subdirectories-ignore-regexp
373          "^\\(\\.\\.?\\|cur\\|tmp\\|new\\)$")
374         elmo-have-link-count)
375     (append
376      (list (elmo-folder-name-internal folder))
377      (elmo-mapcar-list-of-list
378       (function (lambda (x) (concat prefix x)))
379       (elmo-list-subdirectories
380        (elmo-maildir-folder-directory-internal folder)
381        ""
382        one-level)))))
383
384 (defvar elmo-maildir-sequence-number-internal 0)
385
386 (defun elmo-maildir-sequence-number (file)
387   "Get `elmo-maildir' specific sequence number from FILE.
388 Not that FILE is the name without directory."
389   ;; elmo-maildir specific.
390   (if (string-match "^.*_\\([0-9]+\\)\\..*" file)
391       (string-to-number (match-string 1 file))
392     -1))
393
394 (defun elmo-maildir-make-unique-string ()
395   "This function generates a string that can be used as a unique
396 file name for maildir directories."
397   (let ((cur-time (current-time)))
398     (format "%.0f.%d_%d.%s"
399             (+ (* (car cur-time)
400                   (float 65536)) (cadr cur-time))
401             (emacs-pid)
402             (incf elmo-maildir-sequence-number-internal)
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 number)
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