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