MailMate bundles for fun and profit

Published
26 March 2016
Tagged

I usually think of MailMate like the A-10 Warthog of OS X email programs: it's not as sleek and sexy as the average new email app you see coming out of indie devs' doors, but it makes up for that in sheer power and efficiency when it comes to dealing with your email.[1]

One of these is a blunt, efficient instrument of concentrated fury, and the other is a plane.

One of these is a blunt, efficient instrument of concentrated fury, and the other is a plane.

One part of MailMate that seems pretty opaque to newbies is MailMate's ability to run Commands on messages. If you've used MailMate in the past, you'll probably have noticed the "Commands" menu item nestling on the top of your screen, but you may not have investigated it. Or maybe you use one or two of the default command bundles available for download, but have never thought about the fact that you can make your own custom commands.

It's always nice to be able to get at the guts of your emails - especially if you want to act on them outside your mail program, or archive them for later referral. In fact, this is what got me to start looking at custom commands: I wanted to copy an email as Markdown so I could store a copy of it for a project I was working on. As far as I can tell, there's no way to do this with MailMate as it stands, but something like this should be readily doable through commands, right?

The main issue I had when it came to commands for MailMate, was how to get started. While MailMate's manual isn't too bad, its documentation for advanced features is a little lacking. Looking through the table of contents, you can find this page on scripts, which mentions that "bundles" will replace scripts "in a future revision of MailMate". Bundles are also mentioned on the preferences page in the manual, which states:

Creating new bundles is undocumented for now, but a large set of existing bundles allow you to integrate MailMate with various third party applications.

Searching the internet doesn't reveal anything. So I guess it's time to make my own guide.

Bundle structure

For this tutorial, I'm going to make a simple Bundle that allows us to copy an email for pasting into a MediaWiki page, complete with Mediawiki-style formatting.

A bundle is nothing more than a grouped set of commands, with a little metadata wrapped around it. Bundles exist as folders (named with the .mmBundle extension), inside which you store your metadata, commands, and any supporting documentation. Here's how your average bundle will look:

myBundle.mmBundle/
|--Commands/
|  |--Command one.mmCommand
|  +--Command two.mmCommand
|--Support/
|  +--bin/
|     |--bin_one
|     +--bin_two
+--info.plist

To get MailMate to read the bundle, it needs to be situated in ~/Library/Application Support/MailMate/Bundles/ - but we're a while away from that yet.

Populating plists

Every essential file for your bundle is a property list file, or plist. Apple allows a couple of different formats for plist files, but I've had the best luck with old NeXTSTEP-style dictionaries of values. I'll be using these for this guide, since JSON plists (my favoured style) apparently break MailMate like nobody's business.

Info

First, let's edit the file info.plist. This just tells MailMate a little bit about the bundle: chiefly, we give it a UUID to refer to it by, and a title to show in the menu bar:

{
	name = "Copy";
	uuid = "DD86F48D-7276-448C-AC50-9BD156EFBEF9";
}

That's all you need! I've used my own UUID here, if you want to generate one by yourself, open up terminal and type in:

uuidgen | pbcopy

Then ⌘+V wherever you want that UUID to appear. Note that info.plist supports other properties as well, including:

  • contactName: The name of the bundle's author
  • contactEmailRot13: The contact author's email, encoded by ROT13 (no, I don't know why either)
  • description: A quick description of the bundle's purpose

These are all optional fields, presumably for use with a bundle repository (like the one you can access from Preferences). It's worth noting that your bundle will still work if you leave out these properties, but it will fail if you add a property it doesn't recognise.

Commands

Let's get into the nitty-gritty of this: the commands. Each bundle is represented by an entry in the Commands menu, and each command file inside the Bundle's Commands/ folder is represented by a sub-entry under this entry:

A typical bundle entry in the *Commands* menu.

A typical bundle entry in the Commands menu.

Our bundle will start off with just one command, but you can imagine building several commands into the bundle, all related to one program or task.

We'll create our command in the Commands/ folder, calling it Copy to wiki.mmCommand. This will be a text file - in fact, it's going to be a NeXTSTEP-stype plist file just like info.plist.

This is what a .mmCommand file looks like:

{
	name          = "Copy as wiki";
	uuid          = "550219B8-6E4E-4C2B-A1F2-1043951C0947";
	keyEquivalent = "^c";
	input         = "canonical";
	environment   = "MM_FROM=${from}\nMM_TO=${to}\nMM_SUBJECT=${subject}\nMM_DATE=${date}";
	command       = '#!/bin/bash\n"${MM_BUNDLE_SUPPORT}/bin/copy_wiki"';
}

We have a bunch of properties here. This is what they do:

  • name: The name as displayed in the commands menu.
  • uuid: Again, a UUID for your command, for identification. Should be different from the bundle's UUID, obviously.
  • keyEquivalent: The keyboard shortcut of your command. Helpfully, if you have multiple commands mapped to the same keyboard shortcut, MailMate will pop up a small HUD-type display allowing you to pick from the available commands: ! The keyboard shortcut ^c equates to Ctrl+C, while ^C is equal to Ctrl+Shift+C. I'm not sure how to represent "option" or "command" in keyEquivalents at this point.
  • input: By default, MailMate will pass your command a text copy of the email's body. This property allows you to pick how it formats that text. We'll talk about the options in a bit: for the moment, assume that you want your input to be "canonical".
  • environment: Set a list of environment variables for your command. Any value within ${dollar-braces} will be filled in by MailMate itself. I'm not sure if there's an exhaustive list of these, but again I think I've worked out a bit of a hack for this (see below).
  • command: This is the command that MailMate will run. My experimenting indicates that you can't just type a filename: you need to start with a hashbang to run bash, followed by your actual command file. Note that MM_BUNDLE_SUPPORT will automatically be replaced with the path to your Support/ folder.

There's a few other properties you can put in this file as well:

  • output: Just as input decides on the format of the mail message when it goes into your script, output tells MailMate what to do with the output of your script. Leaving this out means that MailMate discards any output.
  • formatString: If you specify the input type as "formatted", this property allows you to set the format of your message.
  • executionMode: This tells the script whether it should run once per highlighted message ("singleMessage"), or once only, regardless of the number of messages you have selected ("multipleMessages"). This is generally most useful for formatted messages: as far as I can tell, MailMate munges canonical message bodies together with no regard for separation.
  • separatorString: Presumably, if you specify a formatted message and set the executionMode to "multipleMessages", MailMate will separate the formatted messages with this string. I haven't had much luck using it yet.

Most of these properties I've discovered by looking through the built-in MailMate bundle (located in MailMate.app/SharedSupport/Bundles), and by looking through the MailMate list archives.

Input and Output

Exactly how can you input your data? Let me list the ways:

  • none: No input is passed to the command.
  • raw: The full email, headers and all. Useful if you want to parse headers, I guess.
  • decoded: "MailMate decodes the body part if it is encoded using quoted-printable/base64." I guess this is handy if you want to save attachements or stuff, but for other uses, not so much.
  • canonical: This is roughly equivalent to a markdown-formatted email message, and is probably the most palatable way of reading your email unless you do some major formatting.
  • html: This is the HTML-formatted email as would be displayed in MailMate. Useful if you're going to be rendering the email itself as HTML someplace, I imagine.
  • selection: Only passes the currently-selected text to the command.
  • formatted: Passes the email through a format string (see notes on formattedString above) before passing it along to the command.

What about output? It looks like there's currently only one form of output that isn't just discarding the message entirely, and that's action. This will make MailMate perform an action (a list of possible actions is available in the previous link). This output requires that you return a plist-style string specifying the action to perform. For example, MailMate's command "New message to Recipients" returns:

{ actions = ( {
	type = createMessage;
		headers = {
			"to" = '${TO}';
		};
		resultActions = ( { type = openMessage; } );
	},);
}

(where ${TO} is a variable set to the recipients of the selected email).

Making it do things

OK, now we've talked forever about what MailMate's commands can do. Let's make it actually do something.

Our command above is looking for a binary called copy_wiki in the Support/bin/ directory. Let's give it something to run. This is the contents of copy_wiki:

#!/usr/bin/ruby
# encoding: UTF-8

require "shellwords"

header = "* '''Date:''' #{ENV["MM_DATE"]}\n* '''From:''' #{ENV["MM_FROM"]}\n* '''To:''' #{ENV["MM_TO"]}\n* '''Subject:''' #{ENV["MM_SUBJECT"]}\n\n"
body = $stdin.read

# Reformat
body = body.gsub(/((?:^> .*)+)/m) do |m|
	"<blockquote>" + m.gsub(/^>\s*/,"") + "</blockquote>"
end

`echo #{(header + body).shellescape} | pbcopy`

What does this do?

  • First, it creates a header from our list of environment variables. Wiki markup syntax requires that we wrap something in a triplet of single quotes to make it bold.
  • Next, we grab the body of the email from $stdin.
  • We then substitute any giant blocks of text prefixed with > (for example, previously-quoted messages) for blockquotes.
  • Finally, we echo the header and body, combined, and pipe the result into pbcopy, which copies it to the clipboard.

As it stands, MailMate won't actually be able to execute this file, so you'll want to give it the right permissions:

chmod +x copy_wiki

Putting it in place

OK! We should now have the following files:

Copy/
|--Commands/
|  +--Copy as wiki.mmCommand
|--Support/
|  +--bin
|     +--copy_wiki
+--info.plist

Now all you need to do is:

  1. Rename the main folder (Copy/ in this case) to have the mmBundle extension.
  2. Place the bundle in ~/Library/Application Support/MailMate/Bundles.
  3. See if your bundle appears in the Commands menu.

My bundle doesn't appear in the Commands menu!

This probably means that something's up with your info.plist file. I highly recommend futzing around with copies of the working bundles (usually located in Managed/Bundles/ within the MailMate App Support folder) to work out how they differ from yours - or drop a comment and we can work it out together.

My bundle appears, but the Command won't work!

Well, the good news is that your info.plist file is fine. The bad news is that there's an error either in your mmCommand file, or the binary itself - and there's no way I know of to find out!

I suggest replacing your binary file with something really simple. For example, I'll often replace the binary script with:

#!/usr/bin/env ruby
File.open(File.join(ENV["HOME"], "Desktop/mailmate_test.txt"),"w"){ |io| io.puts "This is working!"}

That'll drop an annoying file on your desktop. If this works, you know your mmCommand is fine, and it's just the binary that's causing havoc. If this still doesn't work, chances are something horrid is going on in your mmCommand. See solutions for the above problem.

Can I look at your source?

Funny you should ask.


  1. Also, it has a business plan which isn't reliant on being bought out and shuttered by a large software company in a few years - not that I'm bitter or anything. ↩︎