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","johndoe@fastmail.com","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),
        )
        r.raise_for_status()
        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 = requests.post(
            self.api_url,
            auth=(self.username, self.password),
            headers={"Content-Type": "application/json"},
            data=json.dumps(call),
        )
        res.raise_for_status()
        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']
	else:
		if arg.startswith("message://"):
			message_id = arg[10:]
		else:
			message_id = arg 
		
		message_id = urllib.parse.unquote(message_id)

	username = "johndoe@fastmail.com"
	client = TinyJMAPClient(hostname="jmap.fastmail.com",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": [
				[
					"Email/query",
					{
						"accountId": account_id,
						"filter": {"header": ["Message-id", message_id] },
						"limit": 1,
					},
					"a"
				],
			],
		}
	)

	jmap_id = get_res["methodResponses"][0][1]["ids"][0]
	os.system("open https://beta.fastmail.com/mail/show/" + jmap_id)

if __name__ == "__main__":
	main()