Mailing subtrees with Attachments

This is pretty exciting; I’ve figured out how to quickly send org-mode subtrees as MIME-encoded emails. That means that, essentially, I can write in org, as plain text, and very quickly export to HTML, add attachments, and send. The exciting part about this for me is that it should streamline my communications with students, while also letting me stay in Org and keep my records in order. Let’s walk through the process.

Use-case

For the moment, I still use Thunderbird as my primary MUA. It’s pretty easy to use, minimal configuration compared to all things Emacs, and if something goes wrong with it I don’t have to quit Emacs (!), just Thunderbird.

In some cases, though, Thunderbird makes for an awkward workflow. That’s certainly the case for grading, whih has many, poorly-integrated elements. To mark an assignment I need to:

  • log in to Blackboard (in Firefox; I wonder if I could do that in Emacs?)
  • download the set of student papers (one at a time, 2 clicks per paper) to Downlads
  • move papers to a directory (usually ~/COURSENAME/Grading/ASSIGNMENTNAME )
  • Read papers in LigreOffice, comment inline
  • record mark in Libreoffice Calc spreadsheet
  • email paper back to student with comments
  • upload marks back into Blackboard
  • find a place to archive the student paper in case I need it later, e.g. for a contested grade.

The while process basically sucks. I spend maybe 20% of my time fussing with paths and mouse clicks and email addresses. So I am experimenting with moving as much of this process into Emacs. So far, I don’t think there’s any way at all to bulk-download the papers – that sucks, but I can live with it I guess (I have to!). So I start the optimization at the point where I have all my papers ready to go in a subdir.

Org-mime

Org-mime is the library that allows org buffers and other elements to be quickly converted to HTML and prepared for multi-part messaging. Load it and set it up (see http://orgmode.org/worg/org-contrib/org-mime.html):

;; enable HTML email from org
(require 'org-mime)
;; setup org-mime for wanderlust
;; (setq org-mime-library 'semi)
;; or for gnus/message-mode
(setq org-mime-library 'mml)

;; easy access to htmlize in message-mode
(add-hook 'message-mode-hook
          (lambda ()
            (local-set-key "\C-c\M-o" 'org-mime-htmlize)))

;; uncomment this to use the org-mome native functions for htmlizing.
;; (add-hook 'org-mode-hook
;;           (lambda ()
;;             (local-set-key "\C-c\M-o" 'org-mime-org-buffer-htmlize)))

;; uncomment to displyay src blocks with a dark background
;; (add-hook 'org-mime-html-hook
;;           (lambda ()
;;             (org-mime-change-element-style
;;              "pre" (format "color: %s; background-color: %s; padding: 0.5em;"
;;                            "#E6E1DC" "#232323"))))

;; pretty blockquotes
(add-hook 'org-mime-html-hook
          (lambda ()
            (org-mime-change-element-style

             "blockquote" "border-left: 2px solid gray; padding-left: 4px;")))

Fix htmlization

Upstream org-mime-htmlize unfortunately can’t be called uninteractively (bummer!), so we have to rewrite it to make programmatic calls work properly. I found the solution Emacs Stackexchange.

(defun org-mime-htmlize (&optional arg)
"Export a portion of an email body composed using `mml-mode' to
html using `org-mode'.  If called with an active region only
export that region, otherwise export the entire body."
  (interactive "P")
  (require 'ox-org)
  (require 'ox-html)
  (let* ((region-p (org-region-active-p))
         (html-start (or (and region-p (region-beginning))
                         (save-excursion
                           (goto-char (point-min))
                           (search-forward mail-header-separator)
                           (+ (point) 1))))
         (html-end (or (and region-p (region-end))
                       ;; TODO: should catch signature...
                       (point-max)))
         (raw-body (concat org-mime-default-header
                           (buffer-substring html-start html-end)))
         (tmp-file (make-temp-name (expand-file-name
                                    "mail" temporary-file-directory)))
         (body (org-export-string-as raw-body 'org t))
         ;; 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 org-mime-preserve-breaks)
         ;; dvipng for inline latex because MathJax doesn't work in mail
         (org-html-with-latex 'dvipng)
         ;; to hold attachments for inline html images
         (html-and-images
          (org-mime-replace-images
           (org-export-string-as raw-body 'html t) tmp-file))
         (html-images (unless arg (cdr html-and-images)))
         (html (org-mime-apply-html-hook
                (if arg
                    (format org-mime-fixedwith-wrap body)
                  (car html-and-images)))))
    (delete-region html-start html-end)
    (save-excursion
      (goto-char html-start)
      (insert (org-mime-multipart
               body html (mapconcat 'identity html-images "\n"))))))

Actually perform the export!

These functions are crude helpers that gather extra information about the org subtree, of which org-mime is unaware.

  • mwp-org-get-parent-headline traverses the tree to the ancestor headline, because that’s what I want to set the subject to.
  • mwp-org-attachment-list is stolen directly from the Gnorb package, which looks cool, awesome ,and kinda complex; it just iterates through a subtree’s attachments and grabs URLs.
  • mwp-send-subtree-with-attachments performs the export and is bound to C-c M-o

So, if I want to mail a subtree, I just C-c M-o and I’m almost done – the html mail is ready to go, and all org attachments are also attached to the email.

Note there are some real weaknesses here: mwp-org-get-parent-headline actually gets the top-level ancestor – which only happens to be what I want right now. Better would be to use org-element to locate the parent (and other headline attributes) directly, but I’m not sure how to do that.

Similarly, the initial greeting is generated from the current headline value – so this only works because I name my subtrees after the addressee (which I only do because of my use case).

(defun mwp-org-get-parent-headline ()
  "Acquire the parent headline & return."
  (save-excursion
    (org-mark-subtree)
    (re-search-backward  "^\\* ")
    (nth 4 (org-heading-components))))

(defun mwp-send-subtree-with-attachments ()
  "org-mime-subtree and HTMLize"
  (interactive)
  (org-mark-subtree)
  (let ((attachments (mwp-org-attachment-list))
        (subject  (mwp-org-get-parent-headline)))
    (insert "Hello " (nth 4 org-heading-components) ",\n")
    (org-mime-subtree)
    (insert "\nBest,\nMP.\n")
    (message "subject is" )
    (message subject)
    ;;(message-to)
    (org-mime-htmlize)
    ;; this comes from gnorb
    ;; I will reintroduce it if I want to reinstate questions.
    ;; (map-y-or-n-p
    ;;  ;; (lambda (a) (format "Attach %s to outgoing message? "
    ;;  ;;                    (file-name-nondirectory a)))
    ;; (lambda (a)
    ;;   (mml-attach-file a (mm-default-file-encoding a)
    ;;                    nil "attachment"))
    ;; attachments
    ;; '("file" "files" "attach"))
    ;; (message "Attachments: %s" attachments)
    (dolist (a attachments) (message "Attachment: %s" a) (mml-attach-file a (mm-default-file-encoding a) nil "attachment"))
    (message-goto-to)
    ))

;; add a keybinding for org-mode
(add-hook 'org-mode-hook
          (lambda ()
            (local-set-key "\C-c\M-o" 'mwp-send-subtree-with-attachments)))

;; stolen from gnorb; finds attachments in subtree
(defun mwp-org-attachment-list (&optional id)
  "Get a list of files (absolute filenames) attached to the
current heading, or the heading indicated by optional argument ID."
  (when (featurep 'org-attach)
    (let* ((attach-dir (save-excursion
                         (when id
                           (org-id-goto id))
                         (org-attach-dir t)))
           (files
            (mapcar
             (lambda (f)
               (expand-file-name f attach-dir))
             (org-attach-file-list attach-dir))))
      files)))

Contacts

That’s a good start, but there are still some steps to make this truly convenient. For instance, I certainly don’t want to type in students’ email addresses by hand. So I imported my contacts from thunderbird to org-contacts. This was a pain – the process was Thunderbird → Gmail (via gsync plugin) → vcard (via gmail export) → org-contacts (via Titus’s python importer). I wish there was a CSV importer for org-contacts; probably this would be easy to write but I’m so slooooowwww at coding. My org contacts live in GTD/Contacts.org, which is set in Customize, and org reads them on startup with this line

(require 'org-contacts)

With this single line, org-contacts now provides TAB completion in message-mode to headers. It’s very fast, so feels more convenient than Thunderbird.

Making it better
I wish I could get org-contacts to provide tab completion in my subtrees (see below). I would need to access the completion function directly and somehow set the binding for TAB to that completion function.

Adding Attachments with Drag & Drop

After I make inline comments, I fill out a grading template and attach the paper to the resultant subtree (C-c C-a a PATH). This is OK, but sometimes it would nice to be able to drag and drom the files, so I am working on these functions.

Even better
an even better solution be to add the attachments programmatically. The studnet papes follow a strict naming convention, so I should be able to crawl the directory and find the most recent paper with the student’s name in it… I’m worried it wil lbe too error prone though.

Anyway: unfortunately the following code doesn’t work right, so don’t just cut and paste this code!). I *ought to be able to bind the drag and drop action to a function – even several functions – and, if conditions are right, attach the dragged file to the current org header. John Kitchin describes this method here. But I do the following instead, which is also broken right now:

Start by loading org-download, which downloads dragged images as attachments and inserts a link. (yay). THen a modification which fixes handling of file links allowing me to drag-n-drop files links onto org as attachments. Unfortunately, I can’t get org-attach to process the URI’s properly. Darn it.

(require 'org-download)
(require 'org-attach)
;; extending the dnd functionality
;; but doesn't actually work... 
(defun mwp-org-file-link-dnd (uri action)
    "When in `org-mode' and URI points to local file, 
  add as attachment and also add a link. Otherwise, 
  pass URI and Action back to dnd dispatch"
    (let ((img-regexp "\\(png$\\|jp[e]?g$\\)")
          (newuri (replace-regexp-in-string "file:///" "/" uri)))
      (cond ((eq major-mode 'org-mode)
             (message "Hi! newuri: %s " (file-relative-name newuri))
             (cond ((string-match img-regexp newuri)
                    (insert "#+ATTR_ORG: :width 300\n")
                    (insert (concat  "#+CAPTION: " (read-input "Caption: ") "\n"))
                    (insert (format "[[%s]]" uri))
                    (org-display-inline-images t t))
                   (t 
                    (org-attach-new newuri)
                    (insert (format "[[%s]]" uri))))
             )
            (t
             (let ((dnd-protocol-alist
                    (rassq-delete-all
                     'mwp-org-file-link-dnd
                     (copy-alist dnd-protocol-alist))))
               (dnd-handle-one-url nil action uri)))
            )))

  ;; add a new function that DOESN'T open the attachment!
(defun org-attach-new-dont-open (file)
    "Create a new attachment FILE for the current task.
  The attachment is created as an Emacs buffer."
    (interactive "sCreate attachment named: ")
    (when (and org-attach-file-list-property (not org-attach-inherited))
      (org-entry-add-to-multivalued-property
       (point) org-attach-file-list-property file))
    )

(defun mwp-org-file-link-enable ()
    "Enable file drag and drop attachments."
    (unless (eq (cdr (assoc "^\\(file\\)://" dnd-protocol-alist))
                'mwp-org-file-link-dnd)
      (setq dnd-protocol-alist
            `(("^\\(file\\)://" . mwp-org-file-link-dnd) ,@dnd-protocol-alist))))

(defun mwp-org-file-link-disable ()
  "Enable file drag and drop attachments."
  (if (eq (cdr (assoc "^\\(file\\)://" dnd-protocol-alist))
              'mwp-org-file-link-dnd)
      (rassq-delete-all
       'mwp-org-file-link-dnd
       dnd-protocol-alist)

    ))

(mwp-org-file-link-enable)
  • http://pages.ucsd.edu/~tvondermalsburg/ Titus

    Apparently there are add-ons for Thunderbird that can export to vCard directly. I haven’t tried any of them, just saw them mentioned somewhere.

    • matt

      I tried one of them, and it threw a bunch of errors on conversion. Gmail export was easier, in the end.

  • Anders Johansson

    Fun to see someone else doing similar things as me and having similar problems. I also grade student papers using org-mode and have to manage files and stuff.

    My workflow:
    I get the list of students from our system (a locally developed system here at Uppsala University in Sweden), make a subtree of the list (something like: regexp replace “^” -> “**”).

    Get the files, convert them to pdf, place them in the same directory.

    Run some of the functions in the file to add grading tables and links to the pdf files to all student headlines. I usually need some copy-pasting of tables as well to get everything in order as in the example file.

    Then I read and comment in the pdf files with pdf-tools (and they can be opened with links, so it’s a matter of navigating through the tree of students).

    Then I both have individual grades and a summary table (automatically updated using the org table formulas).

    Then I go back and upload the files and comments to the system. This is the boring part, because I need to click a lot in a bad web interface, but copying the comments from org is straightforward.

    Example here:

    https://gist.github.com/andersjohansson/c8189fc4a5435e59223c3b959486a9ee