Launch e-mails with alternative handler (message:// and .eml files) to Fastmail

This is rather niche, but I thought of sharing it anyway for those who use Fastmail. This script allows to open .eml files and message:// links with Fastmail - rather than Mail. The problem is only that Fastmail doesn’t show you the mail when you search based on Message-Id (it rather shows you a message list with the message found which you then have to click).

This code does the following

  • Extract Message-ID from .eml file or message:// handler
  • Get the Fastmail JMAP id for the “Message-ID” found
  • Open the URL Fastmail + jmap_id

Use keyring.set_password("jmap","","s3cretpassw0rd") to set your password.

Use Platypus to create a handler for .eml files and message:// links and set that as the default handler, so launching a .eml file from DT will automatically launch this script. As the script takes 3-4 seconds to produce a result, choose the “Progress bar” option in Platypus so you’ll get a nice loading bar during waiting to stare at :slight_smile:

#!/usr/bin/env python3

import json
import os
import requests
import sys
import keyring
import urllib.parse
import email

class TinyJMAPClient:
    """The tiniest JMAP client you can imagine."""

    def __init__(self, hostname, username, password):
        """Initialize using a hostname, username and password"""
        assert len(hostname) > 0
        assert len(username) > 0
        assert len(password) > 0

        self.hostname = hostname
        self.username = username
        self.password = password
        self.session = None
        self.api_url = None
        self.account_id = None

    def get_session(self):
        """Return the JMAP Session Resource as a Python dict"""
        if self.session:
            return self.session
        r = requests.get(
            "https://" + self.hostname + "/.well-known/jmap",
            auth=(self.username, self.password),
        self.session = session = r.json()
        self.api_url = session["apiUrl"]
        return session

    def get_account_id(self):
        """Return the accountId for the account matching self.username"""
        if self.account_id:
            return self.account_id

        session = self.get_session()

        account_id = session["primaryAccounts"]["urn:ietf:params:jmap:mail"]
        self.account_id = account_id
        return account_id

    def make_jmap_call(self, call):
        """Make a JMAP POST request to the API, returning the reponse as a
        Python data structure."""
        res =
            auth=(self.username, self.password),
            headers={"Content-Type": "application/json"},
        return res.json()

def main():
	arg = sys.argv[1]

	if arg.endswith(".eml"):
		with(open(arg)) as eml:
			msg = email.message_from_file(eml)
			message_id = msg['message-id']
		if arg.startswith("message://"):
			message_id = arg[10:]
			message_id = arg 
		message_id = urllib.parse.unquote(message_id)

	username = ""
	client = TinyJMAPClient(hostname="",username=username,password=keyring.get_password("jmap",username))
	account_id = client.get_account_id()

	get_res = client.make_jmap_call(
			"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
			"methodCalls": [
						"accountId": account_id,
						"filter": {"header": ["Message-id", message_id] },
						"limit": 1,

	jmap_id = get_res["methodResponses"][0][1]["ids"][0]
	os.system("open" + jmap_id)

if __name__ == "__main__":