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