Files

164 lines
5.0 KiB
JavaScript

/**
* utils/graphMailer.js
* Invia email tramite Microsoft Graph API (per Exchange con 2FA/OAuth2).
* Gestisce automaticamente il token OAuth2 con cache e refresh.
*/
'use strict';
let cachedToken = null;
let tokenExpiresAt = 0;
/**
* Ottiene un access token OAuth2 da Microsoft (client credentials flow).
* Il token viene cachato per circa 55 minuti per evitare richieste continue.
*/
async function getAccessToken() {
const now = Date.now();
if (cachedToken && now < tokenExpiresAt) {
return cachedToken;
}
const { AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET } = process.env;
const url = `https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/v2.0/token`;
const body = new URLSearchParams({
client_id: AZURE_CLIENT_ID,
client_secret: AZURE_CLIENT_SECRET,
scope: 'https://graph.microsoft.com/.default',
grant_type: 'client_credentials',
});
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
if (!res.ok) {
const errText = await res.text();
throw new Error(`[Graph Auth] Errore ottenendo token OAuth2: ${res.status} ${errText}`);
}
const data = await res.json();
cachedToken = data.access_token;
// Scade in data.expires_in secondi, refresh 5 minuti prima
tokenExpiresAt = now + (data.expires_in - 300) * 1000;
return cachedToken;
}
/**
* Invia una email tramite Microsoft Graph API.
*
* @param {Object} options
* @param {string[]} options.to - Destinatari (array di email)
* @param {string[]} [options.cc] - CC (array di email)
* @param {string[]} [options.bcc] - BCC (array di email)
* @param {string} options.subject - Oggetto email
* @param {string} options.bodyHtml - Corpo HTML
* @param {Array} [options.attachments] - [{ filename, content (base64), contentType }]
* @param {Array} [options.inlineImages] - [{ cid, content (base64), contentType }]
* @returns {Promise<void>}
*/
async function sendMail({ to, cc = [], bcc = [], subject, bodyHtml, attachments = [], inlineImages = [], inReplyTo, references, messageId }) {
const sender = process.env.AZURE_MAIL_SENDER;
if (!sender) throw new Error('AZURE_MAIL_SENDER non configurato nel .env');
const token = await getAccessToken();
const toRecipients = to.map(addr => ({
emailAddress: { address: addr }
}));
const ccRecipients = cc.map(addr => ({
emailAddress: { address: addr }
}));
const bccRecipients = bcc.map(addr => ({
emailAddress: { address: addr }
}));
// Costruisce gli allegati (file + immagini inline)
const allAttachments = [
...attachments.map(a => ({
'@odata.type': '#microsoft.graph.fileAttachment',
name: a.filename,
contentType: a.contentType || 'application/octet-stream',
contentBytes: a.content, // già base64
})),
...inlineImages.map(img => ({
'@odata.type': '#microsoft.graph.fileAttachment',
name: img.cid,
contentId: img.cid,
contentType: img.contentType || 'image/png',
contentBytes: img.content, // già base64
isInline: true,
})),
];
const headers = [];
if (inReplyTo) {
headers.push({ name: 'In-Reply-To', value: inReplyTo });
}
if (references) {
headers.push({ name: 'References', value: references });
}
const payload = {
message: {
subject,
body: {
contentType: 'HTML',
content: bodyHtml,
},
toRecipients,
ccRecipients,
bccRecipients,
attachments: allAttachments,
internetMessageHeaders: headers.length ? headers : undefined,
},
saveToSentItems: true,
};
const url = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(sender)}/sendMail`;
const res = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (res.status === 202) {
let actualMessageId = messageId;
try {
// Wait 1.5 seconds for Exchange to process and place it in Sent Items
await new Promise(resolve => setTimeout(resolve, 1500));
const searchUrl = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(sender)}/mailFolders/sentItems/messages?$filter=subject eq '${subject.replace(/'/g, "''")}'&$top=1&$select=internetMessageId`;
const searchRes = await fetch(searchUrl, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (searchRes.ok) {
const searchData = await searchRes.json();
if (searchData.value && searchData.value.length > 0) {
actualMessageId = searchData.value[0].internetMessageId;
}
}
} catch (searchErr) {
console.warn('[Graph Mailer] Failed to retrieve actual InternetMessageId from Sent Items:', searchErr.message);
}
return { internetMessageId: actualMessageId };
}
const errText = await res.text();
throw new Error(`[Graph Mail] Errore invio email: ${res.status} ${errText}`);
}
module.exports = { sendMail };