Add a `:function' parameter for maildir to solve special cases.
[gnus] / lisp / mail-source.el
1 ;;; mail-source.el --- functions for fetching mail
2 ;; Copyright (C) 1999 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
5 ;; Keywords: news, mail
6
7 ;; This file is part of GNU Emacs.
8
9 ;; GNU Emacs is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 2, or (at your option)
12 ;; any later version.
13
14 ;; GNU Emacs is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 ;; GNU General Public License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
21 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
22 ;; Boston, MA 02111-1307, USA.
23
24 ;;; Commentary:
25
26 ;;; Code:
27
28 (eval-when-compile (require 'cl))
29 (eval-and-compile
30   (autoload 'pop3-movemail "pop3"))
31 (require 'format-spec)
32
33 (defgroup mail-source nil
34   "The mail-fetching library."
35   :group 'gnus)
36
37 (defcustom mail-sources nil
38   "*Where the mail backends will look for incoming mail.
39 This variable is a list of mail source specifiers."
40   :group 'mail-source
41   :type 'sexp)
42
43 (defcustom mail-source-crash-box "~/.emacs-mail-crash-box"
44   "File where mail will be stored while processing it."
45   :group 'mail-source
46   :type 'file)
47
48 (defcustom mail-source-directory "~/Mail/"
49   "Directory where files (if any) will be stored."
50   :group 'mail-source
51   :type 'directory)
52
53 (defcustom mail-source-default-file-modes 384
54   "Set the mode bits of all new mail files to this integer."
55   :group 'mail-source
56   :type 'integer)
57
58 (defcustom mail-source-delete-incoming nil
59   "*If non-nil, delete incoming files after handling."
60   :group 'mail-source
61   :type 'boolean)
62
63 ;;; Internal variables.
64
65 (defvar mail-source-string ""
66   "A dynamically bound string that says what the current mail source is.")
67
68 (eval-and-compile
69   (defvar mail-source-keyword-map
70     '((file
71        (:prescript)
72        (:prescript-delay)
73        (:postscript)
74        (:path (or (getenv "MAIL")
75                   (concat "/usr/spool/mail/" (user-login-name)))))
76       (directory
77        (:path)
78        (:suffix ".spool")
79        (:predicate identity))
80       (pop
81        (:prescript)
82        (:prescript-delay)
83        (:postscript)
84        (:server (getenv "MAILHOST"))
85        (:port 110)
86        (:user (or (user-login-name) (getenv "LOGNAME") (getenv "USER")))
87        (:program)
88        (:function)
89        (:password)
90        (:authentication password))
91       (maildir
92        (:path "~/Maildir/new/")
93        (:function))
94       (imap
95        (:server (getenv "MAILHOST"))
96        (:port)
97        (:stream)
98        (:authentication)
99        (:user (or (user-login-name) (getenv "LOGNAME") (getenv "USER")))
100        (:password)
101        (:mailbox "INBOX")
102        (:predicate "UNSEEN UNDELETED")))
103     "Mapping from keywords to default values.
104 All keywords that can be used must be listed here."))
105
106 (defvar mail-source-fetcher-alist
107   '((file mail-source-fetch-file)
108     (directory mail-source-fetch-directory)
109     (pop mail-source-fetch-pop)
110     (maildir mail-source-fetch-maildir)
111     (imap mail-source-fetch-imap))
112   "A mapping from source type to fetcher function.")
113
114 (defvar mail-source-password-cache nil)
115
116 ;;; Functions
117
118 (eval-and-compile
119   (defun mail-source-strip-keyword (keyword)
120   "Strip the leading colon off the KEYWORD."
121   (intern (substring (symbol-name keyword) 1))))
122
123 (eval-and-compile
124   (defun mail-source-bind-1 (type)
125     (let* ((defaults (cdr (assq type mail-source-keyword-map)))
126            default bind)
127       (while (setq default (pop defaults))
128         (push (list (mail-source-strip-keyword (car default))
129                     nil)
130               bind))
131       bind)))
132
133 (defmacro mail-source-bind (type-source &rest body)
134   "Return a `let' form that binds all variables in source TYPE.
135 TYPE-SOURCE is a list where the first element is the TYPE, and
136 the second variable is the SOURCE.
137 At run time, the mail source specifier SOURCE will be inspected,
138 and the variables will be set according to it.  Variables not
139 specified will be given default values.
140
141 After this is done, BODY will be executed in the scope
142 of the `let' form.
143
144 The variables bound and their default values are described by
145 the `mail-source-keyword-map' variable."
146   `(let ,(mail-source-bind-1 (car type-source))
147      (mail-source-set-1 ,(cadr type-source))
148      ,@body))
149
150 (put 'mail-source-bind 'lisp-indent-function 1)
151 (put 'mail-source-bind 'edebug-form-spec '(form body))
152
153 (defun mail-source-set-1 (source)
154   (let* ((type (pop source))
155          (defaults (cdr (assq type mail-source-keyword-map)))
156          default value keyword)
157     (while (setq default (pop defaults))
158       (set (mail-source-strip-keyword (setq keyword (car default)))
159            (if (setq value (plist-get source keyword))
160                (mail-source-value value)
161              (mail-source-value (cadr default)))))))
162
163 (defun mail-source-value (value)
164   "Return the value of VALUE."
165   (cond
166    ;; String
167    ((stringp value)
168     value)
169    ;; Function
170    ((and (listp value)
171          (functionp (car value)))
172     (eval value))
173    ;; Just return the value.
174    (t
175     value)))
176
177 (defun mail-source-fetch (source callback)
178   "Fetch mail from SOURCE and call CALLBACK zero or more times.
179 CALLBACK will be called with the name of the file where (some of)
180 the mail from SOURCE is put.
181 Return the number of files that were found."
182   (save-excursion
183     (let ((function (cadr (assq (car source) mail-source-fetcher-alist)))
184           (found 0))
185       (unless function
186         (error "%S is an invalid mail source specification" source))
187       ;; If there's anything in the crash box, we do it first.
188       (when (file-exists-p mail-source-crash-box)
189         (message "Processing mail from %s..." mail-source-crash-box)
190         (setq found (mail-source-callback
191                      callback mail-source-crash-box)))
192       (+ found
193          (condition-case err
194              (funcall function source callback)
195            (error
196             (unless (yes-or-no-p
197                      (format "Mail source error (%s).  Continue? " err))
198               (error "Cannot get new mail."))
199             0))))))
200
201 (defun mail-source-make-complex-temp-name (prefix)
202   (let ((newname (make-temp-name prefix))
203         (newprefix prefix))
204     (while (file-exists-p newname)
205       (setq newprefix (concat newprefix "x"))
206       (setq newname (make-temp-name newprefix)))
207     newname))
208
209 (defun mail-source-callback (callback info)
210   "Call CALLBACK on the mail file, and then remove the mail file.
211 Pass INFO on to CALLBACK."
212   (if (or (not (file-exists-p mail-source-crash-box))
213           (zerop (nth 7 (file-attributes mail-source-crash-box))))
214       (progn
215         (when (file-exists-p mail-source-crash-box)
216           (delete-file mail-source-crash-box))
217         0)
218     (prog1
219         (funcall callback mail-source-crash-box info)
220       (when (file-exists-p mail-source-crash-box)
221         ;; Delete or move the incoming mail out of the way.
222         (if mail-source-delete-incoming
223             (delete-file mail-source-crash-box)
224           (let ((incoming
225                  (mail-source-make-complex-temp-name
226                   (expand-file-name
227                    "Incoming" mail-source-directory))))
228             (unless (file-exists-p (file-name-directory incoming))
229               (make-directory (file-name-directory incoming) t))
230             (rename-file mail-source-crash-box incoming t)))))))
231
232 (defun mail-source-movemail (from to)
233   "Move FROM to TO using movemail."
234   (if (not (file-writable-p to))
235       (error "Can't write to crash box %s.  Not moving mail" to)
236     (let ((to (file-truename (expand-file-name to)))
237           errors result)
238       (setq to (file-truename to)
239             from (file-truename from))
240       ;; Set TO if have not already done so, and rename or copy
241       ;; the file FROM to TO if and as appropriate.
242       (cond
243        ((file-exists-p to)
244         ;; The crash box exists already.
245         t)
246        ((not (file-exists-p from))
247         ;; There is no inbox.
248         (setq to nil))
249        ((zerop (nth 7 (file-attributes from)))
250         ;; Empty file.
251         (setq to nil))
252        (t
253         ;; If getting from mail spool directory, use movemail to move
254         ;; rather than just renaming, so as to interlock with the
255         ;; mailer.
256         (unwind-protect
257             (save-excursion
258               (setq errors (generate-new-buffer " *mail source loss*"))
259               (let ((default-directory "/"))
260                 (setq result
261                       (apply
262                        'call-process
263                        (append
264                         (list
265                          (expand-file-name "movemail" exec-directory)
266                          nil errors nil from to)))))
267               (when (file-exists-p to)
268                 (set-file-modes to mail-source-default-file-modes))
269               (if (and (not (buffer-modified-p errors))
270                        (zerop result))
271                   ;; No output => movemail won.
272                   t
273                 (set-buffer errors)
274                 ;; There may be a warning about older revisions.  We
275                 ;; ignore that.
276                 (goto-char (point-min))
277                 (if (search-forward "older revision" nil t)
278                     t
279                   ;; Probably a real error.
280                   (subst-char-in-region (point-min) (point-max) ?\n ?\  )
281                   (goto-char (point-max))
282                   (skip-chars-backward " \t")
283                   (delete-region (point) (point-max))
284                   (goto-char (point-min))
285                   (when (looking-at "movemail: ")
286                     (delete-region (point-min) (match-end 0)))
287                   (unless (yes-or-no-p
288                            (format "movemail: %s (%d return).  Continue? "
289                                    (buffer-string) result))
290                     (error "%s" (buffer-string)))
291                   (setq to nil)))))))
292       (when (and errors
293                  (buffer-name errors))
294         (kill-buffer errors))
295       ;; Return whether we moved successfully or not.
296       to)))
297
298 (defun mail-source-movemail-and-remove (from to)
299   "Move FROM to TO using movemail, then remove FROM if empty."
300   (or (not (mail-source-movemail from to))
301       (not (zerop (nth 7 (file-attributes from))))
302       (delete-file from)))
303
304 (defvar mail-source-read-passwd nil)
305 (defun mail-source-read-passwd (prompt &rest args)
306   "Read a password using PROMPT.
307 If ARGS, PROMPT is used as an argument to `format'."
308   (let ((prompt
309          (if args
310              (apply 'format prompt args)
311            prompt)))
312     (unless mail-source-read-passwd
313       (if (or (fboundp 'read-passwd) (load "passwd" t))
314           (setq mail-source-read-passwd 'read-passwd)
315         (unless (fboundp 'ange-ftp-read-passwd)
316           (autoload 'ange-ftp-read-passwd "ange-ftp"))
317         (setq mail-source-read-passwd 'ange-ftp-read-passwd)))
318     (funcall mail-source-read-passwd prompt)))
319
320 (defun mail-source-fetch-with-program (program)
321   (zerop (call-process shell-file-name nil nil nil
322                        shell-command-switch program)))
323
324 (defun mail-source-run-script (script spec &optional delay)
325   (when script
326     (if (and (symbolp script) (fboundp script))
327         (funcall script)
328       (mail-source-call-script
329        (format-spec script spec))))
330   (when delay
331     (sleep-for delay)))
332
333 (defun mail-source-call-script (script)
334   (let ((background nil))
335     (when (string-match "& *$" script)
336       (setq script (substring script 0 (match-beginning 0))
337             background 0))
338     (call-process shell-file-name nil background nil
339                   shell-command-switch script)))
340
341 ;;;
342 ;;; Different fetchers
343 ;;;
344
345 (defun mail-source-fetch-file (source callback)
346   "Fetcher for single-file sources."
347   (mail-source-bind (file source)
348     (mail-source-run-script
349      prescript (format-spec-make ?t mail-source-crash-box)
350      prescript-delay)
351     (let ((mail-source-string (format "file:%s" path)))
352       (if (mail-source-movemail path mail-source-crash-box)
353           (prog1
354               (mail-source-callback callback path)
355             (mail-source-run-script
356              postscript (format-spec-make ?t mail-source-crash-box)))
357         0))))
358
359 (defun mail-source-fetch-directory (source callback)
360   "Fetcher for directory sources."
361   (mail-source-bind (directory source)
362     (let ((found 0)
363           (mail-source-string (format "directory:%s" path)))
364       (dolist (file (directory-files
365                      path t (concat (regexp-quote suffix) "$")))
366         (when (and (file-regular-p file)
367                    (funcall predicate file)
368                    (mail-source-movemail file mail-source-crash-box))
369           (incf found (mail-source-callback callback file))))
370       found)))
371
372 (defun mail-source-fetch-pop (source callback)
373   "Fetcher for single-file sources."
374   (mail-source-bind (pop source)
375     (mail-source-run-script
376      prescript
377      (format-spec-make ?p password ?t mail-source-crash-box
378                                       ?s server ?P port ?u user)
379      prescript-delay)
380     (let ((from (format "%s:%s:%s" server user port))
381           (mail-source-string (format "pop:%s@%s" user server))
382           result)
383       (when (eq authentication 'password)
384         (setq password
385               (or password
386                   (cdr (assoc from mail-source-password-cache))
387                   (mail-source-read-passwd
388                    (format "Password for %s at %s: " user server)))))
389       (when server
390         (setenv "MAILHOST" server))
391       (setq result
392             (cond
393              (program
394               (mail-source-fetch-with-program
395                (format-spec
396                 program
397                 (format-spec-make ?p password ?t mail-source-crash-box
398                                   ?s server ?P port ?u user))))
399              (function
400               (funcall function mail-source-crash-box))
401              ;; The default is to use pop3.el.
402              (t
403               (let ((pop3-password password)
404                     (pop3-maildrop user)
405                     (pop3-mailhost server)
406                     (pop3-port port)
407                     (pop3-authentication-scheme
408                      (if (eq authentication 'apop) 'apop 'pass)))
409                 (save-excursion (pop3-movemail mail-source-crash-box))))))
410       (if result
411           (progn
412             (when (eq authentication 'password)
413               (unless (assoc from mail-source-password-cache)
414                 (push (cons from password) mail-source-password-cache)))
415             (prog1
416                 (mail-source-callback callback server)
417               (mail-source-run-script
418                postscript
419                (format-spec-make ?p password ?t mail-source-crash-box
420                                  ?s server ?P port ?u user))))
421         ;; We nix out the password in case the error
422         ;; was because of a wrong password being given.
423         (setq mail-source-password-cache
424               (delq (assoc from mail-source-password-cache)
425                     mail-source-password-cache))
426         0))))
427
428 (defun mail-source-fetch-maildir (source callback)
429   "Fetcher for maildir sources."
430   (mail-source-bind (maildir source)
431     (let ((found 0)
432           (mail-source-string (format "maildir:%s" path)))
433       (dolist (file (directory-files path t))
434         (when (and (file-regular-p file)
435                    (not (if function
436                             (funcall function file mail-source-crash-box)
437                           (rename-file file mail-source-crash-box))))
438           (incf found (mail-source-callback callback file))))
439       found)))
440
441 (eval-and-compile
442   (autoload 'imap-open "imap")
443   (autoload 'imap-authenticate "imap")
444   (autoload 'imap-mailbox-select "imap")
445   (autoload 'imap-search "imap")
446   (autoload 'imap-fetch "imap")
447   (autoload 'imap-mailbox-unselect "imap")
448   (autoload 'imap-close "imap")
449   (autoload 'imap-error-text "imap")
450   (autoload 'nnheader-ms-strip-cr "nnheader"))
451
452 (defun mail-source-fetch-imap (source callback)
453   "Fetcher for imap sources."
454   (mail-source-bind (imap source)
455     (let ((found 0)
456           (buf (get-buffer-create (generate-new-buffer-name " *imap source*")))
457           (mail-source-string (format "imap:%s:%s" server mailbox)))
458       (if (and (imap-open server port stream authentication buf)
459                (imap-authenticate user password buf)
460                (imap-mailbox-select mailbox nil buf))
461           (let (str (coding-system-for-write 'binary))
462             (with-temp-file mail-source-crash-box
463               ;; if predicate is nil, use all uids
464               (dolist (uid (imap-search (or predicate "1:*") buf))
465                 (when (setq str (imap-fetch uid "RFC822" 'RFC822 nil buf))
466                   (insert "From imap " (current-time-string) "\n")
467                   (save-excursion
468                     (insert str "\n\n"))
469                   (while (re-search-forward "^From " nil t)
470                     (replace-match ">From "))
471                   (goto-char (point-max))))
472               (nnheader-ms-strip-cr))
473             (incf found (mail-source-callback callback server))
474             (imap-mailbox-unselect buf)
475             (imap-close buf))
476         (imap-close buf)
477         (error (imap-error-text buf)))
478       (kill-buffer buf)
479       found)))
480
481 (provide 'mail-source)
482
483 ;;; mail-source.el ends here