org-mu4e-compose.el 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. ;;; org-mu4e-compose.el --- Write mu4e messages with org-mode. -*- lexical-binding: t; -*-
  2. ;;; Commentary:
  3. ;; I use evil. This file does not depend on evil, but some of these keybindings
  4. ;; shadow useful org keybinding with message mode keybindings because the org
  5. ;; bindings being shadowed are available with evil under some other key sequence.
  6. ;;; Code:
  7. (require 'mu4e)
  8. (require 'org-mime)
  9. (require 'shr)
  10. (require 'dom)
  11. (require 'sgml-mode)
  12. (require 'cl-lib)
  13. (defvar-local org-mu4e--html-message-p t
  14. "Weather or not the current message should be htmlized.")
  15. (defvar-local org-mu4e--override-org-mode-check nil
  16. "Internal variable.
  17. See `org-mu4e--override-org-mode-check-advice' for information about what this
  18. does.")
  19. (defvar org-mu4e--internal-message-mode-function
  20. (symbol-function 'mu4e-compose-mode)
  21. "The `message-mode' (or derived mode) used by `org-mu4e-compose-mode'.")
  22. (defun org-mu4e--override-org-mode-check-advice (oldfun &rest r)
  23. "Around advice for various org mode functions.
  24. This function will call OLDFUN with arguments R with `major-mode' let-bound to
  25. \\='org-mode when `org-mu4e--override-org-mode-check' is t."
  26. (let ((major-mode (if org-mu4e--override-org-mode-check
  27. 'org-mode
  28. major-mode)))
  29. (apply oldfun r)))
  30. (advice-add 'org-element-at-point :around
  31. 'org-mu4e--override-org-mode-check-advice)
  32. (defun org-mu4e-toggle-htmlize-mssage (&optional arg no-message)
  33. "Toggle weather the current message should be htmlized.
  34. If ARG is a positive number or zero, enable htmlization, if it is negative,
  35. disable it. Otherwise, toggle it. With NO-MESSAGE, don't display a message
  36. about this change."
  37. (interactive "P")
  38. (setq org-mu4e--html-message-p (or (wholenump arg)
  39. (and (not arg)
  40. (not org-mu4e--html-message-p))))
  41. (unless no-message
  42. (message "Message will be %ssent with an HTML part."
  43. (if org-mu4e--html-message-p "" "not ")))
  44. (force-mode-line-update))
  45. (defun org-mu4e--bounds-of-mime-part (type)
  46. "Find the bounds of the mime part for TYPE in the current buffer."
  47. (save-excursion
  48. (goto-char (point-min))
  49. (when (and
  50. (re-search-forward (rx bol (literal mail-header-separator) eol)
  51. nil t)
  52. (re-search-forward (rx "<#multipart" (* any) ">")
  53. nil t)
  54. (re-search-forward (rx "<#part " (* any)
  55. "type=" (literal type) (* any) ">")
  56. nil t))
  57. (let ((start (match-end 0))
  58. (end (point-max)))
  59. (when (re-search-forward
  60. (rx (or (and "<#/" (or "part" "multipart") ">")
  61. (and "<#part" (* any) ">")))
  62. nil t)
  63. (setq end (match-beginning 0)))
  64. (cons (1+ start) end)))))
  65. (defun org-mu4e--pretty-print-fontify-html-part ()
  66. "Pretty print and fontify the HTML part of the current buffer."
  67. (when-let ((bounds (org-mu4e--bounds-of-mime-part "text/html"))
  68. (real-buf (current-buffer)))
  69. (save-excursion
  70. (let ((content
  71. (with-temp-buffer
  72. (insert-buffer-substring real-buf (car bounds) (cdr bounds))
  73. (let (sgml-mode-hook html-mode-hook text-mode-hook)
  74. (html-mode))
  75. (sgml-pretty-print (point-min) (point-max))
  76. (indent-region (point-min) (point-max))
  77. (put-text-property (point-min) (point-max) 'fontified nil)
  78. (font-lock-ensure)
  79. (buffer-string))))
  80. (delete-region (car bounds) (cdr bounds))
  81. (goto-char (car bounds))
  82. (insert content)))))
  83. (defun org-mu4e--htmlize-and-cleanup ()
  84. "HTMLize and cleanup the visible portion of the buffer.
  85. This moves point, wrap it in `save-excursion' if that is a problem."
  86. (org-mime-htmlize)
  87. ;; IDK why, but the above function adds a bunch of newlines to the end
  88. ;; of the buffer.
  89. (goto-char (point-min))
  90. (when (re-search-forward (rx (group (* "\n")) "\n" eos) nil t)
  91. (delete-region (match-beginning 1)
  92. (match-end 1)))
  93. (font-lock-ensure)
  94. (org-mu4e--pretty-print-fontify-html-part))
  95. (defun org-mu4e-preview-html ()
  96. "Preview the HTML version of the current buffer in a new buffer.
  97. Return the newly created buffer."
  98. (interactive)
  99. (let ((msg-buffer (current-buffer))
  100. (buffer (get-buffer-create "*Org-Mu4e HTML Preview*"))
  101. (bounds (point-min))
  102. (cur-max (point-max)))
  103. (without-restriction
  104. (with-current-buffer buffer
  105. (special-mode)
  106. (setq-local org-mu4e--override-org-mode-check t)
  107. ;; Setup font-lock without all the other pesky major mode stuff
  108. (org-set-font-lock-defaults)
  109. (font-lock-add-keywords nil message-font-lock-keywords)
  110. (let ((inhibit-read-only t))
  111. (erase-buffer)
  112. (insert-buffer-substring msg-buffer)
  113. (narrow-to-region bounds cur-max)
  114. (org-mu4e--htmlize-and-cleanup))
  115. (goto-char (point-min))))
  116. (switch-to-buffer-other-window buffer)
  117. buffer))
  118. (defun org-mu4e-render-preview ()
  119. "Render a preview of the HTML message."
  120. (interactive)
  121. (let ((msg-buffer (current-buffer))
  122. (buffer (get-buffer-create "*Org-Mu4e Render Preview*")))
  123. (save-excursion
  124. (without-restriction
  125. (goto-char (point-min))
  126. (if (re-search-forward (rx bol (literal mail-header-separator) eol)
  127. nil t)
  128. (let* ((start (1+ (match-end 0)))
  129. (org-export-with-latex org-mime-org-html-with-latex-default)
  130. (org-preview-latex-image-directory
  131. (expand-file-name "ltximg/" mm-tmp-directory))
  132. (default-directory org-preview-latex-image-directory)
  133. (org-html-postamble nil))
  134. (narrow-to-region start (point-max))
  135. (if-let ((export-data (org-export-as
  136. 'html nil t nil
  137. org-mime-export-options)))
  138. (progn
  139. (with-current-buffer buffer
  140. (special-mode)
  141. (let ((inhibit-read-only t)
  142. (default-directory
  143. org-preview-latex-image-directory))
  144. (erase-buffer)
  145. (insert export-data)
  146. (shr-render-region (point-min) (point-max))
  147. ;; The above function inserts a text directionality
  148. ;; character and then two newlines, just to be safe,
  149. ;; check for them, then hide them
  150. (goto-char (point-min))
  151. (let ((new-start (point-min)))
  152. (when (or (eq (char-after) #x200e)
  153. (eq (char-after) #x200f))
  154. (cl-incf new-start))
  155. (dotimes (_ 2)
  156. (forward-char)
  157. (when (eq (char-after) ?\n)
  158. (cl-incf new-start)))
  159. (narrow-to-region new-start (point-max)))))
  160. (switch-to-buffer-other-window buffer))
  161. (user-error "HTML export failed")))
  162. (user-error "Can't find message start in current buffer"))))))
  163. (defun org-mu4e-send (&optional arg)
  164. "HTMLize and send the message in the current buffer.
  165. ARG is passed directly to `message-send'."
  166. ;; This has to return a non-nil value so that org knows we handled the C-c C-c
  167. (interactive "P")
  168. (let ((modified (buffer-modified-p))
  169. ;; we only restore the restriction if the sending below fails
  170. (old-rest (cons (point-min) (point-max))))
  171. (widen)
  172. (let ((save-text (buffer-substring-no-properties (point-min)
  173. (point-max))))
  174. (condition-case _
  175. (progn
  176. (when org-mu4e--html-message-p
  177. (org-mu4e--htmlize-and-cleanup))
  178. (message-send arg)
  179. 'sent)
  180. ((or error quit)
  181. (erase-buffer)
  182. (insert save-text)
  183. (narrow-to-region (car old-rest) (cdr old-rest))
  184. (restore-buffer-modified-p modified)
  185. 'failed)))))
  186. (defun org-mu4e-send-and-exit (&optional arg)
  187. "Call `org-mu4e-send', the save and kill the buffer.
  188. ARG is passed directly to `message-send'."
  189. (interactive "P")
  190. (when (eq (org-mu4e-send arg) 'sent)
  191. (message-kill-buffer))
  192. t ;; this tells org that we have handled the C-c C-c
  193. )
  194. ;;;###autoload
  195. (defun org-mu4e-compose-new (&rest r)
  196. "This is like `mu4e-compose-new', but it utilizes `org-mu4e-compose-mode'.
  197. Each of the arguments in R are the same as `mu4e-compose-new', and are directly
  198. passed to it."
  199. (interactive)
  200. ;; Save local variables set by `mu4e-compose-new'
  201. (let ((org-mu4e--internal-message-mode-function
  202. (symbol-function 'mu4e-compose-mode)))
  203. (cl-letf (((symbol-function 'mu4e-compose-mode) 'org-mu4e-compose-mode))
  204. (apply 'mu4e-compose-new r))))
  205. ;;;###autoload
  206. (defun org-mu4e-compose-reply-to (&optional to wide)
  207. "This is like `mu4e-compose-reply-to', but utilizes `org-mu4e-compose-mode'.
  208. TO and WIDE are the same as `mu4e-compose-reply-to'."
  209. (interactive)
  210. ;; Save local variables set by `mu4e-compose-reply-to'
  211. (let ((html-part-p (seq-find (lambda (handle)
  212. (equal (mm-handle-media-type (cdr handle))
  213. "text/html"))
  214. gnus-article-mime-handle-alist))
  215. (org-mu4e--internal-message-mode-function
  216. (symbol-function 'mu4e-compose-mode)))
  217. (cl-letf (((symbol-function 'mu4e-compose-mode) 'org-mu4e-compose-mode))
  218. (let ((buf (mu4e-compose-reply-to to wide)))
  219. (with-current-buffer buf
  220. (setq org-mu4e--html-message-p
  221. ;; make the variable look nicer by not having random data in it
  222. (not (not html-part-p))))))))
  223. ;;;###autoload
  224. (defun org-mu4e-compose-reply (&optional wide)
  225. "This is like `mu4e-compose-reply', but utilizes `org-mu4e-compose-mode'.
  226. WIDE is the same as `mu4e-compose-reply'."
  227. (interactive "P")
  228. (org-mu4e-compose-reply-to nil wide))
  229. ;;;###autoload
  230. (defvar-keymap org-mu4e-compose-mode-map
  231. :parent org-mode-map
  232. ;; These come straight from `message-mode-map' and override `org-mode-map'
  233. "C-c C-f C-t" #'message-goto-to
  234. "C-c C-f C-o" #'message-goto-from
  235. "C-c C-f C-b" #'message-goto-bcc
  236. "C-c C-f C-w" #'message-goto-fcc
  237. "C-c C-f C-c" #'message-goto-cc
  238. "C-c C-f C-s" #'message-goto-subject
  239. "C-c C-f C-r" #'message-goto-reply-to
  240. "C-c C-f C-d" #'message-goto-distribution
  241. "C-c C-f C-f" #'message-goto-followup-to
  242. "C-c C-f C-m" #'message-goto-mail-followup-to
  243. "C-c C-f C-k" #'message-goto-keywords
  244. "C-c C-f C-u" #'message-goto-summary
  245. "C-c C-f C-i" #'message-insert-or-toggle-importance
  246. "C-c C-f C-a" #'message-generate-unsubscribed-mail-followup-to
  247. ;; modify headers (and insert notes in body)
  248. "C-c C-f s" #'message-change-subject
  249. ;;
  250. "C-c C-f x" #'message-cross-post-followup-to
  251. ;; prefix+message-cross-post-followup-to = same without cross-post
  252. "C-c C-f t" #'message-reduce-to-to-cc
  253. "C-c C-f a" #'message-add-archive-header
  254. ;; mark inserted text
  255. "C-c M-m" #'message-mark-inserted-region
  256. "C-c M-f" #'message-mark-insert-file
  257. "C-c C-b" #'message-goto-body
  258. "C-c C-i" #'message-goto-signature
  259. "C-c C-t" #'message-insert-to
  260. "C-c C-f w" #'message-insert-wide-reply
  261. "C-c C-f C-e" #'message-insert-expires
  262. "C-c M-u" #'message-insert-or-toggle-importance
  263. "C-c M-n" #'message-insert-disposition-notification-to
  264. "C-c C-y" #'message-yank-original
  265. "C-c C-M-y" #'message-yank-buffer
  266. "C-c C-S-q" #'message-fill-yanked-message
  267. "C-c M-s" #'message-insert-signature
  268. "C-c M-h" #'message-insert-headers
  269. "C-c M-o" #'message-sort-headers
  270. ;; C-c C-c to send and exit is handled by `org-ctrl-c-ctrl-c-hook'
  271. "C-c C-s" #'org-mu4e-send
  272. "C-c C-k" #'message-kill-buffer
  273. "C-c C-d" #'message-dont-send
  274. "C-c M-k" #'message-kill-address
  275. "C-c M-e" #'message-elide-region
  276. "C-c M-v" #'message-delete-not-region
  277. "C-c M-z" #'message-kill-to-signature
  278. "<remap> <split-line>" #'message-split-line
  279. "<remap> <beginning-of-buffer>" #'mu4e-compose-goto-top
  280. "<remap> <end-of-buffer>" #'mu4e-compose-goto-bottom
  281. "C-c M-r" #'message-insert-screenshot
  282. "M-n" #'message-display-abbrev
  283. "C-c C-a" #'mail-add-attachment
  284. "C-c M-t" #'org-mu4e-toggle-htmlize-mssage
  285. "C-c M-p C-p" #'org-mu4e-preview-html
  286. "C-c M-p C-w" #'org-mu4e-render-preview
  287. "C-c C-;" #'mu4e-compose-context-switch)
  288. ;;;###autoload
  289. (define-derived-mode org-mu4e-compose-mode org-mode "mu4e:org-compose"
  290. "Major mode for editing mu4e messages with `org-mode' syntax.
  291. This is derived from `org-mode', but it also essentially runs
  292. `mu4e-compose-mode' and `message-mode'. Therefore, it runs their hooks too."
  293. ;; Enable all the things from `mu4e-compose-mode' (which derives from
  294. ;; `message-mode'), but don't let it change the major mode (or other things we
  295. ;; care about).
  296. (when org-mu4e--internal-message-mode-function
  297. (let ((major-mode major-mode)
  298. (mode-name mode-name)
  299. (local-abbrev-table local-abbrev-table)
  300. (font-lock-defaults font-lock-defaults)
  301. ;; some of these are not actually changed, but they are here just in
  302. ;; case they change in the future...
  303. (comment-start comment-start)
  304. (comment-end comment-end)
  305. (comment-start-skip comment-start-skip)
  306. (comment-add comment-add)
  307. (comment-style comment-style))
  308. (cl-letf (((symbol-function 'kill-all-local-variables) 'ignore)
  309. ((symbol-function 'use-local-map) 'ignore)
  310. ((symbol-function 'set-syntax-table) 'ignore))
  311. (funcall org-mu4e--internal-message-mode-function))))
  312. ;; Add `message-mode' keyword and quote highlighting on top of the org syntax
  313. ;; highlighting
  314. (font-lock-add-keywords nil message-font-lock-keywords)
  315. (setq-local org-mu4e--override-org-mode-check t)
  316. (add-to-list (make-local-variable 'org-ctrl-c-ctrl-c-final-hook)
  317. 'org-mu4e-send-and-exit)
  318. (add-to-list (make-local-variable 'mode-line-misc-info)
  319. '(:eval (if org-mu4e--html-message-p
  320. "Text/HTML "
  321. "Text Only "))))
  322. ;;;###autoload
  323. (define-mail-user-agent 'org-mu4e-user-agent
  324. #'org-mu4e-compose-new
  325. #'org-mu4e-send-and-exit
  326. #'message-kill-buffer
  327. 'message-send-hook)
  328. ;;;###autoload
  329. (defun org-mu4e-user-agent ()
  330. "Return `org-mu4e-user-agent'."
  331. 'org-mu4e-user-agent)
  332. (provide 'org-mu4e-compose)
  333. ;;; org-mu4e-compose.el ends here