JMAP in Node-RED

JMAP is a relatively obscure email protocol mainly (if not solely) used by Fastmail. I really like Fastmail because they have a lot of features and a great service but strangely they also have an API based on JMAP - which they invented. JMAP is basically IMAP+SMTP replacement but with command batching and JSON as a transfer protocol/format. Fastmail uses it in their clients, I don’t know of any third party email client that also uses it.

img

I came across the following resources when implementing this flow:

I recently came across a mailserver that also supports JMAP: Stalwart Labs has developed one.

I will dive into describing the flow that I created to send emails via JMAP and Fastmail. I include the flow that I have created, please respect the license.

License:

Feel free to take and extend at will, the license is simply don’t do evil.

Initialisation

In JMAP to send an email, you first have to create a draft email. To create a draft email, you need to have a draft folder ID. To get a draft folder, you need to have the account ID of your account. For sending emails, the from email address needs to be an ID. This is because you need to register the sender email with Fastmail first, so you need to obtain a list of email addresses that have been registered with Fastmail. You then need a mapping from these emails to IDs so that you can include as email-address-ID in the draft email.

Obtaining all this prerequistes is described in the following sequence diagram.


sequenceDiagram
    OMM->>Fastmail (WellKnown): Please provide Account details
    Fastmail (WellKnown)->>OMM: account_id & uploadUrl for FASTMAIL_API token
    OMM->>OMM: Append account Id to `msg`
    OMM->>OMM: Append upload URL to `msg`
    OMM->>Fastmail (API): Send Draft folder id
    Fastmail (API)->>OMM: draft folder id
    OMM->>OMM: Store draft folder id on `msg` object
    OMM->>Fastmail (API): Provide a list of registered sender emails
    Fastmail (API)->>OMM: Mapping of from email to ID
    OMM->>OMM: Store mapping on `msg` object

These are the three steps that are done in the [fm] init - v1 group. All this configuration is store on the msg object as msg.fastmail = { ... }. I have kept all flows stateless, so this [fm] init - v1 flow is used continuously. The flow assumes that an FASTMAIL_API environment variable has been set. An API key can be obtained from the user account at Fastmail.

[
    {
        "id": "2baec5f863a37d04",
        "type": "group",
        "z": "90196166b57a77e5",
        "name": "[fm] init - v1",
        "style": {
            "label": true
        },
        "nodes": [
            "ec76c338b8557196",
            "13730bd6088ebd5c",
            "6b5447389835315d",
            "8cd65ac22eed3123",
            "7ffcc73c52c06f52",
            "2e0d64da4844738a",
            "b87b4a0317bf4065",
            "be5add6bef62b488",
            "2a39ebb7b2c86d94",
            "74b92df3cbe129f9",
            "adb3070af26d5241"
        ],
        "x": 485,
        "y": 98,
        "w": 671,
        "h": 413
    },
    {
        "id": "ec76c338b8557196",
        "type": "http request",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "Fastmail (WellKnown)",
        "method": "GET",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 969,
        "y": 261,
        "wires": [
            [
                "13730bd6088ebd5c"
            ]
        ]
    },
    {
        "id": "13730bd6088ebd5c",
        "type": "function",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "get account_id",
        "func": "msg.fastmail = {\n    ...msg.fastmail,\n    account: msg.payload.accounts,\n    account_id: Object.keys(msg.payload.accounts)[0]\n};\n\nmsg.fastmail[\"uploadUrl\"] = msg.payload.uploadUrl.replace( \"{accountId}\", msg.fastmail.account_id );\n\nmsg.headers = {\n    \"Authorization\": \"Bearer \" + env.get(\"FASTMAIL_API\")\n}\nmsg.url = \"https://api.fastmail.com/jmap/api\"\n\nmsg.payload = {\n        \"using\": [\n            \"urn:ietf:params:jmap:core\",\n            \"urn:ietf:params:jmap:mail\"\n        ],\n        \"methodCalls\": [\n            [\n                \"Mailbox/query\",\n                {\n                    \"accountId\": msg.fastmail.account_id,\n                    \"filter\": { \n                        \"name\": \"Drafts\" \n                    }\n                },\n                \"a\",\n            ]\n        ],\n    };\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 720,
        "y": 316,
        "wires": [
            [
                "6b5447389835315d"
            ]
        ]
    },
    {
        "id": "6b5447389835315d",
        "type": "http request",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "Fastmail (API)",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 949,
        "y": 316,
        "wires": [
            [
                "8cd65ac22eed3123"
            ]
        ]
    },
    {
        "id": "8cd65ac22eed3123",
        "type": "function",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "get draft mailbox id",
        "func": "msg.fastmail = {\n    ...msg.fastmail,\n    draft_mailbox_id: msg.payload[\"methodResponses\"][0][1][\"ids\"][0]\n};\n\nmsg.headers = {\n    \"Authorization\": \"Bearer \" + env.get(\"FASTMAIL_API\")\n}\n\nmsg.url = \"https://api.fastmail.com/jmap/api\"\n\nmsg.payload = {\n    \"using\": [\n        \"urn:ietf:params:jmap:core\",\n        \"urn:ietf:params:jmap:mail\",\n        \"urn:ietf:params:jmap:submission\",\n    ],\n    \"methodCalls\": [\n        [\n            \"Identity/get\",\n            {\n                \"accountId\": msg.fastmail.account_id,\n            },\n            \"pluckaduck\"\n        ]\n    ]\n};\n\nreturn msg;\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 730,
        "y": 377,
        "wires": [
            [
                "7ffcc73c52c06f52"
            ]
        ]
    },
    {
        "id": "7ffcc73c52c06f52",
        "type": "http request",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "Fastmail (API)",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 949,
        "y": 377,
        "wires": [
            [
                "2e0d64da4844738a"
            ]
        ]
    },
    {
        "id": "2e0d64da4844738a",
        "type": "function",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "email aliases to ids",
        "func": "var emailToId = {};\nvar lst = msg.payload[\"methodResponses\"][0][1]['list'];\n\nfor ( var idx = 0 ; idx < lst.length ; idx++ ) {\n    var dt = lst[idx];\n    emailToId[dt.email] = dt.id;\n}\n\nmsg.fastmail = {\n    ...msg.fastmail,\n    emailsToIds: emailToId\n};\n\ndelete msg.payload;\n\nreturn msg; \n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 836.0001220703125,
        "y": 441,
        "wires": [
            [
                "adb3070af26d5241"
            ]
        ]
    },
    {
        "id": "b87b4a0317bf4065",
        "type": "function",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "init request",
        "func": "msg.headers = {\n    \"Authorization\": \"Bearer \" + env.get(\"FASTMAIL_API\")\n}\nmsg.url = \"https://api.fastmail.com/.well-known/jmap\"\n\nmsg.fastmail = {}\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 710,
        "y": 261,
        "wires": [
            [
                "ec76c338b8557196"
            ]
        ]
    },
    {
        "id": "be5add6bef62b488",
        "type": "catch",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "",
        "scope": [
            "ec76c338b8557196",
            "13730bd6088ebd5c",
            "6b5447389835315d",
            "8cd65ac22eed3123",
            "7ffcc73c52c06f52",
            "2e0d64da4844738a",
            "b87b4a0317bf4065"
        ],
        "uncaught": false,
        "x": 742,
        "y": 139,
        "wires": [
            [
                "2a39ebb7b2c86d94"
            ]
        ]
    },
    {
        "id": "2a39ebb7b2c86d94",
        "type": "debug",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "debug 14",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 946,
        "y": 140,
        "wires": []
    },
    {
        "id": "74b92df3cbe129f9",
        "type": "link in",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "[fm] init",
        "links": [],
        "x": 526,
        "y": 222,
        "wires": [
            [
                "b87b4a0317bf4065"
            ]
        ]
    },
    {
        "id": "adb3070af26d5241",
        "type": "link out",
        "z": "90196166b57a77e5",
        "g": "2baec5f863a37d04",
        "name": "link out 71",
        "mode": "return",
        "links": [],
        "x": 1115,
        "y": 470,
        "wires": []
    }
]

We can optimise this flow because JMAP allows for batching of commands, so we can combine the last two requests into a single request. The last two steps are two commands however they can be sent together, with Fastmail sending one response with the results of both commands. The sequence diagram then becomes this:


sequenceDiagram
    OMM->>Fastmail (WellKnown): Please provide Account details
    Fastmail (WellKnown)->>OMM: account_id for FASTMAIL_API token
    OMM->>OMM: Append account Id to `msg`
    OMM->>OMM: upload URL to `msg`
    OMM->>Fastmail (API): Send Draft folder id & list of registered sender emails
    Fastmail (API)->>OMM: draft folder id & registered emails
    OMM->>OMM: Store draft folder id on `msg` object
    OMM->>OMM: Store sender email mapping on `msg` object

This improved flow is represented by the [fm] init - v2 group.

[
    {
        "id": "1dc237a9d33d5ed3",
        "type": "group",
        "z": "90196166b57a77e5",
        "name": "[fm] init - v2",
        "style": {
            "label": true
        },
        "nodes": [
            "7d74ccf860684abe",
            "95a22bf5e1205c02",
            "efac3587264d74a5",
            "bbd58f295c0e18cf",
            "5d1059e533c2d586",
            "dc70ab87b7721ecc",
            "93b5e2879c7e0daf",
            "b7a122e21273141b",
            "739fdbcd8491e989"
        ],
        "x": 1469,
        "y": 91,
        "w": 752,
        "h": 461
    },
    {
        "id": "7d74ccf860684abe",
        "type": "http request",
        "z": "90196166b57a77e5",
        "g": "1dc237a9d33d5ed3",
        "name": "Fastmail (WellKnown)",
        "method": "GET",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 1976,
        "y": 254,
        "wires": [
            [
                "95a22bf5e1205c02"
            ]
        ]
    },
    {
        "id": "95a22bf5e1205c02",
        "type": "function",
        "z": "90196166b57a77e5",
        "g": "1dc237a9d33d5ed3",
        "name": "store acount_id and upload URL and \\n prepare request draft and emails",
        "func": "msg.fastmail = {\n    ...msg.fastmail,\n    account: msg.payload.accounts,\n    account_id: Object.keys(msg.payload.accounts)[0]\n};\n\nmsg.fastmail[\"uploadUrl\"] = msg.payload.uploadUrl.replace( \"{accountId}\", msg.fastmail.account_id );\n\nmsg.headers = {\n    \"Authorization\": \"Bearer \" + env.get(\"FASTMAIL_API\")\n}\n\nmsg.url = \"https://api.fastmail.com/jmap/api\"\n\nmsg.payload = {\n        \"using\": [\n            \"urn:ietf:params:jmap:core\",\n            \"urn:ietf:params:jmap:mail\",\n            \"urn:ietf:params:jmap:submission\",\n        ],\n        \"methodCalls\": [\n            [\n                \"Mailbox/query\",\n                {\n                    \"accountId\": msg.fastmail.account_id,\n                    \"filter\": { \n                        \"name\": \"Drafts\" \n                    }\n                },\n                \"a\",\n            ],\n            [\n                \"Identity/get\",\n                {\n                    \"accountId\": msg.fastmail.account_id,\n                },\n                \"pluckaduck\"\n            ]\n        ],\n    };\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1686,
        "y": 344,
        "wires": [
            [
                "efac3587264d74a5"
            ]
        ]
    },
    {
        "id": "efac3587264d74a5",
        "type": "http request",
        "z": "90196166b57a77e5",
        "g": "1dc237a9d33d5ed3",
        "name": "Fastmail (API)",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 1986,
        "y": 344,
        "wires": [
            [
                "bbd58f295c0e18cf"
            ]
        ]
    },
    {
        "id": "bbd58f295c0e18cf",
        "type": "function",
        "z": "90196166b57a77e5",
        "g": "1dc237a9d33d5ed3",
        "name": "store draft id and email2ids list",
        "func": "msg.fastmail = {\n    ...msg.fastmail,\n    draft_mailbox_id: msg.payload[\"methodResponses\"][0][1][\"ids\"][0]\n};\n\nvar emailToId = {};\nvar lst = msg.payload[\"methodResponses\"][1][1]['list'];\n\nfor ( var idx = 0 ; idx < lst.length ; idx++ ) {\n    var dt = lst[idx];\n    emailToId[dt.email] = dt.id;\n}\n\nmsg.fastmail = {\n    ...msg.fastmail,\n    emailsToIds: emailToId\n};\n\ndelete msg.payload;\n\nreturn msg; \n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1709.0001220703125,
        "y": 435,
        "wires": [
            [
                "739fdbcd8491e989"
            ]
        ]
    },
    {
        "id": "5d1059e533c2d586",
        "type": "function",
        "z": "90196166b57a77e5",
        "g": "1dc237a9d33d5ed3",
        "name": "init request",
        "func": "msg.headers = {\n    \"Authorization\": \"Bearer \" + env.get(\"FASTMAIL_API\")\n}\nmsg.url = \"https://api.fastmail.com/.well-known/jmap\"\n\nmsg.fastmail = {}\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1717,
        "y": 254,
        "wires": [
            [
                "7d74ccf860684abe"
            ]
        ]
    },
    {
        "id": "dc70ab87b7721ecc",
        "type": "catch",
        "z": "90196166b57a77e5",
        "g": "1dc237a9d33d5ed3",
        "name": "",
        "scope": [
            "7d74ccf860684abe",
            "95a22bf5e1205c02",
            "efac3587264d74a5",
            "bbd58f295c0e18cf",
            "5d1059e533c2d586"
        ],
        "uncaught": false,
        "x": 1749,
        "y": 132,
        "wires": [
            [
                "93b5e2879c7e0daf"
            ]
        ]
    },
    {
        "id": "93b5e2879c7e0daf",
        "type": "debug",
        "z": "90196166b57a77e5",
        "g": "1dc237a9d33d5ed3",
        "name": "debug 34",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1953,
        "y": 133,
        "wires": []
    },
    {
        "id": "b7a122e21273141b",
        "type": "link in",
        "z": "90196166b57a77e5",
        "g": "1dc237a9d33d5ed3",
        "name": "[fm] init - v2",
        "links": [],
        "x": 1510,
        "y": 185,
        "wires": [
            [
                "5d1059e533c2d586"
            ]
        ]
    },
    {
        "id": "739fdbcd8491e989",
        "type": "link out",
        "z": "90196166b57a77e5",
        "g": "1dc237a9d33d5ed3",
        "name": "link out 105",
        "mode": "return",
        "links": [],
        "x": 2180,
        "y": 511,
        "wires": []
    }
]

Now we can begin to send emails, the next flow shows how that is done.

Sending Email

The sequence diagram for sending email:


sequenceDiagram
    OMM->>OMM: Initialise Fastmail
    OMM->>OMM: Construct email object
    OMM->>Fastmail (API): Create draft email in draft folder & send email from there
    Fastmail (API)->>OMM: email sent

The sequence consists of obtaining initial data from Fastmail, creating the email object, filling it with content. The email object created for creating the draft contains the from, to, text content and html content of the email, the details required for an email. The sequence continues by creating a draft email and immediately sending the email. Those two separate commands that Fastmail executes.

The Node-RED flow that I created for that:

[
    {
        "id": "913082e616d84dfa",
        "type": "group",
        "z": "90196166b57a77e5",
        "name": "[fm] send email",
        "style": {
            "label": true
        },
        "nodes": [
            "78152de1e45c8c18",
            "84adc7ab61115326",
            "7f5f8e8f9041be65",
            "414a7c88302f52db",
            "093e9a4fc6067fbe",
            "858a5a9948ca5ce8",
            "ce0e6936b12cc43b",
            "106fe2967e41dced"
        ],
        "x": 485,
        "y": 665,
        "w": 531,
        "h": 372
    },
    {
        "id": "78152de1e45c8c18",
        "type": "function",
        "z": "90196166b57a77e5",
        "g": "913082e616d84dfa",
        "name": "send email",
        "func": "var to_email   = msg.email.to;\nvar cc_email   = msg.email.cc;\nvar from_email = msg.email.from || env.get(\"SNDML_FROM_EMAIL\");\nvar bcc_email  = msg.email.bcc || env.get(\"SNDML_BCC_EMAIL\");\n\nif ( !from_email ) {\n    throw \"Missing from email when sending email\";\n}\n\nvar draft_id = \"draft_\" + RED.util.generateId();\n\nvar draft = {\n    \"from\": [\n        { \n            \"email\": from_email, \n            \"name\": msg.email.from_name || from_email\n        }\n    ],\n    \"to\": [\n        { \n            \"email\": to_email \n        }\n    ],\n    \"subject\": msg.email.subject,\n    \"keywords\": { \"$draft\": true },\n    \"mailboxIds\": {  },\n    \"bodyValues\": {\n        \"body\": { \"value\": msg.email.text, \"charset\": \"utf-8\" },\n        \"bodyHTML\": { \"value\": msg.email.html, \"charset\": \"utf-8\" }\n    },\n    \"textBody\": [{ \"partId\": \"body\", \"type\": \"text/plain\" }],\n    \"htmlBody\": [{ \"partId\": \"bodyHTML\", \"type\": \"text/html\" }],\n};\n\nvar rcptTo = [\n    {\n        \"email\": to_email,\n        \"parameters\": null\n    }\n];\n\nif ( bcc_email && (msg.email.bcc != false)) {\n    draft[\"bcc\"] = [{ \"email\": bcc_email }];\n    rcptTo.push({\n        \"email\": bcc_email,\n            \"parameters\": \"bcc\"\n    });\n}\n\nif (cc_email && (msg.email.cc != false)) {\n    draft[\"cc\"] = [{ \"email\": cc_email }];\n    rcptTo.push({\n        \"email\": cc_email,\n        \"parameters\": \"cc\"\n    });\n}\n\nif (msg.email.attachments) {\n    draft[\"attachments\"] = [];\n    msg.email.attachments.forEach(function(att){\n        draft[\"attachments\"].push({\n            \"blobId\": att.blobId,\n            \"type\": att.type,\n            \"name\": att.filename\n        });\n    });\n}\n\ndraft[\"mailboxIds\"][msg.fastmail.draft_mailbox_id] = true;\n\nvar drafty = {};\ndrafty[draft_id] = draft;\n\nmsg.payload = {\n    \"using\": [\n        \"urn:ietf:params:jmap:core\",\n        \"urn:ietf:params:jmap:mail\",\n        \"urn:ietf:params:jmap:submission\",\n    ],\n    \"methodCalls\": [\n        [\n            \"Email/set\",\n            {\n                \"accountId\": msg.fastmail.account_id,\n                \"create\": drafty\n            },\n            \"a\"\n        ],\n        [\n            \"EmailSubmission/set\",\n            {\n                \"accountId\": msg.fastmail.account_id,\n                \"onSuccessDestroyEmail\": [\"#sendIt\"],\n                \"create\": {\n                    \"sendIt\": {\n                        \"identityId\": msg.fastmail.emailsToIds[from_email],\n                        \"emailId\": \"#\" + draft_id,\n                        \"envelope\": {\n                            \"mailFrom\": {\n                                \"email\": from_email,\n                                \"parameters\": null\n                            },\n                            \"rcptTo\": rcptTo\n                        },\n                    }\n                },\n            },\n            \"b\",\n        ],\n    ],\n}\n\nmsg.headers = {\n    \"Authorization\": \"Bearer \" + env.get(\"FASTMAIL_API\")\n}\n\nmsg.url = \"https://api.fastmail.com/jmap/api\"\n\nreturn msg;\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 736,
        "y": 876,
        "wires": [
            [
                "7f5f8e8f9041be65"
            ]
        ]
    },
    {
        "id": "84adc7ab61115326",
        "type": "link call",
        "z": "90196166b57a77e5",
        "g": "913082e616d84dfa",
        "name": "[fm] init - v2",
        "links": [
            "b7a122e21273141b"
        ],
        "linkType": "static",
        "timeout": "30",
        "x": 739,
        "y": 795,
        "wires": [
            [
                "78152de1e45c8c18"
            ]
        ]
    },
    {
        "id": "7f5f8e8f9041be65",
        "type": "http request",
        "z": "90196166b57a77e5",
        "g": "913082e616d84dfa",
        "name": "Fastmail (API)",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 759,
        "y": 970,
        "wires": [
            [
                "093e9a4fc6067fbe"
            ]
        ]
    },
    {
        "id": "414a7c88302f52db",
        "type": "link in",
        "z": "90196166b57a77e5",
        "g": "913082e616d84dfa",
        "name": "[fm] send email",
        "links": [],
        "x": 526,
        "y": 763,
        "wires": [
            [
                "84adc7ab61115326"
            ]
        ]
    },
    {
        "id": "093e9a4fc6067fbe",
        "type": "link out",
        "z": "90196166b57a77e5",
        "g": "913082e616d84dfa",
        "name": "link out 96",
        "mode": "return",
        "links": [],
        "x": 975,
        "y": 996,
        "wires": []
    },
    {
        "id": "858a5a9948ca5ce8",
        "type": "catch",
        "z": "90196166b57a77e5",
        "g": "913082e616d84dfa",
        "name": "",
        "scope": [
            "78152de1e45c8c18",
            "7f5f8e8f9041be65"
        ],
        "uncaught": false,
        "x": 647,
        "y": 706,
        "wires": [
            [
                "ce0e6936b12cc43b"
            ]
        ]
    },
    {
        "id": "ce0e6936b12cc43b",
        "type": "debug",
        "z": "90196166b57a77e5",
        "g": "913082e616d84dfa",
        "name": "debug 30",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 818,
        "y": 706,
        "wires": []
    },
    {
        "id": "106fe2967e41dced",
        "type": "link in",
        "z": "90196166b57a77e5",
        "g": "913082e616d84dfa",
        "name": "[fm] send email (no warmup)",
        "links": [],
        "x": 526,
        "y": 830,
        "wires": [
            [
                "78152de1e45c8c18"
            ]
        ]
    }
]

The construction and sending of the email is done complete in the single function node labelled send email. The code for the node shows the construction of the email object.

In the group [fm] send email, there are two link-in nodes, one is labelled [fm] send email and is used when Fastmail has not yet been initialised. The other is the [fm] send email (no warmup) link node that is used when sending emails with attachment.

Sending Email with Attachment

For sending an email with an attachment, I always create a zip file from the original content. This makes the email smaller and provides the same flow for multiple files. Sending attachments with JMAP requires an extra step of uploading the contents to the Fastmail “Upload URL” server. The URL is obtained as part of the initialisation of Fastmail client.


sequenceDiagram
    OMM->>OMM: [fm] init - v2
    OMM->>OMM: Construct zip containing original payload
    OMM->>Fastmail (UploadURL): Upload Zip and obtain blobId
    Fastmail (UploadURL)->>OMM: blobId of Zip file
    OMM->>OMM: Create email including attachment details
    OMM->>OMM: [fm] send email (no warmup)

The same sequence diagram becomes this Node-RED flow:

[
    {
        "id": "e6f09bcaca0642ce",
        "type": "group",
        "z": "90196166b57a77e5",
        "name": "[fm] send email with attachments",
        "style": {
            "label": true
        },
        "nodes": [
            "45ec84e93e723b98",
            "f20245aba2053f83",
            "9fe5b2e2ec45b3ef",
            "f04c04f476e9b9c6",
            "5202b587b5556804",
            "e1e9182cbbbb3eb9",
            "3be4415fdf05acaf",
            "0cea4b8a0b7b0cdc",
            "59aaf14c3a34ee6f",
            "a54744c362f980f7",
            "90942be6906dc62e"
        ],
        "x": 1148,
        "y": 660,
        "w": 1081,
        "h": 281
    },
    {
        "id": "45ec84e93e723b98",
        "type": "function",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "create email content",
        "func": "if ( msg.statusCode == 200 ) {\n    msg.email.attachments = [\n        {\n            \"filename\": \"attachment.zip\",\n            \"blobId\": msg.payload.blobId,\n            \"type\": msg.payload.type\n        }\n    ]\n    return msg;\n}",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1930,
        "y": 844,
        "wires": [
            [
                "59aaf14c3a34ee6f"
            ]
        ]
    },
    {
        "id": "f20245aba2053f83",
        "type": "link call",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "[fm] init - v2",
        "links": [
            "b7a122e21273141b"
        ],
        "linkType": "static",
        "timeout": "30",
        "x": 1281,
        "y": 899,
        "wires": [
            [
                "9fe5b2e2ec45b3ef"
            ]
        ]
    },
    {
        "id": "9fe5b2e2ec45b3ef",
        "type": "change",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "email.attachments",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1456,
        "y": 701,
        "wires": [
            [
                "f04c04f476e9b9c6"
            ]
        ]
    },
    {
        "id": "f04c04f476e9b9c6",
        "type": "zip",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "",
        "mode": "compress",
        "filename": "attachment.zip",
        "compressionlevel": "9",
        "outasstring": false,
        "x": 1681,
        "y": 701,
        "wires": [
            [
                "3be4415fdf05acaf"
            ]
        ]
    },
    {
        "id": "5202b587b5556804",
        "type": "link in",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "[fm] send email with attachments",
        "links": [],
        "x": 1189,
        "y": 707,
        "wires": [
            [
                "f20245aba2053f83"
            ]
        ]
    },
    {
        "id": "e1e9182cbbbb3eb9",
        "type": "link out",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "link out 94",
        "mode": "return",
        "links": [],
        "x": 2188,
        "y": 900,
        "wires": []
    },
    {
        "id": "3be4415fdf05acaf",
        "type": "change",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "url",
                "pt": "msg",
                "to": "fastmail.uploadUrl",
                "tot": "msg"
            },
            {
                "t": "set",
                "p": "headers",
                "pt": "msg",
                "to": "{\t    \"Authorization\": \"Bearer \" & $env(\"FASTMAIL_API\")\t}",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1915,
        "y": 701,
        "wires": [
            [
                "0cea4b8a0b7b0cdc"
            ]
        ]
    },
    {
        "id": "0cea4b8a0b7b0cdc",
        "type": "http request",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "Fastmail (uploadURL)",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 1923,
        "y": 775,
        "wires": [
            [
                "45ec84e93e723b98"
            ]
        ]
    },
    {
        "id": "59aaf14c3a34ee6f",
        "type": "link call",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "[fm] send email (no warmup)",
        "links": [
            "106fe2967e41dced"
        ],
        "linkType": "static",
        "timeout": "30",
        "x": 1979,
        "y": 900,
        "wires": [
            [
                "e1e9182cbbbb3eb9"
            ]
        ]
    },
    {
        "id": "a54744c362f980f7",
        "type": "catch",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "",
        "scope": [
            "9fe5b2e2ec45b3ef",
            "f04c04f476e9b9c6",
            "3be4415fdf05acaf",
            "0cea4b8a0b7b0cdc",
            "45ec84e93e723b98"
        ],
        "uncaught": false,
        "x": 1573,
        "y": 808,
        "wires": [
            [
                "90942be6906dc62e"
            ]
        ]
    },
    {
        "id": "90942be6906dc62e",
        "type": "debug",
        "z": "90196166b57a77e5",
        "g": "e6f09bcaca0642ce",
        "name": "debug 31",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1692,
        "y": 899,
        "wires": []
    }
]

I can call the [fm] send email (no warmup) node because I have already initialised Fastmail and the details are still stored in the msg object.

Msg object contents

To send an email, the msg must contain the email details, these are stored on the email attribute of the msg object:

msg.email = {
    /* addresses */
    from: "sender@example.org",
    from_name: "Example Robot",
    to: "recipient@example.org",
    cc: false,
    bcc: false,

    /* content */
    subject: "[Prefix] Subject of Email",
    text: "Text content can differ from html content.\n",
    html: "<h1>HTML</h1> <br><b>Html</b> is a lot of fun<br>",

    /* attachments */
    attachments: [
        {
            payload: JSON.stringify(msg),
            filename: "msg.json"
        }
    ]
}

return msg;

Since this example contains an attachment, it can only be passed to the [fm] send email with attachments link-in node. It can also be passed to the [fm] send email link-in node but the attachment would be ignored.

JMAP - final feelings

My feelings towards JMAP are neutral, simply because I can see the advantages but I had to experience the negatives. The negatives being the lack of documentation (non-RFC documentation) and examples. The advantage is that once it has been understood, it makes sense. JMAP was not designed to send single emails, it was designed for mobile clients where internet connections can be flaky.

License and Disclaimer

Everything described here is in constant flux and improvement, the most current flow for JMAP functionality is available online. The sample given here works but no guarantee of success is given. The flow, as all flows there, can be exported and used at will under the don’t do evil license.[1]

The complete Node-RED flow:

Hope this helps someone, thanks for getting this far!

Last updated: 2023-07-27T11:01:39.641Z

  1. Inspiration for the license came from Douglas Crockfords’ JSLint license, further discussion at Hacker News. ↩︎


Comments powered by giscus

The author is available for Node-RED development and consultancy.