1 ;;; nnmaildir.el --- maildir backend for Gnus
3 ;; This file is in the public domain.
5 ;; Author: Paul Jarc <prj@po.cwru.edu>
7 ;; This file is part of GNU Emacs.
9 ;; GNU Emacs is free software: you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
24 ;; Maildir format is documented at <URL:http://cr.yp.to/proto/maildir.html>
25 ;; and in the maildir(5) man page from qmail (available at
26 ;; <URL:http://www.qmail.org/man/man5/maildir.html>). nnmaildir also stores
27 ;; extra information in the .nnmaildir/ directory within a maildir.
29 ;; Some goals of nnmaildir:
30 ;; * Everything Just Works, and correctly. E.g., NOV data is automatically
31 ;; regenerated when stale; no need for manually running
32 ;; *-generate-nov-databases.
33 ;; * Perfect reliability: [C-g] will never corrupt its data in memory, and
34 ;; SIGKILL will never corrupt its data in the filesystem.
35 ;; * Allow concurrent operation as much as possible. If files change out
36 ;; from under us, adapt to the changes or degrade gracefully.
37 ;; * We use the filesystem as a database, so that, e.g., it's easy to
38 ;; manipulate marks from outside Gnus.
39 ;; * All information about a group is stored in the maildir, for easy backup,
40 ;; copying, restoring, etc.
43 ;; * When moving an article for expiry, copy all the marks except 'expire
44 ;; from the original article.
45 ;; * Add a hook for when moving messages from new/ to cur/, to support
46 ;; nnmail's duplicate detection.
47 ;; * Improve generated Xrefs, so crossposts are detectable.
48 ;; * Improve code readability.
52 ;; eval this before editing
54 (put 'nnmaildir--with-nntp-buffer 'lisp-indent-function 0)
55 (put 'nnmaildir--with-work-buffer 'lisp-indent-function 0)
56 (put 'nnmaildir--with-nov-buffer 'lisp-indent-function 0)
57 (put 'nnmaildir--with-move-buffer 'lisp-indent-function 0)
58 (put 'nnmaildir--condcase 'lisp-indent-function 2)
74 (defconst nnmaildir-version "Gnus")
76 (defconst nnmaildir-flag-mark-mapping
80 "Alist mapping Maildir filename flags to Gnus marks.
81 Maildir filenames are of the form \"unique-id:2,FLAGS\",
82 where FLAGS are a string of characters in ASCII order.
83 Some of the FLAGS correspond to Gnus marks.")
85 (defsubst nnmaildir--mark-to-flag (mark)
86 "Find the Maildir flag that corresponds to MARK (an atom).
87 Return a character, or `nil' if not found.
88 See `nnmaildir-flag-mark-mapping'."
89 (car (rassq mark nnmaildir-flag-mark-mapping)))
91 (defsubst nnmaildir--flag-to-mark (flag)
92 "Find the Gnus mark that corresponds to FLAG (a character).
93 Return an atom, or `nil' if not found.
94 See `nnmaildir-flag-mark-mapping'."
95 (cdr (assq flag nnmaildir-flag-mark-mapping)))
97 (defun nnmaildir--ensure-suffix (filename)
98 "Ensure that FILENAME contains the suffix \":2,\"."
99 (if (gnus-string-match-p ":2," filename)
101 (concat filename ":2,")))
103 (defun nnmaildir--add-flag (flag suffix)
104 "Return a copy of SUFFIX where FLAG is set.
105 SUFFIX should start with \":2,\"."
106 (unless (gnus-string-match-p "^:2," suffix)
107 (error "Invalid suffix `%s'" suffix))
108 (let* ((flags (substring suffix 3))
109 (flags-as-list (append flags nil))
111 (concat (gnus-delete-duplicates
112 ;; maildir flags must be sorted
113 (sort (cons flag flags-as-list) '<)))))
114 (concat ":2," new-flags)))
116 (defun nnmaildir--remove-flag (flag suffix)
117 "Return a copy of SUFFIX where FLAG is cleared.
118 SUFFIX should start with \":2,\"."
119 (unless (gnus-string-match-p "^:2," suffix)
120 (error "Invalid suffix `%s'" suffix))
121 (let* ((flags (substring suffix 3))
122 (flags-as-list (append flags nil))
123 (new-flags (concat (delq flag flags-as-list))))
124 (concat ":2," new-flags)))
126 (defvar nnmaildir-article-file-name nil
127 "*The filename of the most recently requested article. This variable is set
128 by nnmaildir-request-article.")
130 ;; The filename of the article being moved/copied:
131 (defvar nnmaildir--file nil)
133 ;; Variables to generate filenames of messages being delivered:
134 (defvar nnmaildir--delivery-time "")
135 (defconst nnmaildir--delivery-pid (concat "P" (number-to-string (emacs-pid))))
136 (defvar nnmaildir--delivery-count nil)
138 ;; An obarry containing symbols whose names are server names and whose values
140 (defvar nnmaildir--servers (make-vector 3 0))
141 ;; The current server:
142 (defvar nnmaildir--cur-server nil)
144 ;; A copy of nnmail-extra-headers
145 (defvar nnmaildir--extra nil)
147 ;; A NOV structure looks like this (must be prin1-able, so no defstruct):
148 ["subject\tfrom\tdate"
149 "references\tchars\lines"
150 "To: you\tIn-Reply-To: <your.mess@ge>"
151 (12345 67890) ;; modtime of the corresponding article file
152 (to in-reply-to)] ;; contemporary value of nnmail-extra-headers
153 (defconst nnmaildir--novlen 5)
154 (defmacro nnmaildir--nov-new (beg mid end mtime extra)
155 `(vector ,beg ,mid ,end ,mtime ,extra))
156 (defmacro nnmaildir--nov-get-beg (nov) `(aref ,nov 0))
157 (defmacro nnmaildir--nov-get-mid (nov) `(aref ,nov 1))
158 (defmacro nnmaildir--nov-get-end (nov) `(aref ,nov 2))
159 (defmacro nnmaildir--nov-get-mtime (nov) `(aref ,nov 3))
160 (defmacro nnmaildir--nov-get-extra (nov) `(aref ,nov 4))
161 (defmacro nnmaildir--nov-set-beg (nov value) `(aset ,nov 0 ,value))
162 (defmacro nnmaildir--nov-set-mid (nov value) `(aset ,nov 1 ,value))
163 (defmacro nnmaildir--nov-set-end (nov value) `(aset ,nov 2 ,value))
164 (defmacro nnmaildir--nov-set-mtime (nov value) `(aset ,nov 3 ,value))
165 (defmacro nnmaildir--nov-set-extra (nov value) `(aset ,nov 4 ,value))
167 (defstruct nnmaildir--art
168 (prefix nil :type string) ;; "time.pid.host"
169 (suffix nil :type string) ;; ":2,flags"
170 (num nil :type natnum) ;; article number
171 (msgid nil :type string) ;; "<mess.age@id>"
172 (nov nil :type vector)) ;; cached nov structure, or nil
174 (defstruct nnmaildir--grp
175 (name nil :type string) ;; "group.name"
176 (new nil :type list) ;; new/ modtime
177 (cur nil :type list) ;; cur/ modtime
178 (min 1 :type natnum) ;; minimum article number
179 (count 0 :type natnum) ;; count of articles
180 (nlist nil :type list) ;; list of articles, ordered descending by number
181 (flist nil :type vector) ;; obarray mapping filename prefix->article
182 (mlist nil :type vector) ;; obarray mapping message-id->article
183 (cache nil :type vector) ;; nov cache
184 (index nil :type natnum) ;; index of next cache entry to replace
185 (mmth nil :type vector)) ;; obarray mapping mark name->dir modtime
186 ; ("Mark Mod Time Hash")
188 (defstruct nnmaildir--srv
189 (address nil :type string) ;; server address string
190 (method nil :type list) ;; (nnmaildir "address" ...)
191 (prefix nil :type string) ;; "nnmaildir+address:"
192 (dir nil :type string) ;; "/expanded/path/to/server/dir/"
193 (ls nil :type function) ;; directory-files function
194 (groups nil :type vector) ;; obarray mapping group name->group
195 (curgrp nil :type nnmaildir--grp) ;; current group, or nil
196 (error nil :type string) ;; last error message, or nil
197 (mtime nil :type list) ;; modtime of dir
198 (gnm nil) ;; flag: split from mail-sources?
199 (target-prefix nil :type string)) ;; symlink target prefix
201 (defun nnmaildir--article-set-flags (article new-suffix curdir)
202 (let* ((prefix (nnmaildir--art-prefix article))
203 (suffix (nnmaildir--art-suffix article))
204 (article-file (concat curdir prefix suffix))
205 (new-name (concat curdir prefix new-suffix)))
206 (unless (file-exists-p article-file)
207 (error "Couldn't find article file %s" article-file))
208 (rename-file article-file new-name 'replace)
209 (setf (nnmaildir--art-suffix article) new-suffix)))
211 (defun nnmaildir--expired-article (group article)
212 (setf (nnmaildir--art-nov article) nil)
213 (let ((flist (nnmaildir--grp-flist group))
214 (mlist (nnmaildir--grp-mlist group))
215 (min (nnmaildir--grp-min group))
216 (count (1- (nnmaildir--grp-count group)))
217 (prefix (nnmaildir--art-prefix article))
218 (msgid (nnmaildir--art-msgid article))
220 (nlist-pre '(nil . nil))
222 (unless (zerop count)
223 (setq nlist-post (nnmaildir--grp-nlist group)
224 num (nnmaildir--art-num article))
225 (if (eq num (caar nlist-post))
226 (setq new-nlist (cdr nlist-post))
227 (setq new-nlist nlist-post
229 nlist-post (cdr nlist-post))
230 (while (/= num (caar nlist-post))
231 (setq nlist-pre nlist-post
232 nlist-post (cdr nlist-post)))
233 (setq nlist-post (cdr nlist-post))
235 (setq min (caar nlist-pre)))))
236 (let ((inhibit-quit t))
237 (setf (nnmaildir--grp-min group) min)
238 (setf (nnmaildir--grp-count group) count)
239 (setf (nnmaildir--grp-nlist group) new-nlist)
240 (setcdr nlist-pre nlist-post)
241 (unintern prefix flist)
242 (unintern msgid mlist))))
244 (defun nnmaildir--nlist-art (group num)
245 (let ((entry (assq num (nnmaildir--grp-nlist group))))
248 (defmacro nnmaildir--flist-art (list file)
249 `(symbol-value (intern-soft ,file ,list)))
250 (defmacro nnmaildir--mlist-art (list msgid)
251 `(symbol-value (intern-soft ,msgid ,list)))
253 (defun nnmaildir--pgname (server gname)
254 (let ((prefix (nnmaildir--srv-prefix server)))
255 (if prefix (concat prefix gname)
256 (setq gname (gnus-group-prefixed-name gname
257 (nnmaildir--srv-method server)))
258 (setf (nnmaildir--srv-prefix server) (gnus-group-real-prefix gname))
261 (defun nnmaildir--param (pgname param)
262 (setq param (gnus-group-find-parameter pgname param 'allow-list))
263 (if (vectorp param) (setq param (aref param 0)))
266 (defmacro nnmaildir--with-nntp-buffer (&rest body)
267 (declare (debug (body)))
268 `(with-current-buffer nntp-server-buffer
270 (defmacro nnmaildir--with-work-buffer (&rest body)
271 (declare (debug (body)))
272 `(with-current-buffer (get-buffer-create " *nnmaildir work*")
274 (defmacro nnmaildir--with-nov-buffer (&rest body)
275 (declare (debug (body)))
276 `(with-current-buffer (get-buffer-create " *nnmaildir nov*")
278 (defmacro nnmaildir--with-move-buffer (&rest body)
279 (declare (debug (body)))
280 `(with-current-buffer (get-buffer-create " *nnmaildir move*")
283 (defsubst nnmaildir--subdir (dir subdir)
284 (file-name-as-directory (concat dir subdir)))
285 (defsubst nnmaildir--srvgrp-dir (srv-dir gname)
286 (nnmaildir--subdir srv-dir gname))
287 (defsubst nnmaildir--tmp (dir) (nnmaildir--subdir dir "tmp"))
288 (defsubst nnmaildir--new (dir) (nnmaildir--subdir dir "new"))
289 (defsubst nnmaildir--cur (dir) (nnmaildir--subdir dir "cur"))
290 (defsubst nnmaildir--nndir (dir) (nnmaildir--subdir dir ".nnmaildir"))
291 (defsubst nnmaildir--nov-dir (dir) (nnmaildir--subdir dir "nov"))
292 (defsubst nnmaildir--marks-dir (dir) (nnmaildir--subdir dir "marks"))
293 (defsubst nnmaildir--num-dir (dir) (nnmaildir--subdir dir "num"))
295 (defmacro nnmaildir--unlink (file-arg)
296 `(let ((file ,file-arg))
297 (if (file-attributes file) (delete-file file))))
298 (defun nnmaildir--mkdir (dir)
299 (or (file-exists-p (file-name-as-directory dir))
300 (make-directory-internal (directory-file-name dir))))
301 (defun nnmaildir--mkfile (file)
302 (write-region "" nil file nil 'no-message))
303 (defun nnmaildir--delete-dir-files (dir ls)
304 (when (file-attributes dir)
305 (mapc 'delete-file (funcall ls dir 'full "\\`[^.]" 'nosort))
306 (delete-directory dir)))
308 (defun nnmaildir--group-maxnum (server group)
310 (if (zerop (nnmaildir--grp-count group)) (throw 'return 0))
311 (let ((dir (nnmaildir--srvgrp-dir (nnmaildir--srv-dir server)
312 (nnmaildir--grp-name group)))
314 attr ino-opened nlink number-linked)
315 (setq dir (nnmaildir--nndir dir)
316 dir (nnmaildir--num-dir dir))
318 (setq attr (file-attributes
319 (concat dir (number-to-string number-opened))))
320 (or attr (throw 'return (1- number-opened)))
321 (setq ino-opened (nth 10 attr)
323 number-linked (+ number-opened nlink))
324 (if (or (< nlink 1) (< number-linked nlink))
325 (signal 'error '("Arithmetic overflow")))
326 (setq attr (file-attributes
327 (concat dir (number-to-string number-linked))))
328 (or attr (throw 'return (1- number-linked)))
329 (unless (equal ino-opened (nth 10 attr))
330 (setq number-opened number-linked))))))
332 ;; Make the given server, if non-nil, be the current server. Then make the
333 ;; given group, if non-nil, be the current group of the current server. Then
334 ;; return the group object for the current group.
335 (defun nnmaildir--prepare (server group)
339 (unless (setq server nnmaildir--cur-server)
341 (unless (setq server (intern-soft server nnmaildir--servers))
343 (setq server (symbol-value server)
344 nnmaildir--cur-server server))
345 (unless (setq groups (nnmaildir--srv-groups server))
347 (unless (nnmaildir--srv-method server)
348 (setq x (concat "nnmaildir:" (nnmaildir--srv-address server))
349 x (gnus-server-to-method x))
350 (unless x (throw 'return nil))
351 (setf (nnmaildir--srv-method server) x))
353 (unless (setq group (nnmaildir--srv-curgrp server))
355 (unless (setq group (intern-soft group groups))
357 (setq group (symbol-value group)))
360 (defun nnmaildir--tab-to-space (string)
362 (while (string-match "\t" string pos)
363 (aset string (match-beginning 0) ? )
364 (setq pos (match-end 0))))
367 (defmacro nnmaildir--condcase (errsym body &rest handler)
368 (declare (debug (sexp form body)))
369 `(condition-case ,errsym
370 (let ((system-messages-locale "C")) ,body)
373 (defun nnmaildir--emlink-p (err)
374 (and (eq (car err) 'file-error)
375 (string= (downcase (caddr err)) "too many links")))
377 (defun nnmaildir--enoent-p (err)
378 (and (eq (car err) 'file-error)
379 (string= (downcase (caddr err)) "no such file or directory")))
381 (defun nnmaildir--eexist-p (err)
382 (eq (car err) 'file-already-exists))
384 (defun nnmaildir--new-number (nndir)
385 "Allocate a new article number by atomically creating a file under NNDIR."
386 (let ((numdir (nnmaildir--num-dir nndir))
389 number-link previous-number-link path-open path-link ino-open)
390 (nnmaildir--mkdir numdir)
393 (setq path-open (concat numdir (number-to-string number-open)))
394 (if (not make-new-file)
395 (setq previous-number-link number-link)
396 (nnmaildir--mkfile path-open)
397 ;; If Emacs had O_CREAT|O_EXCL, we could return number-open here.
398 (setq make-new-file nil
399 previous-number-link 0))
400 (let* ((attr (file-attributes path-open))
401 (nlink (nth 1 attr)))
402 (setq ino-open (nth 10 attr)
403 number-link (+ number-open nlink))
404 (if (or (< nlink 1) (< number-link nlink))
405 (signal 'error '("Arithmetic overflow"))))
406 (if (= number-link previous-number-link)
407 ;; We've already tried this number, in the previous loop iteration,
409 (signal 'error `("Corrupt internal nnmaildir data" ,path-open)))