8b1f3f581e
- email.ts : remplace Nodemailer par fetch Graph (POST /users/{email}/sendMail)
* getGraphEmailConfig() lit les credentials depuis app_settings
* getAccessToken() : client_credentials flow
* sendInvoiceEmail(senderEmail, senderName, toEmail, pdfPath, pdfFilename)
* sendTestEmail(senderEmail, senderName) : envoie à soi-même
- invoices.ts : retire la vérification SMTP, passe user.email + user.name
- settings.ts : test email utilise désormais Graph (même endpoint /smtp/test)
175 lines
5.3 KiB
TypeScript
175 lines
5.3 KiB
TypeScript
import { db } from '../db';
|
|
import { decrypt } from '../crypto';
|
|
import fs from 'fs/promises';
|
|
|
|
// ─── Types ───────────────────────────────────────────────────
|
|
|
|
interface GraphEmailConfig {
|
|
tenantId: string;
|
|
clientId: string;
|
|
clientSecret: string;
|
|
}
|
|
|
|
// ─── Helpers internes ────────────────────────────────────────
|
|
|
|
async function getGraphEmailConfig(): Promise<GraphEmailConfig> {
|
|
const result = await db.query(
|
|
`SELECT key, value FROM app_settings
|
|
WHERE key IN ('graph_tenant_id', 'graph_client_id', 'graph_client_secret_enc')`
|
|
);
|
|
const cfg: Record<string, string> = {};
|
|
for (const row of result.rows) cfg[row.key] = row.value;
|
|
|
|
if (!cfg.graph_tenant_id || !cfg.graph_client_id || !cfg.graph_client_secret_enc) {
|
|
throw new Error(
|
|
'Microsoft Graph non configuré. Allez dans Paramètres → Microsoft 365 et renseignez le Tenant ID, Client ID et Client Secret.'
|
|
);
|
|
}
|
|
|
|
return {
|
|
tenantId: cfg.graph_tenant_id,
|
|
clientId: cfg.graph_client_id,
|
|
clientSecret: decrypt(cfg.graph_client_secret_enc),
|
|
};
|
|
}
|
|
|
|
async function getAccessToken(cfg: GraphEmailConfig): Promise<string> {
|
|
const response = await fetch(
|
|
`https://login.microsoftonline.com/${cfg.tenantId}/oauth2/v2.0/token`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
grant_type: 'client_credentials',
|
|
client_id: cfg.clientId,
|
|
client_secret: cfg.clientSecret,
|
|
scope: 'https://graph.microsoft.com/.default',
|
|
}),
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const body = await response.text();
|
|
throw new Error(`Obtention token Graph échouée (${response.status}) : ${body.slice(0, 300)}`);
|
|
}
|
|
|
|
const data = (await response.json()) as { access_token: string };
|
|
return data.access_token;
|
|
}
|
|
|
|
// ─── API publique ────────────────────────────────────────────
|
|
|
|
/**
|
|
* Envoie la facture par email via Microsoft Graph (sendMail).
|
|
*
|
|
* Prérequis Azure :
|
|
* - Permission d'application : Mail.Send
|
|
* - L'expéditeur (senderEmail) doit être une boîte Exchange/M365 du tenant.
|
|
*
|
|
* @param senderEmail Email de l'expéditeur (user.email en base)
|
|
* @param senderName Nom affiché de l'expéditeur (user.name)
|
|
* @param toEmail Destinataire (company.email)
|
|
* @param pdfAbsPath Chemin absolu vers le PDF généré
|
|
* @param pdfFilename Nom du fichier à afficher dans la pièce jointe
|
|
*/
|
|
export async function sendInvoiceEmail(
|
|
senderEmail: string,
|
|
senderName: string,
|
|
toEmail: string,
|
|
pdfAbsPath: string,
|
|
pdfFilename: string
|
|
): Promise<void> {
|
|
const cfg = await getGraphEmailConfig();
|
|
const token = await getAccessToken(cfg);
|
|
|
|
// Lecture du PDF et encodage Base64
|
|
const pdfBuffer = await fs.readFile(pdfAbsPath);
|
|
const pdfBase64 = pdfBuffer.toString('base64');
|
|
|
|
const payload = {
|
|
message: {
|
|
subject: 'Facture à rembourser',
|
|
body: {
|
|
contentType: 'Text',
|
|
content: '',
|
|
},
|
|
from: {
|
|
emailAddress: { name: senderName, address: senderEmail },
|
|
},
|
|
toRecipients: [
|
|
{ emailAddress: { address: toEmail } },
|
|
],
|
|
attachments: [
|
|
{
|
|
'@odata.type': '#microsoft.graph.fileAttachment',
|
|
name: pdfFilename,
|
|
contentType: 'application/pdf',
|
|
contentBytes: pdfBase64,
|
|
},
|
|
],
|
|
},
|
|
saveToSentItems: true,
|
|
};
|
|
|
|
const resp = await fetch(
|
|
`https://graph.microsoft.com/v1.0/users/${encodeURIComponent(senderEmail)}/sendMail`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
}
|
|
);
|
|
|
|
if (!resp.ok) {
|
|
const text = await resp.text();
|
|
throw new Error(`Graph sendMail échoué (${resp.status}) : ${text.slice(0, 400)}`);
|
|
}
|
|
// 202 Accepted — Graph ne retourne pas de corps
|
|
}
|
|
|
|
/**
|
|
* Email de test (s'envoie à soi-même) via Microsoft Graph.
|
|
* Utilisé par POST /api/settings/graph/test-email
|
|
*/
|
|
export async function sendTestEmail(
|
|
senderEmail: string,
|
|
senderName: string
|
|
): Promise<void> {
|
|
const cfg = await getGraphEmailConfig();
|
|
const token = await getAccessToken(cfg);
|
|
|
|
const payload = {
|
|
message: {
|
|
subject: '[NotesFrais] Test de configuration email',
|
|
body: {
|
|
contentType: 'Text',
|
|
content: `Bonjour ${senderName},\n\nVotre configuration Microsoft Graph fonctionne correctement ✓\n\n— NotesFrais`,
|
|
},
|
|
toRecipients: [
|
|
{ emailAddress: { address: senderEmail } },
|
|
],
|
|
},
|
|
saveToSentItems: false,
|
|
};
|
|
|
|
const resp = await fetch(
|
|
`https://graph.microsoft.com/v1.0/users/${encodeURIComponent(senderEmail)}/sendMail`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
}
|
|
);
|
|
|
|
if (!resp.ok) {
|
|
const text = await resp.text();
|
|
throw new Error(`Graph sendMail test échoué (${resp.status}) : ${text.slice(0, 400)}`);
|
|
}
|
|
}
|