;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
+
+;;; Commentary:
+;;
+
;;; Code:
(require 'pces)
(require 'pcustom)
(require 'mail-utils) ; mail-strip-quoted-names
+(require 'sasl)
(defgroup smtp nil
"SMTP protocol for sending mail."
:group 'mail)
+(defgroup smtp-extensions nil
+ "SMTP service extensions (RFC1869)."
+ :group 'smtp)
+
(defcustom smtp-default-server nil
"Specify default SMTP server."
:type '(choice (const nil) string)
:group 'smtp)
(defcustom smtp-server (or (getenv "SMTPSERVER") smtp-default-server)
- "The name of the host running SMTP server. It can also be a function
+ "The name of the host running SMTP server.
+It can also be a function
called from `smtp-via-smtp' with arguments SENDER and RECIPIENTS."
:type '(choice (string :tag "Name")
(function :tag "Function"))
:group 'smtp)
(defcustom smtp-service "smtp"
- "SMTP service port number. \"smtp\" or 25."
+ "SMTP service port number. \"smtp\" or 25."
:type '(choice (integer :tag "25" 25)
(string :tag "smtp" "smtp"))
:group 'smtp)
:type '(choice (const nil) string)
:group 'smtp)
-(defvar smtp-open-connection-function (function open-network-stream))
+(defcustom smtp-use-8bitmime t
+ "If non-nil, use ESMTP 8BITMIME (RFC1652) if available."
+ :type 'boolean
+ :group 'smtp-extensions)
+
+(defcustom smtp-use-size t
+ "If non-nil, use ESMTP SIZE (RFC1870) if available."
+ :type 'boolean
+ :group 'smtp-extensions)
+
+(defcustom smtp-use-starttls nil
+ "If non-nil, use STARTTLS (RFC2595) if available."
+ :type 'boolean
+ :group 'smtp-extensions)
+
+(defcustom smtp-use-sasl nil
+ "If non-nil, use SMTP Authentication (RFC2554) if available."
+ :type 'boolean
+ :group 'smtp-extensions)
+
+(defcustom smtp-sasl-user-name (user-login-name)
+ "Identification to be used for authorization."
+ :type 'string
+ :group 'smtp-extensions)
+
+(defcustom smtp-sasl-properties nil
+ "Properties set to SASL client."
+ :type 'string
+ :group 'smtp-extensions)
+
+(defcustom smtp-sasl-mechanisms nil
+ "List of authentication mechanisms."
+ :type '(repeat string)
+ :group 'smtp-extensions)
+
+(defvar sasl-mechanisms)
+
+(defvar smtp-open-connection-function #'open-network-stream)
(defvar smtp-read-point nil)
(defvar smtp-connection-alist nil)
+(defvar smtp-submit-package-function #'smtp-submit-package)
+
;;; @ SMTP package structure
;;; A package contains a mail message, an envelope sender address,
;;; and one or more envelope recipient addresses. In ESMTP model
-;;; we should guarantee the hook methods to access the current sending package.
+;;; the current sending package should be guaranteed to be accessible
+;;; anywhere from the hook methods (or SMTP commands).
-(defmacro smtp-package-sender-internal (package)
+(defmacro smtp-package-sender (package)
+ "Return the sender of PACKAGE, a string."
`(aref ,package 0))
-(defmacro smtp-package-recipients-internal (package)
+(defmacro smtp-package-recipients (package)
+ "Return the recipients of PACKAGE, a list of strings."
`(aref ,package 1))
-(defmacro smtp-package-buffer-internal (package)
+(defmacro smtp-package-buffer (package)
+ "Return the data of PACKAGE, a buffer."
`(aref ,package 2))
(defmacro smtp-make-package (sender recipients buffer)
+ "Create a new package structure.
+A package is a unit of SMTP message
+SENDER specifies the package sender, a string.
+RECIPIENTS is a list of recipients.
+BUFFER may be a buffer or a buffer name which contains mail message."
`(vector ,sender ,recipients ,buffer))
+(defun smtp-package-buffer-size (package)
+ "Return the size of PACKAGE, an integer."
+ (save-excursion
+ (set-buffer (smtp-package-buffer package))
+ (let ((size
+ (+ (buffer-size)
+ ;; Add one byte for each change-of-line
+ ;; because or CR-LF representation:
+ (count-lines (point-min) (point-max))
+ ;; For some reason, an empty line is
+ ;; added to the message. Maybe this
+ ;; is a bug, but it can't hurt to add
+ ;; those two bytes anyway:
+ 2)))
+ (goto-char (point-min))
+ (while (re-search-forward "^\\." nil t)
+ (setq size (1+ size)))
+ size)))
+
;;; @ SMTP connection structure
-;;; We should take care of emulation for other network streams.
-;;; They are likely to be implemented with sub program and the function
-;;; `process-contact' returns process ID instead of `(HOST SERVICE)' pair.
+;;; We should consider the function `open-network-stream' is a emulation
+;;; for another network stream. They are likely to be implemented with an
+;;; external program and the function `process-contact' returns the
+;;; process id instead of `(HOST SERVICE)' pair.
-(defmacro smtp-connection-process-internal (connection)
+(defmacro smtp-connection-process (connection)
+ "Return the subprocess-object of CONNECTION."
`(aref ,connection 0))
-(defmacro smtp-connection-server-internal (connection)
+(defmacro smtp-connection-server (connection)
+ "Return the server of CONNECTION, a string."
`(aref ,connection 1))
-(defmacro smtp-connection-service-internal (connection)
+(defmacro smtp-connection-service (connection)
+ "Return the service of CONNECTION, a string or an integer."
`(aref ,connection 2))
+(defmacro smtp-connection-extensions (connection)
+ "Return the SMTP extensions of CONNECTION, a list of strings."
+ `(aref ,connection 3))
+
+(defmacro smtp-connection-set-extensions (connection extensions)
+ "Set the SMTP extensions of CONNECTION.
+EXTENSIONS is a list of cons cells of the form \(EXTENSION . PARAMETERS).
+Where EXTENSION is a symbol and PARAMETERS is a list of strings."
+ `(aset ,connection 3 ,extensions))
+
(defmacro smtp-make-connection (process server service)
- `(vector ,process ,server ,service))
+ "Create a new connection structure.
+PROCESS is an internal subprocess-object. SERVER is name of the host
+to connect to. SERVICE is name of the service desired."
+ `(vector ,process ,server ,service nil))
(defun smtp-connection-opened (connection)
"Say whether the CONNECTION to server has been opened."
- (let ((process (smtp-connection-process-internal connection)))
+ (let ((process (smtp-connection-process connection)))
(if (memq (process-status process) '(open run))
t)))
(defun smtp-close-connection (connection)
"Close the CONNECTION to server."
- (let ((process (smtp-connection-process-internal connection)))
+ (let ((process (smtp-connection-process connection)))
(delete-process process)))
(defun smtp-make-fqdn ()
((string-match "[^.]\\.[^.]" system-name)
system-name)
(t
- (error "Cannot generate valid FQDN. Set `smtp-fqdn' \
-or `smtp-local-domain' correctly."))))))
+ (error "Cannot generate valid FQDN"))))))
(defun smtp-find-connection (buffer)
"Find the connection delivering to BUFFER."
(delq entry smtp-connection-alist))
nil))))
+(eval-and-compile
+ (autoload 'starttls-open-stream "starttls")
+ (autoload 'starttls-negotiate "starttls"))
+
(defun smtp-open-connection (buffer server service)
+ "Open a SMTP connection for a service to a host.
+Return a newly allocated connection-object.
+BUFFER is the buffer to associate with the connection. SERVER is name
+of the host to connect to. SERVICE is name of the service desired."
(let ((process
(as-binary-process
(funcall smtp-open-connection-function
;;;###autoload
(defun smtp-via-smtp (sender recipients buffer)
+ (condition-case nil
+ (progn
+ (smtp-send-buffer sender recipients buffer)
+ t)
+ (smtp-error)))
+
+(make-obsolete 'smtp-via-smtp "It's old API.")
+
+;;;###autoload
+(defun smtp-send-buffer (sender recipients buffer)
(let ((server
(if (functionp smtp-server)
(funcall smtp-server sender recipients)
smtp-server))
(package
- (smtp-make-package sender recipients buffer)))
+ (smtp-make-package sender recipients buffer))
+ (smtp-open-connection-function
+ (if smtp-use-starttls
+ #'starttls-open-stream
+ smtp-open-connection-function)))
(save-excursion
(set-buffer
(get-buffer-create
(smtp-open-connection (current-buffer) server smtp-service))
(make-local-variable 'smtp-read-point)
(setq smtp-read-point (point-min))
- (condition-case nil
- (progn
- (smtp-commit package)
- t)
- (smtp-response-error)))))
+ (funcall smtp-submit-package-function package))))
-(defun smtp-commit (package)
+(defun smtp-submit-package (package)
(unwind-protect
(progn
(smtp-primitive-greeting package)
- (smtp-primitive-helo package)
+ (condition-case nil
+ (smtp-primitive-ehlo package)
+ (smtp-response-error
+ (smtp-primitive-helo package)))
+ (if smtp-use-starttls
+ (smtp-primitive-starttls package))
+ (if smtp-use-sasl
+ (smtp-primitive-auth package))
(smtp-primitive-mailfrom package)
(smtp-primitive-rcptto package)
(smtp-primitive-data package))
(smtp-primitive-quit package)
(smtp-close-connection connection)))))
-;;; @ hook methods for `smtp-commit'
+;;; @ hook methods for `smtp-submit-package'
;;;
+
(defun smtp-primitive-greeting (package)
(let* ((connection
(smtp-find-connection (current-buffer)))
(response
(smtp-read-response
- (smtp-connection-process-internal connection))))
+ (smtp-connection-process connection))))
(if (/= (car response) 220)
(smtp-response-error response))))
+(defun smtp-primitive-ehlo (package)
+ (let* ((connection
+ (smtp-find-connection (current-buffer)))
+ (process
+ (smtp-connection-process connection))
+ response)
+ (smtp-send-command process (format "EHLO %s" (smtp-make-fqdn)))
+ (setq response (smtp-read-response process))
+ (if (/= (car response) 250)
+ (smtp-response-error response))
+ (smtp-connection-set-extensions
+ connection (mapcar
+ (lambda (extension)
+ (let ((extensions
+ (split-string extension)))
+ (setcar extensions
+ (car (read-from-string
+ (downcase (car extensions)))))
+ extensions))
+ (cdr response)))))
+
(defun smtp-primitive-helo (package)
(let* ((connection
(smtp-find-connection (current-buffer)))
(process
- (smtp-connection-process-internal connection))
+ (smtp-connection-process connection))
response)
(smtp-send-command process (format "HELO %s" (smtp-make-fqdn)))
(setq response (smtp-read-response process))
(if (/= (car response) 250)
(smtp-response-error response))))
+(defun smtp-primitive-auth (package)
+ (let* ((connection
+ (smtp-find-connection (current-buffer)))
+ (process
+ (smtp-connection-process connection))
+ (mechanisms
+ (cdr (assq 'auth (smtp-connection-extensions connection))))
+ (sasl-mechanisms
+ (or smtp-sasl-mechanisms sasl-mechanisms))
+ (mechanism
+ (sasl-find-mechanism mechanisms))
+ client
+ name
+ step
+ response)
+ (unless mechanism
+ (error "No authentication mechanism available"))
+ (setq client (sasl-make-client mechanism smtp-sasl-user-name "smtp"
+ (smtp-connection-server connection)))
+ (if smtp-sasl-properties
+ (sasl-client-set-properties client smtp-sasl-properties))
+ (setq name (sasl-mechanism-name mechanism)
+ ;; Retrieve the initial response
+ step (sasl-next-step client nil))
+ (smtp-send-command
+ process
+ (if (sasl-step-data step)
+ (format "AUTH %s %s" name (base64-encode-string (sasl-step-data step) t))
+ (format "AUTH %s" name)))
+ (catch 'done
+ (while t
+ (setq response (smtp-read-response process))
+ (when (= (car response) 235)
+ ;; The authentication process is finished.
+ (setq step (sasl-next-step client step))
+ (if (null step)
+ (throw 'done nil))
+ (smtp-response-error response)) ;Bogus server?
+ (if (/= (car response) 334)
+ (smtp-response-error response))
+ (sasl-step-set-data step (base64-decode-string (nth 1 response)))
+ (setq step (sasl-next-step client step))
+ (smtp-send-command
+ process (if (sasl-step-data step)
+ (base64-encode-string (sasl-step-data step) t)
+ ""))))))
+
+(defun smtp-primitive-starttls (package)
+ (let* ((connection
+ (smtp-find-connection (current-buffer)))
+ (process
+ (smtp-connection-process connection))
+ response)
+ ;; STARTTLS --- begin a TLS negotiation (RFC 2595)
+ (smtp-send-command process "STARTTLS")
+ (setq response (smtp-read-response process))
+ (if (/= (car response) 220)
+ (smtp-response-error response))
+ (starttls-negotiate process)))
+
(defun smtp-primitive-mailfrom (package)
(let* ((connection
(smtp-find-connection (current-buffer)))
(process
- (smtp-connection-process-internal connection))
+ (smtp-connection-process connection))
+ (extensions
+ (smtp-connection-extensions
+ connection))
+ (sender
+ (smtp-package-sender package))
+ extension
response)
+ ;; SIZE --- Message Size Declaration (RFC1870)
+ (if (and smtp-use-size
+ (assq 'size extensions))
+ (setq extension (format "SIZE=%d" (smtp-package-buffer-size package))))
+ ;; 8BITMIME --- 8bit-MIMEtransport (RFC1652)
+ (if (and smtp-use-8bitmime
+ (assq '8bitmime extensions))
+ (setq extension (concat extension " BODY=8BITMIME")))
(smtp-send-command
- process (format "MAIL FROM:<%s>" (smtp-package-sender-internal package)))
+ process
+ (if extension
+ (format "MAIL FROM:<%s> %s" sender extension)
+ (format "MAIL FROM:<%s>" sender)))
(setq response (smtp-read-response process))
(if (/= (car response) 250)
(smtp-response-error response))))
(let* ((connection
(smtp-find-connection (current-buffer)))
(process
- (smtp-connection-process-internal connection))
+ (smtp-connection-process connection))
(recipients
- (smtp-package-recipients-internal package))
+ (smtp-package-recipients package))
response)
(while recipients
(smtp-send-command
- process (format "RCPT TO:<%s>" (pop recipients))))
- (setq response (smtp-read-response process))
- (unless (memq (car response) '(250 251))
- (smtp-response-error response))))
+ process (format "RCPT TO:<%s>" (pop recipients)))
+ (setq response (smtp-read-response process))
+ (unless (memq (car response) '(250 251))
+ (smtp-response-error response)))))
(defun smtp-primitive-data (package)
(let* ((connection
(smtp-find-connection (current-buffer)))
(process
- (smtp-connection-process-internal connection))
+ (smtp-connection-process connection))
response)
(smtp-send-command process "DATA")
(setq response (smtp-read-response process))
(if (/= (car response) 354)
(smtp-response-error response))
(save-excursion
- (set-buffer (smtp-package-buffer-internal package))
+ (set-buffer (smtp-package-buffer package))
(goto-char (point-min))
(while (not (eobp))
(smtp-send-data
(let* ((connection
(smtp-find-connection (current-buffer)))
(process
- (smtp-connection-process-internal connection))
+ (smtp-connection-process connection))
response)
(smtp-send-command process "QUIT")
(setq response (smtp-read-response process))
(goto-char (point-max))
(insert output)))
+(put 'smtp-error 'error-message "SMTP error")
+(put 'smtp-error 'error-conditions '(smtp-error error))
+
(put 'smtp-response-error 'error-message "SMTP response error")
-(put 'smtp-response-error 'error-conditions '(smtp-response-error error))
+(put 'smtp-response-error 'error-conditions '(smtp-response-error smtp-error error))
(defun smtp-response-error (response)
(signal 'smtp-response-error response))
(goto-char
(prog1 smtp-read-point
(setq smtp-read-point (point))))
- (when (looking-at "[1-5][0-9][0-9] ")
- (setq response-continue nil)
- (push (read (point-marker)) response)))
+ (if (looking-at "[1-5][0-9][0-9] ")
+ (setq response (cons (read (point-marker)) response)
+ response-continue nil)))
response))
(defun smtp-send-command (process command)