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 { 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 = {}; 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 { 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 { 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 { 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)}`); } }