Sending Emails via Outlook with Nodemailer and Microsoft Graph
Background As a Node.js developer, I've been using the Nodemailer library almost exclusively to send emails from my applications. It’s a solid, battle-tested library that works well with SMTP servers. However, when I recently needed to send emails from an Outlook account, I encountered a roadblock: SMTP mail is disabled in Azure by default. While I wanted to continue using Nodemailer for consistency, I needed an alternative approach since SMTP wasn't an option. The solution? Microsoft Graph API. Solution: A Custom Nodemailer Transport for Microsoft Graph API To integrate Microsoft Graph API with Nodemailer, I built a custom transport that acts as a simple wrapper around the Graph API for sending emails. This allows me to use Nodemailer's familiar interface while leveraging Azure’s modern authentication mechanisms. Implementation The custom transport, AzureTransport, uses the Microsoft Authentication Library (msal-node) to authenticate via OAuth 2.0. It then sends emails through the Graph API’s /sendMail endpoint. Here’s the full implementation of AzureTransport: import * as msal from "@azure/msal-node"; import { SentMessageInfo, Transport, TransportOptions } from "nodemailer"; import MailMessage from "nodemailer/lib/mailer/mail-message"; /** * Interface representing the options for AzureTransport. */ export interface AzureTransportOptions extends TransportOptions { /** Azure AD Application Client ID */ clientId: string; /** Azure AD Application Client Secret */ clientSecret: string; /** Azure AD Tenant ID */ tenantId: string; /** Option to save the sent email to Sent Items */ saveToSentItems?: boolean; } /** * Transport implementation for sending emails using Microsoft Graph API. */ export class AzureTransport implements Transport { /** Transport name */ name: string; /** Transport version */ version: string; /** Configuration options for AzureTransport */ protected config: AzureTransportOptions; /** Microsoft Graph API endpoint */ protected graphEndpoint: string; /** Cached access token information */ protected tokenInfo: msal.AuthenticationResult | null; /** * Constructs an instance of AzureTransport. * @param {AzureTransportOptions} config - Configuration options. */ public constructor(config: AzureTransportOptions) { this.name = "Azure"; this.version = "0.1"; this.config = config; this.graphEndpoint = "https://graph.microsoft.com"; this.tokenInfo = null; } /** * Retrieves an access token for Microsoft Graph API. * @returns {Promise} The authentication result containing the access token. */ protected async getAccessToken(): Promise { if (!this.tokenInfo) { const { tenantId, clientId, clientSecret } = this.config || {}; const msalConfig = { auth: { clientId, clientSecret, authority: `https://login.microsoftonline.com/${tenantId}` } }; const tokenRequest = { scopes: [`${this.graphEndpoint}/.default`] }; const cca = new msal.ConfidentialClientApplication(msalConfig); this.tokenInfo = await cca.acquireTokenByClientCredential(tokenRequest); } return this.tokenInfo; } /** * Sends an email using Microsoft Graph API. * @param {MailMessage} mail - The mail message to be sent. * @param {(err: Error | null, info: SentMessageInfo) => void} callback - Callback function to handle the response. */ public async send( mail: MailMessage, callback: (err: Error | null, info: SentMessageInfo) => void ): Promise { const { subject, from, to, text, attachments = [] } = mail.data || {}; const mailMessage = { subject, from: { emailAddress: { address: from } }, toRecipients: [ { emailAddress: { address: to } } ], body: { content: text, contentType: mail.data.html ? "html" : "text" }, attachments: attachments.map((item) => { return { "@odata.type": "#microsoft.graph.fileAttachment", name: item.filename, contentType: item.contentType, contentBytes: item.content }; }) }; try { const { accessToken } = (await this.getAccessToken()) || {}; const options = { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "

Background
As a Node.js developer, I've been using the Nodemailer library almost exclusively to send emails from my applications. It’s a solid, battle-tested library that works well with SMTP servers. However, when I recently needed to send emails from an Outlook account, I encountered a roadblock: SMTP mail is disabled in Azure by default.
While I wanted to continue using Nodemailer for consistency, I needed an alternative approach since SMTP wasn't an option. The solution? Microsoft Graph API.
Solution: A Custom Nodemailer Transport for Microsoft Graph API
To integrate Microsoft Graph API with Nodemailer, I built a custom transport that acts as a simple wrapper around the Graph API for sending emails. This allows me to use Nodemailer's familiar interface while leveraging Azure’s modern authentication mechanisms.
Implementation
The custom transport, AzureTransport
, uses the Microsoft Authentication Library (msal-node
) to authenticate via OAuth 2.0. It then sends emails through the Graph API’s /sendMail
endpoint.
Here’s the full implementation of AzureTransport
:
import * as msal from "@azure/msal-node";
import { SentMessageInfo, Transport, TransportOptions } from "nodemailer";
import MailMessage from "nodemailer/lib/mailer/mail-message";
/**
* Interface representing the options for AzureTransport.
*/
export interface AzureTransportOptions extends TransportOptions {
/** Azure AD Application Client ID */
clientId: string;
/** Azure AD Application Client Secret */
clientSecret: string;
/** Azure AD Tenant ID */
tenantId: string;
/** Option to save the sent email to Sent Items */
saveToSentItems?: boolean;
}
/**
* Transport implementation for sending emails using Microsoft Graph API.
*/
export class AzureTransport implements Transport<SentMessageInfo> {
/** Transport name */
name: string;
/** Transport version */
version: string;
/** Configuration options for AzureTransport */
protected config: AzureTransportOptions;
/** Microsoft Graph API endpoint */
protected graphEndpoint: string;
/** Cached access token information */
protected tokenInfo: msal.AuthenticationResult | null;
/**
* Constructs an instance of AzureTransport.
* @param {AzureTransportOptions} config - Configuration options.
*/
public constructor(config: AzureTransportOptions) {
this.name = "Azure";
this.version = "0.1";
this.config = config;
this.graphEndpoint = "https://graph.microsoft.com";
this.tokenInfo = null;
}
/**
* Retrieves an access token for Microsoft Graph API.
* @returns {Promise} The authentication result containing the access token.
*/
protected async getAccessToken(): Promise<msal.AuthenticationResult | null> {
if (!this.tokenInfo) {
const { tenantId, clientId, clientSecret } = this.config || {};
const msalConfig = {
auth: {
clientId,
clientSecret,
authority: `https://login.microsoftonline.com/${tenantId}`
}
};
const tokenRequest = {
scopes: [`${this.graphEndpoint}/.default`]
};
const cca = new msal.ConfidentialClientApplication(msalConfig);
this.tokenInfo = await cca.acquireTokenByClientCredential(tokenRequest);
}
return this.tokenInfo;
}
/**
* Sends an email using Microsoft Graph API.
* @param {MailMessage} mail - The mail message to be sent.
* @param {(err: Error | null, info: SentMessageInfo) => void} callback - Callback function to handle the response.
*/
public async send(
mail: MailMessage<SentMessageInfo>,
callback: (err: Error | null, info: SentMessageInfo) => void
): Promise<void> {
const { subject, from, to, text, attachments = [] } = mail.data || {};
const mailMessage = {
subject,
from: {
emailAddress: {
address: from
}
},
toRecipients: [
{
emailAddress: {
address: to
}
}
],
body: {
content: text,
contentType: mail.data.html ? "html" : "text"
},
attachments: attachments.map((item) => {
return {
"@odata.type": "#microsoft.graph.fileAttachment",
name: item.filename,
contentType: item.contentType,
contentBytes: item.content
};
})
};
try {
const { accessToken } = (await this.getAccessToken()) || {};
const options = {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ message: mailMessage, saveToSentItems: this.config.saveToSentItems })
};
const resp = await fetch(this.graphEndpoint + `/v1.0/users/${from}/sendMail`, options);
callback(null, await resp.text());
} catch (err: any) {
callback(err, null);
}
}
}
Usage Example
To use this transport in a Nodemailer setup, initialize it with your Azure credentials:
import * as nodemailer from "nodemailer";
import { AzureTransport } from "./AzureTransport";
/**
* Main function to send an email using AzureTransport with Microsoft Graph API.
*/
async function main() {
try {
// Create a transport service using AzureTransport with Microsoft authentication credentials.
const service = nodemailer.createTransport(
new AzureTransport({
clientId: "YOUR_CLIENT_ID", // Replace with your Azure AD Application Client ID
clientSecret: "YOUR_CLIENT_SECRET", // Replace with your Azure AD Application Client Secret
tenantId: "YOUR_TENANT_ID" // Replace with your Azure AD Tenant ID
})
);
// Send an email using the configured transport service.
await service.sendMail({
to: "recipient@example.com", // Replace with recipient's email address
from: "sender@example.com", // Replace with sender's email address
subject: `Test Email ${new Date()}`, // Email subject with timestamp
html: `Hello: ${new Date()}`, // Email body content
attachments: [
{
filename: "text1.txt", // Name of the attachment file
content: "aGVsbG8gd29ybGQh", // Base64-encoded string content
encoding: "base64" // Encoding type
}
]
});
console.log("Email sent successfully");
} catch (err: any) {
console.error("ERROR: Failed to send email");
console.error(err);
if (err.cause?.body) {
console.error("Error details:", await err.cause.body.text());
}
}
}
// Execute the main function
main();
Conclusion
If you’re running into Azure's SMTP limitations but still want to leverage Nodemailer, using Microsoft Graph API with a custom transport is a powerful and flexible solution.
This solution provides a simplistic approach, but it should give you enough information to enhance it according to your needs.
Happy coding!