Merge from emacs-23
[bpt/emacs.git] / lisp / gnus / nnml.el
CommitLineData
eec82323 1;;; nnml.el --- mail spool access for Gnus
e84b4b86
TTN
2
3;; Copyright (C) 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003,
114f9c96 4;; 2004, 2005, 2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc.
eec82323 5
01c52d31
MB
6;; Authors: Didier Verna <didier@xemacs.org> (adding compaction)
7;; Simon Josefsson <simon@josefsson.org> (adding MARKS)
8;; Lars Magne Ingebrigtsen <larsi@gnus.org>
23f87bed 9;; Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
eec82323
LMI
10;; Keywords: news, mail
11
12;; This file is part of GNU Emacs.
13
5e809f55 14;; GNU Emacs is free software: you can redistribute it and/or modify
eec82323 15;; it under the terms of the GNU General Public License as published by
5e809f55
GM
16;; the Free Software Foundation, either version 3 of the License, or
17;; (at your option) any later version.
eec82323
LMI
18
19;; GNU Emacs is distributed in the hope that it will be useful,
20;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22;; GNU General Public License for more details.
23
24;; You should have received a copy of the GNU General Public License
5e809f55 25;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
eec82323
LMI
26
27;;; Commentary:
28
29;; Based on nnspool.el by Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>.
30;; For an overview of what the interface functions do, please see the
31;; Gnus sources.
32
33;;; Code:
34
23f87bed 35(require 'gnus)
eec82323
LMI
36(require 'nnheader)
37(require 'nnmail)
38(require 'nnoo)
349f4e97 39(eval-when-compile (require 'cl))
23f87bed 40
8abf1b22
GM
41;; FIXME first is unused in this file.
42(autoload 'gnus-article-unpropagatable-p "gnus-sum")
43(autoload 'gnus-backlog-remove-article "gnus-bcklg")
eec82323
LMI
44
45(nnoo-declare nnml)
46
47(defvoo nnml-directory message-directory
a8151ef7 48 "Spool directory for the nnml mail backend.")
eec82323
LMI
49
50(defvoo nnml-active-file
16409b0b 51 (expand-file-name "active" nnml-directory)
eec82323
LMI
52 "Mail active file.")
53
54(defvoo nnml-newsgroups-file
16409b0b 55 (expand-file-name "newsgroups" nnml-directory)
eec82323
LMI
56 "Mail newsgroups description file.")
57
58(defvoo nnml-get-new-mail t
59 "If non-nil, nnml will check the incoming mail file and split the mail.")
60
61(defvoo nnml-nov-is-evil nil
23f87bed 62 "If non-nil, Gnus will never generate and use nov databases for mail spools.
eec82323
LMI
63Using nov databases will speed up header fetching considerably.
64This variable shouldn't be flipped much. If you have, for some reason,
65set this to t, and want to set it to nil again, you should always run
66the `nnml-generate-nov-databases' command. The function will go
67through all nnml directories and generate nov databases for them
68all. This may very well take some time.")
69
23f87bed
MB
70(defvoo nnml-marks-is-evil nil
71 "If non-nil, Gnus will never generate and use marks file for mail spools.
72Using marks files makes it possible to backup and restore mail groups
73separately from `.newsrc.eld'. If you have, for some reason, set this
74to t, and want to set it to nil again, you should always remove the
75corresponding marks file (usually named `.marks' in the nnml group
76directory, but see `nnml-marks-file-name') for the group. Then the
77marks file will be regenerated properly by Gnus.")
78
eec82323
LMI
79(defvoo nnml-prepare-save-mail-hook nil
80 "Hook run narrowed to an article before saving.")
81
82(defvoo nnml-inhibit-expiry nil
83 "If non-nil, inhibit expiry.")
84
23f87bed 85(defvoo nnml-use-compressed-files nil
01c52d31
MB
86 "If non-nil, allow using compressed message files.
87
88If it is a string, use it as the file extension which specifies
89the compression program. You can set it to \".bz2\" if your Emacs
90supports auto-compression using the bzip2 program. A value of t
91is equivalent to \".gz\".")
92
93(defvoo nnml-compressed-files-size-threshold 1000
94 "Default size threshold for compressed message files.
95Message files with bodies larger than that many characters will
96be automatically compressed if `nnml-use-compressed-files' is
97non-nil.")
eec82323
LMI
98
99\f
100
101(defconst nnml-version "nnml 1.0"
102 "nnml version.")
103
104(defvoo nnml-nov-file-name ".overview")
23f87bed 105(defvoo nnml-marks-file-name ".marks")
eec82323
LMI
106
107(defvoo nnml-current-directory nil)
108(defvoo nnml-current-group nil)
109(defvoo nnml-status-string "")
110(defvoo nnml-nov-buffer-alist nil)
111(defvoo nnml-group-alist nil)
112(defvoo nnml-active-timestamp nil)
113(defvoo nnml-article-file-alist nil)
114
115(defvoo nnml-generate-active-function 'nnml-generate-active-info)
116
6748645f
LMI
117(defvar nnml-nov-buffer-file-name nil)
118
16409b0b
GM
119(defvoo nnml-file-coding-system nnmail-file-coding-system)
120
23f87bed
MB
121(defvoo nnml-marks nil)
122
123(defvar nnml-marks-modtime (gnus-make-hashtable))
eec82323 124
23f87bed 125\f
eec82323
LMI
126;;; Interface functions.
127
128(nnoo-define-basics nnml)
129
01c52d31
MB
130(eval-when-compile
131 (defsubst nnml-group-name-charset (group server-or-method)
132 (gnus-group-name-charset
133 (if (stringp server-or-method)
134 (gnus-server-to-method
135 (if (string-match "\\+" server-or-method)
136 (concat (substring server-or-method 0 (match-beginning 0))
137 ":" (substring server-or-method (match-end 0)))
138 (concat "nnml:" server-or-method)))
139 (or server-or-method gnus-command-method '(nnml "")))
140 group)))
141
142(defun nnml-decoded-group-name (group &optional server-or-method)
143 "Return a decoded group name of GROUP on SERVER-OR-METHOD."
144 (if nnmail-group-names-not-encoded-p
145 group
146 (mm-decode-coding-string
147 group
148 (nnml-group-name-charset group server-or-method))))
149
150(defun nnml-encoded-group-name (group &optional server-or-method)
151 "Return an encoded group name of GROUP on SERVER-OR-METHOD."
152 (mm-encode-coding-string
153 group
154 (nnml-group-name-charset group server-or-method)))
155
156(defun nnml-group-pathname (group &optional file server)
157 "Return an absolute file name of FILE for GROUP on SERVER."
158 (nnmail-group-pathname (inline (nnml-decoded-group-name group server))
159 nnml-directory file))
160
eec82323
LMI
161(deffoo nnml-retrieve-headers (sequence &optional group server fetch-old)
162 (when (nnml-possibly-change-directory group server)
163 (save-excursion
164 (set-buffer nntp-server-buffer)
165 (erase-buffer)
23f87bed
MB
166 (let* ((file nil)
167 (number (length sequence))
168 (count 0)
169 (file-name-coding-system nnmail-pathname-coding-system)
170 beg article)
eec82323
LMI
171 (if (stringp (car sequence))
172 'headers
173 (if (nnml-retrieve-headers-with-nov sequence fetch-old)
174 'nov
175 (while sequence
176 (setq article (car sequence))
177 (setq file (nnml-article-to-file article))
178 (when (and file
179 (file-exists-p file)
180 (not (file-directory-p file)))
181 (insert (format "221 %d Article retrieved.\n" article))
182 (setq beg (point))
183 (nnheader-insert-head file)
184 (goto-char beg)
23f87bed 185 (if (re-search-forward "\n\r?\n" nil t)
eec82323
LMI
186 (forward-char -1)
187 (goto-char (point-max))
188 (insert "\n\n"))
189 (insert ".\n")
190 (delete-region (point) (point-max)))
191 (setq sequence (cdr sequence))
192 (setq count (1+ count))
193 (and (numberp nnmail-large-newsgroup)
194 (> number nnmail-large-newsgroup)
195 (zerop (% count 20))
196 (nnheader-message 6 "nnml: Receiving headers... %d%%"
197 (/ (* count 100) number))))
198
199 (and (numberp nnmail-large-newsgroup)
200 (> number nnmail-large-newsgroup)
201 (nnheader-message 6 "nnml: Receiving headers...done"))
202
203 (nnheader-fold-continuation-lines)
204 'headers))))))
205
206(deffoo nnml-open-server (server &optional defs)
207 (nnoo-change-server 'nnml server defs)
208 (when (not (file-exists-p nnml-directory))
16409b0b 209 (ignore-errors (make-directory nnml-directory t)))
eec82323
LMI
210 (cond
211 ((not (file-exists-p nnml-directory))
212 (nnml-close-server)
213 (nnheader-report 'nnml "Couldn't create directory: %s" nnml-directory))
214 ((not (file-directory-p (file-truename nnml-directory)))
215 (nnml-close-server)
216 (nnheader-report 'nnml "Not a directory: %s" nnml-directory))
217 (t
218 (nnheader-report 'nnml "Opened server %s using directory %s"
219 server nnml-directory)
220 t)))
221
23f87bed 222(deffoo nnml-request-regenerate (server)
eec82323 223 (nnml-possibly-change-directory nil server)
23f87bed 224 (nnml-generate-nov-databases server)
eec82323
LMI
225 t)
226
227(deffoo nnml-request-article (id &optional group server buffer)
228 (nnml-possibly-change-directory group server)
229 (let* ((nntp-server-buffer (or buffer nntp-server-buffer))
16409b0b 230 (file-name-coding-system nnmail-pathname-coding-system)
eec82323
LMI
231 path gpath group-num)
232 (if (stringp id)
01c52d31 233 (when (and (setq group-num (nnml-find-group-number id server))
eec82323
LMI
234 (cdr
235 (assq (cdr group-num)
236 (nnheader-article-to-file-alist
01c52d31
MB
237 (setq gpath (nnml-group-pathname (car group-num)
238 nil server))))))
eec82323
LMI
239 (setq path (concat gpath (int-to-string (cdr group-num)))))
240 (setq path (nnml-article-to-file id)))
241 (cond
242 ((not path)
243 (nnheader-report 'nnml "No such article: %s" id))
244 ((not (file-exists-p path))
245 (nnheader-report 'nnml "No such file: %s" path))
246 ((file-directory-p path)
247 (nnheader-report 'nnml "File is a directory: %s" path))
16409b0b
GM
248 ((not (save-excursion (let ((nnmail-file-coding-system
249 nnml-file-coding-system))
250 (nnmail-find-file path))))
eec82323
LMI
251 (nnheader-report 'nnml "Couldn't read file: %s" path))
252 (t
253 (nnheader-report 'nnml "Article %s retrieved" id)
254 ;; We return the article number.
255 (cons (if group-num (car group-num) group)
e9bd5782 256 (string-to-number (file-name-nondirectory path)))))))
eec82323
LMI
257
258(deffoo nnml-request-group (group &optional server dont-check)
1428d46b
MB
259 (let ((file-name-coding-system nnmail-pathname-coding-system)
260 (decoded (nnml-decoded-group-name group server)))
d49d7823
KH
261 (cond
262 ((not (nnml-possibly-change-directory group server))
263 (nnheader-report 'nnml "Invalid group (no such directory)"))
264 ((not (file-exists-p nnml-current-directory))
265 (nnheader-report 'nnml "Directory %s does not exist"
266 nnml-current-directory))
267 ((not (file-directory-p nnml-current-directory))
268 (nnheader-report 'nnml "%s is not a directory" nnml-current-directory))
269 (dont-check
1428d46b 270 (nnheader-report 'nnml "Group %s selected" decoded)
d49d7823
KH
271 t)
272 (t
273 (nnheader-re-read-dir nnml-current-directory)
274 (nnmail-activate 'nnml)
275 (let ((active (nth 1 (assoc group nnml-group-alist))))
276 (if (not active)
1428d46b
MB
277 (nnheader-report 'nnml "No such group: %s" decoded)
278 (nnheader-report 'nnml "Selected group %s" decoded)
d49d7823
KH
279 (nnheader-insert "211 %d %d %d %s\n"
280 (max (1+ (- (cdr active) (car active))) 0)
281 (car active) (cdr active) group)))))))
eec82323
LMI
282
283(deffoo nnml-request-scan (&optional group server)
284 (setq nnml-article-file-alist nil)
285 (nnml-possibly-change-directory group server)
286 (nnmail-get-new-mail 'nnml 'nnml-save-nov nnml-directory group))
287
288(deffoo nnml-close-group (group &optional server)
289 (setq nnml-article-file-alist nil)
290 t)
291
292(deffoo nnml-request-create-group (group &optional server args)
16409b0b 293 (nnml-possibly-change-directory nil server)
eec82323 294 (nnmail-activate 'nnml)
6748645f 295 (cond
01c52d31
MB
296 ((let ((file (directory-file-name (nnml-group-pathname group nil server)))
297 (file-name-coding-system nnmail-pathname-coding-system))
298 (and (file-exists-p file)
299 (not (file-directory-p file))))
300 (nnheader-report 'nnml "%s is a file"
301 (directory-file-name (nnml-group-pathname group
302 nil server))))
6748645f
LMI
303 ((assoc group nnml-group-alist)
304 t)
6748645f 305 (t
eec82323
LMI
306 (let (active)
307 (push (list group (setq active (cons 1 0)))
308 nnml-group-alist)
01c52d31 309 (nnml-possibly-create-directory group server)
eec82323 310 (nnml-possibly-change-directory group server)
01c52d31
MB
311 (let* ((file-name-coding-system nnmail-pathname-coding-system)
312 (articles (nnml-directory-articles nnml-current-directory)))
eec82323
LMI
313 (when articles
314 (setcar active (apply 'min articles))
315 (setcdr active (apply 'max articles))))
6748645f
LMI
316 (nnmail-save-active nnml-group-alist nnml-active-file)
317 t))))
eec82323
LMI
318
319(deffoo nnml-request-list (&optional server)
320 (save-excursion
d49d7823 321 (let ((nnmail-file-coding-system nnmail-active-file-coding-system)
16409b0b
GM
322 (file-name-coding-system nnmail-pathname-coding-system))
323 (nnmail-find-file nnml-active-file))
eec82323
LMI
324 (setq nnml-group-alist (nnmail-get-active))
325 t))
326
327(deffoo nnml-request-newgroups (date &optional server)
328 (nnml-request-list server))
329
330(deffoo nnml-request-list-newsgroups (&optional server)
331 (save-excursion
332 (nnmail-find-file nnml-newsgroups-file)))
333
16409b0b 334(deffoo nnml-request-expire-articles (articles group &optional server force)
eec82323 335 (nnml-possibly-change-directory group server)
01c52d31
MB
336 (let* ((file-name-coding-system nnmail-pathname-coding-system)
337 (active-articles
338 (nnml-directory-articles nnml-current-directory))
339 (is-old t)
340 (decoded (nnml-decoded-group-name group server))
341 article rest mod-time number target)
eec82323
LMI
342 (nnmail-activate 'nnml)
343
6748645f
LMI
344 (setq active-articles (sort active-articles '<))
345 ;; Articles not listed in active-articles are already gone,
346 ;; so don't try to expire them.
347 (setq articles (gnus-sorted-intersection articles active-articles))
348
eec82323 349 (while (and articles is-old)
23f87bed
MB
350 (if (and (setq article (nnml-article-to-file
351 (setq number (pop articles))))
352 (setq mod-time (nth 5 (file-attributes article)))
353 (nnml-deletable-article-p group number)
354 (setq is-old (nnmail-expired-article-p group mod-time force
355 nnml-inhibit-expiry)))
356 (progn
357 ;; Allow a special target group.
01c52d31
MB
358 (setq target nnmail-expiry-target)
359 (unless (eq target 'delete)
23f87bed
MB
360 (with-temp-buffer
361 (nnml-request-article number group server (current-buffer))
362 (let (nnml-current-directory
363 nnml-current-group
364 nnml-article-file-alist)
01c52d31
MB
365 (when (functionp target)
366 (setq target (funcall target group)))
9091f2d3
MB
367 (when (and target (not (eq target 'delete)))
368 (if (or (gnus-request-group target)
369 (gnus-request-create-group target))
370 (nnmail-expiry-target-group target group)
371 (setq target nil)))))
23f87bed
MB
372 ;; Maybe directory is changed during nnmail-expiry-target-group.
373 (nnml-possibly-change-directory group server))
01c52d31
MB
374 (if target
375 (progn
376 (nnheader-message 5 "Deleting article %s in %s"
377 number decoded)
378 (condition-case ()
379 (funcall nnmail-delete-file-function article)
380 (file-error
381 (push number rest)))
382 (setq active-articles (delq number active-articles))
383 (nnml-nov-delete-article group number))
384 (push number rest)))
23f87bed 385 (push number rest)))
eec82323
LMI
386 (let ((active (nth 1 (assoc group nnml-group-alist))))
387 (when active
388 (setcar active (or (and active-articles
389 (apply 'min active-articles))
390 (1+ (cdr active)))))
391 (nnmail-save-active nnml-group-alist nnml-active-file))
392 (nnml-save-nov)
393 (nconc rest articles)))
394
395(deffoo nnml-request-move-article
01c52d31 396 (article group server accept-form &optional last move-is-internal)
eec82323 397 (let ((buf (get-buffer-create " *nnml move*"))
01c52d31 398 (file-name-coding-system nnmail-pathname-coding-system)
eec82323
LMI
399 result)
400 (nnml-possibly-change-directory group server)
401 (nnml-update-file-alist)
402 (and
403 (nnml-deletable-article-p group article)
404 (nnml-request-article article group server)
a1506d29
JB
405 (let (nnml-current-directory
406 nnml-current-group
16409b0b
GM
407 nnml-article-file-alist)
408 (save-excursion
409 (set-buffer buf)
410 (insert-buffer-substring nntp-server-buffer)
411 (setq result (eval accept-form))
412 (kill-buffer (current-buffer))
413 result))
eec82323
LMI
414 (progn
415 (nnml-possibly-change-directory group server)
416 (condition-case ()
417 (funcall nnmail-delete-file-function
418 (nnml-article-to-file article))
419 (file-error nil))
420 (nnml-nov-delete-article group article)
421 (when last
422 (nnml-save-nov)
423 (nnmail-save-active nnml-group-alist nnml-active-file))))
424 result))
425
426(deffoo nnml-request-accept-article (group &optional server last)
427 (nnml-possibly-change-directory group server)
428 (nnmail-check-syntax)
429 (let (result)
430 (when nnmail-cache-accepted-message-ids
01c52d31 431 (nnmail-cache-insert (nnmail-fetch-field "message-id")
23f87bed
MB
432 group
433 (nnmail-fetch-field "subject")
434 (nnmail-fetch-field "from")))
eec82323
LMI
435 (if (stringp group)
436 (and
437 (nnmail-activate 'nnml)
438 (setq result (car (nnml-save-mail
01c52d31
MB
439 (list (cons group (nnml-active-number group
440 server)))
441 server)))
eec82323
LMI
442 (progn
443 (nnmail-save-active nnml-group-alist nnml-active-file)
444 (and last (nnml-save-nov))))
445 (and
446 (nnmail-activate 'nnml)
01c52d31
MB
447 (if (and (not (setq result (nnmail-article-group
448 `(lambda (group)
449 (nnml-active-number group ,server)))))
eec82323
LMI
450 (yes-or-no-p "Moved to `junk' group; delete article? "))
451 (setq result 'junk)
01c52d31 452 (setq result (car (nnml-save-mail result server))))
eec82323
LMI
453 (when last
454 (nnmail-save-active nnml-group-alist nnml-active-file)
455 (when nnmail-cache-accepted-message-ids
456 (nnmail-cache-close))
457 (nnml-save-nov))))
458 result))
459
23f87bed
MB
460(deffoo nnml-request-post (&optional server)
461 (nnmail-do-request-post 'nnml-request-accept-article server))
462
eec82323
LMI
463(deffoo nnml-request-replace-article (article group buffer)
464 (nnml-possibly-change-directory group)
465 (save-excursion
466 (set-buffer buffer)
467 (nnml-possibly-create-directory group)
468 (let ((chars (nnmail-insert-lines))
469 (art (concat (int-to-string article) "\t"))
470 headers)
16409b0b
GM
471 (when (ignore-errors
472 (nnmail-write-region
473 (point-min) (point-max)
474 (or (nnml-article-to-file article)
475 (expand-file-name (int-to-string article)
476 nnml-current-directory))
477 nil (if (nnheader-be-verbose 5) nil 'nomesg))
478 t)
eec82323
LMI
479 (setq headers (nnml-parse-head chars article))
480 ;; Replace the NOV line in the NOV file.
481 (save-excursion
482 (set-buffer (nnml-open-nov group))
483 (goto-char (point-min))
484 (if (or (looking-at art)
485 (search-forward (concat "\n" art) nil t))
486 ;; Delete the old NOV line.
23f87bed 487 (gnus-delete-line)
eec82323
LMI
488 ;; The line isn't here, so we have to find out where
489 ;; we should insert it. (This situation should never
490 ;; occur, but one likes to make sure...)
491 (while (and (looking-at "[0-9]+\t")
e9bd5782 492 (< (string-to-number
eec82323
LMI
493 (buffer-substring
494 (match-beginning 0) (match-end 0)))
495 article)
496 (zerop (forward-line 1)))))
497 (beginning-of-line)
498 (nnheader-insert-nov headers)
499 (nnml-save-nov)
500 t)))))
501
502(deffoo nnml-request-delete-group (group &optional force server)
503 (nnml-possibly-change-directory group server)
01c52d31
MB
504 (let ((file (directory-file-name nnml-current-directory))
505 (file-name-coding-system nnmail-pathname-coding-system))
506 (if (file-exists-p file)
507 (if (file-directory-p file)
508 (progn
509 (when force
510 ;; Delete all articles in GROUP.
511 (let ((articles
512 (directory-files
513 nnml-current-directory t
514 (concat
515 nnheader-numerical-short-files
516 "\\|" (regexp-quote nnml-nov-file-name) "$"
517 "\\|" (regexp-quote nnml-marks-file-name) "$")))
518 (decoded (nnml-decoded-group-name group server)))
519 (dolist (article articles)
520 (when (file-writable-p article)
521 (nnheader-message 5 "Deleting article %s in %s..."
522 (file-name-nondirectory article)
523 decoded)
524 (funcall nnmail-delete-file-function article))))
525 ;; Try to delete the directory itself.
526 (ignore-errors (delete-directory nnml-current-directory))))
527 (nnheader-report 'nnml "%s is not a directory" file))
528 (nnheader-report 'nnml "No such directory: %s/" file))
529 ;; Remove the group from all structures.
530 (setq nnml-group-alist
531 (delq (assoc group nnml-group-alist) nnml-group-alist)
532 nnml-current-group nil
533 nnml-current-directory nil)
534 ;; Save the active file.
535 (nnmail-save-active nnml-group-alist nnml-active-file))
eec82323
LMI
536 t)
537
538(deffoo nnml-request-rename-group (group new-name &optional server)
539 (nnml-possibly-change-directory group server)
01c52d31 540 (let ((new-dir (nnml-group-pathname new-name nil server))
4d8a28ec
MB
541 (old-dir (nnml-group-pathname group nil server))
542 (file-name-coding-system nnmail-pathname-coding-system))
16409b0b
GM
543 (when (ignore-errors
544 (make-directory new-dir t)
545 t)
eec82323
LMI
546 ;; We move the articles file by file instead of renaming
547 ;; the directory -- there may be subgroups in this group.
548 ;; One might be more clever, I guess.
01c52d31
MB
549 (dolist (file (nnheader-article-to-file-alist old-dir))
550 (rename-file
551 (concat old-dir (cdr file))
552 (concat new-dir (cdr file))))
eec82323
LMI
553 ;; Move .overview file.
554 (let ((overview (concat old-dir nnml-nov-file-name)))
555 (when (file-exists-p overview)
556 (rename-file overview (concat new-dir nnml-nov-file-name))))
23f87bed
MB
557 ;; Move .marks file.
558 (let ((marks (concat old-dir nnml-marks-file-name)))
559 (when (file-exists-p marks)
560 (rename-file marks (concat new-dir nnml-marks-file-name))))
eec82323 561 (when (<= (length (directory-files old-dir)) 2)
16409b0b 562 (ignore-errors (delete-directory old-dir)))
eec82323
LMI
563 ;; That went ok, so we change the internal structures.
564 (let ((entry (assoc group nnml-group-alist)))
565 (when entry
566 (setcar entry new-name))
567 (setq nnml-current-directory nil
568 nnml-current-group nil)
569 ;; Save the new group alist.
570 (nnmail-save-active nnml-group-alist nnml-active-file)
571 t))))
572
573(deffoo nnml-set-status (article name value &optional group server)
574 (nnml-possibly-change-directory group server)
575 (let ((file (nnml-article-to-file article)))
576 (cond
577 ((not (file-exists-p file))
578 (nnheader-report 'nnml "File %s does not exist" file))
579 (t
16409b0b 580 (with-temp-file file
eec82323
LMI
581 (nnheader-insert-file-contents file)
582 (nnmail-replace-status name value))
583 t))))
584
585\f
586;;; Internal functions.
587
588(defun nnml-article-to-file (article)
589 (nnml-update-file-alist)
590 (let (file)
23f87bed
MB
591 (if (setq file
592 (if nnml-use-compressed-files
593 (cdr (assq article nnml-article-file-alist))
594 (number-to-string article)))
16409b0b 595 (expand-file-name file nnml-current-directory)
23f87bed
MB
596 (when (not nnheader-directory-files-is-safe)
597 ;; Just to make sure nothing went wrong when reading over NFS --
598 ;; check once more.
599 (when (file-exists-p
600 (setq file (expand-file-name (number-to-string article)
601 nnml-current-directory)))
602 (nnml-update-file-alist t)
603 file)))))
eec82323
LMI
604
605(defun nnml-deletable-article-p (group article)
606 "Say whether ARTICLE in GROUP can be deleted."
01c52d31
MB
607 (let ((file-name-coding-system nnmail-pathname-coding-system)
608 path)
eec82323
LMI
609 (when (setq path (nnml-article-to-file article))
610 (when (file-writable-p path)
611 (or (not nnmail-keep-last-article)
612 (not (eq (cdr (nth 1 (assoc group nnml-group-alist)))
613 article)))))))
614
615;; Find an article number in the current group given the Message-ID.
01c52d31 616(defun nnml-find-group-number (id server)
eec82323
LMI
617 (save-excursion
618 (set-buffer (get-buffer-create " *nnml id*"))
eec82323
LMI
619 (let ((alist nnml-group-alist)
620 number)
621 ;; We want to look through all .overview files, but we want to
622 ;; start with the one in the current directory. It seems most
623 ;; likely that the article we are looking for is in that group.
01c52d31 624 (if (setq number (nnml-find-id nnml-current-group id server))
eec82323 625 (cons nnml-current-group number)
23f87bed 626 ;; It wasn't there, so we look through the other groups as well.
eec82323
LMI
627 (while (and (not number)
628 alist)
629 (or (string= (caar alist) nnml-current-group)
01c52d31 630 (setq number (nnml-find-id (caar alist) id server)))
eec82323
LMI
631 (or number
632 (setq alist (cdr alist))))
633 (and number
634 (cons (caar alist) number))))))
635
01c52d31 636(defun nnml-find-id (group id server)
eec82323 637 (erase-buffer)
01c52d31 638 (let ((nov (nnml-group-pathname group nnml-nov-file-name server))
eec82323
LMI
639 number found)
640 (when (file-exists-p nov)
641 (nnheader-insert-file-contents nov)
642 (while (and (not found)
643 (search-forward id nil t)) ; We find the ID.
644 ;; And the id is in the fourth field.
645 (if (not (and (search-backward "\t" nil t 4)
01c52d31 646 (not (search-backward "\t" (point-at-bol) t))))
eec82323
LMI
647 (forward-line 1)
648 (beginning-of-line)
649 (setq found t)
650 ;; We return the article number.
651 (setq number
16409b0b 652 (ignore-errors (read (current-buffer))))))
eec82323
LMI
653 number)))
654
655(defun nnml-retrieve-headers-with-nov (articles &optional fetch-old)
656 (if (or gnus-nov-is-evil nnml-nov-is-evil)
657 nil
16409b0b 658 (let ((nov (expand-file-name nnml-nov-file-name nnml-current-directory)))
eec82323
LMI
659 (when (file-exists-p nov)
660 (save-excursion
661 (set-buffer nntp-server-buffer)
662 (erase-buffer)
663 (nnheader-insert-file-contents nov)
664 (if (and fetch-old
665 (not (numberp fetch-old)))
666 t ; Don't remove anything.
667 (nnheader-nov-delete-outside-range
668 (if fetch-old (max 1 (- (car articles) fetch-old))
669 (car articles))
670 (car (last articles)))
671 t))))))
672
673(defun nnml-possibly-change-directory (group &optional server)
674 (when (and server
675 (not (nnml-server-opened server)))
676 (nnml-open-server server))
677 (if (not group)
678 t
01c52d31 679 (let ((pathname (nnml-group-pathname group nil server))
16409b0b 680 (file-name-coding-system nnmail-pathname-coding-system))
eec82323
LMI
681 (when (not (equal pathname nnml-current-directory))
682 (setq nnml-current-directory pathname
683 nnml-current-group group
684 nnml-article-file-alist nil))
685 (file-exists-p nnml-current-directory))))
686
01c52d31
MB
687(defun nnml-possibly-create-directory (group &optional server)
688 (let ((dir (nnml-group-pathname group nil server))
689 (file-name-coding-system nnmail-pathname-coding-system))
16409b0b
GM
690 (unless (file-exists-p dir)
691 (make-directory (directory-file-name dir) t)
692 (nnheader-message 5 "Creating mail directory %s" dir))))
eec82323 693
01c52d31
MB
694(defun nnml-save-mail (group-art &optional server)
695 "Save a mail into the groups GROUP-ART in the nnml server SERVER.
696GROUP-ART is a list that each element is a cons of a group name and an
697article number. This function is called narrowed to an article."
698 (let* ((chars (nnmail-insert-lines))
699 (extension (and nnml-use-compressed-files
700 (> chars nnml-compressed-files-size-threshold)
701 (if (stringp nnml-use-compressed-files)
702 nnml-use-compressed-files
703 ".gz")))
704 decoded dec file first headers)
705 (when nnmail-group-names-not-encoded-p
706 (dolist (ga (prog1 group-art (setq group-art nil)))
707 (setq group-art (nconc group-art
708 (list (cons (nnml-encoded-group-name (car ga)
709 server)
710 (cdr ga))))
711 decoded (nconc decoded (list (car ga)))))
712 (setq dec decoded))
eec82323
LMI
713 (nnmail-insert-xref group-art)
714 (run-hooks 'nnmail-prepare-save-mail-hook)
715 (run-hooks 'nnml-prepare-save-mail-hook)
716 (goto-char (point-min))
717 (while (looking-at "From ")
718 (replace-match "X-From-Line: ")
719 (forward-line 1))
720 ;; We save the article in all the groups it belongs in.
01c52d31
MB
721 (dolist (ga group-art)
722 (if nnmail-group-names-not-encoded-p
723 (progn
724 (nnml-possibly-create-directory (car decoded) server)
725 (setq file (nnmail-group-pathname
726 (pop decoded) nnml-directory
727 (concat (number-to-string (cdr ga)) extension))))
728 (nnml-possibly-create-directory (car ga) server)
729 (setq file (nnml-group-pathname
730 (car ga) (concat (number-to-string (cdr ga)) extension)
731 server)))
732 (if first
733 ;; It was already saved, so we just make a hard link.
734 (let ((file-name-coding-system nnmail-pathname-coding-system))
735 (funcall nnmail-crosspost-link-function first file t))
736 ;; Save the article.
737 (nnmail-write-region (point-min) (point-max) file nil
738 (if (nnheader-be-verbose 5) nil 'nomesg))
739 (setq first file)))
eec82323
LMI
740 ;; Generate a nov line for this article. We generate the nov
741 ;; line after saving, because nov generation destroys the
742 ;; header.
743 (setq headers (nnml-parse-head chars))
744 ;; Output the nov line to all nov databases that should have it.
01c52d31
MB
745 (if nnmail-group-names-not-encoded-p
746 (dolist (ga group-art)
747 (nnml-add-nov (pop dec) (cdr ga) headers))
748 (dolist (ga group-art)
749 (nnml-add-nov (car ga) (cdr ga) headers))))
750 group-art)
751
752(defun nnml-active-number (group &optional server)
753 "Compute the next article number in GROUP on SERVER."
754 (let ((active (cadr (assoc (if nnmail-group-names-not-encoded-p
755 (nnml-encoded-group-name group server)
756 group)
757 nnml-group-alist))))
eec82323
LMI
758 ;; The group wasn't known to nnml, so we just create an active
759 ;; entry for it.
760 (unless active
761 ;; Perhaps the active file was corrupt? See whether
762 ;; there are any articles in this group.
01c52d31
MB
763 (nnml-possibly-create-directory group server)
764 (nnml-possibly-change-directory group server)
eec82323
LMI
765 (unless nnml-article-file-alist
766 (setq nnml-article-file-alist
767 (sort
23f87bed 768 (nnml-current-group-article-to-file-alist)
6748645f 769 'car-less-than-car)))
eec82323
LMI
770 (setq active
771 (if nnml-article-file-alist
772 (cons (caar nnml-article-file-alist)
773 (caar (last nnml-article-file-alist)))
774 (cons 1 0)))
775 (push (list group active) nnml-group-alist))
776 (setcdr active (1+ (cdr active)))
777 (while (file-exists-p
01c52d31 778 (nnml-group-pathname group (int-to-string (cdr active)) server))
eec82323
LMI
779 (setcdr active (1+ (cdr active))))
780 (cdr active)))
781
782(defun nnml-add-nov (group article headers)
783 "Add a nov line for the GROUP base."
784 (save-excursion
785 (set-buffer (nnml-open-nov group))
786 (goto-char (point-max))
787 (mail-header-set-number headers article)
788 (nnheader-insert-nov headers)))
789
790(defsubst nnml-header-value ()
01c52d31 791 (buffer-substring (match-end 0) (point-at-eol)))
eec82323
LMI
792
793(defun nnml-parse-head (chars &optional number)
794 "Parse the head of the current buffer."
795 (save-excursion
796 (save-restriction
6748645f
LMI
797 (unless (zerop (buffer-size))
798 (narrow-to-region
799 (goto-char (point-min))
23f87bed
MB
800 (if (re-search-forward "\n\r?\n" nil t)
801 (1- (point))
802 (point-max))))
803 (let ((headers (nnheader-parse-naked-head)))
eec82323
LMI
804 (mail-header-set-chars headers chars)
805 (mail-header-set-number headers number)
806 headers))))
807
23f87bed 808(defun nnml-get-nov-buffer (group)
01c52d31
MB
809 (let* ((decoded (nnml-decoded-group-name group))
810 (buffer (get-buffer-create (format " *nnml overview %s*" decoded)))
811 (file-name-coding-system nnmail-pathname-coding-system))
23f87bed
MB
812 (save-excursion
813 (set-buffer buffer)
814 (set (make-local-variable 'nnml-nov-buffer-file-name)
01c52d31 815 (nnmail-group-pathname decoded nnml-directory nnml-nov-file-name))
23f87bed
MB
816 (erase-buffer)
817 (when (file-exists-p nnml-nov-buffer-file-name)
818 (nnheader-insert-file-contents nnml-nov-buffer-file-name)))
819 buffer))
820
eec82323
LMI
821(defun nnml-open-nov (group)
822 (or (cdr (assoc group nnml-nov-buffer-alist))
23f87bed 823 (let ((buffer (nnml-get-nov-buffer group)))
eec82323
LMI
824 (push (cons group buffer) nnml-nov-buffer-alist)
825 buffer)))
826
827(defun nnml-save-nov ()
828 (save-excursion
829 (while nnml-nov-buffer-alist
830 (when (buffer-name (cdar nnml-nov-buffer-alist))
831 (set-buffer (cdar nnml-nov-buffer-alist))
832 (when (buffer-modified-p)
9930767b
SM
833 (nnmail-write-region (point-min) (point-max)
834 nnml-nov-buffer-file-name nil 'nomesg))
eec82323
LMI
835 (set-buffer-modified-p nil)
836 (kill-buffer (current-buffer)))
837 (setq nnml-nov-buffer-alist (cdr nnml-nov-buffer-alist)))))
838
839;;;###autoload
23f87bed 840(defun nnml-generate-nov-databases (&optional server)
eec82323 841 "Generate NOV databases in all nnml directories."
23f87bed 842 (interactive (list (or (nnoo-current-server 'nnml) "")))
eec82323
LMI
843 ;; Read the active file to make sure we don't re-use articles
844 ;; numbers in empty groups.
845 (nnmail-activate 'nnml)
23f87bed
MB
846 (unless (nnml-server-opened server)
847 (nnml-open-server server))
eec82323
LMI
848 (setq nnml-directory (expand-file-name nnml-directory))
849 ;; Recurse down the directories.
01c52d31 850 (nnml-generate-nov-databases-directory nnml-directory nil t)
eec82323
LMI
851 ;; Save the active file.
852 (nnmail-save-active nnml-group-alist nnml-active-file))
853
01c52d31
MB
854(defun nnml-generate-nov-databases-directory (dir &optional seen no-active)
855 "Regenerate the NOV database in DIR.
856
857Unless no-active is non-nil, update the active file too."
858 (interactive (list (let ((file-name-coding-system
859 nnmail-pathname-coding-system))
860 (read-directory-name "Regenerate NOV in: "
861 nnml-directory nil t))))
eec82323 862 (setq dir (file-name-as-directory dir))
01c52d31
MB
863 (let ((file-name-coding-system nnmail-pathname-coding-system))
864 ;; Only scan this sub-tree if we haven't been here yet.
865 (unless (member (file-truename dir) seen)
866 (push (file-truename dir) seen)
867 ;; We descend recursively
868 (dolist (dir (directory-files dir t nil t))
16409b0b 869 (when (and (not (string-match "^\\." (file-name-nondirectory dir)))
eec82323 870 (file-directory-p dir))
01c52d31
MB
871 (nnml-generate-nov-databases-directory dir seen)))
872 ;; Do this directory.
873 (let ((files (sort (nnheader-article-to-file-alist dir)
874 'car-less-than-car)))
875 (if (not files)
876 (let* ((group (nnheader-file-to-group
877 (directory-file-name dir) nnml-directory))
878 (info (cadr (assoc group nnml-group-alist))))
879 (when info
880 (setcar info (1+ (cdr info)))))
881 (funcall nnml-generate-active-function dir)
882 ;; Generate the nov file.
883 (nnml-generate-nov-file dir files)
884 (unless no-active
885 (nnmail-save-active nnml-group-alist nnml-active-file)))))))
eec82323 886
9efa445f 887(defvar files)
eec82323
LMI
888(defun nnml-generate-active-info (dir)
889 ;; Update the active info for this group.
01c52d31
MB
890 (let ((group (directory-file-name dir))
891 entry last)
892 (setq group (nnheader-file-to-group (nnml-encoded-group-name group)
893 nnml-directory)
894 entry (assoc group nnml-group-alist)
895 last (or (caadr entry) 0)
896 nnml-group-alist (delq entry nnml-group-alist))
eec82323 897 (push (list group
23f87bed
MB
898 (cons (or (caar files) (1+ last))
899 (max last
01c52d31 900 (or (caar (last files))
23f87bed 901 0))))
eec82323
LMI
902 nnml-group-alist)))
903
904(defun nnml-generate-nov-file (dir files)
905 (let* ((dir (file-name-as-directory dir))
906 (nov (concat dir nnml-nov-file-name))
907 (nov-buffer (get-buffer-create " *nov*"))
908 chars file headers)
909 (save-excursion
910 ;; Init the nov buffer.
911 (set-buffer nov-buffer)
16409b0b 912 (buffer-disable-undo)
eec82323
LMI
913 (erase-buffer)
914 (set-buffer nntp-server-buffer)
915 ;; Delete the old NOV file.
916 (when (file-exists-p nov)
917 (funcall nnmail-delete-file-function nov))
918 (while files
919 (unless (file-directory-p (setq file (concat dir (cdar files))))
920 (erase-buffer)
921 (nnheader-insert-file-contents file)
922 (narrow-to-region
923 (goto-char (point-min))
924 (progn
23f87bed 925 (re-search-forward "\n\r?\n" nil t)
eec82323 926 (setq chars (- (point-max) (point)))
9930767b 927 (max (point-min) (1- (point)))))
a8151ef7 928 (unless (zerop (buffer-size))
eec82323
LMI
929 (goto-char (point-min))
930 (setq headers (nnml-parse-head chars (caar files)))
931 (save-excursion
932 (set-buffer nov-buffer)
933 (goto-char (point-max))
934 (nnheader-insert-nov headers)))
935 (widen))
936 (setq files (cdr files)))
937 (save-excursion
938 (set-buffer nov-buffer)
9930767b 939 (nnmail-write-region (point-min) (point-max) nov nil 'nomesg)
eec82323
LMI
940 (kill-buffer (current-buffer))))))
941
942(defun nnml-nov-delete-article (group article)
943 (save-excursion
944 (set-buffer (nnml-open-nov group))
945 (when (nnheader-find-nov-line article)
946 (delete-region (point) (progn (forward-line 1) (point)))
947 (when (bobp)
948 (let ((active (cadr (assoc group nnml-group-alist)))
949 num)
950 (when active
951 (if (eobp)
952 (setf (car active) (1+ (cdr active)))
953 (when (and (setq num (ignore-errors (read (current-buffer))))
954 (numberp num))
955 (setf (car active) num)))))))
956 t))
957
a8151ef7 958(defun nnml-update-file-alist (&optional force)
23f87bed
MB
959 (when nnml-use-compressed-files
960 (when (or (not nnml-article-file-alist)
961 force)
962 (setq nnml-article-file-alist
963 (nnml-current-group-article-to-file-alist)))))
964
965(defun nnml-directory-articles (dir)
966 "Return a list of all article files in a directory.
967Use the nov database for that directory if available."
968 (if (or gnus-nov-is-evil nnml-nov-is-evil
969 (not (file-exists-p
970 (expand-file-name nnml-nov-file-name dir))))
971 (nnheader-directory-articles dir)
972 ;; build list from .overview if available
973 ;; We would use nnml-open-nov, except that nnml-nov-buffer-alist is
974 ;; defvoo'd, and we might get called when it hasn't been swapped in.
975 (save-excursion
976 (let ((list nil)
977 art
978 (buffer (nnml-get-nov-buffer nnml-current-group)))
979 (set-buffer buffer)
980 (goto-char (point-min))
981 (while (not (eobp))
982 (setq art (read (current-buffer)))
983 (push art list)
984 (forward-line 1))
985 list))))
986
987(defun nnml-current-group-article-to-file-alist ()
988 "Return an alist of article/file pairs in the current group.
989Use the nov database for the current group if available."
990 (if (or nnml-use-compressed-files
991 gnus-nov-is-evil
992 nnml-nov-is-evil
993 (not (file-exists-p
994 (expand-file-name nnml-nov-file-name
995 nnml-current-directory))))
996 (nnheader-article-to-file-alist nnml-current-directory)
997 ;; build list from .overview if available
998 (save-excursion
999 (let ((alist nil)
1000 (buffer (nnml-get-nov-buffer nnml-current-group))
1001 art)
1002 (set-buffer buffer)
1003 (goto-char (point-min))
1004 (while (not (eobp))
1005 (setq art (read (current-buffer)))
1006 ;; assume file name is unadorned (ie. not compressed etc)
1007 (push (cons art (int-to-string art)) alist)
1008 (forward-line 1))
1009 alist))))
1010
1011(deffoo nnml-request-set-mark (group actions &optional server)
1012 (nnml-possibly-change-directory group server)
1013 (unless nnml-marks-is-evil
1014 (nnml-open-marks group server)
1015 (dolist (action actions)
1016 (let ((range (nth 0 action))
1017 (what (nth 1 action))
1018 (marks (nth 2 action)))
c1d7d285 1019 (assert (or (eq what 'add) (eq what 'del)) nil
23f87bed
MB
1020 "Unknown request-set-mark action: %s" what)
1021 (dolist (mark marks)
1022 (setq nnml-marks (gnus-update-alist-soft
1023 mark
1024 (funcall (if (eq what 'add) 'gnus-range-add
1025 'gnus-remove-from-range)
1026 (cdr (assoc mark nnml-marks)) range)
1027 nnml-marks)))))
1028 (nnml-save-marks group server))
1029 nil)
1030
1031(deffoo nnml-request-update-info (group info &optional server)
1032 (nnml-possibly-change-directory group server)
01c52d31 1033 (when (and (not nnml-marks-is-evil) (nnml-marks-changed-p group server))
23f87bed
MB
1034 (nnheader-message 8 "Updating marks for %s..." group)
1035 (nnml-open-marks group server)
1036 ;; Update info using `nnml-marks'.
01c52d31
MB
1037 (mapc (lambda (pred)
1038 (unless (memq (cdr pred) gnus-article-unpropagated-mark-lists)
1039 (gnus-info-set-marks
1040 info
1041 (gnus-update-alist-soft
1042 (cdr pred)
1043 (cdr (assq (cdr pred) nnml-marks))
1044 (gnus-info-marks info))
1045 t)))
1046 gnus-article-mark-lists)
23f87bed
MB
1047 (let ((seen (cdr (assq 'read nnml-marks))))
1048 (gnus-info-set-read info
1049 (if (and (integerp (car seen))
1050 (null (cdr seen)))
1051 (list (cons (car seen) (car seen)))
1052 seen)))
1053 (nnheader-message 8 "Updating marks for %s...done" group))
1054 info)
1055
01c52d31
MB
1056(defun nnml-marks-changed-p (group server)
1057 (let ((file (nnml-group-pathname group nnml-marks-file-name server)))
23f87bed
MB
1058 (if (null (gnus-gethash file nnml-marks-modtime))
1059 t ;; never looked at marks file, assume it has changed
1060 (not (equal (gnus-gethash file nnml-marks-modtime)
1061 (nth 5 (file-attributes file)))))))
1062
1063(defun nnml-save-marks (group server)
1064 (let ((file-name-coding-system nnmail-pathname-coding-system)
01c52d31 1065 (file (nnml-group-pathname group nnml-marks-file-name server)))
23f87bed
MB
1066 (condition-case err
1067 (progn
01c52d31 1068 (nnml-possibly-create-directory group server)
23f87bed
MB
1069 (with-temp-file file
1070 (erase-buffer)
1071 (gnus-prin1 nnml-marks)
1072 (insert "\n"))
1073 (gnus-sethash file
1074 (nth 5 (file-attributes file))
1075 nnml-marks-modtime))
1076 (error (or (gnus-yes-or-no-p
1077 (format "Could not write to %s (%s). Continue? " file err))
5145dbc5 1078 (error "Cannot write to %s (%s)" file err))))))
23f87bed
MB
1079
1080(defun nnml-open-marks (group server)
01c52d31
MB
1081 (let* ((decoded (nnml-decoded-group-name group server))
1082 (file (nnmail-group-pathname decoded nnml-directory
1083 nnml-marks-file-name))
1084 (file-name-coding-system nnmail-pathname-coding-system))
23f87bed
MB
1085 (if (file-exists-p file)
1086 (condition-case err
1087 (with-temp-buffer
1088 (gnus-sethash file (nth 5 (file-attributes file))
1089 nnml-marks-modtime)
1090 (nnheader-insert-file-contents file)
1091 (setq nnml-marks (read (current-buffer)))
1092 (dolist (el gnus-article-unpropagated-mark-lists)
1093 (setq nnml-marks (gnus-remassoc el nnml-marks))))
1094 (error (or (gnus-yes-or-no-p
1095 (format "Error reading nnml marks file %s (%s). Continuing will use marks from .newsrc.eld. Continue? " file err))
1096 (error "Cannot read nnml marks file %s (%s)" file err))))
1097 ;; User didn't have a .marks file. Probably first time
1098 ;; user of the .marks stuff. Bootstrap it from .newsrc.eld.
1099 (let ((info (gnus-get-info
1100 (gnus-group-prefixed-name
1101 group
01c52d31
MB
1102 (gnus-server-to-method
1103 (format "nnml:%s" (or server "")))))))
1104 (setq decoded (if (member server '(nil ""))
1105 (concat "nnml:" decoded)
1106 (format "nnml+%s:%s" server decoded)))
1107 (nnheader-message 7 "Bootstrapping marks for %s..." decoded)
23f87bed
MB
1108 (setq nnml-marks (gnus-info-marks info))
1109 (push (cons 'read (gnus-info-read info)) nnml-marks)
1110 (dolist (el gnus-article-unpropagated-mark-lists)
1111 (setq nnml-marks (gnus-remassoc el nnml-marks)))
1112 (nnml-save-marks group server)
01c52d31
MB
1113 (nnheader-message 7 "Bootstrapping marks for %s...done" decoded)))))
1114
1115
1116;;;
1117;;; Group and server compaction. -- dvl
1118;;;
1119
1120;; #### FIXME: this function handles self Xref: entry correctly, but I don't
1121;; #### know how to handle external cross-references. I actually don't know if
1122;; #### this is handled correctly elsewhere. For instance, what happens if you
1123;; #### move all articles to a new group (that's what people do for manual
1124;; #### compaction) ?
1125
1126;; #### NOTE: the function below handles the article backlog. This is
1127;; #### conceptually the wrong place to do it because the backend is at a
1128;; #### lower level. However, this is the only place where we have the needed
1129;; #### information to do the job. Ideally, this function should not handle
1130;; #### the backlog by itself, but return a list of moved groups / articles to
1131;; #### the caller. This will become important to avoid code duplication when
1132;; #### other backends get a compaction feature. Also, note that invalidating
1133;; #### the "original article buffer" is already done at an upper level.
1134
1135;; Shouldn't `nnml-request-compact-group' be interactive? --rsteib
1136
1137(defun nnml-request-compact-group (group &optional server save)
1138 (nnml-possibly-change-directory group server)
1139 (unless nnml-article-file-alist
1140 (setq nnml-article-file-alist
1141 (sort (nnml-current-group-article-to-file-alist)
1142 'car-less-than-car)))
1143 (if (not nnml-article-file-alist)
1144 ;; The group is empty: do nothing but return t
1145 t
1146 ;; The group is not empty:
1147 (let* ((group-full-name
1148 (gnus-group-prefixed-name
1149 group
1150 (gnus-server-to-method (format "nnml:%s" server))))
1151 (info (gnus-get-info group-full-name))
1152 (new-number 1)
1153 compacted)
1154 (let ((articles nnml-article-file-alist)
1155 article)
1156 (while (setq article (pop articles))
1157 (let ((old-number (car article)))
1158 (when (> old-number new-number)
1159 ;; There is a gap here:
1160 (let ((old-number-string (int-to-string old-number))
1161 (new-number-string (int-to-string new-number)))
1162 (setq compacted t)
1163 ;; #### NOTE: `nnml-article-to-file' calls
1164 ;; #### `nnml-update-file-alist' (which in turn calls
1165 ;; #### `nnml-current-group-article-to-file-alist', which
1166 ;; #### might use the NOV database). This might turn out to be
1167 ;; #### inefficient. In that case, we will do the work
1168 ;; #### manually.
1169 ;; 1/ Move the article to a new file:
1170 (let* ((oldfile (nnml-article-to-file old-number))
1171 (newfile
1172 (gnus-replace-in-string
1173 oldfile
1174 ;; nnml-use-compressed-files might be any string, but
1175 ;; probably it's sufficient to take into account only
1176 ;; "\\.[a-z0-9]+". Note that we can't only use the
1177 ;; value of nnml-use-compressed-files because old
1178 ;; articles might have been saved with a different
1179 ;; value.
1180 (concat
1181 "\\(" old-number-string "\\)\\(\\(\\.[a-z0-9]+\\)?\\)$")
1182 (concat new-number-string "\\2"))))
1183 (with-current-buffer nntp-server-buffer
1184 (nnmail-find-file oldfile)
1185 ;; Update the Xref header in the article itself:
1186 (when (and (re-search-forward "^Xref: [^ ]+ " nil t)
1187 (re-search-forward
1188 (concat "\\<"
1189 (regexp-quote
1190 (concat group ":" old-number-string))
1191 "\\>")
1192 (point-at-eol) t))
1193 (replace-match
1194 (concat group ":" new-number-string)))
1195 ;; Save to the new file:
1196 (nnmail-write-region (point-min) (point-max) newfile))
1197 (funcall nnmail-delete-file-function oldfile))
1198 ;; 2/ Update all marks for this article:
1199 ;; #### NOTE: it is possible that the new article number
1200 ;; #### already belongs to a range, whereas the corresponding
1201 ;; #### article doesn't exist (for example, if you delete an
1202 ;; #### article). For that reason, it is important to update
5a89f0a7 1203 ;; #### the ranges (meaning remove inexistent articles) before
01c52d31
MB
1204 ;; #### doing anything on them.
1205 ;; 2 a/ read articles:
1206 (let ((read (gnus-info-read info)))
1207 (setq read (gnus-remove-from-range read (list new-number)))
1208 (when (gnus-member-of-range old-number read)
1209 (setq read (gnus-remove-from-range read (list old-number)))
1210 (setq read (gnus-add-to-range read (list new-number))))
1211 (gnus-info-set-read info read))
1212 ;; 2 b/ marked articles:
1213 (let ((oldmarks (gnus-info-marks info))
1214 mark newmarks)
1215 (while (setq mark (pop oldmarks))
1216 (setcdr mark (gnus-remove-from-range (cdr mark)
1217 (list new-number)))
1218 (when (gnus-member-of-range old-number (cdr mark))
1219 (setcdr mark (gnus-remove-from-range (cdr mark)
1220 (list old-number)))
1221 (setcdr mark (gnus-add-to-range (cdr mark)
1222 (list new-number))))
1223 (push mark newmarks))
1224 (gnus-info-set-marks info newmarks))
1225 ;; 3/ Update the NOV entry for this article:
1226 (unless nnml-nov-is-evil
1227 (save-excursion
1228 (set-buffer (nnml-open-nov group))
1229 (when (nnheader-find-nov-line old-number)
1230 ;; Replace the article number:
1231 (looking-at old-number-string)
1232 (replace-match new-number-string nil t)
1233 ;; Update the Xref header:
1234 (when (re-search-forward
1235 (concat "\\(Xref:[^\t\n]* \\)\\<"
1236 (regexp-quote
1237 (concat group ":" old-number-string))
1238 "\\>")
1239 (point-at-eol) t)
1240 (replace-match
1241 (concat "\\1" group ":" new-number-string))))))
1242 ;; 4/ Possibly remove the article from the backlog:
1243 (when gnus-keep-backlog
1244 ;; #### NOTE: instead of removing the article, we could
1245 ;; #### modify the backlog to reflect the numbering change,
1246 ;; #### but I don't think it's worth it.
1247 (gnus-backlog-remove-article group-full-name old-number)
1248 (gnus-backlog-remove-article group-full-name new-number))))
1249 (setq new-number (1+ new-number)))))
1250 (if (not compacted)
1251 ;; No compaction had to be done:
1252 t
1253 ;; Some articles have actually been renamed:
1254 ;; 1/ Rebuild active information:
1255 (let ((entry (assoc group nnml-group-alist))
1256 (active (cons 1 (1- new-number))))
1257 (setq nnml-group-alist (delq entry nnml-group-alist))
1258 (push (list group active) nnml-group-alist)
1259 ;; Update the active hashtable to let the *Group* buffer display
1260 ;; up-to-date lines. I don't think that either gnus-newsrc-hashtb or
1261 ;; gnus-newwrc-alist are out of date, since all we did is to modify
1262 ;; the info of the group internally.
1263 (gnus-set-active group-full-name active))
1264 ;; 1 bis/
1265 ;; #### NOTE: normally, we should save the overview (NOV) file
1266 ;; #### here, just like we save the marks file. However, there is no
1267 ;; #### such function as nnml-save-nov for a single group. Only for
1268 ;; #### all groups. Gnus inconsistency is getting worse every day...
1269 ;; 2/ Rebuild marks file:
1270 (unless nnml-marks-is-evil
1271 ;; #### NOTE: this constant use of global variables everywhere is
1272 ;; #### truly disgusting. Gnus really needs a *major* cleanup.
1273 (setq nnml-marks (gnus-info-marks info))
1274 (push (cons 'read (gnus-info-read info)) nnml-marks)
1275 (dolist (el gnus-article-unpropagated-mark-lists)
1276 (setq nnml-marks (gnus-remassoc el nnml-marks)))
1277 (nnml-save-marks group server))
1278 ;; 3/ Save everything if this was not part of a bigger operation:
1279 (if (not save)
1280 ;; Nothing to save (yet):
1281 t
1282 ;; Something to save:
1283 ;; a/ Save the NOV databases:
1284 ;; #### NOTE: this should be done directory per directory in 1bis
1285 ;; #### above. See comment there.
1286 (nnml-save-nov)
1287 ;; b/ Save the active file:
1288 (nnmail-save-active nnml-group-alist nnml-active-file)
1289 t)))))
1290
1291(defun nnml-request-compact (&optional server)
1292 "Request compaction of all SERVER nnml groups."
1293 (interactive (list (or (nnoo-current-server 'nnml) "")))
1294 (nnmail-activate 'nnml)
1295 (unless (nnml-server-opened server)
1296 (nnml-open-server server))
1297 (setq nnml-directory (expand-file-name nnml-directory))
1298 (let* ((groups (gnus-groups-from-server
1299 (gnus-server-to-method (format "nnml:%s" server))))
1300 (first (pop groups))
1301 group)
1302 (when first
1303 (while (setq group (pop groups))
1304 (nnml-request-compact-group (gnus-group-real-name group) server))
1305 (nnml-request-compact-group (gnus-group-real-name first) server t))))
1306
eec82323
LMI
1307
1308(provide 'nnml)
1309
cbee283d 1310;; arch-tag: 52c97dc3-9735-45de-b439-9e4d23b52004
eec82323 1311;;; nnml.el ends here