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