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., 59 Temple Place - Suite 330,
21 ;; Boston, MA 02111-1307, 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 ;; * Merge the information from <URL:http://multivac.cwru.edu./nnmaildir/>
45 ;; into the Gnus manual.
46 ;; * Allow create-directory = ".", and configurable prefix of maildir names,
47 ;; stripped off to produce group names.
48 ;; * Add a hook for when moving messages from new/ to cur/, to support
49 ;; nnmail's duplicate detection.
50 ;; * Allow each mark directory in a group to have its own inode for mark
51 ;; files, to accommodate AFS.
52 ;; * Improve generated Xrefs, so crossposts are detectable.
53 ;; * Improve readability.
57 ;; eval this before editing
59 (put 'nnmaildir--with-nntp-buffer 'lisp-indent-function 0)
60 (put 'nnmaildir--with-work-buffer 'lisp-indent-function 0)
61 (put 'nnmaildir--with-nov-buffer 'lisp-indent-function 0)
62 (put 'nnmaildir--with-move-buffer 'lisp-indent-function 0)
78 (defconst nnmaildir-version "Gnus")
80 (defvar nnmaildir-article-file-name nil
81 "*The filename of the most recently requested article. This variable is set
82 by nnmaildir-request-article.")
84 ;; The filename of the article being moved/copied:
85 (defvar nnmaildir--file nil)
87 ;; Variables to generate filenames of messages being delivered:
88 (defvar nnmaildir--delivery-time "")
89 (defconst nnmaildir--delivery-pid (number-to-string (emacs-pid)))
90 (defvar nnmaildir--delivery-ct nil)
92 ;; An obarry containing symbols whose names are server names and whose values
94 (defvar nnmaildir--servers (make-vector 3 0))
95 ;; The current server:
96 (defvar nnmaildir--cur-server nil)
98 ;; A copy of nnmail-extra-headers
99 (defvar nnmaildir--extra nil)
101 ;; A NOV structure looks like this (must be prin1-able, so no defstruct):
102 ["subject\tfrom\tdate"
103 "references\tchars\lines"
104 "To: you\tIn-Reply-To: <your.mess@ge>"
105 (12345 67890) ;; modtime of the corresponding article file
106 (to in-reply-to)] ;; contemporary value of nnmail-extra-headers
107 (defconst nnmaildir--novlen 5)
108 (defmacro nnmaildir--nov-new (beg mid end mtime extra)
109 `(vector ,beg ,mid ,end ,mtime ,extra))
110 (defmacro nnmaildir--nov-get-beg (nov) `(aref ,nov 0))
111 (defmacro nnmaildir--nov-get-mid (nov) `(aref ,nov 1))
112 (defmacro nnmaildir--nov-get-end (nov) `(aref ,nov 2))
113 (defmacro nnmaildir--nov-get-mtime (nov) `(aref ,nov 3))
114 (defmacro nnmaildir--nov-get-extra (nov) `(aref ,nov 4))
115 (defmacro nnmaildir--nov-set-beg (nov value) `(aset ,nov 0 ,value))
116 (defmacro nnmaildir--nov-set-mid (nov value) `(aset ,nov 1 ,value))
117 (defmacro nnmaildir--nov-set-end (nov value) `(aset ,nov 2 ,value))
118 (defmacro nnmaildir--nov-set-mtime (nov value) `(aset ,nov 3 ,value))
119 (defmacro nnmaildir--nov-set-extra (nov value) `(aset ,nov 4 ,value))
121 (defstruct nnmaildir--art
122 (prefix nil :type string) ;; "time.pid.host"
123 (suffix nil :type string) ;; ":2,flags"
124 (num nil :type natnum) ;; article number
125 (msgid nil :type string) ;; "<mess.age@id>"
126 (nov nil :type vector)) ;; cached nov structure, or nil
128 (defstruct nnmaildir--grp
129 (name nil :type string) ;; "group.name"
130 (new nil :type list) ;; new/ modtime
131 (cur nil :type list) ;; cur/ modtime
132 (min 1 :type natnum) ;; minimum article number
133 (count 0 :type natnum) ;; count of articles
134 (nlist nil :type list) ;; list of articles, ordered descending by number
135 (flist nil :type vector) ;; obarray mapping filename prefix->article
136 (mlist nil :type vector) ;; obarray mapping message-id->article
137 (cache nil :type vector) ;; nov cache
138 (index nil :type natnum) ;; index of next cache entry to replace
139 (mmth nil :type vector)) ;; obarray mapping mark name->dir modtime
140 ; ("Mark Mod Time Hash")
142 (defstruct nnmaildir--srv
143 (address nil :type string) ;; server address string
144 (method nil :type list) ;; (nnmaildir "address" ...)
145 (prefix nil :type string) ;; "nnmaildir+address:"
146 (dir nil :type string) ;; "/expanded/path/to/server/dir/"
147 (ls nil :type function) ;; directory-files function
148 (groups nil :type vector) ;; obarray mapping group names->groups
149 (curgrp nil :type nnmaildir--grp) ;; current group, or nil
150 (error nil :type string) ;; last error message, or nil
151 (mtime nil :type list) ;; modtime of dir
152 (gnm nil) ;; flag: split from mail-sources?
153 (create-dir nil :type string)) ;; group creation directory
155 (defun nnmaildir--expired-article (group article)
156 (setf (nnmaildir--art-nov article) nil)
157 (let ((flist (nnmaildir--grp-flist group))
158 (mlist (nnmaildir--grp-mlist group))
159 (min (nnmaildir--grp-min group))
160 (count (1- (nnmaildir--grp-count group)))
161 (prefix (nnmaildir--art-prefix article))
162 (msgid (nnmaildir--art-msgid article))
164 (nlist-pre '(nil . nil))
166 (unless (zerop count)
167 (setq nlist-post (nnmaildir--grp-nlist group)
168 num (nnmaildir--art-num article))
169 (if (eq num (caar nlist-post))
170 (setq new-nlist (cdr nlist-post))
171 (setq new-nlist nlist-post
173 nlist-post (cdr nlist-post))
174 (while (/= num (caar nlist-post))
175 (setq nlist-pre nlist-post
176 nlist-post (cdr nlist-post)))
177 (setq nlist-post (cdr nlist-post))
179 (setq min (caar nlist-pre)))))
180 (let ((inhibit-quit t))
181 (setf (nnmaildir--grp-min group) min)
182 (setf (nnmaildir--grp-count group) count)
183 (setf (nnmaildir--grp-nlist group) new-nlist)
184 (setcdr nlist-pre nlist-post)
185 (unintern prefix flist)
186 (unintern msgid mlist))))
188 (defun nnmaildir--nlist-art (group num)
189 (let ((entry (assq num (nnmaildir--grp-nlist group))))
192 (defmacro nnmaildir--flist-art (list file)
193 `(symbol-value (intern-soft ,file ,list)))
194 (defmacro nnmaildir--mlist-art (list msgid)
195 `(symbol-value (intern-soft ,msgid ,list)))
197 (defun nnmaildir--pgname (server gname)
198 (let ((prefix (nnmaildir--srv-prefix server)))
199 (if prefix (concat prefix gname)
200 (setq gname (gnus-group-prefixed-name gname
201 (nnmaildir--srv-method server)))
202 (setf (nnmaildir--srv-prefix server) (gnus-group-real-prefix gname))
205 (defun nnmaildir--param (pgname param)
206 (setq param (gnus-group-find-parameter pgname param 'allow-list))
207 (if (vectorp param) (setq param (aref param 0)))
210 (defmacro nnmaildir--with-nntp-buffer (&rest body)
212 (set-buffer nntp-server-buffer)
214 (defmacro nnmaildir--with-work-buffer (&rest body)
216 (set-buffer (get-buffer-create " *nnmaildir work*"))
218 (defmacro nnmaildir--with-nov-buffer (&rest body)
220 (set-buffer (get-buffer-create " *nnmaildir nov*"))
222 (defmacro nnmaildir--with-move-buffer (&rest body)
224 (set-buffer (get-buffer-create " *nnmaildir move*"))
227 (defmacro nnmaildir--subdir (dir subdir)
228 `(file-name-as-directory (concat ,dir ,subdir)))
229 (defmacro nnmaildir--srvgrp-dir (srv-dir gname)
230 `(nnmaildir--subdir ,srv-dir ,gname))
231 (defmacro nnmaildir--tmp (dir) `(nnmaildir--subdir ,dir "tmp"))
232 (defmacro nnmaildir--new (dir) `(nnmaildir--subdir ,dir "new"))
233 (defmacro nnmaildir--cur (dir) `(nnmaildir--subdir ,dir "cur"))
234 (defmacro nnmaildir--nndir (dir) `(nnmaildir--subdir ,dir ".nnmaildir"))
235 (defmacro nnmaildir--nov-dir (dir) `(nnmaildir--subdir ,dir "nov"))
236 (defmacro nnmaildir--marks-dir (dir) `(nnmaildir--subdir ,dir "marks"))
237 (defmacro nnmaildir--num-dir (dir) `(nnmaildir--subdir ,dir "num"))
238 (defmacro nnmaildir--num-file (dir) `(concat ,dir ":"))
240 (defmacro nnmaildir--unlink (file-arg)
241 `(let ((file ,file-arg))
242 (if (file-attributes file) (delete-file file))))
243 (defun nnmaildir--mkdir (dir)
244 (or (file-exists-p (file-name-as-directory dir))
245 (make-directory-internal (directory-file-name dir))))
246 (defun nnmaildir--delete-dir-files (dir ls)
247 (mapcar 'delete-file (funcall ls dir 'full "\\`[^.]" 'nosort))
248 (delete-directory dir))
250 (defun nnmaildir--group-maxnum (server group)
251 (if (zerop (nnmaildir--grp-count group)) 0
252 (let ((x (nnmaildir--srvgrp-dir (nnmaildir--srv-dir server)
253 (nnmaildir--grp-name group))))
254 (setq x (nnmaildir--nndir x)
255 x (nnmaildir--num-dir x)
256 x (nnmaildir--num-file x)
257 x (file-attributes x))
258 (if x (1- (nth 1 x)) 0))))
260 ;; Make the given server, if non-nil, be the current server. Then make the
261 ;; given group, if non-nil, be the current group of the current server. Then
262 ;; return the group object for the current group.
263 (defun nnmaildir--prepare (server group)
267 (unless (setq server nnmaildir--cur-server)
269 (unless (setq server (intern-soft server nnmaildir--servers))
271 (setq server (symbol-value server)
272 nnmaildir--cur-server server))
273 (unless (setq groups (nnmaildir--srv-groups server))
275 (unless (nnmaildir--srv-method server)
276 (setq x (concat "nnmaildir:" (nnmaildir--srv-address server))
277 x (gnus-server-to-method x))
278 (unless x (throw 'return nil))
279 (setf (nnmaildir--srv-method server) x))
281 (unless (setq group (nnmaildir--srv-curgrp server))
283 (unless (setq group (intern-soft group groups))
285 (setq group (symbol-value group)))
288 (defun nnmaildir--tab-to-space (string)
290 (while (string-match "\t" string pos)
291 (aset string (match-beginning 0) ? )
292 (setq pos (match-end 0))))
295 (defun nnmaildir--update-nov (server group article)
296 (let ((nnheader-file-coding-system 'binary)
297 (srv-dir (nnmaildir--srv-dir server))
298 (storage-version 1) ;; [version article-number msgid [...nov...]]
299 dir gname pgname msgdir prefix suffix file attr mtime novdir novfile
300 nov msgid nov-beg nov-mid nov-end field val old-extra num numdir
303 (setq gname (nnmaildir--grp-name group)
304 pgname (nnmaildir--pgname server gname)
305 dir (nnmaildir--srvgrp-dir srv-dir gname)
306 msgdir (if (nnmaildir--param pgname 'read-only)
307 (nnmaildir--new dir) (nnmaildir--cur dir))
308 prefix (nnmaildir--art-prefix article)
309 suffix (nnmaildir--art-suffix article)
310 file (concat msgdir prefix suffix)
311 attr (file-attributes file))
313 (nnmaildir--expired-article group article)
315 (setq mtime (nth 5 attr)
317 nov (nnmaildir--art-nov article)
318 dir (nnmaildir--nndir dir)
319 novdir (nnmaildir--nov-dir dir)
320 novfile (concat novdir prefix))
321 (unless (equal nnmaildir--extra nnmail-extra-headers)
322 (setq nnmaildir--extra (copy-sequence nnmail-extra-headers)))
323 (nnmaildir--with-nov-buffer
324 ;; First we'll check for already-parsed NOV data.
325 (cond ((not (file-exists-p novfile))
326 ;; The NOV file doesn't exist; we have to parse the message.
329 ;; The file exists, but the data isn't in memory; read the file.
331 (nnheader-insert-file-contents novfile)
332 (setq nov (read (current-buffer)))
333 (if (not (and (vectorp nov)
335 (equal storage-version (aref nov 0))))
336 ;; This NOV data seems to be in the wrong format.
338 (unless (nnmaildir--art-num article)
339 (setf (nnmaildir--art-num article) (aref nov 1)))
340 (unless (nnmaildir--art-msgid article)
341 (setf (nnmaildir--art-msgid article) (aref nov 2)))
342 (setq nov (aref nov 3)))))
343 ;; Now check whether the already-parsed data (if we have any) is
344 ;; usable: if the message has been edited or if nnmail-extra-headers
345 ;; has been augmented since this data was parsed from the message,
346 ;; then we have to reparse. Otherwise it's up-to-date.
347 (when (and nov (equal mtime (nnmaildir--nov-get-mtime nov)))
348 ;; The timestamp matches. Now check nnmail-extra-headers.
349 (setq old-extra (nnmaildir--nov-get-extra nov))
350 (when (equal nnmaildir--extra old-extra) ;; common case
351 ;; Save memory; use a single copy of the list value.
352 (nnmaildir--nov-set-extra nov nnmaildir--extra)
354 ;; They're not equal, but maybe the new is a subset of the old.
355 (if (null nnmaildir--extra)
356 ;; The empty set is a subset of every set.
358 (if (not (memq nil (mapcar (lambda (e) (memq e old-extra))
360 (throw 'return nov)))
361 ;; Parse the NOV data out of the message.
363 (nnheader-insert-file-contents file)
365 (goto-char (point-min))
367 (if (search-forward "\n\n" nil 'noerror)
369 (setq nov-mid (count-lines (point) (point-max)))
370 (narrow-to-region (point-min) (1- (point))))
372 (goto-char (point-min))
374 (setq nov (nnheader-parse-naked-head)
375 field (or (mail-header-lines nov) 0)))
376 (unless (or (zerop field) (nnmaildir--param pgname 'distrust-Lines:))
377 (setq nov-mid field))
378 (setq nov-mid (number-to-string nov-mid)
379 nov-mid (concat (number-to-string attr) "\t" nov-mid))
381 (setq field (or (mail-header-references nov) ""))
382 (nnmaildir--tab-to-space field)
383 (setq nov-mid (concat field "\t" nov-mid)
385 (lambda (f) (nnmaildir--tab-to-space (or f "")))
386 (list (mail-header-subject nov)
387 (mail-header-from nov)
388 (mail-header-date nov)) "\t")
391 (setq field (symbol-name (car extra))