diff --git a/web/src/components/admin/AdminEmail.jsx b/web/src/components/admin/AdminEmail.jsx index a33a467..8ec99d1 100644 --- a/web/src/components/admin/AdminEmail.jsx +++ b/web/src/components/admin/AdminEmail.jsx @@ -66,14 +66,19 @@ export function EmailCreatorTab() { } }; - const downloadOutlook = () => { + const downloadOutlook = async () => { const slug = form.subject.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'email'; - downloadOutlookDraft({ - subject: form.subject, - html: data.html, - filename: `${slug}-outlook.eml`, - }); - flash('outlook'); + try { + setError(''); + await downloadOutlookDraft({ + subject: form.subject, + html: data.html, + filename: `${slug}-outlook.eml`, + }); + flash('outlook'); + } catch (err) { + setError(err.message || "Impossible de créer le brouillon Outlook."); + } }; const download = () => { diff --git a/web/src/components/admin/AdminTabs.jsx b/web/src/components/admin/AdminTabs.jsx index 0840912..b643f7b 100644 --- a/web/src/components/admin/AdminTabs.jsx +++ b/web/src/components/admin/AdminTabs.jsx @@ -393,14 +393,19 @@ function EmailModal({ renc, onClose }) { } }; - const downloadOutlook = () => { + const downloadOutlook = async () => { const slug = renc.titre.normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase(); - downloadOutlookDraft({ - subject: data.subject, - html: data.html, - filename: `invitation-${slug}-outlook.eml`, - }); - flash('outlook'); + try { + setError(''); + await downloadOutlookDraft({ + subject: data.subject, + html: data.html, + filename: `invitation-${slug}-outlook.eml`, + }); + flash('outlook'); + } catch (err) { + setError(err.message || "Impossible de créer le brouillon Outlook."); + } }; const download = () => { diff --git a/web/src/lib/outlookDraft.js b/web/src/lib/outlookDraft.js index 12e8ee2..58c1a3b 100644 --- a/web/src/lib/outlookDraft.js +++ b/web/src/lib/outlookDraft.js @@ -1,5 +1,7 @@ -function utf8Base64(value) { - const bytes = new TextEncoder().encode(String(value ?? '')); +const LOGO_CID = 'sbc-business-club-logo'; +const MIME_BOUNDARY = '----=_SBC_Outlook_Draft_Logo'; + +function bytesBase64(bytes) { let binary = ''; for (let offset = 0; offset < bytes.length; offset += 0x8000) { @@ -9,27 +11,70 @@ function utf8Base64(value) { return globalThis.btoa(binary); } +function utf8Base64(value) { + return bytesBase64(new TextEncoder().encode(String(value ?? ''))); +} + function wrapBase64(value) { return value.match(/.{1,76}/g)?.join('\r\n') ?? ''; } -export function buildOutlookDraftMime({ subject, html }) { +function logoSource(html) { + return String(html).match(/\bsrc=["']([^"']*\/assets\/logo\.jpg(?:\?[^"']*)?)["']/i)?.[1] || '/assets/logo.jpg'; +} + +function useInlineLogo(html) { + return String(html).replace( + /(\bsrc\s*=\s*)(["'])(?:https?:\/\/[^"']+)?\/assets\/logo\.jpg(?:\?[^"']*)?\2/gi, + (_match, prefix, quote) => `${prefix}${quote}cid:${LOGO_CID}${quote}`, + ); +} + +export function buildOutlookDraftMime({ subject, html, logoBase64 = '' }) { // X-Unsent asks Outlook to open the MIME message as a message to compose. // Loading HTML as a real MIME body bypasses Outlook's destructive paste merge. + if (!logoBase64) { + return [ + 'X-Unsent: 1', + `Subject: =?UTF-8?B?${utf8Base64(subject)}?=`, + 'MIME-Version: 1.0', + 'Content-Type: text/html; charset=UTF-8', + 'Content-Transfer-Encoding: base64', + '', + wrapBase64(utf8Base64(html)), + '', + ].join('\r\n'); + } + return [ 'X-Unsent: 1', `Subject: =?UTF-8?B?${utf8Base64(subject)}?=`, 'MIME-Version: 1.0', + `Content-Type: multipart/related; type="text/html"; boundary="${MIME_BOUNDARY}"`, + '', + `--${MIME_BOUNDARY}`, 'Content-Type: text/html; charset=UTF-8', 'Content-Transfer-Encoding: base64', '', - wrapBase64(utf8Base64(html)), + wrapBase64(utf8Base64(useInlineLogo(html))), + `--${MIME_BOUNDARY}`, + 'Content-Type: image/jpeg; name="logo.jpg"', + 'Content-Transfer-Encoding: base64', + `Content-ID: <${LOGO_CID}>`, + 'Content-Disposition: inline; filename="logo.jpg"', + '', + wrapBase64(logoBase64), + `--${MIME_BOUNDARY}--`, '', ].join('\r\n'); } -export function downloadOutlookDraft({ subject, html, filename }) { - const mime = buildOutlookDraftMime({ subject, html }); +export async function downloadOutlookDraft({ subject, html, filename }) { + const response = await fetch(logoSource(html), { cache: 'force-cache' }); + if (!response.ok) throw new Error("Le logo de l'e-mail n'a pas pu être chargé."); + + const logoBase64 = bytesBase64(new Uint8Array(await response.arrayBuffer())); + const mime = buildOutlookDraftMime({ subject, html, logoBase64 }); const blob = new Blob([mime], { type: 'message/rfc822' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a');