1 ;;; nnmaildir.el --- maildir backend for Gnus
4 ;; Author: Paul Jarc <prj@po.cwru.edu>
6 ;; This file is part of GNU Emacs.
8 ;; GNU Emacs is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; GNU Emacs is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with GNU Emacs; see the file COPYING. If not, write to the
20 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 ;; Boston, MA 02110-1301, USA.
25 ;; Maildir format is documented at <URL:http://cr.yp.to/proto/maildir.html>
26 ;; and in the maildir(5) man page from qmail (available at
27 ;; <URL:http://www.qmail.org/man/man5/maildir.html>). nnmaildir also stores
28 ;; extra information in the .nnmaildir/ directory within a maildir.
30 ;; Some goals of nnmaildir:
31 ;; * Everything Just Works, and correctly. E.g., NOV data is automatically
32 ;; regenerated when stale; no need for manually running
33 ;; *-generate-nov-databases.
34 ;; * Perfect reliability: [C-g] will never corrupt its data in memory, and
35 ;; SIGKILL will never corrupt its data in the filesystem.
36 ;; * Allow concurrent operation as much as possible. If files change out
37 ;; from under us, adapt to the changes or degrade gracefully.
38 ;; * We use the filesystem as a database, so that, e.g., it's easy to
39 ;; manipulate marks from outside Gnus.
40 ;; * All information about a group is stored in the maildir, for easy backup,
41 ;; copying, restoring, etc.
44 ;; * When moving an article for expiry, copy all the marks except 'expire
45 ;; from the original article.
46 ;; * Add a hook for when moving messages from new/ to cur/, to support
47 ;; nnmail's duplicate detection.
48 ;; * Improve generated Xrefs, so crossposts are detectable.
49 ;; * Improve code readability.
53 ;; eval this before editing
55 (put 'nnmaildir--with-nntp-buffer 'lisp-indent-function 0)
56 (put 'nnmaildir--with-work-buffer 'lisp-indent-function 0)
57 (put 'nnmaildir--with-nov-buffer 'lisp-indent-function 0)
58 (put 'nnmaildir--with-move-buffer 'lisp-indent-function 0)
59 (put 'nnmaildir--condcase 'lisp-indent-function 2)
75 (defconst nnmaildir-version "Gnus")
77 (defvar nnmaildir-article-file-name nil
78 "*The filename of the most recently requested article. This variable is set
79 by nnmaildir-request-article.")
81 ;; The filename of the article being moved/copied:
82 (defvar nnmaildir--file nil)
84 ;; Variables to generate filenames of messages being delivered:
85 (defvar nnmaildir--delivery-time "")
86 (defconst nnmaildir--delivery-pid (concat "P" (number-to-string (emacs-pid))))
87 (defvar nnmaildir--delivery-count nil)
89 ;; An obarry containing symbols whose names are server names and whose values
91 (defvar nnmaildir--servers (make-vector 3 0))
92 ;; The current server:
93 (defvar nnmaildir--cur-server nil)
95 ;; A copy of nnmail-extra-headers
96 (defvar nnmaildir--extra nil)
98 ;; A NOV structure looks like this (must be prin1-able, so no defstruct):
99 ["subject\tfrom\tdate"
100 "references\tchars\lines"
101 "To: you\tIn-Reply-To: <your.mess@ge>"
102 (12345 67890) ;; modtime of the corresponding article file
103 (to in-reply-to)] ;; contemporary value of nnmail-extra-headers
104 (defconst nnmaildir--novlen 5)
105 (defmacro nnmaildir--nov-new (beg mid end mtime extra)
106 `(vector ,beg ,mid ,end ,mtime ,extra))
107 (defmacro nnmaildir--nov-get-beg (nov) `(aref ,nov 0))
108 (defmacro nnmaildir--nov-get-mid (nov) `(aref ,nov 1))
109 (defmacro nnmaildir--nov-get-end (nov) `(aref ,nov 2))
110 (defmacro nnmaildir--nov-get-mtime (nov) `(aref ,nov 3))
111 (defmacro nnmaildir--nov-get-extra (nov) `(aref ,nov 4))
112 (defmacro nnmaildir--nov-set-beg (nov value) `(aset ,nov 0 ,value))
113 (defmacro nnmaildir--nov-set-mid (nov value) `(aset ,nov 1 ,value))
114 (defmacro nnmaildir--nov-set-end (nov value) `(aset ,nov 2 ,value))
115 (defmacro nnmaildir--nov-set-mtime (nov value) `(aset ,nov 3 ,value))
116 (defmacro nnmaildir--nov-set-extra (nov value) `(aset ,nov 4 ,value))
118 (defstruct nnmaildir--art
119 (prefix nil :type string) ;; "time.pid.host"
120 (suffix nil :type string) ;; ":2,flags"
121 (num nil :type natnum) ;; article number
122 (msgid nil :type string) ;; "<mess.age@id>"
123 (nov nil :type vector)) ;; cached nov structure, or nil
125 (defstruct nnmaildir--grp
126 (name nil :type string) ;; "group.name"
127 (new nil :type list) ;; new/ modtime
128 (cur nil :type list) ;; cur/ modtime
129 (min 1 :type natnum) ;; minimum article number
130 (count 0 :type natnum) ;; count of articles
131 (nlist nil :type list) ;; list of articles, ordered descending by number
132 (flist nil :type vector) ;; obarray mapping filename prefix->article
133 (mlist nil :type vector) ;; obarray mapping message-id->article
134 (cache nil :type vector) ;; nov cache
135 (index nil :type natnum) ;; index of next cache entry to replace
136 (mmth nil :type vector)) ;; obarray mapping mark name->dir modtime
137 ; ("Mark Mod Time Hash")
139 (defstruct nnmaildir--srv
140 (address nil :type string) ;; server address string
141 (method nil :type list) ;; (nnmaildir "address" ...)
142 (prefix nil :type string) ;; "nnmaildir+address:"
143 (dir nil :type string) ;; "/expanded/path/to/server/dir/"
144 (ls nil :type function) ;; directory-files function
145 (groups nil :type vector) ;; obarray mapping group name->group
146 (curgrp nil :type nnmaildir--grp) ;; current group, or nil
147 (error nil :type string) ;; last error message, or nil
148 (mtime nil :type list) ;; modtime of dir
149 (gnm nil) ;; flag: split from mail-sources?
150 (target-prefix nil :type string)) ;; symlink target prefix
152 (defun nnmaildir--expired-article (group article)
153 (setf (nnmaildir--art-nov article) nil)
154 (let ((flist (nnmaildir--grp-flist group))
155 (mlist (nnmaildir--grp-mlist group))
156 (min (nnmaildir--grp-min group))
157 (count (1- (nnmaildir--grp-count group)))
158 (prefix (nnmaildir--art-prefix article))
159 (msgid (nnmaildir--art-msgid article))
161 (nlist-pre '(nil . nil))
163 (unless (zerop count)
164 (setq nlist-post (nnmaildir--grp-nlist group)
165 num (nnmaildir--art-num article))
166 (if (eq num (caar nlist-post))
167 (setq new-nlist (cdr nlist-post))
168 (setq new-nlist nlist-post
170 nlist-post (cdr nlist-post))
171 (while (/= num (caar nlist-post))
172 (setq nlist-pre nlist-post
173 nlist-post (cdr nlist-post)))
174 (setq nlist-post (cdr nlist-post))
176 (setq min (caar nlist-pre)))))
177 (let ((inhibit-quit t))
178 (setf (nnmaildir--grp-min group) min)
179 (setf (nnmaildir--grp-count group) count)
180 (setf (nnmaildir--grp-nlist group) new-nlist)
181 (setcdr nlist-pre nlist-post)
182 (unintern prefix flist)
183 (unintern msgid mlist))))
185 (defun nnmaildir--nlist-art (group num)
186 (let ((entry (assq num (nnmaildir--grp-nlist group))))
189 (defmacro nnmaildir--flist-art (list file)
190 `(symbol-value (intern-soft ,file ,list)))
191 (defmacro nnmaildir--mlist-art (list msgid)
192 `(symbol-value (intern-soft ,msgid ,list)))
194 (defun nnmaildir--pgname (server gname)
195 (let ((prefix (nnmaildir--srv-prefix server)))
196 (if prefix (concat prefix gname)
197 (setq gname (gnus-group-prefixed-name gname
198 (nnmaildir--srv-method server)))
199 (setf (nnmaildir--srv-prefix server) (gnus-group-real-prefix gname))
202 (defun nnmaildir--param (pgname param)
203 (setq param (gnus-group-find-parameter pgname param 'allow-list))
204 (if (vectorp param) (setq param (aref param 0)))
207 (defmacro nnmaildir--with-nntp-buffer (&rest body)
209 (set-buffer nntp-server-buffer)
211 (defmacro nnmaildir--with-work-buffer (&rest body)
213 (set-buffer (get-buffer-create " *nnmaildir work*"))
215 (defmacro nnmaildir--with-nov-buffer (&rest body)
217 (set-buffer (get-buffer-create " *nnmaildir nov*"))
219 (defmacro nnmaildir--with-move-buffer (&rest body)
221 (set-buffer (get-buffer-create " *nnmaildir move*"))
224 (defmacro nnmaildir--subdir (dir subdir)
225 `(file-name-as-directory (concat ,dir ,subdir)))
226 (defmacro nnmaildir--srvgrp-dir (srv-dir gname)
227 `(nnmaildir--subdir ,srv-dir ,gname))
228 (defmacro nnmaildir--tmp (dir) `(nnmaildir--subdir ,dir "tmp"))
229 (defmacro nnmaildir--new (dir) `(nnmaildir--subdir ,dir "new"))
230 (defmacro nnmaildir--cur (dir) `(nnmaildir--subdir ,dir "cur"))
231 (defmacro nnmaildir--nndir (dir) `(nnmaildir--subdir ,dir ".nnmaildir"))
232 (defmacro nnmaildir--nov-dir (dir) `(nnmaildir--subdir ,dir "nov"))
233 (defmacro nnmaildir--marks-dir (dir) `(nnmaildir--subdir ,dir "marks"))
234 (defmacro nnmaildir--num-dir (dir) `(nnmaildir--subdir ,dir "num"))
236 (defmacro nnmaildir--unlink (file-arg)
237 `(let ((file ,file-arg))
238 (if (file-attributes file) (delete-file file))))
239 (defun nnmaildir--mkdir (dir)
240 (or (file-exists-p (file-name-as-directory dir))
241 (make-directory-internal (directory-file-name dir))))
242 (defun nnmaildir--mkfile (file)
243 (write-region "" nil file nil 'no-message))
244 (defun nnmaildir--delete-dir-files (dir ls)
245 (when (file-attributes dir)
246 (mapcar 'delete-file (funcall ls dir 'full "\\`[^.]" 'nosort))
247 (delete-directory dir)))
249 (defun nnmaildir--group-maxnum (server group)
251 (if (zerop (nnmaildir--grp-count group)) (throw 'return 0))
252 (let ((dir (nnmaildir--srvgrp-dir (nnmaildir--srv-dir server)
253 (nnmaildir--grp-name group)))
255 attr ino-opened nlink number-linked)
256 (setq dir (nnmaildir--nndir dir)
257 dir (nnmaildir--num-dir dir))
259 (setq attr (file-attributes
260 (concat dir (number-to-string number-opened))))
261 (or attr (throw 'return (1- number-opened)))
262 (setq ino-opened (nth 10 attr)
264 number-linked (+ number-opened nlink))
265 (if (or (< nlink 1) (< number-linked nlink))
266 (signal 'error '("Arithmetic overflow")))
267 (setq attr (file-attributes
268 (concat dir (number-to-string number-linked))))
269 (or attr (throw 'return (1- number-linked)))
270 (if (/= ino-opened (nth 10 attr))
271 (setq number-opened number-linked))))))
273 ;; Make the given server, if non-nil, be the current server. Then make the
274 ;; given group, if non-nil, be the current group of the current server. Then
275 ;; return the group object for the current group.
276 (defun nnmaildir--prepare (server group)
280 (unless (setq server nnmaildir--cur-server)
282 (unless (setq server (intern-soft server nnmaildir--servers))
284 (setq server (symbol-value server)
285 nnmaildir--cur-server server))
286 (unless (setq groups (nnmaildir--srv-groups server))
288 (unless (nnmaildir--srv-method server)
289 (setq x (concat "nnmaildir:" (nnmaildir--srv-address server))
290 x (gnus-server-to-method x))
291 (unless x (throw 'return nil))
292 (setf (nnmaildir--srv-method server) x))
294 (unless (setq group (nnmaildir--srv-curgrp server))
296 (unless (setq group (intern-soft group groups))
298 (setq group (symbol-value group)))
301 (defun nnmaildir--tab-to-space (string)
303 (while (string-match "\t" string pos)
304 (aset string (match-beginning 0) ? )
305 (setq pos (match-end 0))))
308 (defmacro nnmaildir--condcase (errsym body &rest handler)
309 `(condition-case ,errsym
310 (let ((system-messages-locale "C")) ,body)
313 (defun nnmaildir--emlink-p (err)
314 (and (eq (car err) 'file-error)
315 (string= (downcase (caddr err)) "too many links")))
317 (defun nnmaildir--enoent-p (err)
318 (and (eq (car err) 'file-error)
319 (string= (downcase (caddr err)) "no such file or directory")))
321 (defun nnmaildir--eexist-p (err)
322 (eq (car err) 'file-already-exists))
324 (defun nnmaildir--new-number (nndir)
325 "Allocate a new article number by atomically creating a file under NNDIR."
326 (let ((numdir (nnmaildir--num-dir nndir))
329 number-link previous-number-link path-open path-link ino-open)
330 (nnmaildir--mkdir numdir)
333 (setq path-open (concat numdir (number-to-string number-open)))
334 (if (not make-new-file)
335 (setq previous-number-link number-link)
336 (nnmaildir--mkfile path-open)
337 ;; If Emacs had O_CREAT|O_EXCL, we could return number-open here.
338 (setq make-new-file nil
339 previous-number-link 0))
340 (let* ((attr (file-attributes path-open))
341 (nlink (nth 1 attr)))
342 (setq ino-open (nth 10 attr)
343 number-link (+ number-open nlink))
344 (if (or (< nlink 1) (< number-link nlink))
345 (signal 'error '("Arithmetic overflow"))))
346 (if (= number-link previous-number-link)
347 ;; We've already tried this number, in the previous loop iteration,
349 (signal 'error `("Corrupt internal nnmaildir data" ,path-open)))
350 (setq path-link (concat numdir (number-to-string number-link)))
351 (nnmaildir--condcase err
353 (add-name-to-file path-open path-link)
354 (throw 'return number-link))
356 ((nnmaildir--emlink-p err)
357 (setq make-new-file t
358 number-open number-link))
359 ((nnmaildir--eexist-p err)
360 (let ((attr (file-attributes path-link)))
361 (if (/= (nth 10 attr) ino-open)
362 (setq number-open number-link
364 (t (signal (car err) (cdr err)))))))))
366 (defun nnmaildir--update-nov (server group article)
367 (let ((nnheader-file-coding-system 'binary)
368 (srv-dir (nnmaildir--srv-dir server))
369 (storage-version 1) ;; [version article-number msgid [...nov...]]
370 dir gname pgname msgdir prefix suffix file attr mtime novdir novfile
371 nov msgid nov-beg nov-mid nov-end field val old-extra num numdir
374 (setq gname (nnmaildir--grp-name group)
375 pgname (nnmaildir--pgname server gname)
376 dir (nnmaildir--srvgrp-dir srv-dir gname)
377 msgdir (if (nnmaildir--param pgname 'read-only)
378 (nnmaildir--new dir) (nnmaildir--cur dir))
379 prefix (nnmaildir--art-prefix article)
380 suffix (nnmaildir--art-suffix article)
381 file (concat msgdir prefix suffix)
382 attr (file-attributes file))
384 (nnmaildir--expired-article group article)
386 (setq mtime (nth 5 attr)
388 nov (nnmaildir--art-nov article)
389 dir (nnmaildir--nndir dir)
390 novdir (nnmaildir--nov-dir dir)
391 novfile (concat novdir prefix))
392 (unless (equal nnmaildir--extra nnmail-extra-headers)
393 (setq nnmaildir--extra (copy-sequence nnmail-extra-headers)))
394 (nnmaildir--with-nov-buffer
395 ;; First we'll check for already-parsed NOV data.
396 (cond ((not (file-exists-p novfile))
397 ;; The NOV file doesn't exist; we have to parse the message.
400 ;; The file exists, but the data isn't in memory; read the file.
402 (nnheader-insert-file-contents novfile)
403 (setq nov (read (current-buffer)))
404 (if (not (and (vectorp nov)
406 (equal storage-version (aref nov 0))))
407 ;; This NOV data seems to be in the wrong format.
409 (unless (nnmaildir--art-num article)
410 (setf (nnmaildir--art-num article) (aref nov 1)))
411 (unless (nnmaildir--art-msgid article)
412 (setf (nnmaildir--art-msgid article) (aref nov 2)))
413 (setq nov (aref nov 3)))))
414 ;; Now check whether the already-parsed data (if we have any) is
415 ;; usable: if the message has been edited or if nnmail-extra-headers
416 ;; has been augmented since this data was parsed from the message,
417 ;; then we have to reparse. Otherwise it's up-to-date.
418 (when (and nov (equal mtime (nnmaildir--nov-get-mtime nov)))
419 ;; The timestamp matches. Now check nnmail-extra-headers.
420 (setq old-extra (nnmaildir--nov-get-extra nov))
421 (when (equal nnmaildir--extra old-extra) ;; common case
422 ;; Save memory; use a single copy of the list value.
423 (nnmaildir--nov-set-extra nov nnmaildir--extra)
425 ;; They're not equal, but maybe the new is a subset of the old.
426 (if (null nnmaildir--extra)
427 ;; The empty set is a subset of every set.
429 (if (not (memq nil (mapcar (lambda (e) (memq e old-extra))
431 (throw 'return nov)))
432 ;; Parse the NOV data out of the message.
434 (nnheader-insert-file-contents file)
436 (goto-char (point-min))
438 (if (search-forward "\n\n" nil 'noerror)
440 (setq nov-mid (count-lines (point) (point-max)))
441 (narrow-to-region (point-min) (1- (point))))
443 (goto-char (point-min))
445 (setq nov (nnheader-parse-naked-head)
446 field (or (mail-header-lines nov) 0)))
447 (unless (or (zerop field) (nnmaildir--param pgname 'distrust-Lines:))
448 (setq nov-mid field))
449 (setq nov-mid (number-to-string nov-mid)
450 nov-mid (concat (number-to-string attr) "\t" nov-mid))
452 (setq field (or (mail-header-references nov) ""))
453 (nnmaildir--tab-to-space field)
454 (setq nov-mid (concat field "\t" nov-mid)
456 (lambda (f) (nnmaildir--tab-to-space (or f "")))
457 (list (mail-header-subject nov)
458 (mail-header-from nov)
459 (mail-header-date nov)) "\t")
462 (setq field (symbol-name (car extra))
464 (nnmaildir--tab-to-space field)
465 (nnmaildir--tab-to-space val)
466 (concat field ": " val))
467 (mail-header-extra nov) "\t")))
468 (setq msgid (mail-header-id nov))
469 (if (or (null msgid) (nnheader-fake-message-id-p msgid))
470 (setq msgid (concat "<" prefix "@nnmaildir>")))
471 (nnmaildir--tab-to-space msgid)
472 ;; The data is parsed; create an nnmaildir NOV structure.
473 (setq nov (nnmaildir--nov-new nov-beg nov-mid nov-end mtime
475 num (nnmaildir--art-num article))
477 (setq num (nnmaildir--new-number dir))
478 (setf (nnmaildir--art-num article) num))
479 ;; Store this new NOV data in a file
481 (prin1 (vector storage-version num msgid nov) (current-buffer))
482 (setq file (concat novfile ":"))
483 (nnmaildir--unlink file)
484 (write-region (point-min) (point-max) file nil 'no-message nil 'excl))
485 (rename-file file novfile 'replace)
486 (setf (nnmaildir--art-msgid article) msgid)
489 (defun nnmaildir--cache-nov (group article nov)
490 (let ((cache (nnmaildir--grp-cache group))
491 (index (nnmaildir--grp-index group))
493 (unless (nnmaildir--art-nov article)
494 (setq goner (aref cache index))
495 (if goner (setf (nnmaildir--art-nov goner) nil))
496 (aset cache index article)
497 (setf (nnmaildir--grp-index group) (% (1+ index) (length cache))))
498 (setf (nnmaildir--art-nov article) nov)))
500 (defun nnmaildir--grp-add-art (server group article)
501 (let ((nov (nnmaildir--update-nov server group article))
502 count num min nlist nlist-cdr insert-nlist)
504 (setq count (1+ (nnmaildir--grp-count group))
505 num (nnmaildir--art-num article)
506 min (if (= count 1) num
507 (min num (nnmaildir--grp-min group)))
508 nlist (nnmaildir--grp-nlist group))
509 (if (or (null nlist) (> num (caar nlist)))
510 (setq nlist (cons (cons num article) nlist))
512 nlist-cdr (cdr nlist))
513 (while (and nlist-cdr (< num (caar nlist-cdr)))
514 (setq nlist nlist-cdr
515 nlist-cdr (cdr nlist))))
516 (let ((inhibit-quit t))
517 (setf (nnmaildir--grp-count group) count)
518 (setf (nnmaildir--grp-min group) min)
520 (setcdr nlist (cons (cons num article) nlist-cdr))
521 (setf (nnmaildir--grp-nlist group) nlist))
522 (set (intern (nnmaildir--art-prefix article)
523 (nnmaildir--grp-flist group))
525 (set (intern (nnmaildir--art-msgid article)
526 (nnmaildir--grp-mlist group))
528 (set (intern (nnmaildir--grp-name group)
529 (nnmaildir--srv-groups server))
531 (nnmaildir--cache-nov group article nov)
534 (defun nnmaildir--group-ls (server pgname)
535 (or (nnmaildir--param pgname 'directory-files)
536 (nnmaildir--srv-ls server)))
538 (defun nnmaildir-article-number-to-file-name
539 (number group-name server-address-string)
540 (let ((group (nnmaildir--prepare server-address-string group-name))
544 ;; The given group or server does not exist.
546 (setq article (nnmaildir--nlist-art group number))
548 ;; The given article number does not exist in this group.
550 (setq pgname (nnmaildir--pgname nnmaildir--cur-server group-name)
551 dir (nnmaildir--srv-dir nnmaildir--cur-server)
552 dir (nnmaildir--srvgrp-dir dir group-name)
553 dir (if (nnmaildir--param pgname 'read-only)
554 (nnmaildir--new dir) (nnmaildir--cur dir)))
555 (concat dir (nnmaildir--art-prefix article)
556 (nnmaildir--art-suffix article)))))
558 (defun nnmaildir-article-number-to-base-name
559 (number group-name server-address-string)
560 (let ((x (nnmaildir--prepare server-address-string group-name)))
562 (setq x (nnmaildir--nlist-art x number))
563 (and x (cons (nnmaildir--art-prefix x)
564 (nnmaildir--art-suffix x))))))
566 (defun nnmaildir-base-name-to-article-number
567 (base-name group-name server-address-string)
568 (let ((x (nnmaildir--prepare server-address-string group-name)))
570 (setq x (nnmaildir--grp-flist x)
571 x (nnmaildir--flist-art x base-name))
572 (and x (nnmaildir--art-num x)))))
574 (defun nnmaildir--nlist-iterate (nlist ranges func)
575 (let (entry high low nlist2)
577 (setq ranges `((1 . ,(caar nlist)))))
579 (setq entry (car ranges) ranges (cdr ranges))
580 (while (and ranges (eq entry (car ranges)))
581 (setq ranges (cdr ranges))) ;; skip duplicates
585 (setq low (car entry)
587 (setq nlist2 nlist) ;; Don't&nb