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