Emails from Microsoft365 using modern authentication

Hi there.

I have a flow that periodically checks a mailbox via POP3\IMAP and parses any emails in there for info etc which works great.

Microsoft are shortly turning off basic authentication for services including Exchange, this leaves me with an issue because I can't find any nodes that support anything other than basic authentication.

Has anyone found a solution for this event, I understand that I should be able to setup NodeRed as an authenticated application and create an application password but afaik there's no node which will then allow me to use an application password.....

We had the same problem (but for sending email), my approach was to register an application in our tenant (this allows to obtain an application secret), you need to at least grant the application access to the users mailbox within the AzureAD Portal

With this, you can use the graph API, to query emails for such mailbox.
I used the example Graph API calls to construct HTTP based queries.

sadly, I am not in the office, so cant grab any HTTP request examples at this time.

But here is the starting point.

2 Likes

I am currently investigating the same using the AzureAD Portal. I have currently been able to pull emails but then need to now filter through them as it will only return 10 under each API request. This maybe the only option until a new node is created.

The AzureAD portal is not too bad to use, I use it for Teams presence and posting messages into my channels. Also recently started uploading my Node Red backups to OneDrive. I started with flow example from the library on here to get the auth side working, once you have that setup the rest is just API calls. But it comes down to are you comfortable doing the coding to go with it.

This example should work with a function node if you allow packages, you would need to add nodemailer to the node:

node.js - Send Email Using Microsoft 365 Email Server In NodeJS - Stack Overflow

This example should work with a function node if you allow packages, you would need to add nodemailer to the node:

Does this still use BASIC AUTH?

MS will soon be stopping the use of BASIC SMTP auth (they have been progressing it for the last year), hence why I switched to the Graph API for email - I use it to automate our AD Accounts, so luckily wasn't to much of a task to understand.

SMTP Auth was momentarily (for 2 hours at a time) not allowing connections if using BASIC AUTH, so we decided to move away from it, in favor of Graph

However, If i can remember correctly, I think you can "opt in" to use BASIC - at the tenant level, but they are pushing really hard to keep it disabled.

In saying that i think the goal here is to query a mailbox :sweat_smile:

This doesn't answer the question, but here is what I do for sending email (i'm in the office :smiley: )
(our Access Token has access to accounts denoted in msg.email.from

Given the below, it shouldn't be to difficult to query a mailbox instead of sending from one.

so instead of https://graph.microsoft.com/v1.0/users/${msg.email.from}/sendMail
you will do https://graph.microsoft.com/v1.0/users/${msg.email.from}/messages

of if the access token is user specific:
https://graph.microsoft.com/v1.0/me/messages

Obviously making sure the request body is correct, below is sending an email.

const AccessToken = "#########################"
const URL = `https://graph.microsoft.com/v1.0/users/${msg.email.from}/sendMail`;
const Body = {
    "message": {
        "subject": msg.email.subject,
        "body": {
            "contentType": "HTML",
            "content": msg.email.body
        },
        "importance": msg.email.importance,
        "toRecipients": [],
        "from": {
            "emailAddress": {
                "address": msg.email.from
            }
        }
    }
}

const Headers = {
    "Authorization": `Bearer ${AccessToken}`,
    "Content-Type": "application/json"
}

if(msg.email.attachments !== undefined){
    Body.message.attachments = [];
    for (let i = 0; i < msg.email.attachments.length; i++) {
        Body.message.attachments.push({
            "@odata.type": "#microsoft.graph.fileAttachment",
            "name": msg.email.attachments[i].name,
            "contentType": msg.email.attachments[i].type,
            "contentBytes": msg.email.attachments[i].content
        });
        
    }
       
}

if (msg.email.replyto !== undefined) {
    Body.message.replyTo = [];
    Body.message.replyTo.push({
         "emailAddress": {
                "address": msg.email.replyto
            }
    });
}

if (msg.email.bcc !== undefined) {
    Body.message.bccRecipients = []
    for (let i = 0; i < msg.email.bcc.length; i++) {
        Body.message.bccRecipients.push({
            "emailAddress": {
                "address": msg.email.bcc[i]
            }
        });
    }
}

if (msg.email.cc !== undefined) {
    Body.message.ccRecipients = []
    for (let i = 0; i < msg.email.cc.length; i++) {
        Body.message.ccRecipients.push({
            "emailAddress": {
                "address": msg.email.cc[i]
            }
        });
    }
}

for (let i = 0; i < msg.email.to.length; i++) {
    Body.message.toRecipients.push({
        "emailAddress": {
            "address": msg.email.to[i]
        }
    });
}

return {
    payload: Body,
    url: URL,
    headers: Headers
}

Brill thanks, I'll give that a try to see how far I get!

Not sure but there was also a link to some MS docs that may give the replacement approach.

Below is the API calls.
Use me for your own mailbox or the userID if another mailbox.

The below will return the child folders if you are looking for another folder rather than INBOX

https://graph.microsoft.com/v1.0/me/mailFolders/(FolderID)/childFolders

You can use ?%24skip=30 to move on down your folder list if large, url links for next are included in the returned message.

Then using the folder ID from above you then pull the emails from the folder.

https://graph.microsoft.com/v1.0/me/mailFolders/(FolderID)/messages

I then use the isRead (true or false) to see for new email. This will only return the first 10 items, but a URL is provided to pull the next 10 and so on. So you may have to do multiple pulls. I use mailbox rules to clean-up so I can get away with only one pull.

Try the explorer to test it out
Graph Explorer

Below is the flow to get your tokens and they refresh evrey 30mins. Unable to give credit to who wrote this as I cannot find it now. It was part of a presence flow that was upload. Just enter your tenant ID and client ID. Then put in the scope you require. On first inject you will get an URL which you need to enter with the code provide to give the auth.

[
    {
        "id": "40792ca0.843c24",
        "type": "http request",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "authType": "",
        "x": 710,
        "y": 160,
        "wires": [
            [
                "44c485e1.fa8d2c",
                "643ed329.5bdfec"
            ]
        ]
    },
    {
        "id": "419648fb.f1a818",
        "type": "inject",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "launch device code request",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 180,
        "y": 160,
        "wires": [
            [
                "427185e9.4132fc"
            ]
        ]
    },
    {
        "id": "657e48c9.bcda48",
        "type": "function",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "Set refresh_token",
        "func": "flow.get('refresh_token', function(err, refresh_token) {\n    if (err) {\n        node.error(err, msg);\n    } else {\n        // initialise the counter to 0 if it doesn't exist already\n        refresh_token = msg.payload.refresh_token;\n        // store the value back\n        flow.set('refresh_token',refresh_token, function(err) {\n            if (err) {\n                node.error(err, msg);\n            } else {\n                // make it part of the outgoing msg object\n                msg.refresh_token = refresh_token;\n                // send the message\n                node.status({fill:\"green\",shape:\"dot\",text:`refresh_token: ${msg.refresh_token}`});\n                node.send(msg);\n            }\n        });\n    }\n});\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 990,
        "y": 340,
        "wires": [
            []
        ]
    },
    {
        "id": "d1253feb.c9362",
        "type": "function",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "Set access_token",
        "func": "flow.get('access_token', function(err, access_token) {\n    if (err) {\n        node.error(err, msg);\n    } else {\n        // initialise the counter to 0 if it doesn't exist already\n        access_token = msg.payload.access_token;\n        // store the value back\n        flow.set('access_token',access_token, function(err) {\n            if (err) {\n                node.error(err, msg);\n            } else {\n                // make it part of the outgoing msg object\n                msg.access_token = access_token;\n                // send the message\n                node.status({fill:\"green\",shape:\"dot\",text:`access_token: ${msg.access_token}`});\n                node.send(msg);\n            }\n        });\n    }\n});\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 990,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "44c485e1.fa8d2c",
        "type": "function",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "Set device_code",
        "func": "flow.get('device_code', function(err, refresh_token) {\n    if (err) {\n        node.error(err, msg);\n    } else {\n        // initialise the counter to 0 if it doesn't exist already\n        device_code = msg.payload.device_code;\n        // store the value back\n        flow.set('device_code',device_code, function(err) {\n            if (err) {\n                node.error(err, msg);\n            } else {\n                // make it part of the outgoing msg object\n                msg.device_code = device_code;\n                // send the message\n                node.status({fill:\"green\",shape:\"dot\",text:`device_code: ${msg.device_code}`});\n                node.send(msg);\n            }\n        });\n    }\n});\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 950,
        "y": 160,
        "wires": [
            []
        ]
    },
    {
        "id": "648d5fe3.74d0b",
        "type": "function",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "",
        "func": "var context = flow.get(['tenant_id','client_id','scope','device_code']);\nvar tenant_id = context[0];\nvar client_id = context[1];\nvar scope     = context[2];\nvar device_code = context[3];\n\nif(!device_code)\n{\n    msg.delay = 5*1000;\n    return [msg, null];\n}\n\nif(tenant_id && client_id && scope && device_code)\n{\n    msg.url = \"https://login.microsoftonline.com/\"+tenant_id+\"/oauth2/v2.0/token\";\n    msg.headers = { \"Content-Type\": \"application/x-www-form-urlencoded\"};\n    msg.payload = {\n        \"client_id\": client_id,\n        \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n        \"scope\": scope,\n        \"code\": device_code\n    }\n    node.status({fill:\"green\",shape:\"dot\",text:`device_code: ${device_code.substring(0, 10)}`});\n    return [null, msg];\n}\n",
        "outputs": 2,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 320,
        "y": 320,
        "wires": [
            [
                "ddaa0b36.e42108"
            ],
            [
                "1fc0a330.6fe22d"
            ]
        ]
    },
    {
        "id": "ec8a6999.9abcd8",
        "type": "inject",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 130,
        "y": 320,
        "wires": [
            [
                "648d5fe3.74d0b"
            ]
        ]
    },
    {
        "id": "1fc0a330.6fe22d",
        "type": "http request",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "authType": "",
        "x": 510,
        "y": 320,
        "wires": [
            [
                "d32c769a.1c42b8"
            ]
        ]
    },
    {
        "id": "ba3c9093.320a7",
        "type": "comment",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "Retrieve tokens ...",
        "info": "... after login has been made in a browser",
        "x": 130,
        "y": 260,
        "wires": []
    },
    {
        "id": "d4b1dae2.c6c538",
        "type": "comment",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "refresh tokens every 30 minutes",
        "info": "",
        "x": 170,
        "y": 400,
        "wires": []
    },
    {
        "id": "396bdeb0.93db72",
        "type": "inject",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "1800",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 130,
        "y": 440,
        "wires": [
            [
                "8739bad6.405738"
            ]
        ]
    },
    {
        "id": "8739bad6.405738",
        "type": "function",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "refresh request",
        "func": "var context = flow.get(['tenant_id','client_id','scope','refresh_token']);\nvar tenant_id = context[0];\nvar client_id = context[1];\nvar scope     = context[2];\nvar refresh_token = context[3];\n\nmsg.url = \"https://login.microsoftonline.com/\"+tenant_id+\"/oauth2/v2.0/token\"; \nmsg.headers = {\n    \"Content-Type\": \"application/x-www-form-urlencoded\"\n};\nmsg.payload = {\n        \"grant_type\": \"refresh_token\",\n        \"client_id\": client_id,\n        \"refresh_token\": `${refresh_token}`,\n        \"scope\": scope\n\n};\n\nif(tenant_id && client_id && scope && refresh_token )\n    return msg;\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 340,
        "y": 440,
        "wires": [
            [
                "1aafbb4a.e050b5"
            ]
        ]
    },
    {
        "id": "1aafbb4a.e050b5",
        "type": "http request",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "authType": "",
        "x": 550,
        "y": 440,
        "wires": [
            [
                "d32c769a.1c42b8"
            ]
        ]
    },
    {
        "id": "b28b4494.18e868",
        "type": "inject",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "change my values",
        "props": [
            {
                "p": "scope",
                "v": "Presence.Read offline_access Calendars.Read Calendars.Read.Shared Files.ReadWrite.All Mail.ReadWrite",
                "vt": "str"
            },
            {
                "p": "tenant_id",
                "v": "",
                "vt": "str"
            },
            {
                "p": "client_id",
                "v": "",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "x": 150,
        "y": 100,
        "wires": [
            [
                "4bb4827.9197c7c"
            ]
        ]
    },
    {
        "id": "4bb4827.9197c7c",
        "type": "function",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "prepare context",
        "func": "flow.get('tenant_id', function(err, tenant_id) {\n    if (err) {\n        node.error(err, msg);\n    } else {\n        // store the value\n        flow.set('tenant_id',msg.tenant_id, function(err) {\n            if (err) {\n                node.error(err, msg);\n            } else {\n                flow.get('scope', function(err, scope) {\n                    if (err) {\n                        node.error(err, msg);\n                    } else {\n                        // store the value\n                        flow.set('scope',msg.scope, function(err) {\n                            if (err) {\n                                node.error(err, msg);\n                            } else {\n                                flow.get('client_id', function(err, client_id) {\n                                    if (err) {\n                                        node.error(err, msg);\n                                    } else {\n                                        // store the value\n                                        flow.set('client_id',msg.client_id, function(err) {\n                                            if (err) {\n                                                node.error(err, msg);\n                                            } \n                                            // no else here\n                                        });\n                                    }\n                                });\n                            }\n                        });\n                    }\n                });\n                node.status({fill:\"green\",shape:\"dot\",text:`OK: context prepared`});\n            }\n        });\n    }\n});",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 380,
        "y": 100,
        "wires": [
            []
        ]
    },
    {
        "id": "427185e9.4132fc",
        "type": "function",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "prepare device code request",
        "func": "msg.headers = { \"Content-Type\": \"application/x-www-form-urlencoded\"};\n\nvar context = flow.get(['tenant_id','client_id','scope']);\nvar tenant_id = context[0];\nvar client_id = context[1];\nvar scope     = context[2];\nif(tenant_id && client_id && scope)\n{\n    msg.url = \"https://login.microsoftonline.com/\"+tenant_id+\"/oauth2/v2.0/devicecode\"\n    msg.payload = {    \n        \"client_id\": client_id,\n        \"scope\": scope\n    };\n    node.status({fill:\"green\",shape:\"dot\",text:`Values passed on`});\n    return msg;\n}\n\nnode.status({fill:\"red\",shape:\"dot\",text:`ERROR: context not prepared`});",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 460,
        "y": 160,
        "wires": [
            [
                "40792ca0.843c24"
            ]
        ]
    },
    {
        "id": "ddaa0b36.e42108",
        "type": "delay",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "",
        "pauseType": "delayv",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "outputs": 1,
        "x": 520,
        "y": 240,
        "wires": [
            [
                "648d5fe3.74d0b"
            ]
        ]
    },
    {
        "id": "d32c769a.1c42b8",
        "type": "function",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "",
        "func": "var context = flow.get(['access_token','refresh_token']);\nvar access_token  = context[0]; \nvar refresh_token = context[1]; \n\nif(msg.payload.hasOwnProperty('access_token') && \nmsg.payload.hasOwnProperty('refresh_token'))\n{\n    flow.set('device_code',undefined);\n    node.status({fill:\"green\",shape:\"dot\",text:`device now logged in, pass on message`});\n    return [null, msg];     \n}\n\n\nif(access_token && refresh_token)\n{\n    flow.set('device_code',undefined);\n    node.status({fill:\"green\",shape:\"dot\",text:`device already logged in`});\n    return [];\n}\n\nif(msg.payload.hasOwnProperty('error'))\n{\n    if(msg.payload.error == \"authorization_pending\")\n    {\n        node.status({fill:\"blue\",shape:\"dot\",text:`Browser login pending`});\n        msg.delay = 5*1000;\n        return [msg, null]; \n    }\n    node.status({fill:\"red\",shape:\"dot\",text:`Error: ${msg.payload.error_description}`});\n    return [];\n}\n",
        "outputs": 2,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 740,
        "y": 320,
        "wires": [
            [
                "ddaa0b36.e42108"
            ],
            [
                "d1253feb.c9362",
                "657e48c9.bcda48"
            ]
        ]
    },
    {
        "id": "643ed329.5bdfec",
        "type": "debug",
        "z": "7c76e545.92af9c",
        "g": "8cd0bb2bd73e9d46",
        "name": "Auth link and device code",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 980,
        "y": 100,
        "wires": []
    }
]
2 Likes