Upteda. sasl-mech anonymous
[elisp/flim.git] / smtp.el
1 ;;; smtp.el --- basic functions to send mail with SMTP server
2
3 ;; Copyright (C) 1995, 1996, 1998, 1999 Free Software Foundation, Inc.
4
5 ;; Author: Tomoji Kagatani <kagatani@rbc.ncl.omron.co.jp>
6 ;;      Simon Leinen <simon@switch.ch> (ESMTP support)
7 ;;      Shuhei KOBAYASHI <shuhei@aqua.ocn.ne.jp>
8 ;;      Kenichi OKADA <okada@opaopa.org> (SASL support)
9 ;;      Daiki Ueno <ueno@ueda.info.waseda.ac.jp>
10 ;; Maintainer: Kenichi OKADA <okada@opaopa.org>
11 ;; Keywords: SMTP, mail, SASL
12
13 ;; This file is part of FLIM (Faithful Library about Internet Message).
14
15 ;; This program is free software; you can redistribute it and/or
16 ;; modify it under the terms of the GNU General Public License as
17 ;; published by the Free Software Foundation; either version 2, or (at
18 ;; your option) any later version.
19
20 ;; This program is distributed in the hope that it will be useful, but
21 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
23 ;; General Public License for more details.
24
25 ;; You should have received a copy of the GNU General Public License
26 ;; along with this program; see the file COPYING.  If not, write to the
27 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
28 ;; Boston, MA 02111-1307, USA.
29
30 ;;; Code:
31
32 (require 'poe)
33 (require 'poem)
34 (require 'pcustom)
35 (require 'mail-utils)                   ; mail-strip-quoted-names
36
37 (eval-and-compile
38   (autoload 'starttls-open-stream "starttls")
39   (autoload 'starttls-negotiate "starttls")
40   (autoload 'sasl-cram-md5 "sasl")
41   (autoload 'sasl-plain "sasl"))
42                        
43 (eval-when-compile (require 'cl))       ; push
44
45 (defgroup smtp nil
46   "SMTP protocol for sending mail."
47   :group 'mail)
48
49 (defcustom smtp-default-server nil
50   "*Specify default SMTP server."
51   :type '(choice (const nil) string)
52   :group 'smtp)
53
54 (defcustom smtp-server (or (getenv "SMTPSERVER") smtp-default-server)
55   "*The name of the host running SMTP server.  It can also be a function
56 called from `smtp-via-smtp' with arguments SENDER and RECIPIENTS."
57   :type '(choice (string :tag "Name")
58                  (function :tag "Function"))
59   :group 'smtp)
60
61 (defcustom smtp-service "smtp"
62   "*SMTP service port number. \"smtp\" or 25."
63   :type '(choice (integer :tag "25" 25)
64                  (string :tag "smtp" "smtp"))
65   :group 'smtp)
66
67 (defcustom smtp-use-8bitmime t
68   "*If non-nil, use ESMTP 8BITMIME if available."
69   :type 'boolean
70   :group 'smtp)
71
72 (defcustom smtp-local-domain nil
73   "*Local domain name without a host name.
74 If the function (system-name) returns the full internet address,
75 don't define this value."
76   :type '(choice (const nil) string)
77   :group 'smtp)
78
79 (defcustom smtp-debug-info nil
80   "*smtp debug info printout. messages and process buffer."
81   :type 'boolean
82   :group 'smtp)
83
84 (defcustom smtp-notify-success nil
85   "*If non-nil, notification for successful mail delivery is returned 
86  to user (RFC1891)."
87   :type 'boolean
88   :group 'smtp)
89
90 (defcustom smtp-authentication-type nil
91   "*SMTP authentication mechanism (RFC2554)."
92   :type 'symbol
93   :group 'smtp)
94
95 (defvar smtp-authentication-user nil)
96 (defvar smtp-authentication-passphrase nil)
97
98 (defvar smtp-authentication-method-alist
99   '((cram-md5 smtp-auth-cram-md5)
100     (plain smtp-auth-plain)
101     (login smtp-auth-login)
102     (anonymous smtp-auth-anonymous)
103     ))
104
105 (defcustom smtp-connection-type nil
106   "*SMTP connection type."
107   :type '(choice (const nil) (const :tag "TLS" starttls))
108   :group 'smtp)
109  
110 (defvar smtp-read-point nil)
111
112 (defun smtp-make-fqdn ()
113   "Return user's fully qualified domain name."
114   (let ((system-name (system-name)))
115     (cond
116      (smtp-local-domain
117       (concat system-name "." smtp-local-domain))
118      ((string-match "[^.]\\.[^.]" system-name)
119       system-name)
120      (t
121       (error "Cannot generate valid FQDN. Set `smtp-local-domain' correctly.")))))
122
123 (defun smtp-via-smtp (sender recipients smtp-text-buffer)
124   (let ((server (if (functionp smtp-server)
125                     (funcall smtp-server sender recipients)
126                   smtp-server))
127         process response extensions)
128     (save-excursion
129       (set-buffer
130        (get-buffer-create
131         (format "*trace of SMTP session to %s*" server)))
132       (erase-buffer)
133       (make-local-variable 'smtp-read-point)
134       (setq smtp-read-point (point-min))
135
136       (unwind-protect
137           (catch 'done
138             (setq process 
139                   (if smtp-connection-type
140                       (starttls-open-stream
141                        "SMTP" (current-buffer) server smtp-service)
142                     (open-network-stream-as-binary
143                      "SMTP" (current-buffer) server smtp-service)))
144             (or process (throw 'done nil))
145
146             (set-process-filter process 'smtp-process-filter)
147
148             (if (eq smtp-connection-type 'force)
149                 (starttls-negotiate process))
150
151             ;; Greeting
152             (setq response (smtp-read-response process))
153             (if (or (null (car response))
154                     (not (integerp (car response)))
155                     (>= (car response) 400))
156                 (throw 'done (car (cdr response))))
157
158             ;; EHLO
159             (smtp-send-command process
160                                (format "EHLO %s" (smtp-make-fqdn)))
161             (setq response (smtp-read-response process))
162             (if (or (null (car response))
163                     (not (integerp (car response)))
164                     (>= (car response) 400))
165                 (progn
166                   ;; HELO
167                   (smtp-send-command process
168                                      (format "HELO %s" (smtp-make-fqdn)))
169                   (setq response (smtp-read-response process))
170                   (if (or (null (car response))
171                           (not (integerp (car response)))
172                           (>= (car response) 400))
173                       (throw 'done (car (cdr response)))))
174               (let ((extension-lines (cdr (cdr response)))
175                     extension)
176                 (while extension-lines
177                   (if (string-match
178                        "^auth "
179                        (setq extension
180                              (downcase (substring (car extension-lines) 4))))
181                       (while (string-match "\\([^ ]+\\)" extension (match-end 1))
182                         (push (intern (match-string 1 extension)) extensions))
183                     (push (intern extension) extensions))
184                   (setq extension-lines (cdr extension-lines)))))
185
186             ;; STARTTLS --- begin a TLS negotiation (RFC 2595)
187             (when (and smtp-connection-type 
188                        (null (eq smtp-connection-type 'force))
189                        (memq 'starttls extensions))
190               (smtp-send-command process "STARTTLS")
191               (setq response (smtp-read-response process))
192               (if (or (null (car response))
193                       (not (integerp (car response)))
194                       (>= (car response) 400))
195                   (throw 'done (car (cdr response))))
196               (starttls-negotiate process))
197
198             ;; AUTH --- SMTP Service Extension for Authentication (RFC2554)
199             (when smtp-authentication-type
200               (let ((auth (intern smtp-authentication-type)) method)
201                 (if (and 
202                      (memq auth extensions)
203                      (setq method (nth 1 (assq auth smtp-authentication-method-alist))))
204                     (funcall method process)
205                   (throw 'smtp-error
206                          (format "AUTH mechanism %s not available" auth)))))
207
208             ;; ONEX --- One message transaction only (sendmail extension?)
209             (if (or (memq 'onex extensions)
210                     (memq 'xone extensions))
211                 (progn
212                   (smtp-send-command process "ONEX")
213                   (setq response (smtp-read-response process))
214                   (if (or (null (car response))
215                           (not (integerp (car response)))
216                           (>= (car response) 400))
217                       (throw 'done (car (cdr response))))))
218
219             ;; VERB --- Verbose (sendmail extension?)
220             (if (and smtp-debug-info
221                      (or (memq 'verb extensions)
222                          (memq 'xvrb extensions)))
223                 (progn
224                   (smtp-send-command process "VERB")
225                   (setq response (smtp-read-response process))
226                   (if (or (null (car response))
227                           (not (integerp (car response)))
228                           (>= (car response) 400))
229                       (throw 'done (car (cdr response))))))
230
231             ;; XUSR --- Initial (user) submission (sendmail extension?)
232             (if (memq 'xusr extensions)
233                 (progn
234                   (smtp-send-command process "XUSR")
235                   (setq response (smtp-read-response process))
236                   (if (or (null (car response))
237                           (not (integerp (car response)))
238                           (>= (car response) 400))
239                       (throw 'done (car (cdr response))))))
240
241             ;; MAIL FROM:<sender>
242             (smtp-send-command
243              process
244              (format "MAIL FROM:<%s>%s%s"
245                      sender
246                      ;; SIZE --- Message Size Declaration (RFC1870)
247                      (if (memq 'size extensions)
248                          (format " SIZE=%d"
249                                  (save-excursion
250                                    (set-buffer smtp-text-buffer)
251                                    (+ (- (point-max) (point-min))
252                                       ;; Add one byte for each change-of-line
253                                       ;; because or CR-LF representation:
254                                       (count-lines (point-min) (point-max))
255                                       ;; For some reason, an empty line is
256                                       ;; added to the message.  Maybe this
257                                       ;; is a bug, but it can't hurt to add
258                                       ;; those two bytes anyway:
259                                       2)))
260                        "")
261                      ;; 8BITMIME --- 8bit-MIMEtransport (RFC1652)
262                      (if (and (memq '8bitmime extensions)
263                               smtp-use-8bitmime)
264                          " BODY=8BITMIME"
265                        "")))
266             (setq response (smtp-read-response process))
267             (if (or (null (car response))
268                     (not (integerp (car response)))
269                     (>= (car response) 400))
270                 (throw 'done (car (cdr response))))
271
272             ;; RCPT TO:<recipient>
273             (while recipients
274               (smtp-send-command process
275                                  (format
276                                   (if smtp-notify-success
277                                       "RCPT TO:<%s> NOTIFY=SUCCESS" 
278                                     "RCPT TO:<%s>")
279                                   (car recipients)))
280               (setq recipients (cdr recipients))
281               (setq response (smtp-read-response process))
282               (if (or (null (car response))
283                       (not (integerp (car response)))
284                       (>= (car response) 400))
285                   (throw 'done (car (cdr response)))))
286
287             ;; DATA
288             (smtp-send-command process "DATA")
289             (setq response (smtp-read-response process))
290             (if (or (null (car response))
291                     (not (integerp (car response)))
292                     (>= (car response) 400))
293                 (throw 'done (car (cdr response))))
294
295             ;; Mail contents
296             (smtp-send-data process smtp-text-buffer)
297
298             ;; DATA end "."
299             (smtp-send-command process ".")
300             (setq response (smtp-read-response process))
301             (if (or (null (car response))
302                     (not (integerp (car response)))
303                     (>= (car response) 400))
304                 (throw 'done (car (cdr response))))
305
306             t)
307
308         (if (and process
309                  (eq (process-status process) 'open))
310             (progn
311               ;; QUIT
312               (smtp-send-command process "QUIT")
313               (smtp-read-response process)
314               (delete-process process)))))))
315
316 (defun smtp-process-filter (process output)
317   (save-excursion
318     (set-buffer (process-buffer process))
319     (goto-char (point-max))
320     (insert output)))
321
322 (defun smtp-read-response (process)
323   (let ((case-fold-search nil)
324         (response-strings nil)
325         (response-continue t)
326         (return-value '(nil ()))
327         match-end)
328
329     (while response-continue
330       (goto-char smtp-read-point)
331       (while (not (search-forward "\r\n" nil t))
332         (accept-process-output process)
333         (goto-char smtp-read-point))
334
335       (setq match-end (point))
336       (setq response-strings
337             (cons (buffer-substring smtp-read-point (- match-end 2))
338                   response-strings))
339         
340       (goto-char smtp-read-point)
341       (if (looking-at "[0-9]+ ")
342           (let ((begin (match-beginning 0))
343                 (end (match-end 0)))
344             (if smtp-debug-info
345                 (message "%s" (car response-strings)))
346
347             (setq smtp-read-point match-end)
348
349             ;; ignore lines that start with "0"
350             (if (looking-at "0[0-9]+ ")
351                 nil
352               (setq response-continue nil)
353               (setq return-value
354                     (cons (string-to-int
355                            (buffer-substring begin end))
356                           (nreverse response-strings)))))
357         
358         (if (looking-at "[0-9]+-")
359             (progn (if smtp-debug-info
360                      (message "%s" (car response-strings)))
361                    (setq smtp-read-point match-end)
362                    (setq response-continue t))
363           (progn
364             (setq smtp-read-point match-end)
365             (setq response-continue nil)
366             (setq return-value
367                   (cons nil (nreverse response-strings)))))))
368     (setq smtp-read-point match-end)
369     return-value))
370
371 (defun smtp-send-command (process command)
372   (goto-char (point-max))
373   (insert command "\r\n")
374   (setq smtp-read-point (point))
375   (process-send-string process command)
376   (process-send-string process "\r\n"))
377
378 (defun smtp-send-data-1 (process data)
379   (goto-char (point-max))
380   (if smtp-debug-info
381       (insert data "\r\n"))
382   (setq smtp-read-point (point))
383   ;; Escape "." at start of a line.
384   (if (eq (string-to-char data) ?.)
385       (process-send-string process "."))
386   (process-send-string process data)
387   (process-send-string process "\r\n"))
388
389 (defun smtp-send-data (process buffer)
390   (let ((data-continue t)
391         (sending-data nil)
392         this-line
393         this-line-end)
394
395     (save-excursion
396       (set-buffer buffer)
397       (goto-char (point-min)))
398
399     (while data-continue
400       (save-excursion
401         (set-buffer buffer)
402         (beginning-of-line)
403         (setq this-line (point))
404         (end-of-line)
405         (setq this-line-end (point))
406         (setq sending-data nil)
407         (setq sending-data (buffer-substring this-line this-line-end))
408         (if (or (/= (forward-line 1) 0) (eobp))
409             (setq data-continue nil)))
410
411       (smtp-send-data-1 process sending-data))))
412
413 (defun smtp-deduce-address-list (smtp-text-buffer header-start header-end)
414   "Get address list suitable for smtp RCPT TO:<address>."
415   (let ((case-fold-search t)
416         (simple-address-list "")
417         this-line
418         this-line-end
419         addr-regexp
420         (smtp-address-buffer (generate-new-buffer " *smtp-mail*")))
421     (unwind-protect
422         (save-excursion
423           ;;
424           (set-buffer smtp-address-buffer)
425           (erase-buffer)
426           (insert (save-excursion
427                     (set-buffer smtp-text-buffer)
428                     (buffer-substring-no-properties header-start header-end)))
429           (goto-char (point-min))
430           ;; RESENT-* fields should stop processing of regular fields.
431           (save-excursion
432             (if (re-search-forward "^RESENT-TO:" header-end t)
433                 (setq addr-regexp
434                       "^\\(RESENT-TO:\\|RESENT-CC:\\|RESENT-BCC:\\)")
435               (setq addr-regexp  "^\\(TO:\\|CC:\\|BCC:\\)")))
436
437           (while (re-search-forward addr-regexp header-end t)
438             (replace-match "")
439             (setq this-line (match-beginning 0))
440             (forward-line 1)
441             ;; get any continuation lines.
442             (while (and (looking-at "^[ \t]+") (< (point) header-end))
443               (forward-line 1))
444             (setq this-line-end (point-marker))
445             (setq simple-address-list
446                   (concat simple-address-list " "
447                           (mail-strip-quoted-names
448                            (buffer-substring this-line this-line-end)))))
449           (erase-buffer)
450           (insert-string " ")
451           (insert-string simple-address-list)
452           (insert-string "\n")
453           ;; newline --> blank
454           (subst-char-in-region (point-min) (point-max) 10 ?  t)
455           ;; comma   --> blank
456           (subst-char-in-region (point-min) (point-max) ?, ?  t)
457           ;; tab     --> blank
458           (subst-char-in-region (point-min) (point-max)  9 ?  t)
459
460           (goto-char (point-min))
461           ;; tidyness in case hook is not robust when it looks at this
462           (while (re-search-forward "[ \t]+" header-end t) (replace-match " "))
463
464           (goto-char (point-min))
465           (let (recipient-address-list)
466             (while (re-search-forward " \\([^ ]+\\) " (point-max) t)
467               (backward-char 1)
468               (setq recipient-address-list
469                     (cons (buffer-substring (match-beginning 1) (match-end 1))
470                           recipient-address-list)))
471             recipient-address-list))
472       (kill-buffer smtp-address-buffer))))
473
474 (defun smtp-auth-cram-md5 (process)
475   (let ((secure-word (copy-sequence smtp-authentication-passphrase))
476         response)
477     (smtp-send-command process "AUTH CRAM-MD5")
478     (setq response (smtp-read-response process))
479     (if (or (null (car response))
480             (not (integerp (car response)))
481             (>= (car response) 400))
482         (throw 'done (car (cdr response))))
483     (smtp-send-command
484      process
485      (setq secure-word (unwind-protect
486                            (sasl-cram-md5
487                             smtp-authentication-user secure-word
488                             (base64-decode-string
489                              (substring (car (cdr response)) 4)))
490                          (fillarray secure-word 0))
491            secure-word (unwind-protect
492                            (base64-encode-string secure-word)
493                          (fillarray secure-word 0))))
494     (fillarray secure-word 0)
495     (setq response (smtp-read-response process))
496     (if (or (null (car response))
497             (not (integerp (car response)))
498             (>= (car response) 400))
499         (throw 'done (car (cdr response))))))
500  
501 (defun smtp-auth-plain (process)
502   (let ((secure-word (copy-sequence smtp-authentication-passphrase))
503         response)
504     (smtp-send-command
505      process
506      (setq secure-word (unwind-protect
507                            (sasl-plain "" smtp-authentication-user secure-word)
508                          (fillarray secure-word 0))
509            secure-word (unwind-protect
510                            (base64-encode-string secure-word)
511                          (fillarray secure-word 0))
512            secure-word (unwind-protect
513                            (concat "AUTH PLAIN " secure-word)
514                          (fillarray secure-word 0))))
515     (fillarray secure-word 0)
516     (setq response (smtp-read-response process))
517     (if (or (null (car response))
518             (not (integerp (car response)))
519             (>= (car response) 400))
520         (throw 'done (car (cdr response))))))
521
522 (defun smtp-auth-login (process)
523   (let ((secure-word (copy-sequence smtp-authentication-passphrase))
524         response)
525     (smtp-send-command
526      process
527      (concat "AUTH LOGIN " smtp-authentication-user))
528     (setq response (smtp-read-response process))
529     (if (or (null (car response))
530             (not (integerp (car response)))
531             (>= (car response) 400))
532         (throw 'done (car (cdr response))))
533     (smtp-send-command
534      process
535      (setq secure-word (unwind-protect
536                            (base64-encode-string secure-word)
537                          (fillarray secure-word 0))))
538     (fillarray secure-word 0)
539     (setq response (smtp-read-response process))
540     (if (or (null (car response))
541             (not (integerp (car response)))
542             (>= (car response) 400))
543         (throw 'done (car (cdr response))))))
544
545 (defun smtp-auth-anonymous (process &optional token)
546   (let (response)
547     (smtp-send-command
548      process "AUTH ANONYMOUS")
549     (setq response (smtp-read-response process))
550     (if (or (null (car response))
551             (not (integerp (car response)))
552             (>= (car response) 400))
553         (throw 'done (car (cdr response))))
554     (smtp-send-command process
555                        (base64-encode-string 
556                         (or token
557                             user-mail-address
558                             "")))
559     (setq response (smtp-read-response process))
560     (if (or (null (car response))
561             (not (integerp (car response)))
562             (>= (car response) 400))
563         (throw 'done (car (cdr response))))))
564  
565 (provide 'smtp)
566
567 ;;; smtp.el ends here