(gnus-revision-number): Increment to 03.
[elisp/gnus.git-] / 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        (:connection)
91        (:authentication password))
92       (maildir
93        (:path "~/Maildir/new/")))
94     "Mapping from keywords to default values.
95 All keywords that can be used must be listed here."))
96
97 (defvar mail-source-fetcher-alist
98   '((file mail-source-fetch-file)
99     (directory mail-source-fetch-directory)
100     (pop mail-source-fetch-pop)
101     (maildir mail-source-fetch-maildir))
102   "A mapping from source type to fetcher function.")
103
104 (defvar mail-source-password-cache nil)
105
106 ;;; Functions
107
108 (eval-and-compile
109   (defun mail-source-strip-keyword (keyword)
110   "Strip the leading colon off the KEYWORD."
111   (intern (substring (symbol-name keyword) 1))))
112
113 (eval-and-compile
114   (defun mail-source-bind-1 (type)
115     (let* ((defaults (cdr (assq type mail-source-keyword-map)))
116            default bind)
117       (while (setq default (pop defaults))
118         (push (list (mail-source-strip-keyword (car default))
119                     nil)
120               bind))
121       bind)))
122
123 (defmacro mail-source-bind (type-source &rest body)
124   "Return a `let' form that binds all variables in source TYPE.
125 TYPE-SOURCE is a list where the first element is the TYPE, and
126 the second variable is the SOURCE.
127 At run time, the mail source specifier SOURCE will be inspected,
128 and the variables will be set according to it.  Variables not
129 specified will be given default values.
130
131 After this is done, BODY will be executed in the scope
132 of the `let' form.
133
134 The variables bound and their default values are described by
135 the `mail-source-keyword-map' variable."
136   `(let ,(mail-source-bind-1 (car type-source))
137      (mail-source-set-1 ,(cadr type-source))
138      ,@body))
139
140 (put 'mail-source-bind 'lisp-indent-function 1)
141 (put 'mail-source-bind 'edebug-form-spec '(form body))
142
143 (defun mail-source-set-1 (source)
144   (let* ((type (pop source))
145          (defaults (cdr (assq type mail-source-keyword-map)))
146          default value keyword)
147     (while (setq default (pop defaults))
148       (set (mail-source-strip-keyword (setq keyword (car default)))
149            (if (setq value (plist-get source keyword))
150                (mail-source-value value)
151              (mail-source-value (cadr default)))))))
152
153 (defun mail-source-value (value)
154   "Return the value of VALUE."
155   (cond
156    ;; String
157    ((stringp value)
158     value)
159    ;; Function
160    ((and (listp value)
161          (functionp (car value)))
162     (eval value))
163    ;; Just return the value.
164    (t
165     value)))
166
167 (defun mail-source-fetch (source callback)
168   "Fetch mail from SOURCE and call CALLBACK zero or more times.
169 CALLBACK will be called with the name of the file where (some of)
170 the mail from SOURCE is put.
171 Return the number of files that were found."
172   (save-excursion
173     (let ((function (cadr (assq (car source) mail-source-fetcher-alist)))
174           (found 0))
175       (unless function
176         (error "%S is an invalid mail source specification" source))
177       ;; If there's anything in the crash box, we do it first.
178       (when (file-exists-p mail-source-crash-box)
179         (message "Processing mail from %s..." mail-source-crash-box)
180         (setq found (mail-source-callback
181                      callback mail-source-crash-box)))
182       (+ found
183          (condition-case err
184              (funcall function source callback)
185            (error
186             (unless (yes-or-no-p
187                      (format "Mail source error (%s).  Continue? " err))
188               (error "Cannot get new mail."))
189             0))))))
190
191 (defun mail-source-make-complex-temp-name (prefix)
192   (let ((newname (make-temp-name prefix))
193         (newprefix prefix))
194     (while (file-exists-p newname)
195       (setq newprefix (concat newprefix "x"))
196       (setq newname (make-temp-name newprefix)))
197     newname))
198
199 (defun mail-source-callback (callback info)
200   "Call CALLBACK on the mail file, and then remove the mail file.
201 Pass INFO on to CALLBACK."
202   (if (or (not (file-exists-p mail-source-crash-box))
203           (zerop (nth 7 (file-attributes mail-source-crash-box))))
204       (progn
205         (when (file-exists-p mail-source-crash-box)
206           (delete-file mail-source-crash-box))
207         0)
208     (prog1
209         (funcall callback mail-source-crash-box info)
210       (when (file-exists-p mail-source-crash-box)
211         ;; Delete or move the incoming mail out of the way.
212         (if mail-source-delete-incoming
213             (delete-file mail-source-crash-box)
214           (let ((incoming
215                  (mail-source-make-complex-temp-name
216                   (expand-file-name
217                    "Incoming" mail-source-directory))))
218             (unless (file-exists-p (file-name-directory incoming))
219               (make-directory (file-name-directory incoming) t))
220             (rename-file mail-source-crash-box incoming t)))))))
221
222 (defun mail-source-movemail (from to)
223   "Move FROM to TO using movemail."
224   (if (not (file-writable-p to))
225       (error "Can't write to crash box %s.  Not moving mail" to)
226     (let ((to (file-truename (expand-file-name to)))
227           errors result)
228       (setq to (file-truename to)
229             from (file-truename from))
230       ;; Set TO if have not already done so, and rename or copy
231       ;; the file FROM to TO if and as appropriate.
232       (cond
233        ((file-exists-p to)
234         ;; The crash box exists already.
235         t)
236        ((not (file-exists-p from))
237         ;; There is no inbox.
238         (setq to nil))
239        ((zerop (nth 7 (file-attributes from)))
240         ;; Empty file.
241         (setq to nil))
242        (t
243         ;; If getting from mail spool directory, use movemail to move
244         ;; rather than just renaming, so as to interlock with the
245         ;; mailer.
246         (unwind-protect
247             (save-excursion
248               (setq errors (generate-new-buffer " *mail source loss*"))
249               (let ((default-directory "/"))
250                 (setq result
251                       (apply
252                        'call-process
253                        (append
254                         (list
255                          (expand-file-name "movemail" exec-directory)
256                          nil errors nil from to)))))
257               (when (file-exists-p to)
258                 (set-file-modes to mail-source-default-file-modes))
259               (if (and (not (buffer-modified-p errors))
260                        (zerop result))
261                   ;; No output => movemail won.
262                   t
263                 (set-buffer errors)
264                 ;; There may be a warning about older revisions.  We
265                 ;; ignore that.
266                 (goto-char (point-min))
267                 (if (search-forward "older revision" nil t)
268                     t
269                   ;; Probably a real error.
270                   (subst-char-in-region (point-min) (point-max) ?\n ?\  )
271                   (goto-char (point-max))
272                   (skip-chars-backward " \t")
273                   (delete-region (point) (point-max))
274                   (goto-char (point-min))
275                   (when (looking-at "movemail: ")
276                     (delete-region (point-min) (match-end 0)))
277                   (unless (yes-or-no-p
278                            (format "movemail: %s (%d return).  Continue? "
279                                    (buffer-string) result))
280                     (error "%s" (buffer-string)))
281                   (setq to nil)))))))
282       (when (and errors
283                  (buffer-name errors))
284         (kill-buffer errors))
285       ;; Return whether we moved successfully or not.
286       to)))
287
288 (defvar mail-source-read-passwd nil)
289 (defun mail-source-read-passwd (prompt &rest args)
290   "Read a password using PROMPT.
291 If ARGS, PROMPT is used as an argument to `format'."
292   (let ((prompt
293          (if args
294              (apply 'format prompt args)
295            prompt)))
296     (unless mail-source-read-passwd
297       (if (or (fboundp 'read-passwd) (load "passwd" t))
298           (setq mail-source-read-passwd 'read-passwd)
299         (unless (fboundp 'ange-ftp-read-passwd)
300           (autoload 'ange-ftp-read-passwd "ange-ftp"))
301         (setq mail-source-read-passwd 'ange-ftp-read-passwd)))
302     (funcall mail-source-read-passwd prompt)))
303
304 (defun mail-source-fetch-with-program (program)
305   (zerop (call-process shell-file-name nil nil nil
306                        shell-command-switch program)))
307
308 (defun mail-source-run-script (script spec &optional delay)
309   (when script
310     (if (and (symbolp script) (fboundp script))
311         (funcall script)
312       (mail-source-call-script
313        (format-spec script spec))))
314   (when delay
315     (sleep-for delay)))
316
317 (defun mail-source-call-script (script)
318   (let ((background nil))
319     (when (string-match "& *$" script)
320       (setq script (substring script 0 (match-beginning 0))
321             background 0))
322     (call-process shell-file-name nil background nil
323                   shell-command-switch script)))
324
325 ;;;
326 ;;; Different fetchers
327 ;;;
328
329 (defun mail-source-fetch-file (source callback)
330   "Fetcher for single-file sources."
331   (mail-source-bind (file source)
332     (mail-source-run-script
333      prescript (format-spec-make ?t mail-source-crash-box)
334      prescript-delay)
335     (let ((mail-source-string (format "file:%s" path)))
336       (if (mail-source-movemail path mail-source-crash-box)
337           (prog1
338               (mail-source-callback callback path)
339             (mail-source-run-script
340              postscript (format-spec-make ?t mail-source-crash-box)))
341         0))))
342
343 (defun mail-source-fetch-directory (source callback)
344   "Fetcher for directory sources."
345   (mail-source-bind (directory source)
346     (let ((found 0)
347           (mail-source-string (format "directory:%s" path)))
348       (dolist (file (directory-files
349                      path t (concat (regexp-quote suffix) "$")))
350         (when (and (file-regular-p file)
351                    (funcall predicate file)
352                    (mail-source-movemail file mail-source-crash-box))
353           (incf found (mail-source-callback callback file))))
354       found)))
355
356 (defun mail-source-fetch-pop (source callback)
357   "Fetcher for single-file sources."
358   (mail-source-bind (pop source)
359     (mail-source-run-script
360      prescript
361      (format-spec-make ?p password ?t mail-source-crash-box
362                                       ?s server ?P port ?u user)
363      prescript-delay)
364     (let ((from (format "%s:%s:%s" server user port))
365           (mail-source-string (format "pop:%s@%s" user server))
366           result)
367       (when (eq authentication 'password)
368         (setq password
369               (or password
370                   (cdr (assoc from mail-source-password-cache))
371                   (mail-source-read-passwd
372                    (format "Password for %s at %s: " user server))))
373         (unless (assoc from mail-source-password-cache)
374           (push (cons from password) mail-source-password-cache)))
375       (when server
376         (setenv "MAILHOST" server))
377       (setq result
378             (cond
379              (program
380               (mail-source-fetch-with-program
381                (format-spec
382                 program
383                 (format-spec-make ?p password ?t mail-source-crash-box
384                                   ?s server ?P port ?u user))))
385              (function
386               (funcall function mail-source-crash-box))
387              ;; The default is to use pop3.el.
388              (t
389               (let ((pop3-password password)
390                     (pop3-maildrop user)
391                     (pop3-mailhost server)
392                     (pop3-port port)
393                     (pop3-authentication-scheme
394                      (if (eq authentication 'apop) 'apop 'pass))
395                     (pop3-connection-type connection))
396                 (save-excursion (pop3-movemail mail-source-crash-box))))))
397       (if result
398           (prog1
399               (mail-source-callback callback server)
400             (mail-source-run-script
401              postscript
402              (format-spec-make ?p password ?t mail-source-crash-box
403                                ?s server ?P port ?u user)))
404         ;; We nix out the password in case the error
405         ;; was because of a wrong password being given.
406         (setq mail-source-password-cache
407               (delq (assoc from mail-source-password-cache)
408                     mail-source-password-cache))
409         0))))
410
411 (defun mail-source-fetch-maildir (source callback)
412   "Fetcher for maildir sources."
413   (mail-source-bind (maildir source)
414     (let ((found 0)
415           (mail-source-string (format "maildir:%s" path)))
416       (dolist (file (directory-files path t))
417         (when (and (file-regular-p file)
418                    (not (rename-file file mail-source-crash-box)))
419           (incf found (mail-source-callback callback file))))
420       found)))
421
422 (provide 'mail-source)
423
424 ;;; mail-source.el ends here