2018-12-31 An info about the size of message attachments

I have been using mu4e for quite a long time as my email client now. I would never switch back to Claws Mail again (maybe mutt, with emacsclient as the editor;-)). However, mu4e has some rough edges. One of them (which is really a problem with Emacs’ message-mode) is displaying of attachments when writing the message. (I already solved two problems with attaching files to messages in Emacs, but this feature needs even more tweaking.)

I have a rather slow internet connection, and sending emails in mu4e is synchronous, so I’d like to be warned when sending a large attachment. Many graphical email clients display the size of the attachment. Not only does message-mode not do this, but in general the inserted attachments look ugly and illegible.

So, I set out to fix both these issues. The most difficult part is adding the size information. When I attach a file to a message in Emacs, something like this is inserted in the buffer:

<#part type="image/jpeg" filename="..." disposition=attachment>
<#/part>

It turns out that the code responsible for parsing the above text is well hidden in the mml.el file. While I could not understand it completely, after a while of grepping and edebugging I came to the conclusion that adding a size="..." part should be safe. The natural choice was to advise the mml-attach-file function.

This, however, won’t probably work. The part responsible for actually inserting the attachment “marker” is buried deeply in that function. So I decided to choose a more hackish solution, and advise mml-insert-empty-tag instead. This is definitely not something I’d be very proud of, but I can’t see any other way without making larger modifications.

So, here was my first attempt.

(defun mml-add-size-to-message-part (name &rest plist)
  "Add filesize to the inserted part if applicable"
  (if (and (eq name 'part)
	   (plist-member plist 'filename))
      (plist-put plist 'size
		 (number-to-string
		  (file-attribute-size
		   (file-attributes
		    (plist-get plist filename))))))
  (cons name plist))

(advice-add 'mml-insert-empty-tag
	    :filter-args
	    'mml-add-size-to-message-part)

Unfortunately, it didn’t work as expected (actually, it didn’t work at all). It turned out that the problem was with the :filter-args advice combinator. In its case, the piece of advice is called with a funcall and not apply – in other words, we need to take a list of arguments as a single argument to mml-add-size-to-message-part and decompose it ourselves to name and plist.

So, here is the working one.

(defun mml-add-size-to-message-part (args)
  "Add filesize to the inserted part if applicable"
  (let ((name (car args))
	(plist (cdr args)))
    (if (and (string= name "part")
	     (plist-member plist 'filename))
	(plist-put plist 'size
		   (human-readable-size
		    (file-attribute-size
		     (file-attributes
		      (plist-get plist 'filename))))))
    (cons name plist)))

(advice-add 'mml-insert-tag
	    :filter-args
	    'mml-add-size-to-message-part)

Notice how I used the human-readable-size function I blogged about previously.

This is all nice, but it’s not enough for me. I don’t really care that much about the exact path to the attachment or other stuff in the part line; I’m mainly interested in the filename and size of the attachments.

So, my goal now is to hack the mml insertion functions more to make things I want to see at first glance more prominent.

Since all this is hackish enough already, I decided to add insult to injury and apply yet another “clever” hack: since both MIME type and filename contain the most interesting parts after a slash, I’m going to make the part after the slash of each mml tag stand out.

Since Emacs does not have a function for mapping over plists, I quickly wrote my own; it turned out to be very similar to the one by Drew Adams.

(defun embolden-after-slash (string)
  "Return a copy of STRING with the part after the last slash boldface.
If there are no slashes, make all of it bold."
  (let* ((result (substring string))
	 (slash-pos (string-match "/[^/]+$" result)))
    (add-face-text-property
     (if slash-pos (1+ slash-pos) 0)
     (length result)
     'bold t result)
    result))

(defun transform-plist (fun plist)
  "Run FUN on each key-value of PLIST.
Return the new value.  FUN should receive two arguments."
  (let ((result nil))
    (while plist
      (push (car plist) result)
      (push (funcall fun (car plist) (cadr plist)) result)
      (setq plist (cddr plist)))
    (nreverse result)))

(defun mml-prepare-tag (args)
  "Prepare NAME and PLIST for `mml-insert-tag'.
Make the interesting parts of values more prominent."
  (let ((name (car args))
	(plist (cdr args)))
    (cons name (transform-plist
		(lambda (key value)
		  (if (stringp value)
		      (embolden-after-slash value)
		    value))
		plist))))

(advice-add 'mml-insert-tag
	    :filter-args
	    'mml-prepare-tag)

After spending some time writing and debugging the above code, it turned out that it won’t work… One reason is that mml-insert-tag for some reason uses prin1 when inserting the values of tags. Another is that as soon as all tags are inserted, font lock kicks in and renders the whole attachment in the message-mml face.

Fail.

I asked a suitable question on the Emacs mailing list and got a whole lot of interesting answers. For starters, it turned out that if I want to manually add some faces to a text in a buffer with font-lock mode turned on, I should use the font-lock-face property and not the face one. Changing add-face-text-property to add-text-properties turned out to (partially) solve the problem. Parts of the attachment tag were actually printed in boldface, but this somehow turned off the fontification of the rest. (This is probably not a surprise, since looking at the font-lock settings in mml.el reveals that the tag is highlighted as a whole. I suspect that when font-lock sees the font-lock-face property anywhere in the text it is about to transform, it just refuses to touch it, which is not unreasonable.)

Of course, this doesn’t solve the prin1 issue, which is way harder to fix. It seems that a simple advice filtering the argument to mml-inser-t-tag won’t work here – the prin1 is buried deeply in mml-insert-tag. Of course, I wouldn’t like to rewrite this function, either, especially that the prin1 does serve a purpose.

My solution to the problem is way less elegant, but at the same time much simpler. Instead of doing the highlighting work before calling mml-insert-tag, let’s do it afterwards. And instead of working on a string, let’s work on the stuff already inserted in the buffer.

The last problem that remains is that we still break font-lock due to using the face-font-lock property. This, however, is easy to fix – instead of text properties, we can use overlays. (They are separate objects that consume memory, but if there is no variable bound to an overlay and the buffer the overlay was in gets killed, the overlay is subject to garbage collection, so we do not have to worry about it.)

(require 'cl)

(defun embolden-with-overlay (start end)
  "Embolden the text between START and END, using an overlay."
  (let ((tmp-overlay (make-overlay start end)))
    (overlay-put tmp-overlay 'face 'bold)))

(defun make-mml-tag-before-point-more-legible (name &rest plist)
  "Apply boldface to relevant parts of mml tag before point."
  (save-excursion
    (let ((opoint (point)))
      (when (search-backward (format "<#%s" name) nil nil)
	(goto-char (match-end 0))
	(while (looking-at " [a-z]+=")
	  (goto-char (match-end 0))
	  (if (eq (char-after) ?\")
	      (embolden-with-overlay
	       (progn
		 (forward-char)
		 (point))
	       (progn (while (and (search-forward "\"" opoint t)
				  (save-excursion
				    (backward-char)
				    (cl-oddp (skip-chars-backward "\\\\")))))
		      (point)))
	    (embolden-with-overlay
	     (point)
	     (progn
	       (re-search-forward "[ >]" opoint t)
	       (backward-char)
	       (point)))))

(advice-add 'mml-insert-tag :after 'make-mml-tag-before-point-more-legible)

This may be kind of fragile, since I am not sure how (un)reliable manual parsing of the mml tag can be. (Also, I make the whole value of each “property” bold, not only the part after the last slash.) However, since the only thing that depends on it will be the fontification, I have the luxury of not needing to care too much about edge cases: the worst that can happen is that the wrong parts of the tag will be set in boldface. And I have been now using this code for a while, and it seems to work very well.

CategoryEnglish, CategoryBlog, CategoryEmacs