Journal

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

Comments on this page

2018-12-24 Merry Christmas

As usual at this time of year, I have best Christmas wishes for everyone reading this. I wish that you all discover the beauty and truth of the Catholic faith. Don’t let the sins of the Catholic people - even the clergy - deceive you: the one, holy, Catholic and apostolic Church is where you can find truth about God and man. Merry Christmas!

CategoryEnglish, CategoryBlog, CategoryFaith

Comments on this page

2018-12-16 A simple tip on using destructive functions

This is something fairly obvious to every seasoned Lisp programmer, but let’s not forget that there are novices, too.

Many Elisp functions are noted to be “destructive”, which means that they can change their arguments. For instance, if you want to sort a list, you may use the sort function, which is said to modify its argument by side effects (this is exactly what “destructive” means). This does not necessarily mean, however, that after executing (sort some-list), the variable some-list will magically contain a sorted version of it previous self! Let’s consider two examples.

(setq some-list '(1 3 2))
(sort some-list #'<)
some-list

After evaluating the above code, some-list is bound to (1 2 3), i.e., the sorted version, and hence you might think that this is always going to be the case.

Wrong.

Let’s look at this code.

(setq some-list '(3 1 2))
(sort some-list #'<)
some-list

See? Now some-list is just (3).

What is going on here?

Well, in order to understand what happens, we will use the famous cons cell diagrams, found in so many books on Lisp.

Remember that each cons cell contains two “slots”: car and cdr. A list is simply a chain of cons cells whose cars contain (more precisely: point to) values held in the list, and cdrs point to the next cons cell in the chain.

Hence, the list in our first example looks like this.

some-list
    |
    V
 +-A---+-----+   +-B---+-----+   +-C---+-----+
 |  1  |  ------>|  3  |  ------>|  2  |     |
 +-----+-----+   +-----+-----+   +-----+-----+

(An empty slot means nil, of course.)

After the first sort, cons cell A’s cdr is modified to point at C, C’s cdr points at B, and B’s cdr is set to nil. Since some-list still points to A (as we did not assign anything else to this variable), our list looks like this:

some-list
    |     +-------------------------+
    |     |                         |
    V     |                         V
 +-A---+--|--+   +-B---+-----+   +-C---+-----+
 |  1  |  |  |   |  3  |     |   |  2  |  |  |
 +-----+-----+   +-----+-----+   +-----+--|--+
		   ^                      |
		   |                      |
		   +----------------------+

Let’s consider now the second example. We start with this:

some-list
    |
    V
 +-A---+-----+   +-B---+-----+   +-C---+-----+
 |  3  |  ------>|  1  |  ------>|  2  |     |
 +-----+-----+   +-----+-----+   +-----+-----+

and after sorting, our cons cells look like this:

some-list
    |
    |     +-------------------------------+
    |     |                               |
    V     V                               |
 +-A---+-----+   +-B---+-----+   +-C---+--|--+
 |  3  |     |   |  1  |  ------>|  2  |  |  |
 +-----+-----+   +-----+-----+   +-----+-----+

See what happened? Since some-list points at A now, and A’s cdr is nil, we get (3) as some-list.

So, what we should do about it? It turns out that sort returns “the sorted sequence”, which means cons cell B. Therefore, if we want some-list to be sorted, we should say this instead.

(setq some-list '(3 1 2))
(setq some-list (sort some-list #'<))
some-list

And of course, the same goes for other destructive functions, like nreverse or cl-delete.

(You might also look at my earlier post about destructive functions and comments to that post.)

CategoryEnglish, CategoryBlog, CategoryEmacs

Comments on this page

More...