Emacs

Configuration notes for Emacs

Table Of Contents

  1. Signing e-mails with additional certificates
  2. Connecting to an SMTP/TLS server with GnuTLS on Windows

Signing e-mails with additional certificates

The smime-keys variable allows additional certificates to be associated to each mail address, but the function (mml-secure-message-sign-smime) only prompts for a keyfile and ignores them. (More informations about this problem here: http://lists.gnu.org/archive/html/emacs-bug-tracker/2011-01/msg00118.html.) The following code is a patch I use in my .gnus file to change the semantics of the keyfile argument of the MML tag <#secure> for being a mail address and not a filename. This mail address is resolved with (smime-get-key-with-certs-by-email) on sending to include the full certification chain.

;; Patch to smime-sign-buffer and mml-smime-openssl-sign-query to put
;; the email address in the keyfile argument of <#secure> tag instead
;; of the key filename to take additional certificates into account when
;; signing a message.

(require 'smime) ; smime-sign-buffer comes from here

(defun smime-sign-buffer (&optional keyfile buffer)
  "S/MIME sign BUFFER with key in KEYFILE.
KEYFILE should contain a PEM encoded key and certificate."
  (interactive)
  (with-current-buffer (or buffer (current-buffer))
    (unless (smime-sign-region
	     (point-min) (point-max)
	     (if keyfile
                 (smime-get-key-with-certs-by-email keyfile)
	       (smime-get-key-with-certs-by-email
		(completing-read
		 (concat "Sign using key"
			 (if smime-keys
			     (concat " (default " (caar smime-keys) "): ")
			   ": "))
		 smime-keys nil nil (car-safe (car-safe smime-keys))))))
      (error "Signing failed"))))

(require 'mml-smime) ; mml-smime-openssl-sign-query comes from here

(defun mml-smime-openssl-sign-query ()
  ;; query information (what certificate) from user when MML tag is
  ;; added, for use later by the signing process
  (when (null smime-keys)
    (customize-variable 'smime-keys)
    (error "No S/MIME keys configured, use customize to add your key"))
  (list 'keyfile
	(if (= (length smime-keys) 1)
	    (caar smime-keys)
	  (or (let ((from (cadr (funcall (if (boundp
					      'gnus-extract-address-components)
					     gnus-extract-address-components
					   'mail-extract-address-components)
					 (or (save-excursion
					       (save-restriction
						 (message-narrow-to-headers)
						 (message-fetch-field "from")))
					     "")))))
		(and from from))
              (completing-read "Sign this part with what signature? "
                               smime-keys nil nil
                               (and (listp (car-safe smime-keys))
                                    (caar smime-keys)))))))

Connecting to an SMTP/TLS server with GnuTLS on Windows

The TLS support of Gnus is twice broken on Windows: first, lines in the process output buffer end with ^M and this character prevents smtpmail.el to parse the supported extensions correctly; second, GnuTLS requires a signal (or EOF) to be send for handshaking, and this signal never occurs since Win32 doesn't have Unix signals. (See http://lists.gnu.org/archive/html/bug-gnu-emacs/2011-01/msg00442.html for more informations.) The following code is a patch I use in my .gnus file to remove the ^M from extension names to correctly recognize the STARTTLS extension, and to replace the SIGALRM signal by the ^Z character which corresponds to EOF on Windows.

(if (eq system-type 'windows-nt)
(progn
;; Patch smtpmail-via-smtp to ignore the ^M characters at the end of
;; each line of the process buffer when reading extensions.

(setq starttls-use-gnutls t)

(require 'smtpmail) ; smtpmail-via-smtp comes from here

(defun smtpmail-via-smtp (recipient smtpmail-text-buffer)
  (let ((process nil)
	(host (or smtpmail-smtp-server
		  (error "`smtpmail-smtp-server' not defined")))
	(port smtpmail-smtp-service)
        ;; `smtpmail-mail-address' should be set to the appropriate
        ;; buffer-local value by the caller, but in case not:
        (envelope-from (or smtpmail-mail-address
                           (and mail-specify-envelope-from
                                (mail-envelope-from))
                           user-mail-address))
	response-code
	greeting
	process-buffer
	(supported-extensions '()))
    (unwind-protect
	(catch 'done
	  ;; get or create the trace buffer
	  (setq process-buffer
		(get-buffer-create (format "*trace of SMTP session to %s*" host)))

	  ;; clear the trace buffer of old output
	  (with-current-buffer process-buffer
	    (setq buffer-undo-list t)
	    (erase-buffer))

	  ;; open the connection to the server
	  (setq process (smtpmail-open-stream process-buffer host port))
	  (and (null process) (throw 'done nil))

	  ;; set the send-filter
	  (set-process-filter process 'smtpmail-process-filter)

	  (with-current-buffer process-buffer
	    (set-buffer-process-coding-system 'raw-text-unix 'raw-text-unix)
	    (make-local-variable 'smtpmail-read-point)
	    (setq smtpmail-read-point (point-min))


	    (if (or (null (car (setq greeting (smtpmail-read-response process))))
		    (not (integerp (car greeting)))
		    (>= (car greeting) 400))
		(throw 'done nil))

	    (let ((do-ehlo t)
		  (do-starttls t))
	      (while do-ehlo
                ;; EHLO
                (smtpmail-send-command process (format "EHLO %s" (smtpmail-fqdn)))

                (if (or (null (car (setq response-code
                                         (smtpmail-read-response process))))
                        (not (integerp (car response-code)))
                        (>= (car response-code) 400))
                    (progn
                      ;; HELO
                      (smtpmail-send-command
                       process (format "HELO %s" (smtpmail-fqdn)))

                      (if (or (null (car (setq response-code
                                               (smtpmail-read-response process))))
                              (not (integerp (car response-code)))
                              (>= (car response-code) 400))
                          (throw 'done nil)))
                  (dolist (line (cdr (cdr response-code)))
                    (let ((name
                           (with-case-table ascii-case-table
                             (mapcar (lambda (s) (intern (downcase s)))
                                     (split-string (substring line 4 (- (length line) 1)) "[ ]")))))
                      (and (eq (length name) 1)
                           (setq name (car name)))
                      (and name
                           (cond ((memq (if (consp name) (car name) name)
                                        '(verb xvrb 8bitmime onex xone
                                               expn size dsn etrn
                                               enhancedstatuscodes
                                               help xusr
                                               auth=login auth starttls))
                                  (setq supported-extensions
                                        (cons name supported-extensions)))
                                 (smtpmail-warn-about-unknown-extensions
                                  (message "Unknown extension %s" name)))))))

                (if (and do-starttls
                         (smtpmail-find-credentials smtpmail-starttls-credentials host port)
                         (member 'starttls supported-extensions)
                         (numberp (process-id process)))
                    (progn
                      (smtpmail-send-command process (format "STARTTLS"))
                      (if (or (null (car (setq response-code (smtpmail-read-response process))))
                              (not (integerp (car response-code)))
                              (>= (car response-code) 400))
                          (throw 'done nil))
                      (starttls-negotiate process)
                      (setq do-starttls nil))
                  (setq do-ehlo nil))))

	    (smtpmail-try-auth-methods process supported-extensions host port)

	    (if (or (member 'onex supported-extensions)
		    (member 'xone supported-extensions))
		(progn
		  (smtpmail-send-command process (format "ONEX"))
		  (if (or (null (car (setq response-code (smtpmail-read-response process))))
			  (not (integerp (car response-code)))
			  (>= (car response-code) 400))
		      (throw 'done nil))))

	    (if (and smtpmail-debug-verb
		     (or (member 'verb supported-extensions)
			 (member 'xvrb supported-extensions)))
		(progn
		  (smtpmail-send-command process (format "VERB"))
		  (if (or (null (car (setq response-code (smtpmail-read-response process))))
			  (not (integerp (car response-code)))
			  (>= (car response-code) 400))
		      (throw 'done nil))))

	    (if (member 'xusr supported-extensions)
		(progn
		  (smtpmail-send-command process (format "XUSR"))
		  (if (or (null (car (setq response-code (smtpmail-read-response process))))
			  (not (integerp (car response-code)))
			  (>= (car response-code) 400))
		      (throw 'done nil))))

	    ;; MAIL FROM:<sender>
	    (let ((size-part
		   (if (or (member 'size supported-extensions)
			   (assoc 'size supported-extensions))
		       (format " SIZE=%d"
			       (with-current-buffer smtpmail-text-buffer
				 ;; size estimate:
				 (+ (- (point-max) (point-min))
				    ;; Add one byte for each change-of-line
				    ;; because of CR-LF representation:
				    (count-lines (point-min) (point-max)))))
		     ""))
		  (body-part
		   (if (member '8bitmime supported-extensions)
		       ;; FIXME:
		       ;; Code should be added here that transforms
		       ;; the contents of the message buffer into
		       ;; something the receiving SMTP can handle.
		       ;; For a receiver that supports 8BITMIME, this
		       ;; may mean converting BINARY to BASE64, or
		       ;; adding Content-Transfer-Encoding and the
		       ;; other MIME headers.  The code should also
		       ;; return an indication of what encoding the
		       ;; message buffer is now, i.e. ASCII or
		       ;; 8BITMIME.
		       (if nil
			   " BODY=8BITMIME"
			 "")
		     "")))
              ;; (smtpmail-send-command process (format "MAIL FROM:%s@%s" (user-login-name) (smtpmail-fqdn)))
	      (smtpmail-send-command process (format "MAIL FROM:<%s>%s%s"
                                                     envelope-from
						     size-part
						     body-part))

	      (if (or (null (car (setq response-code (smtpmail-read-response process))))
		      (not (integerp (car response-code)))
		      (>= (car response-code) 400))
		  (throw 'done nil)))

	    ;; RCPT TO:<recipient>
	    (let ((n 0))
	      (while (not (null (nth n recipient)))
		(smtpmail-send-command process (format "RCPT TO:<%s>" (smtpmail-maybe-append-domain (nth n recipient))))
		(setq n (1+ n))

		(setq response-code (smtpmail-read-response process))
		(if (or (null (car response-code))
			(not (integerp (car response-code)))
			(>= (car response-code) 400))
		    (throw 'done nil))))

	    ;; DATA
	    (smtpmail-send-command process "DATA")

	    (if (or (null (car (setq response-code (smtpmail-read-response process))))
		    (not (integerp (car response-code)))
		    (>= (car response-code) 400))
		(throw 'done nil))

	    ;; Mail contents
	    (smtpmail-send-data process smtpmail-text-buffer)

	    ;; DATA end "."
	    (smtpmail-send-command process ".")

	    (if (or (null (car (setq response-code (smtpmail-read-response process))))
		    (not (integerp (car response-code)))
		    (>= (car response-code) 400))
		(throw 'done nil))

	    ;; QUIT
            ;; (smtpmail-send-command process "QUIT")
            ;; (and (null (car (smtpmail-read-response process)))
            ;;      (throw 'done nil))
	    t))
      (if process
	  (with-current-buffer (process-buffer process)
	    (smtpmail-send-command process "QUIT")
	    (smtpmail-read-response process)

            ;; (if (or (null (car (setq response-code (smtpmail-read-response process))))
            ;;         (not (integerp (car response-code)))
            ;;         (>= (car response-code) 400))
            ;;	   (throw 'done nil))
	    (delete-process process)
	    (unless smtpmail-debug-info
	      (kill-buffer process-buffer)))))))

;; Patch starttls-negotiate-gnutls to send ^Z (Windows EOF) instead of
;; a signal.

(require 'starttls) ; starttls-negotiate-gnutls comes from here

(defun starttls-negotiate-gnutls (process)
  "Negotiate TLS on PROCESS opened by `open-starttls-stream'.
This should typically only be done once.  It typically returns a
multi-line informational message with information about the
handshake, or nil on failure."
  (let (buffer info old-max done-ok done-bad)
    (if (null (setq buffer (process-buffer process)))
        ;; XXX How to remove/extract the TLS negotiation junk?
        ; (signal-process (process-id process) 'SIGALRM)
        (process-send-string process "\x1a") ; EOF for GnuTLS on Windows
      (with-current-buffer buffer
        (save-excursion
          (setq old-max (goto-char (point-max)))
          ; (signal-process (process-id process) 'SIGALRM)
          (process-send-string process "\x1a") ; EOF for GnuTLS on Windows
          (while (and (processp process)
                      (eq (process-status process) 'run)
                      (save-excursion
                        (goto-char old-max)
                        (not (or (setq done-ok (re-search-forward
                                                starttls-success nil t))
                                 (setq done-bad (re-search-forward
                                                 starttls-failure nil t))))))
            (accept-process-output process 1 100)
            (sit-for 0.1))
          (setq info (buffer-substring-no-properties old-max (point-max)))
          (delete-region old-max (point-max))
          (if (or (and done-ok (not done-bad))
                  ;; Prevent mitm that fake success msg after failure msg.
                  (and done-ok done-bad (< done-ok done-bad)))
              info
            (message "STARTTLS negotiation failed: %s" info)
            nil))))))
))