(require 'mh-e)
(mh-require-cl)
(autoload 'message-fetch-field "message")
(defvar mh-show-xface-function
(cond ((and mh-xemacs-flag (locate-library "x-face") (not (featurep 'xface)))
(load "x-face" t t)
#'mh-face-display-function)
((>= emacs-major-version 21)
#'mh-face-display-function)
(t #'ignore))
"Determine at run time what function should be called to display X-Face.")
(defvar mh-uncompface-executable
(and (fboundp 'executable-find) (executable-find "uncompface")))
(defun mh-show-xface ()
"Display X-Face."
(when (and window-system mh-show-use-xface-flag
(or mh-decode-mime-flag mh-mhl-format-file
mh-clean-message-header-flag))
(funcall mh-show-xface-function)))
(defvar default-enable-multibyte-characters)
(defun mh-face-display-function ()
"Display a Face, X-Face, or X-Image-URL header field.
If more than one of these are present, then the first one found
in this order is used."
(save-restriction
(goto-char (point-min))
(re-search-forward "\n\n" (point-max) t)
(narrow-to-region (point-min) (point))
(let* ((case-fold-search t)
(default-enable-multibyte-characters nil)
(face (message-fetch-field "face" t))
(x-face (message-fetch-field "x-face" t))
(url (message-fetch-field "x-image-url" t))
raw type)
(cond (face (setq raw (mh-face-to-png face)
type 'png))
(x-face (setq raw (mh-uncompface x-face)
type 'pbm))
(url (setq type 'url))
(t (multiple-value-setq (type raw) (mh-picon-get-image))))
(when type
(goto-char (point-min))
(when (re-search-forward "^from:" (point-max) t)
(mh-do-in-gnu-emacs
(if (eq type 'url)
(mh-x-image-url-display url)
(mh-funcall-if-exists
insert-image (create-image
raw type t
:foreground
(mh-face-foreground 'mh-show-xface nil t)
:background
(mh-face-background 'mh-show-xface nil t))
" ")))
(mh-do-in-xemacs
(cond
((eq type 'url)
(mh-x-image-url-display url))
((eq type 'png)
(when (featurep 'png)
(set-extent-begin-glyph
(make-extent (point) (point))
(make-glyph (vector 'png ':data (mh-face-to-png face))))))
((and (eq type 'pbm) (featurep 'xface))
(set-glyph-face
(set-extent-begin-glyph
(make-extent (point) (point))
(make-glyph (vector 'xface ':data (concat "X-Face: " x-face))))
'mh-show-xface))
((and (eq type 'pbm)
(fboundp 'x-face-xmas-wl-display-x-face)
(fboundp 'executable-find) (executable-find "uncompface"))
(mh-funcall-if-exists x-face-xmas-wl-display-x-face))
((and raw (member type '(xpm xbm gif)))
(when (featurep type)
(set-extent-begin-glyph
(make-extent (point) (point))
(make-glyph (vector type ':data raw))))))
(when raw (insert " "))))))))
(defun mh-face-to-png (data)
"Convert base64 encoded DATA to png image."
(with-temp-buffer
(insert data)
(ignore-errors (base64-decode-region (point-min) (point-max)))
(buffer-string)))
(defun mh-uncompface (data)
"Run DATA through `uncompface' to generate bitmap."
(with-temp-buffer
(insert data)
(when (and mh-uncompface-executable
(equal (call-process-region (point-min) (point-max)
mh-uncompface-executable t '(t nil))
0))
(mh-icontopbm)
(buffer-string))))
(defun mh-icontopbm ()
"Elisp substitute for `icontopbm'."
(goto-char (point-min))
(let ((end (point-max)))
(while (re-search-forward "0x\\(..\\)\\(..\\)," nil t)
(save-excursion
(goto-char (point-max))
(insert (string-to-number (match-string 1) 16))
(insert (string-to-number (match-string 2) 16))))
(delete-region (point-min) end)
(goto-char (point-min))
(insert "P4\n48 48\n")))
(defvar mh-picon-directory-list
'("~/.picons" "~/.picons/users" "~/.picons/usenix" "~/.picons/news"
"~/.picons/domains" "~/.picons/misc"
"/usr/share/picons/" "/usr/share/picons/users" "/usr/share/picons/usenix"
"/usr/share/picons/news" "/usr/share/picons/domains"
"/usr/share/picons/misc")
"List of directories where picons reside.
The directories are searched for in the order they appear in the list.")
(defvar mh-picon-existing-directory-list 'unset
"List of directories to search in.")
(defvar mh-picon-cache (make-hash-table :test #'equal))
(defvar mh-picon-image-types
(loop for type in '(xpm xbm gif)
when (or (mh-do-in-gnu-emacs
(ignore-errors
(mh-funcall-if-exists image-type-available-p type)))
(mh-do-in-xemacs (featurep type)))
collect type))
(autoload 'message-tokenize-header "sendmail")
(defun* mh-picon-get-image ()
"Find the best possible match and return contents."
(mh-picon-set-directory-list)
(save-restriction
(let* ((from-field (ignore-errors (car (message-tokenize-header
(mh-get-header-field "from:")))))
(from (car (ignore-errors
(mh-funcall-if-exists ietf-drums-parse-address
from-field))))
(host (and from
(string-match "\\([^+]*\\)\\(+.*\\)?@\\(.*\\)" from)
(downcase (match-string 3 from))))
(user (and host (downcase (match-string 1 from))))
(canonical-address (format "%s@%s" user host))
(cached-value (gethash canonical-address mh-picon-cache))
(host-list (and host (delete "" (split-string host "\\."))))
(match nil))
(cond (cached-value (return-from mh-picon-get-image cached-value))
((not host-list) (return-from mh-picon-get-image nil)))
(setq match
(block 'loop
(loop for dir in mh-picon-existing-directory-list
do (loop for type in mh-picon-image-types
for file1 = (format "%s/%s.%s"
dir canonical-address type)
when (file-exists-p file1)
do (return-from 'loop file1)
for file2 = (format "%s/%s.%s" dir user type)
when (file-exists-p file2)
do (return-from 'loop file2)
for file3 = (format "%s/%s.%s" dir host type)
when (file-exists-p file3)
do (return-from 'loop file3)))
(loop for u in (list user "unknown")
do (loop for dir in mh-picon-existing-directory-list
do (loop for x on host-list by #'cdr
for y = (mh-picon-generate-path x u dir)
do (loop for type in mh-picon-image-types
for z1 = (format "%s.%s" y type)
when (file-exists-p z1)
do (return-from 'loop z1)
for z2 = (format "%s/face.%s"
y type)
when (file-exists-p z2)
do (return-from 'loop z2)))))))
(setf (gethash canonical-address mh-picon-cache)
(mh-picon-file-contents match)))))
(defun mh-picon-set-directory-list ()
"Update `mh-picon-existing-directory-list' if needed."
(when (eq mh-picon-existing-directory-list 'unset)
(setq mh-picon-existing-directory-list
(loop for x in mh-picon-directory-list
when (file-directory-p x) collect x))))
(defun mh-picon-generate-path (host-list user directory)
"Generate the image file path.
HOST-LIST is the parsed host address of the email address, USER
the username and DIRECTORY is the directory relative to which the
path is generated."
(loop with acc = ""
for elem in host-list
do (setq acc (format "%s/%s" elem acc))
finally return (format "%s/%s%s" directory acc user)))
(defun mh-picon-file-contents (file)
"Return details about FILE.
A list of consisting of a symbol for the type of the file and the
file contents as a string is returned. If FILE is nil, then both
elements of the list are nil."
(if (stringp file)
(with-temp-buffer
(let ((type (and (string-match ".*\\.\\(...\\)$" file)
(intern (match-string 1 file)))))
(insert-file-contents-literally file)
(values type (buffer-string))))
(values nil nil)))
(defvar mh-x-image-scaling-function
(cond ((executable-find "convert")
'mh-x-image-scale-with-convert)
((and (executable-find "anytopnm") (executable-find "pnmscale")
(executable-find "pnmtopng"))
'mh-x-image-scale-with-pnm)
(t 'ignore))
"Function to use to scale image to proper size.")
(defun mh-x-image-scale-with-pnm (input output)
"Scale image in INPUT file and write to OUTPUT file using pnm tools."
(let ((res (shell-command-to-string
(format "anytopnm < %s | pnmscale -xysize 96 48 | pnmtopng > %s"
input output))))
(unless (equal res "")
(delete-file output))))
(defun mh-x-image-scale-with-convert (input output)
"Scale image in INPUT file and write to OUTPUT file using ImageMagick."
(call-process "convert" nil nil nil "-geometry" "96x48" input output))
(defvar mh-wget-executable nil)
(defvar mh-wget-choice
(or (and (setq mh-wget-executable (executable-find "wget")) 'wget)
(and (setq mh-wget-executable (executable-find "fetch")) 'fetch)
(and (setq mh-wget-executable (executable-find "curl")) 'curl)))
(defvar mh-wget-option
(cdr (assoc mh-wget-choice '((curl . "-o") (fetch . "-o") (wget . "-O")))))
(defvar mh-x-image-temp-file nil)
(defvar mh-x-image-url nil)
(defvar mh-x-image-marker nil)
(defvar mh-x-image-url-cache-file nil)
(defun mh-x-image-url-display (url)
"Display image from location URL.
If the URL isn't present in the cache then it is fetched with wget."
(let* ((cache-filename (mh-x-image-url-cache-canonicalize url))
(state (mh-x-image-get-download-state cache-filename))
(marker (set-marker (make-marker) (point))))
(set (make-local-variable 'mh-x-image-marker) marker)
(cond ((not (mh-x-image-url-sane-p url)))
((eq state 'ok)
(mh-x-image-display cache-filename marker))
((or (not mh-wget-executable)
(eq mh-x-image-scaling-function 'ignore)))
((eq state 'never))
((not mh-fetch-x-image-url)
(set-marker marker nil))
((eq state 'try-again)
(mh-x-image-set-download-state cache-filename nil)
(mh-x-image-url-fetch-image url cache-filename marker
'mh-x-image-scale-and-display))
((and (eq mh-fetch-x-image-url 'ask)
(not (y-or-n-p (format "Fetch %s? " url))))
(mh-x-image-set-download-state cache-filename 'never))
((eq state nil)
(mh-x-image-url-fetch-image url cache-filename marker
'mh-x-image-scale-and-display)))))
(defvar mh-x-image-cache-directory nil
"Directory where X-Image-URL images are cached.")
(defun mh-set-x-image-cache-directory (directory)
"Set the DIRECTORY where X-Image-URL images are cached.
This is only done if `mh-x-image-cache-directory' is nil."
(unless mh-x-image-cache-directory
(setq mh-x-image-cache-directory directory)))
(defun mh-x-image-url-cache-canonicalize (url)
"Canonicalize URL.
Replace the ?/ character with a ?! character and append .png.
Also replaces special characters with `mh-url-hexify-string'
since not all characters, such as :, are legal within Windows
filenames. In addition, replaces * with %2a. See URL
`http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/ifaces/iitemnamelimits/GetValidCharacters.asp'."
(format "%s/%s.png" mh-x-image-cache-directory
(mh-replace-regexp-in-string
"\*" "%2a"
(mh-url-hexify-string
(with-temp-buffer
(insert url)
(mh-replace-string "/" "!")
(buffer-string))))))
(defun mh-x-image-get-download-state (file)
"Check the state of FILE by following any symbolic links."
(unless (file-exists-p mh-x-image-cache-directory)
(call-process "mkdir" nil nil nil mh-x-image-cache-directory))
(cond ((file-symlink-p file)
(intern (file-name-nondirectory (file-chase-links file))))
((not (file-exists-p file)) nil)
(t 'ok)))
(defun mh-x-image-set-download-state (file data)
"Setup a symbolic link from FILE to DATA."
(if data
(make-symbolic-link (symbol-name data) file t)
(delete-file file)))
(defun mh-x-image-url-sane-p (url)
"Check if URL is something sensible."
(let ((len (length url)))
(cond ((< len 5) nil)
((not (equal (substring url 0 5) "http:")) nil)
((> len 100) nil)
(t t))))
(defun mh-x-image-display (image marker)
"Display IMAGE at MARKER."
(save-excursion
(set-buffer (marker-buffer marker))
(let ((buffer-read-only nil)
(default-enable-multibyte-characters nil)
(buffer-modified-flag (buffer-modified-p)))
(unwind-protect
(when (and (file-readable-p image) (not (file-symlink-p image))
(eq marker mh-x-image-marker))
(goto-char marker)
(mh-do-in-gnu-emacs
(mh-funcall-if-exists insert-image (create-image image 'png)))
(mh-do-in-xemacs
(when (featurep 'png)
(set-extent-begin-glyph
(make-extent (point) (point))
(make-glyph
(vector 'png ':data (with-temp-buffer
(insert-file-contents-literally image)
(buffer-string))))))))
(set-buffer-modified-p buffer-modified-flag)))))
(defun mh-x-image-url-fetch-image (url cache-file marker sentinel)
"Fetch and display the image specified by URL.
After the image is fetched, it is stored in CACHE-FILE. It will
be displayed in a buffer and position specified by MARKER. The
actual display is carried out by the SENTINEL function."
(if mh-wget-executable
(let ((buffer (get-buffer-create (generate-new-buffer-name
mh-temp-fetch-buffer)))
(filename (or (mh-funcall-if-exists make-temp-file "mhe-fetch")
(expand-file-name (make-temp-name "~/mhe-fetch")))))
(save-excursion
(set-buffer buffer)
(set (make-local-variable 'mh-x-image-url-cache-file) cache-file)
(set (make-local-variable 'mh-x-image-marker) marker)
(set (make-local-variable 'mh-x-image-temp-file) filename))
(set-process-sentinel
(start-process "*mh-x-image-url-fetch*" buffer
mh-wget-executable mh-wget-option filename url)
sentinel))
(mh-x-image-set-download-state cache-file 'try-again)))
(defun mh-x-image-scale-and-display (process change)
"When the wget PROCESS terminates scale and display image.
The argument CHANGE is ignored."
(when (eq (process-status process) 'exit)
(let (marker temp-file cache-filename wget-buffer)
(save-excursion
(set-buffer (setq wget-buffer (process-buffer process)))
(setq marker mh-x-image-marker
cache-filename mh-x-image-url-cache-file
temp-file mh-x-image-temp-file))
(cond
((eq mh-x-image-scaling-function 'ignore)
(message "The \"convert\" program is needed to display X-Image-URL")
(mh-x-image-set-download-state cache-filename 'try-again))
((and (funcall mh-x-image-scaling-function temp-file cache-filename)
nil))
((file-exists-p cache-filename)
(mh-x-image-display cache-filename marker))
(t (mh-x-image-set-download-state cache-filename 'try-again)))
(ignore-errors
(set-marker marker nil)
(delete-process process)
(kill-buffer wget-buffer)
(delete-file temp-file)))))
(provide 'mh-xface)