123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- ;;; org-mu4e-compose.el --- Write mu4e messages with org-mode. -*- lexical-binding: t; -*-
- ;;; Commentary:
- ;; I use evil. This file does not depend on evil, but some of these keybindings
- ;; shadow useful org keybinding with message mode keybindings because the org
- ;; bindings being shadowed are available with evil under some other key sequence.
- ;;; Code:
- (require 'mu4e)
- (require 'org-mime)
- (require 'shr)
- (require 'dom)
- (require 'sgml-mode)
- (require 'cl-lib)
- (defvar-local org-mu4e--html-message-p t
- "Weather or not the current message should be htmlized.")
- (defvar-local org-mu4e--override-org-mode-check nil
- "Internal variable.
- See `org-mu4e--override-org-mode-check-advice' for information about what this
- does.")
- (defvar org-mu4e--internal-message-mode-function
- (symbol-function 'mu4e-compose-mode)
- "The `message-mode' (or derived mode) used by `org-mu4e-compose-mode'.")
- (defun org-mu4e--override-org-mode-check-advice (oldfun &rest r)
- "Around advice for various org mode functions.
- This function will call OLDFUN with arguments R with `major-mode' let-bound to
- \\='org-mode when `org-mu4e--override-org-mode-check' is t."
- (let ((major-mode (if org-mu4e--override-org-mode-check
- 'org-mode
- major-mode)))
- (apply oldfun r)))
- (advice-add 'org-element-at-point :around
- 'org-mu4e--override-org-mode-check-advice)
- (defun org-mu4e-toggle-htmlize-mssage (&optional arg no-message)
- "Toggle weather the current message should be htmlized.
- If ARG is a positive number or zero, enable htmlization, if it is negative,
- disable it. Otherwise, toggle it. With NO-MESSAGE, don't display a message
- about this change."
- (interactive "P")
- (setq org-mu4e--html-message-p (or (wholenump arg)
- (and (not arg)
- (not org-mu4e--html-message-p))))
- (unless no-message
- (message "Message will be %ssent with an HTML part."
- (if org-mu4e--html-message-p "" "not ")))
- (force-mode-line-update))
- (defun org-mu4e--bounds-of-mime-part (type)
- "Find the bounds of the mime part for TYPE in the current buffer."
- (save-excursion
- (goto-char (point-min))
- (when (and
- (re-search-forward (rx bol (literal mail-header-separator) eol)
- nil t)
- (re-search-forward (rx "<#multipart" (* any) ">")
- nil t)
- (re-search-forward (rx "<#part " (* any)
- "type=" (literal type) (* any) ">")
- nil t))
- (let ((start (match-end 0))
- (end (point-max)))
- (when (re-search-forward
- (rx (or (and "<#/" (or "part" "multipart") ">")
- (and "<#part" (* any) ">")))
- nil t)
- (setq end (match-beginning 0)))
- (cons (1+ start) end)))))
- (defun org-mu4e--pretty-print-fontify-html-part ()
- "Pretty print and fontify the HTML part of the current buffer."
- (when-let ((bounds (org-mu4e--bounds-of-mime-part "text/html"))
- (real-buf (current-buffer)))
- (save-excursion
- (let ((content
- (with-temp-buffer
- (insert-buffer-substring real-buf (car bounds) (cdr bounds))
- (let (sgml-mode-hook html-mode-hook text-mode-hook)
- (html-mode))
- (sgml-pretty-print (point-min) (point-max))
- (indent-region (point-min) (point-max))
- (put-text-property (point-min) (point-max) 'fontified nil)
- (font-lock-ensure)
- (buffer-string))))
- (delete-region (car bounds) (cdr bounds))
- (goto-char (car bounds))
- (insert content)))))
- (defun org-mu4e--htmlize-and-cleanup ()
- "HTMLize and cleanup the visible portion of the buffer.
- This moves point, wrap it in `save-excursion' if that is a problem."
- (org-mime-htmlize)
- ;; IDK why, but the above function adds a bunch of newlines to the end
- ;; of the buffer.
- (goto-char (point-min))
- (when (re-search-forward (rx (group (* "\n")) "\n" eos) nil t)
- (delete-region (match-beginning 1)
- (match-end 1)))
- (font-lock-ensure)
- (org-mu4e--pretty-print-fontify-html-part))
- (defun org-mu4e-preview-html ()
- "Preview the HTML version of the current buffer in a new buffer.
- Return the newly created buffer."
- (interactive)
- (let ((msg-buffer (current-buffer))
- (buffer (get-buffer-create "*Org-Mu4e HTML Preview*"))
- (bounds (point-min))
- (cur-max (point-max)))
- (without-restriction
- (with-current-buffer buffer
- (special-mode)
- (setq-local org-mu4e--override-org-mode-check t)
- ;; Setup font-lock without all the other pesky major mode stuff
- (org-set-font-lock-defaults)
- (font-lock-add-keywords nil message-font-lock-keywords)
- (let ((inhibit-read-only t))
- (erase-buffer)
- (insert-buffer-substring msg-buffer)
- (narrow-to-region bounds cur-max)
- (org-mu4e--htmlize-and-cleanup))
- (goto-char (point-min))))
- (switch-to-buffer-other-window buffer)
- buffer))
- (defun org-mu4e-render-preview ()
- "Render a preview of the HTML message."
- (interactive)
- (let ((msg-buffer (current-buffer))
- (buffer (get-buffer-create "*Org-Mu4e Render Preview*")))
- (save-excursion
- (without-restriction
- (goto-char (point-min))
- (if (re-search-forward (rx bol (literal mail-header-separator) eol)
- nil t)
- (let* ((start (1+ (match-end 0)))
- (org-export-with-latex org-mime-org-html-with-latex-default)
- (org-preview-latex-image-directory
- (expand-file-name "ltximg/" mm-tmp-directory))
- (default-directory org-preview-latex-image-directory)
- (org-html-postamble nil))
- (narrow-to-region start (point-max))
- (if-let ((export-data (org-export-as
- 'html nil t nil
- org-mime-export-options)))
- (progn
- (with-current-buffer buffer
- (special-mode)
- (let ((inhibit-read-only t)
- (default-directory
- org-preview-latex-image-directory))
- (erase-buffer)
- (insert export-data)
- (shr-render-region (point-min) (point-max))
- ;; The above function inserts a text directionality
- ;; character and then two newlines, just to be safe,
- ;; check for them, then hide them
- (goto-char (point-min))
- (let ((new-start (point-min)))
- (when (or (eq (char-after) #x200e)
- (eq (char-after) #x200f))
- (cl-incf new-start))
- (dotimes (_ 2)
- (forward-char)
- (when (eq (char-after) ?\n)
- (cl-incf new-start)))
- (narrow-to-region new-start (point-max)))))
- (switch-to-buffer-other-window buffer))
- (user-error "HTML export failed")))
- (user-error "Can't find message start in current buffer"))))))
- (defun org-mu4e-send (&optional arg)
- "HTMLize and send the message in the current buffer.
- ARG is passed directly to `message-send'."
- ;; This has to return a non-nil value so that org knows we handled the C-c C-c
- (interactive "P")
- (let ((modified (buffer-modified-p))
- ;; we only restore the restriction if the sending below fails
- (old-rest (cons (point-min) (point-max))))
- (widen)
- (let ((save-text (buffer-substring-no-properties (point-min)
- (point-max))))
- (condition-case _
- (progn
- (when org-mu4e--html-message-p
- (org-mu4e--htmlize-and-cleanup))
- (message-send arg)
- 'sent)
- ((or error quit)
- (erase-buffer)
- (insert save-text)
- (narrow-to-region (car old-rest) (cdr old-rest))
- (restore-buffer-modified-p modified)
- 'failed)))))
- (defun org-mu4e-send-and-exit (&optional arg)
- "Call `org-mu4e-send', the save and kill the buffer.
- ARG is passed directly to `message-send'."
- (interactive "P")
- (when (eq (org-mu4e-send arg) 'sent)
- (message-kill-buffer))
- t ;; this tells org that we have handled the C-c C-c
- )
- ;;;###autoload
- (defun org-mu4e-compose-new (&rest r)
- "This is like `mu4e-compose-new', but it utilizes `org-mu4e-compose-mode'.
- Each of the arguments in R are the same as `mu4e-compose-new', and are directly
- passed to it."
- (interactive)
- ;; Save local variables set by `mu4e-compose-new'
- (let ((org-mu4e--internal-message-mode-function
- (symbol-function 'mu4e-compose-mode)))
- (cl-letf (((symbol-function 'mu4e-compose-mode) 'org-mu4e-compose-mode))
- (apply 'mu4e-compose-new r))))
- ;;;###autoload
- (defun org-mu4e-compose-reply-to (&optional to wide)
- "This is like `mu4e-compose-reply-to', but utilizes `org-mu4e-compose-mode'.
- TO and WIDE are the same as `mu4e-compose-reply-to'."
- (interactive)
- ;; Save local variables set by `mu4e-compose-reply-to'
- (let ((html-part-p (seq-find (lambda (handle)
- (equal (mm-handle-media-type (cdr handle))
- "text/html"))
- gnus-article-mime-handle-alist))
- (org-mu4e--internal-message-mode-function
- (symbol-function 'mu4e-compose-mode)))
- (cl-letf (((symbol-function 'mu4e-compose-mode) 'org-mu4e-compose-mode))
- (let ((buf (mu4e-compose-reply-to to wide)))
- (with-current-buffer buf
- (setq org-mu4e--html-message-p
- ;; make the variable look nicer by not having random data in it
- (not (not html-part-p))))))))
- ;;;###autoload
- (defun org-mu4e-compose-reply (&optional wide)
- "This is like `mu4e-compose-reply', but utilizes `org-mu4e-compose-mode'.
- WIDE is the same as `mu4e-compose-reply'."
- (interactive "P")
- (org-mu4e-compose-reply-to nil wide))
- ;;;###autoload
- (defvar-keymap org-mu4e-compose-mode-map
- :parent org-mode-map
- ;; These come straight from `message-mode-map' and override `org-mode-map'
- "C-c C-f C-t" #'message-goto-to
- "C-c C-f C-o" #'message-goto-from
- "C-c C-f C-b" #'message-goto-bcc
- "C-c C-f C-w" #'message-goto-fcc
- "C-c C-f C-c" #'message-goto-cc
- "C-c C-f C-s" #'message-goto-subject
- "C-c C-f C-r" #'message-goto-reply-to
- "C-c C-f C-d" #'message-goto-distribution
- "C-c C-f C-f" #'message-goto-followup-to
- "C-c C-f C-m" #'message-goto-mail-followup-to
- "C-c C-f C-k" #'message-goto-keywords
- "C-c C-f C-u" #'message-goto-summary
- "C-c C-f C-i" #'message-insert-or-toggle-importance
- "C-c C-f C-a" #'message-generate-unsubscribed-mail-followup-to
- ;; modify headers (and insert notes in body)
- "C-c C-f s" #'message-change-subject
- ;;
- "C-c C-f x" #'message-cross-post-followup-to
- ;; prefix+message-cross-post-followup-to = same without cross-post
- "C-c C-f t" #'message-reduce-to-to-cc
- "C-c C-f a" #'message-add-archive-header
- ;; mark inserted text
- "C-c M-m" #'message-mark-inserted-region
- "C-c M-f" #'message-mark-insert-file
- "C-c C-b" #'message-goto-body
- "C-c C-i" #'message-goto-signature
- "C-c C-t" #'message-insert-to
- "C-c C-f w" #'message-insert-wide-reply
- "C-c C-f C-e" #'message-insert-expires
- "C-c M-u" #'message-insert-or-toggle-importance
- "C-c M-n" #'message-insert-disposition-notification-to
- "C-c C-y" #'message-yank-original
- "C-c C-M-y" #'message-yank-buffer
- "C-c C-S-q" #'message-fill-yanked-message
- "C-c M-s" #'message-insert-signature
- "C-c M-h" #'message-insert-headers
- "C-c M-o" #'message-sort-headers
- ;; C-c C-c to send and exit is handled by `org-ctrl-c-ctrl-c-hook'
- "C-c C-s" #'org-mu4e-send
- "C-c C-k" #'message-kill-buffer
- "C-c C-d" #'message-dont-send
- "C-c M-k" #'message-kill-address
- "C-c M-e" #'message-elide-region
- "C-c M-v" #'message-delete-not-region
- "C-c M-z" #'message-kill-to-signature
- "<remap> <split-line>" #'message-split-line
- "<remap> <beginning-of-buffer>" #'mu4e-compose-goto-top
- "<remap> <end-of-buffer>" #'mu4e-compose-goto-bottom
- "C-c M-r" #'message-insert-screenshot
- "M-n" #'message-display-abbrev
- "C-c C-a" #'mail-add-attachment
- "C-c M-t" #'org-mu4e-toggle-htmlize-mssage
- "C-c M-p C-p" #'org-mu4e-preview-html
- "C-c M-p C-w" #'org-mu4e-render-preview
- "C-c C-;" #'mu4e-compose-context-switch)
- ;;;###autoload
- (define-derived-mode org-mu4e-compose-mode org-mode "mu4e:org-compose"
- "Major mode for editing mu4e messages with `org-mode' syntax.
- This is derived from `org-mode', but it also essentially runs
- `mu4e-compose-mode' and `message-mode'. Therefore, it runs their hooks too."
- ;; Enable all the things from `mu4e-compose-mode' (which derives from
- ;; `message-mode'), but don't let it change the major mode (or other things we
- ;; care about).
- (when org-mu4e--internal-message-mode-function
- (let ((major-mode major-mode)
- (mode-name mode-name)
- (local-abbrev-table local-abbrev-table)
- (font-lock-defaults font-lock-defaults)
- ;; some of these are not actually changed, but they are here just in
- ;; case they change in the future...
- (comment-start comment-start)
- (comment-end comment-end)
- (comment-start-skip comment-start-skip)
- (comment-add comment-add)
- (comment-style comment-style))
- (cl-letf (((symbol-function 'kill-all-local-variables) 'ignore)
- ((symbol-function 'use-local-map) 'ignore)
- ((symbol-function 'set-syntax-table) 'ignore))
- (funcall org-mu4e--internal-message-mode-function))))
- ;; Add `message-mode' keyword and quote highlighting on top of the org syntax
- ;; highlighting
- (font-lock-add-keywords nil message-font-lock-keywords)
- (setq-local org-mu4e--override-org-mode-check t)
- (add-to-list (make-local-variable 'org-ctrl-c-ctrl-c-final-hook)
- 'org-mu4e-send-and-exit)
- (add-to-list (make-local-variable 'mode-line-misc-info)
- '(:eval (if org-mu4e--html-message-p
- "Text/HTML "
- "Text Only "))))
- ;;;###autoload
- (define-mail-user-agent 'org-mu4e-user-agent
- #'org-mu4e-compose-new
- #'org-mu4e-send-and-exit
- #'message-kill-buffer
- 'message-send-hook)
- ;;;###autoload
- (defun org-mu4e-user-agent ()
- "Return `org-mu4e-user-agent'."
- 'org-mu4e-user-agent)
- (provide 'org-mu4e-compose)
- ;;; org-mu4e-compose.el ends here
|