;;; qpdecode.el --- decode mime quoted-printable messages

;; Copyright (C) 1996 Noah S. Friedman

;; Author: Noah Friedman <friedman@prep.ai.mit.edu>
;; Maintainer: friedman@prep.ai.mit.edu
;; Keywords: mime, decoding, extensions, vm, rmail
;; Created: 1996-09-23

;; $Id: qpdecode.el,v 1.6 1996/10/18 08:56:02 friedman Exp $

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, you can either send email to this
;; program's maintainer or write to: The Free Software Foundation,
;; Inc.; 59 Temple Place, Suite 330; Boston, MA 02111-1307, USA.

;;; Commentary:

;; I recommend putting this in your .emacs, after preloading the
;; appropriate packages or using eval-after-load:
;;
;;         (autoload 'qpdecode-decode-message "qpdecode" nil t)
;;         (define-key vm-mode-map "D" 'qpdecode-decode-message)
;;         (define-key rmail-mode-map "D" 'qpdecode-decode-message)

;; TODO: Recompute or remove Content-MD5 header?
;; The gnus article mode hacking doesn't seem to work quite right yet.

;;; Code:

(require 'sendmail)

(defconst qpdecode-decode-headers '("From"))

(defconst qpdecode-content-type-header "Content-Transfer-Encoding")
(defconst qpdecode-content-length-header "Content-Length")

;; These keys are compared against Content-Transfer-Encoding.
(defconst qpdecode-decode-body-methods
  '(("quoted-printable" . qpdecode-quoted-printable-decode-body)))

(defconst qpdecode-decode-header-methods
  '(("=\\?iso-8859-1\\?\\(q\\|quoted-printable\\)\\?"
    . qpdecode-quoted-printable-decode-header)))

;; Mapping between major mode and method used to expose "missing" mail headers.
;; Some mail readers have an option to hide uninteresting mail headers,
;; i.e. to move them outside the narrowed region to display, but offer a
;; way to display them.  This command should toggle between those states.
(defconst qpdecode-toggle-all-mail-headers-method
  '((rmail-mode        . rmail-toggle-header)
    (vm-mode           . vm-expose-hidden-headers)
    (gnus-article-mode . qpdecode-gnus-toggle-article-headers)))

;; This function should widen the currently-narrowed region of the buffer
;; to expose all the headers associated with the current message.
;; It should return whatever state information is required to restore the
;; condition of the headers by qpdecode-hide-exposed-mail-headers-method.
(defconst qpdecode-expose-all-mail-headers-method
  '((rmail-mode        . qpdecode-rmail-widen-message-headers)
    (vm-mode           . qpdecode-vm-widen-message-headers)
    (gnus-article-mode . qpdecode-gnus-widen-article-headers)))

;; This function should use the state information given by
;; qpdecode-expose-all-mail-headers-method to restore the condition of the
;; mail headers (e.g. hide those not normally exposed).
(defconst qpdecode-hide-exposed-mail-headers-method
  '((rmail-mode        . qpdecode-rmail-narrow-message-headers)
    (vm-mode           . qpdecode-vm-narrow-message-headers)
    (gnus-article-mode . qpdecode-gnus-narrow-article-headers)))


(defun qpdecode-decode-message ()
  (interactive)
  (let* ((mail-header-separator "")
         (buffer-read-only nil)
         (case-fold-search t)
         (headers qpdecode-decode-headers)
         (type (qpdecode-get-message-type)))
    (while headers
      (qpdecode-decode-header (car headers))
      (setq headers (cdr headers)))
    (and (null type)
         (yes-or-no-p "Cannot determine msg type.  Assume quoted-printable? ")
         (setq type '("quoted-printable")))
    (cond (type
           (setq type (downcase (car type)))
           (qpdecode-decode-body type)
           (qpdecode-update-encoding-headers type)))))

(defun qpdecode-decode-body (type)
  (let ((method (assoc type qpdecode-decode-body-methods)))
    (and method
         (funcall (cdr method)))))

;; Update the Content-Transfer-Encoding and Content-Length headers.
(defun qpdecode-update-encoding-headers (&rest ignore)
  (let ((re-header-sep (concat "^" (regexp-quote mail-header-separator) "$"))
        (expand-fn (cdr (assq major-mode
                              qpdecode-expose-all-mail-headers-method)))
        (hide-fn (cdr (assq major-mode
                            qpdecode-hide-exposed-mail-headers-method)))
        (state nil)
        (len 0))
    (cond ((and expand-fn
                (setq state (funcall expand-fn)))
           (qpdecode-put-unique-header qpdecode-content-type-header "8bit" t)
           (cond ((mail-position-on-field qpdecode-content-length-header 'soft)
                  (re-search-forward re-header-sep)
                  (setq len (- (point-max) (point) 1))
                  (qpdecode-put-unique-header qpdecode-content-length-header
                                          (int-to-string len) t)))
           (funcall hide-fn state)))))


(defun qpdecode-get-message-type ()
  (let ((mail-header-separator ""))
    (cond ((qpdecode-get-header-contents qpdecode-content-type-header))
          ((let ((fn (cdr (assq major-mode
                                qpdecode-toggle-all-mail-headers-method))))
             (and fn
                  (save-excursion
                    (funcall fn)
                    (prog1
                        (qpdecode-get-header-contents
                         qpdecode-content-type-header)
                      (funcall fn)))))))))

(defun qpdecode-decode-header (header)
  "Decode and replace mime-quoted header contents in current mail message."
  (let* ((contents (car (qpdecode-get-header-contents header)))
         (new-contents nil)
         (methods qpdecode-decode-header-methods))
    (while methods
      (cond ((string-match (car (car methods)) contents)
             (setq new-contents (funcall (cdr (car methods)) contents))
             (setq methods nil)
             (or (eq new-contents contents)
                 (qpdecode-put-unique-header header new-contents t)))
            (t
             (setq methods (cdr methods)))))))

;; Decodes quoted-printable mail headers, returning the replacement string.
;;
;; Example:
;;   (qpdecode-header-decode-quoted-printable
;;    "From: =?ISO-8859-1?Q?Fran=E7ois_Pinard?= <pinard@iro.umontreal.ca>")
;;   => "François Pinard <pinard@iro.umontreal.ca>"
;;
(defun qpdecode-quoted-printable-decode-header (contents)
  (if (string-match "=\\?[^?]*\\?[^?]*\\?\\([^?]*\\)\\?=" contents)
      ;; skip past mime formatting data in `beg' and `end'
      (let ((beg (substring contents 0 (match-beginning 0)))
            (end (substring contents (match-end 0)))
            (mid (substring contents (match-beginning 1) (match-end 1))))
        (concat beg (qpdecode-quoted-printable-decode-string mid) end))
    contents))

(defun qpdecode-quoted-printable-decode-body ()
  (save-excursion
    (goto-char (point-min))
    (re-search-forward (concat "^" (regexp-quote mail-header-separator) "$"))
    (goto-char (match-end 0))
    (qpdecode-quoted-printable-decode-region (point) (point-max))))

(defun qpdecode-quoted-printable-decode-region (beg end)
  (save-restriction
    (narrow-to-region beg end)
    (goto-char (point-min))
    (while (search-forward "=\n" nil t)
      (replace-match ""))
    (goto-char (point-min))
    (while (re-search-forward "=[0123456789ABCDEF][0123456789ABCDEF]" nil t)
      (replace-match (qpdecode-quoted-printable-decode-string
                      (buffer-substring (match-beginning 0)
                                        (match-end 0)))))))

(defun qpdecode-quoted-printable-decode-string (str)
  (let (quoted-p high-nybble low-nybble)
    (mapconcat (function
                (lambda (char)
                  (cond ((eq char ?_)
                         " ")

                        ((eq char ?=)
                         (setq quoted-p t)
                         "")

                        (quoted-p
                         (setq high-nybble
                               (cond ((<= ?a char) (+ (- char ?a) 10))
                                     ((<= ?A char) (+ (- char ?A) 10))
                                     ((<= ?0 char) (- char ?0))))
                         (setq quoted-p nil)
                         "")

                        (high-nybble
                         (setq low-nybble
                               (cond ((<= ?a char) (+ (- char ?a) 10))
                                     ((<= ?A char) (+ (- char ?A) 10))
                                     ((<= ?0 char) (- char ?0))))
                         (prog1
                             (char-to-string (logior (ash high-nybble 4)
                                                     low-nybble))
                           (setq high-nybble nil)))

                        (t (char-to-string char)))))
               str "")))

(defun qpdecode-get-header-contents (header)
  "Return a list containing contents of any headers named HEADER.
If no occurrences of HEADER exist in the current mail buffer, return nil."
  (save-excursion
    (save-restriction
      (let (contents-list
            end
            beg)
        (while (mail-position-on-field header 'soft)
          (setq end (point)
                beg (progn
                      (re-search-backward (concat header ": "))
                      (goto-char (match-end 0)))
                contents-list (cons (buffer-substring beg end) contents-list))
          (narrow-to-region (1+ end) (point-max)))
        (nreverse contents-list)))))

(defun qpdecode-put-unique-header (header contents &optional replace)
  "Add HEADER to the current mail message, with the given CONTENTS.
If the header already exists, the contents are left unchanged, unless optional
argument REPLACE is non-nil."
  (save-excursion
    (let ((header-exists (mail-position-on-field header)))
      ;; Delete old contents if replace is set
      (if (and header-exists replace)
          (let ((end (point))
                (beg (progn
                       (re-search-backward (concat header ": "))
                       (goto-char (match-end 0)))))
            (delete-region beg end)))
      ;; Add new contents if replace is set, or this is a new header.
      (if (or (not header-exists) replace)
          (progn (insert contents) contents)))))


;; Note that these functions change the narrowed region by side effect!

;;; VM

;; Expose all headers of the current message, returning a marker pointing
;; to the original start of the narrowed region.
(defun qpdecode-vm-widen-message-headers ()
  (let ((beg (point-min))
        (end (point-max))
        (re-header-start "^From "))
    (goto-char (point-min))
    (widen)
    ;; In VM, if the headers are already toggled show-all, then the
    ;; envelope address is also already in view (i.e. be at point-min), so
    ;; first scan forward to the end of the message headers, then go back.
    (re-search-forward "^$" nil t)
    (cond ((re-search-backward re-header-start nil t)
           (end-of-line)
           (forward-char 1)
           (narrow-to-region (point) end)
           (set-marker (make-marker) beg))
          (t
           ;; Couldn't find start of messages; restore original region and
           ;; return nil to indicate no changes.
           (narrow-to-region beg end)
           nil))))

;; Use the saved marker to re-hide temporarily-exposed headers.
(defun qpdecode-vm-narrow-message-headers (mark)
  (and mark
       (narrow-to-region mark (point-max))))

;;; RMAIL

(defun qpdecode-rmail-all-headers-exposed-p ()
  (save-restriction
    (narrow-to-region (rmail-msgbeg rmail-current-message) (point-max))
    (goto-char (point-min))
    (forward-line 1)
    (= (following-char) ?0)))

(defun qpdecode-rmail-widen-message-headers ()
  (cond ((qpdecode-rmail-all-headers-exposed-p)
         'ignore)
        (t
         (rmail-toggle-header)
         'hide)))

(defun qpdecode-rmail-narrow-message-headers (action)
  (and (eq action 'hide)
       (rmail-toggle-header)))

;;; GNUS

(defun qpdecode-gnus-toggle-article-headers ()
  (gnus-article-hide-headers 0))

(defun qpdecode-gnus-widen-article-headers ()
  (gnus-article-hide-headers -1))

(defun qpdecode-gnus-narrow-article-headers (&rest ignore)
  (gnus-article-hide-headers 1))

(provide 'qpdecode)

;;; qpdecode.el ends here
