Embed logo in Outlook drafts

This commit is contained in:
Michael committed 2026-07-14 17:03:19 +02:00
1 parent 40f314c540
commit 7bbf2e0d4d
3 files changed
+75 -20

No files matched your search

+12 -7
View File
@@ -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'; const slug = form.subject.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'email';
downloadOutlookDraft({ try {
subject: form.subject, setError('');
html: data.html, await downloadOutlookDraft({
filename: `${slug}-outlook.eml`, subject: form.subject,
}); html: data.html,
flash('outlook'); filename: `${slug}-outlook.eml`,
});
flash('outlook');
} catch (err) {
setError(err.message || "Impossible de créer le brouillon Outlook.");
}
}; };
const download = () => { const download = () => {
+12 -7
View File
@@ -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(); const slug = renc.titre.normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase();
downloadOutlookDraft({ try {
subject: data.subject, setError('');
html: data.html, await downloadOutlookDraft({
filename: `invitation-${slug}-outlook.eml`, subject: data.subject,
}); html: data.html,
flash('outlook'); filename: `invitation-${slug}-outlook.eml`,
});
flash('outlook');
} catch (err) {
setError(err.message || "Impossible de créer le brouillon Outlook.");
}
}; };
const download = () => { const download = () => {
+51 -6
View File
@@ -1,5 +1,7 @@
function utf8Base64(value) { const LOGO_CID = 'sbc-business-club-logo';
const bytes = new TextEncoder().encode(String(value ?? '')); const MIME_BOUNDARY = '----=_SBC_Outlook_Draft_Logo';
function bytesBase64(bytes) {
let binary = ''; let binary = '';
for (let offset = 0; offset < bytes.length; offset += 0x8000) { for (let offset = 0; offset < bytes.length; offset += 0x8000) {
@@ -9,27 +11,70 @@ function utf8Base64(value) {
return globalThis.btoa(binary); return globalThis.btoa(binary);
} }
function utf8Base64(value) {
return bytesBase64(new TextEncoder().encode(String(value ?? '')));
}
function wrapBase64(value) { function wrapBase64(value) {
return value.match(/.{1,76}/g)?.join('\r\n') ?? ''; 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. // 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. // 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 [ return [
'X-Unsent: 1', 'X-Unsent: 1',
`Subject: =?UTF-8?B?${utf8Base64(subject)}?=`, `Subject: =?UTF-8?B?${utf8Base64(subject)}?=`,
'MIME-Version: 1.0', 'MIME-Version: 1.0',
`Content-Type: multipart/related; type="text/html"; boundary="${MIME_BOUNDARY}"`,
'',
`--${MIME_BOUNDARY}`,
'Content-Type: text/html; charset=UTF-8', 'Content-Type: text/html; charset=UTF-8',
'Content-Transfer-Encoding: base64', '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'); ].join('\r\n');
} }
export function downloadOutlookDraft({ subject, html, filename }) { export async function downloadOutlookDraft({ subject, html, filename }) {
const mime = buildOutlookDraftMime({ subject, html }); 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 blob = new Blob([mime], { type: 'message/rfc822' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const anchor = document.createElement('a'); const anchor = document.createElement('a');