mh-xface.el 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. ;;; mh-xface.el --- MH-E X-Face and Face header field display
  2. ;; Copyright (C) 2002-2003, 2005-2012 Free Software Foundation, Inc.
  3. ;; Author: Bill Wohler <wohler@newt.com>
  4. ;; Maintainer: Bill Wohler <wohler@newt.com>
  5. ;; Keywords: mail
  6. ;; See: mh-e.el
  7. ;; This file is part of GNU Emacs.
  8. ;; GNU Emacs is free software: you can redistribute it and/or modify
  9. ;; it under the terms of the GNU General Public License as published by
  10. ;; the Free Software Foundation, either version 3 of the License, or
  11. ;; (at your option) any later version.
  12. ;; GNU Emacs is distributed in the hope that it will be useful,
  13. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;; GNU General Public License for more details.
  16. ;; You should have received a copy of the GNU General Public License
  17. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  18. ;;; Commentary:
  19. ;;; Change Log:
  20. ;;; Code:
  21. (require 'mh-e)
  22. (mh-require-cl)
  23. (autoload 'message-fetch-field "message")
  24. (defvar mh-show-xface-function
  25. (cond ((and (featurep 'xemacs) (locate-library "x-face") (not (featurep 'xface)))
  26. (load "x-face" t t)
  27. #'mh-face-display-function)
  28. ((>= emacs-major-version 21)
  29. #'mh-face-display-function)
  30. (t #'ignore))
  31. "Determine at run time what function should be called to display X-Face.")
  32. (defvar mh-uncompface-executable
  33. (and (fboundp 'executable-find) (executable-find "uncompface")))
  34. ;;; X-Face Display
  35. ;;;###mh-autoload
  36. (defun mh-show-xface ()
  37. "Display X-Face."
  38. (when (and window-system mh-show-use-xface-flag
  39. (or mh-decode-mime-flag mh-mhl-format-file
  40. mh-clean-message-header-flag))
  41. (funcall mh-show-xface-function)))
  42. (defun mh-face-display-function ()
  43. "Display a Face, X-Face, or X-Image-URL header field.
  44. If more than one of these are present, then the first one found
  45. in this order is used."
  46. (save-restriction
  47. (goto-char (point-min))
  48. (re-search-forward "\n\n" (point-max) t)
  49. (narrow-to-region (point-min) (point))
  50. (let* ((case-fold-search t)
  51. (face (message-fetch-field "face" t))
  52. (x-face (message-fetch-field "x-face" t))
  53. (url (message-fetch-field "x-image-url" t))
  54. raw type)
  55. (cond (face (setq raw (mh-face-to-png face)
  56. type 'png))
  57. (x-face (setq raw (mh-uncompface x-face)
  58. type 'pbm))
  59. (url (setq type 'url))
  60. (t (multiple-value-setq (type raw)
  61. (values-list (mh-picon-get-image)))))
  62. (when type
  63. (goto-char (point-min))
  64. (when (re-search-forward "^from:" (point-max) t)
  65. ;; GNU Emacs
  66. (mh-do-in-gnu-emacs
  67. (if (eq type 'url)
  68. (mh-x-image-url-display url)
  69. (mh-funcall-if-exists
  70. insert-image (create-image
  71. raw type t
  72. :foreground
  73. (mh-face-foreground 'mh-show-xface nil t)
  74. :background
  75. (mh-face-background 'mh-show-xface nil t))
  76. " ")))
  77. ;; XEmacs
  78. (mh-do-in-xemacs
  79. (cond
  80. ((eq type 'url)
  81. (mh-x-image-url-display url))
  82. ((eq type 'png)
  83. (when (featurep 'png)
  84. (set-extent-begin-glyph
  85. (make-extent (point) (point))
  86. (make-glyph (vector 'png ':data (mh-face-to-png face))))))
  87. ;; Try internal xface support if available...
  88. ((and (eq type 'pbm) (featurep 'xface))
  89. (set-glyph-face
  90. (set-extent-begin-glyph
  91. (make-extent (point) (point))
  92. (make-glyph (vector 'xface ':data (concat "X-Face: " x-face))))
  93. 'mh-show-xface))
  94. ;; Otherwise try external support with x-face...
  95. ((and (eq type 'pbm)
  96. (fboundp 'x-face-xmas-wl-display-x-face)
  97. (fboundp 'executable-find) (executable-find "uncompface"))
  98. (mh-funcall-if-exists x-face-xmas-wl-display-x-face))
  99. ;; Picon display
  100. ((and raw (member type '(xpm xbm gif)))
  101. (when (featurep type)
  102. (set-extent-begin-glyph
  103. (make-extent (point) (point))
  104. (make-glyph (vector type ':data raw))))))
  105. (when raw (insert " "))))))))
  106. (defun mh-face-to-png (data)
  107. "Convert base64 encoded DATA to png image."
  108. (with-temp-buffer
  109. (if (fboundp 'set-buffer-multibyte)
  110. (set-buffer-multibyte nil))
  111. (insert data)
  112. (ignore-errors (base64-decode-region (point-min) (point-max)))
  113. (buffer-string)))
  114. (defun mh-uncompface (data)
  115. "Run DATA through `uncompface' to generate bitmap."
  116. (with-temp-buffer
  117. (if (fboundp 'set-buffer-multibyte)
  118. (set-buffer-multibyte nil))
  119. (insert data)
  120. (when (and mh-uncompface-executable
  121. (equal (call-process-region (point-min) (point-max)
  122. mh-uncompface-executable t '(t nil))
  123. 0))
  124. (mh-icontopbm)
  125. (buffer-string))))
  126. (defun mh-icontopbm ()
  127. "Elisp substitute for `icontopbm'."
  128. (goto-char (point-min))
  129. (let ((end (point-max)))
  130. (while (re-search-forward "0x\\(..\\)\\(..\\)," nil t)
  131. (save-excursion
  132. (goto-char (point-max))
  133. (insert (string-to-number (match-string 1) 16))
  134. (insert (string-to-number (match-string 2) 16))))
  135. (delete-region (point-min) end)
  136. (goto-char (point-min))
  137. (insert "P4\n48 48\n")))
  138. ;;; Picon Display
  139. ;; XXX: This should be customizable. As a side-effect of setting this
  140. ;; variable, arrange to reset mh-picon-existing-directory-list to 'unset.
  141. (defvar mh-picon-directory-list
  142. '("~/.picons" "~/.picons/users" "~/.picons/usenix" "~/.picons/news"
  143. "~/.picons/domains" "~/.picons/misc"
  144. "/usr/share/picons/" "/usr/share/picons/users" "/usr/share/picons/usenix"
  145. "/usr/share/picons/news" "/usr/share/picons/domains"
  146. "/usr/share/picons/misc")
  147. "List of directories where picons reside.
  148. The directories are searched for in the order they appear in the list.")
  149. (defvar mh-picon-existing-directory-list 'unset
  150. "List of directories to search in.")
  151. (defvar mh-picon-cache (make-hash-table :test #'equal))
  152. (defvar mh-picon-image-types
  153. (loop for type in '(xpm xbm gif)
  154. when (or (mh-do-in-gnu-emacs
  155. (ignore-errors
  156. (mh-funcall-if-exists image-type-available-p type)))
  157. (mh-do-in-xemacs (featurep type)))
  158. collect type))
  159. (autoload 'message-tokenize-header "sendmail")
  160. (defun* mh-picon-get-image ()
  161. "Find the best possible match and return contents."
  162. (mh-picon-set-directory-list)
  163. (save-restriction
  164. (let* ((from-field (ignore-errors (car (message-tokenize-header
  165. (mh-get-header-field "from:")))))
  166. (from (car (ignore-errors
  167. (mh-funcall-if-exists ietf-drums-parse-address
  168. from-field))))
  169. (host (and from
  170. (string-match "\\([^+]*\\)\\(+.*\\)?@\\(.*\\)" from)
  171. (downcase (match-string 3 from))))
  172. (user (and host (downcase (match-string 1 from))))
  173. (canonical-address (format "%s@%s" user host))
  174. (cached-value (gethash canonical-address mh-picon-cache))
  175. (host-list (and host (delete "" (split-string host "\\."))))
  176. (match nil))
  177. (cond (cached-value (return-from mh-picon-get-image cached-value))
  178. ((not host-list) (return-from mh-picon-get-image nil)))
  179. (setq match
  180. (block loop
  181. ;; u@h search
  182. (loop for dir in mh-picon-existing-directory-list
  183. do (loop for type in mh-picon-image-types
  184. ;; [path]user@host
  185. for file1 = (format "%s/%s.%s"
  186. dir canonical-address type)
  187. when (file-exists-p file1)
  188. do (return-from loop file1)
  189. ;; [path]user
  190. for file2 = (format "%s/%s.%s" dir user type)
  191. when (file-exists-p file2)
  192. do (return-from loop file2)
  193. ;; [path]host
  194. for file3 = (format "%s/%s.%s" dir host type)
  195. when (file-exists-p file3)
  196. do (return-from loop file3)))
  197. ;; facedb search
  198. ;; Search order for user@foo.net:
  199. ;; [path]net/foo/user
  200. ;; [path]net/foo/user/face
  201. ;; [path]net/user
  202. ;; [path]net/user/face
  203. ;; [path]net/foo/unknown
  204. ;; [path]net/foo/unknown/face
  205. ;; [path]net/unknown
  206. ;; [path]net/unknown/face
  207. (loop for u in (list user "unknown")
  208. do (loop for dir in mh-picon-existing-directory-list
  209. do (loop for x on host-list by #'cdr
  210. for y = (mh-picon-generate-path x u dir)
  211. do (loop for type in mh-picon-image-types
  212. for z1 = (format "%s.%s" y type)
  213. when (file-exists-p z1)
  214. do (return-from loop z1)
  215. for z2 = (format "%s/face.%s"
  216. y type)
  217. when (file-exists-p z2)
  218. do (return-from loop z2)))))))
  219. (setf (gethash canonical-address mh-picon-cache)
  220. (mh-picon-file-contents match)))))
  221. (defun mh-picon-set-directory-list ()
  222. "Update `mh-picon-existing-directory-list' if needed."
  223. (when (eq mh-picon-existing-directory-list 'unset)
  224. (setq mh-picon-existing-directory-list
  225. (loop for x in mh-picon-directory-list
  226. when (file-directory-p x) collect x))))
  227. (defun mh-picon-generate-path (host-list user directory)
  228. "Generate the image file path.
  229. HOST-LIST is the parsed host address of the email address, USER
  230. the username and DIRECTORY is the directory relative to which the
  231. path is generated."
  232. (loop with acc = ""
  233. for elem in host-list
  234. do (setq acc (format "%s/%s" elem acc))
  235. finally return (format "%s/%s%s" directory acc user)))
  236. (defun mh-picon-file-contents (file)
  237. "Return details about FILE.
  238. A list of consisting of a symbol for the type of the file and the
  239. file contents as a string is returned. If FILE is nil, then both
  240. elements of the list are nil."
  241. (if (stringp file)
  242. (with-temp-buffer
  243. (if (fboundp 'set-buffer-multibyte)
  244. (set-buffer-multibyte nil))
  245. (let ((type (and (string-match ".*\\.\\(...\\)$" file)
  246. (intern (match-string 1 file)))))
  247. (insert-file-contents-literally file)
  248. (list type (buffer-string))))
  249. (list nil nil)))
  250. ;;; X-Image-URL Display
  251. (defvar mh-x-image-scaling-function
  252. (cond ((executable-find "convert")
  253. 'mh-x-image-scale-with-convert)
  254. ((and (executable-find "anytopnm") (executable-find "pnmscale")
  255. (executable-find "pnmtopng"))
  256. 'mh-x-image-scale-with-pnm)
  257. (t 'ignore))
  258. "Function to use to scale image to proper size.")
  259. (defun mh-x-image-scale-with-pnm (input output)
  260. "Scale image in INPUT file and write to OUTPUT file using pnm tools."
  261. (let ((res (shell-command-to-string
  262. (format "anytopnm < %s | pnmscale -xysize 96 48 | pnmtopng > %s"
  263. input output))))
  264. (unless (equal res "")
  265. (delete-file output))))
  266. (defun mh-x-image-scale-with-convert (input output)
  267. "Scale image in INPUT file and write to OUTPUT file using ImageMagick."
  268. (call-process "convert" nil nil nil "-geometry" "96x48" input output))
  269. (defvar mh-wget-executable nil)
  270. (defvar mh-wget-choice
  271. (or (and (setq mh-wget-executable (executable-find "wget")) 'wget)
  272. (and (setq mh-wget-executable (executable-find "fetch")) 'fetch)
  273. (and (setq mh-wget-executable (executable-find "curl")) 'curl)))
  274. (defvar mh-wget-option
  275. (cdr (assoc mh-wget-choice '((curl . "-o") (fetch . "-o") (wget . "-O")))))
  276. (defvar mh-x-image-temp-file nil)
  277. (defvar mh-x-image-url nil)
  278. (defvar mh-x-image-marker nil)
  279. (defvar mh-x-image-url-cache-file nil)
  280. (defun mh-x-image-url-display (url)
  281. "Display image from location URL.
  282. If the URL isn't present in the cache then it is fetched with wget."
  283. (let* ((cache-filename (mh-x-image-url-cache-canonicalize url))
  284. (state (mh-x-image-get-download-state cache-filename))
  285. (marker (set-marker (make-marker) (point))))
  286. (set (make-local-variable 'mh-x-image-marker) marker)
  287. (cond ((not (mh-x-image-url-sane-p url)))
  288. ((eq state 'ok)
  289. (mh-x-image-display cache-filename marker))
  290. ((or (not mh-wget-executable)
  291. (eq mh-x-image-scaling-function 'ignore)))
  292. ((eq state 'never))
  293. ((not mh-fetch-x-image-url)
  294. (set-marker marker nil))
  295. ((eq state 'try-again)
  296. (mh-x-image-set-download-state cache-filename nil)
  297. (mh-x-image-url-fetch-image url cache-filename marker
  298. 'mh-x-image-scale-and-display))
  299. ((and (eq mh-fetch-x-image-url 'ask)
  300. (not (y-or-n-p (format "Fetch %s? " url))))
  301. (mh-x-image-set-download-state cache-filename 'never))
  302. ((eq state nil)
  303. (mh-x-image-url-fetch-image url cache-filename marker
  304. 'mh-x-image-scale-and-display)))))
  305. (defvar mh-x-image-cache-directory nil
  306. "Directory where X-Image-URL images are cached.")
  307. ;;;###mh-autoload
  308. (defun mh-set-x-image-cache-directory (directory)
  309. "Set the DIRECTORY where X-Image-URL images are cached.
  310. This is only done if `mh-x-image-cache-directory' is nil."
  311. ;; XXX This is the code that used to be in find-user-path. Is there
  312. ;; a good reason why the variable is set conditionally? Do we expect
  313. ;; the user to have set this variable directly?
  314. (unless mh-x-image-cache-directory
  315. (setq mh-x-image-cache-directory directory)))
  316. (defun mh-x-image-url-cache-canonicalize (url)
  317. "Canonicalize URL.
  318. Replace the ?/ character with a ?! character and append .png.
  319. Also replaces special characters with `mh-url-hexify-string'
  320. since not all characters, such as :, are valid within Windows
  321. filenames. In addition, replaces * with %2a. See URL
  322. `http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/ifaces/iitemnamelimits/GetValidCharacters.asp'."
  323. (format "%s/%s.png" mh-x-image-cache-directory
  324. (mh-replace-regexp-in-string
  325. "\*" "%2a"
  326. (mh-url-hexify-string
  327. (with-temp-buffer
  328. (insert url)
  329. (mh-replace-string "/" "!")
  330. (buffer-string))))))
  331. (defun mh-x-image-get-download-state (file)
  332. "Check the state of FILE by following any symbolic links."
  333. (unless (file-exists-p mh-x-image-cache-directory)
  334. (call-process "mkdir" nil nil nil mh-x-image-cache-directory))
  335. (cond ((file-symlink-p file)
  336. (intern (file-name-nondirectory (file-chase-links file))))
  337. ((not (file-exists-p file)) nil)
  338. (t 'ok)))
  339. (defun mh-x-image-set-download-state (file data)
  340. "Setup a symbolic link from FILE to DATA."
  341. (if data
  342. (make-symbolic-link (symbol-name data) file t)
  343. (delete-file file)))
  344. (defun mh-x-image-url-sane-p (url)
  345. "Check if URL is something sensible."
  346. (let ((len (length url)))
  347. (cond ((< len 5) nil)
  348. ((not (equal (substring url 0 5) "http:")) nil)
  349. ((> len 100) nil)
  350. (t t))))
  351. (defun mh-x-image-display (image marker)
  352. "Display IMAGE at MARKER."
  353. (with-current-buffer (marker-buffer marker)
  354. (let ((inhibit-read-only t)
  355. (buffer-modified-flag (buffer-modified-p)))
  356. (unwind-protect
  357. (when (and (file-readable-p image) (not (file-symlink-p image))
  358. (eq marker mh-x-image-marker))
  359. (goto-char marker)
  360. (mh-do-in-gnu-emacs
  361. (mh-funcall-if-exists insert-image (create-image image 'png)))
  362. (mh-do-in-xemacs
  363. (when (featurep 'png)
  364. (set-extent-begin-glyph
  365. (make-extent (point) (point))
  366. (make-glyph
  367. (vector 'png ':data (with-temp-buffer
  368. (insert-file-contents-literally image)
  369. (buffer-string))))))))
  370. (set-buffer-modified-p buffer-modified-flag)))))
  371. (defun mh-x-image-url-fetch-image (url cache-file marker sentinel)
  372. "Fetch and display the image specified by URL.
  373. After the image is fetched, it is stored in CACHE-FILE. It will
  374. be displayed in a buffer and position specified by MARKER. The
  375. actual display is carried out by the SENTINEL function."
  376. (if mh-wget-executable
  377. (let ((buffer (get-buffer-create (generate-new-buffer-name
  378. mh-temp-fetch-buffer)))
  379. (filename (or (mh-funcall-if-exists make-temp-file "mhe-fetch")
  380. (expand-file-name (make-temp-name "~/mhe-fetch")))))
  381. (with-current-buffer buffer
  382. (set (make-local-variable 'mh-x-image-url-cache-file) cache-file)
  383. (set (make-local-variable 'mh-x-image-marker) marker)
  384. (set (make-local-variable 'mh-x-image-temp-file) filename))
  385. (set-process-sentinel
  386. (start-process "*mh-x-image-url-fetch*" buffer
  387. mh-wget-executable mh-wget-option filename url)
  388. sentinel))
  389. ;; Temporary failure
  390. (mh-x-image-set-download-state cache-file 'try-again)))
  391. (defun mh-x-image-scale-and-display (process change)
  392. "When the wget PROCESS terminates scale and display image.
  393. The argument CHANGE is ignored."
  394. (when (eq (process-status process) 'exit)
  395. (let (marker temp-file cache-filename wget-buffer)
  396. (with-current-buffer (setq wget-buffer (process-buffer process))
  397. (setq marker mh-x-image-marker
  398. cache-filename mh-x-image-url-cache-file
  399. temp-file mh-x-image-temp-file))
  400. (cond
  401. ;; Check if we have `convert'
  402. ((eq mh-x-image-scaling-function 'ignore)
  403. (message "The \"convert\" program is needed to display X-Image-URL")
  404. (mh-x-image-set-download-state cache-filename 'try-again))
  405. ;; Scale fetched image
  406. ((and (funcall mh-x-image-scaling-function temp-file cache-filename)
  407. nil))
  408. ;; Attempt to display image if we have it
  409. ((file-exists-p cache-filename)
  410. (mh-x-image-display cache-filename marker))
  411. ;; We didn't find the image. Should we try to display it the next time?
  412. (t (mh-x-image-set-download-state cache-filename 'try-again)))
  413. (ignore-errors
  414. (set-marker marker nil)
  415. (delete-process process)
  416. (kill-buffer wget-buffer)
  417. (delete-file temp-file)))))
  418. (provide 'mh-xface)
  419. ;; Local Variables:
  420. ;; indent-tabs-mode: nil
  421. ;; sentence-end-double-space: nil
  422. ;; End:
  423. ;;; mh-xface.el ends here