From a493ffdb1d62d6171a65c72f9c59e9b4f70b7823 Mon Sep 17 00:00:00 2001 From: djcb Date: Wed, 23 May 2012 17:04:26 +0300 Subject: [PATCH] * WIP: use org-mode for writing rich-text messages --- emacs/mu4e-compose.el | 54 +++++++------ emacs/org-mu4e.el | 181 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 210 insertions(+), 25 deletions(-) diff --git a/emacs/mu4e-compose.el b/emacs/mu4e-compose.el index 5841d4d4..b6e2c684 100644 --- a/emacs/mu4e-compose.el +++ b/emacs/mu4e-compose.el @@ -237,7 +237,7 @@ separator is never written to file. Also see ;; search for the first empty line (if (search-forward-regexp (concat "^$")) (replace-match - (concat (propertize mail-header-separator 'read-only t 'intangible t))) + (concat (propertize mail-header-separator 'intangible t))) ;; no empty line? then append one (progn (goto-char (point-max)) @@ -423,16 +423,20 @@ needed, set the Fcc header, and register the handler function." ;; update the file on disk -- ie., without the separator (mu4e~proc-add (buffer-file-name) mu4e-drafts-folder)) nil t)) +(defconst mu4e~compose-hidden-headers + `("^References:" "^Face:" "^X-Face:" + "^X-Draft-From:" "^User-agent:") + "Hidden headers when composing.") + (define-derived-mode mu4e-compose-mode message-mode "mu4e:compose" "Major mode for the mu4e message composition, derived from `message-mode'. \\{message-mode-map}." - (let ((message-hidden-headers - `("^References:" "^Face:" "^X-Face:" "^X-Draft-From:" "^User-agent:"))) + (let ((message-hidden-headers mu4e~compose-hidden-headers)) (use-local-map mu4e-compose-mode-map) - (message-hide-headers) (make-local-variable 'message-default-charset) - + (message-hide-headers) + ;; if the default charset is not set, use UTF-8 (unless message-default-charset (setq message-default-charset 'utf-8)) @@ -443,7 +447,21 @@ needed, set the Fcc header, and register the handler function." ;; set the default directory to the user's home dir; this is probably more ;; useful e.g. when finding an attachment file the directory the current ;; mail files lives in... - (setq default-directory (expand-file-name "~/")))) + (setq default-directory (expand-file-name "~/")) + + ;; setup the fcc-stuff, if needed + (add-hook 'message-send-hook + (lambda () + ;; for safety, always save the draft before sending + (set-buffer-modified-p t) + (save-buffer) + (mu4e~setup-fcc-maybe)) nil t) + + ;; when the message has been sent. + (add-hook 'message-sent-hook + (lambda () + (setq mu4e-sent-func 'mu4e-sent-handler) + (mu4e~proc-sent (buffer-file-name) mu4e-drafts-folder)) nil))) (defconst mu4e~compose-buffer-max-name-length 30 @@ -520,23 +538,9 @@ Gnus' `message-mode'." (unless (eq compose-type 'edit) (when message-signature (message-insert-signature))) - + ;; set compose mode -- so now hooks can run (mu4e-compose-mode) - - ;; setup the fcc-stuff, if needed - (add-hook 'message-send-hook - (lambda () - ;; for safety, always save the draft before sending - (set-buffer-modified-p t) - (save-buffer) - (mu4e~setup-fcc-maybe)) nil t) - - ;; when the message has been sent. - (add-hook 'message-sent-hook - (lambda () - (setq mu4e-sent-func 'mu4e-sent-handler) - (mu4e~proc-sent (buffer-file-name) mu4e-drafts-folder)) nil t)) ;; buffer is not user-modified yet (mu4e~compose-set-friendly-buffer-name compose-type) @@ -545,8 +549,7 @@ Gnus' `message-mode'." ;; now jump to some use positions, and start writing that mail! (if (member compose-type '(new forward)) (message-goto-to) - (message-goto-body))) - + (message-goto-body)))) (defun mu4e-sent-handler (docid path) "Handler function, called with DOCID and PATH for the just-sent @@ -695,5 +698,8 @@ message." 'message-kill-buffer 'message-send-hook) + + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(provide 'mu4e-compose) +(provide 'mu4e-compose) diff --git a/emacs/org-mu4e.el b/emacs/org-mu4e.el index 2e815022..0392203e 100644 --- a/emacs/org-mu4e.el +++ b/emacs/org-mu4e.el @@ -1,4 +1,5 @@ -;;; org-mu4e -- Support for links to mu4e messages/queries from within org-mode +;;; org-mu4e -- Support for links to mu4e messages/queries from within org-mode, +;;; and for writing message in org-mode, sending them as rich-text ;; ;; Copyright (C) 2012 Dirk-Jan C. Binnema @@ -71,6 +72,184 @@ the query (for paths starting with 'query:')." (t (message "mu4e: unrecognized link type '%s'" path)))) + + + + +;;; editing with org-mode ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; below, some functions for the org->html conversion +;; based on / inspired by Eric Schulte's org-mime.el +;; Homepage: http://orgmode.org/worg/org-contrib/org-mime.php +(defun org~mu4e-mime-file (ext path id) + "Create a file for an attachment." + (format (concat "<#part type=\"%s\" filename=\"%s\" " + "disposition=inline id=\"<%s>\">\n<#/part>\n") + ext path id)) + +(defun org~mu4e-mime-multipart (plain html &optional images) + "Create a multipart/alternative with text/plain and text/html alternatives. +If the html portion of the message includes images, wrap the html +and images in a multipart/related part." + (concat "<#multipart type=alternative><#part type=text/plain>" + plain + (when images "<#multipart type=related>") + "<#part type=text/html>" + html + images + (when images "<#/multipart>\n") + "<#/multipart>\n")) + +(defun org~mu4e-mime-replace-images (str current-file) + "Replace images in html files with cid links." + (let (html-images) + (cons + (replace-regexp-in-string ;; replace images in html + "src=\"\\([^\"]+\\)\"" + (lambda (text) + (format + "src=\"cid:%s\"" + (let* ((url (and (string-match "src=\"\\([^\"]+\\)\"" text) + (match-string 1 text))) + (path (expand-file-name + url (file-name-directory current-file))) + (ext (file-name-extension path)) + (id (replace-regexp-in-string "[\/\\\\]" "_" path))) + (add-to-list 'html-images + (org~mu4e-mime-file + (concat "image/" ext) path id)) + id))) + str) + html-images))) + +(defun org~mu4e-mime-convert-to-html () + "Convert the current body to html." + (let* ((begin + (save-excursion + (goto-char (point-min)) + (search-forward mail-header-separator))) + (end (point-max)) + (raw-body (buffer-substring begin end)) + (tmp-file (make-temp-name (expand-file-name "mail" + temporary-file-directory))) + (body (org-export-string raw-body 'org (file-name-directory tmp-file))) + ;; because we probably don't want to skip part of our mail + (org-export-skip-text-before-1st-heading nil) + ;; because we probably don't want to export a huge style file + (org-export-htmlize-output-type 'inline-css) + ;; makes the replies with ">"s look nicer + (org-export-preserve-breaks t) + ;; dvipng for inline latex because MathJax doesn't work in mail + (org-export-with-LaTeX-fragments 'dvipng) + ;; to hold attachments for inline html images + (html-and-images + (org~mu4e-mime-replace-images + (org-export-string raw-body 'html (file-name-directory tmp-file)) + tmp-file)) + (html-images (cdr html-and-images)) + (html (car html-and-images))) + (delete-region begin end) + (save-excursion + (goto-char begin) + (newline) + (insert (org~mu4e-mime-multipart + body html (mapconcat 'identity html-images "\n")))))) + +;; next some functions to make the org/mu4e-compose-mode switch as smooth as +;; possible. + + (defun org~mu4e-mime-decorate-headers (where) + "If WHERE is the symbol 'body, prefix headers with '#' block; +when it is 'headers, remove this." + (save-excursion + (message-narrow-to-headers) + (goto-char (point-min)) + (case where + (headers + (when (looking-at "#") + (goto-char (point-min)) + (while (re-search-forward "^#" nil t) + (replace-match "")))) + (body + (unless (looking-at "#") + (goto-char (point-min)) + (while (re-search-forward "^" nil t) + (replace-match "#"))))) + (widen))) + +(defun org~mu4e-mime-convert-to-html-maybe () + "Convert to html if `org-mu4e-convert-to-html' is non-nil. This +function is called when sending a message (from +`message-send-hook') and, if non-nil, will send the message as the +rich-text version of the what is assumed to be an org-mode body." + (message "Converting to html") + (when org-mu4e-convert-to-html + (org~mu4e-mime-convert-to-html))) + + +(defun org~mu4e-mime-switch-headers-or-body (&optional firsttime) + "Switch the buffer to either mu4e-compose-mode (when in headers) +or org-mode (when in the body)," + (interactive) + (let* ((sepapoint + (save-excursion + (goto-char (point-min)) + (search-forward-regexp mail-header-separator))) + (where ;; is point in the headers or in the body? + (if (> (point) sepapoint) 'body 'headers)) + (first-char (buffer-substring (point-min) (1+ (point-min)))) + (state-changed + (if (string= first-char "#") + (eq where 'headers) + (eq where 'body)))) + (when (or firsttime state-changed) + ;; now make sure the headers correspond to the mode... + (case where + (headers + (org~mu4e-mime-decorate-headers 'headers) + (mu4e-compose-mode) + (add-hook 'message-send-hook + 'org~mu4e-mime-convert-to-html-maybe t t)) + (body + (org-mode) + (org~mu4e-mime-decorate-headers 'body) + (local-set-key (kbd "M-m") + (lambda () + (interactive) + (org~mu4e-mime-decorate-headers 'headers) + (mu4e-compose-mode) + (setq org-mu4e-compose-org-mode t) + (add-hook 'message-send-hook + 'org~mu4e-mime-convert-to-html-maybe t t)))) + (otherwise (error "unexpected where %S" where))) + (setq org-mu4e-compose-org-mode t) + ;; and add the hook + (add-hook 'post-command-hook + 'org~mu4e-mime-switch-headers-or-body t t)))) + +(defvar org-mu4e-compose-org-mode nil) +(setq org-mu4e-compose-org-mode nil) + +(defun org-mu4e-compose-org-mode () + "Pseudo-Minor mode for mu4e-compose-mode, to edit the message + body using org-mode." + (interactive) + (unless (member major-mode '(org-mode mu4e-compose-mode)) + (error "Need org-mode or mu4e-compose-mode")) + (if (not org-mu4e-compose-org-mode) + (progn + (org~mu4e-mime-switch-headers-or-body t) + (message "org-mu4e-compose-org-mode enabled")) + (progn ;; otherwise, remove crap + (remove-hook 'post-command-hook 'org~mu4e-mime-switch-headers-or-body t) + (org~mu4e-mime-decorate-headers 'headers) ;; shut off org-mode stuff + (setq org-mu4e-compose-org-mode nil) + (mu4e-compose-mode) + (message "org-mu4e-compose-org-mode disabled")))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (provide 'org-mu4e) ;;; org-mu4e.el ends here