diff --git a/mail.js b/mail.js index 9d63bbc..168b47e 100644 --- a/mail.js +++ b/mail.js @@ -143,11 +143,12 @@ async function folders() { const dropFolderCache = () => folderCache.delete(users.current().id); async function withInline(html, attachments, mid) { - const inl = (attachments || []).filter(a => a.isInline && a.contentId && /^image\//i.test(a.contentType || '') && (a.size || 0) < 1.5 * 1024 * 1024).slice(0, 15); + // (contentId only exists on file attachments, so it cannot be selected in the list: fetch each small inline picture whole) + const inl = (attachments || []).filter(a => a.isInline && /^image\//i.test(a.contentType || '') && (a.size || 0) < 1.5 * 1024 * 1024).slice(0, 15); for (const a of inl) { try { - const full = await graph(`/me/messages/${id(mid)}/attachments/${id(a.id)}?$select=contentBytes,contentType`); - if (full && full.contentBytes) html = html.split('cid:' + String(a.contentId).replace(/^<|>$/g, '')).join(`data:${full.contentType};base64,${full.contentBytes}`); + const full = await graph(`/me/messages/${id(mid)}/attachments/${id(a.id)}`); + if (full && full.contentBytes && full.contentId) html = html.split('cid:' + String(full.contentId).replace(/^<|>$/g, '')).join(`data:${full.contentType || a.contentType};base64,${full.contentBytes}`); } catch { /* leave that picture out */ } } return html; @@ -207,11 +208,11 @@ async function handle(req, res) { } if (p === '/api/mail/message') { const mid = q.get('id'); - const m = await graph(`/me/messages/${id(mid)}?$select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,isRead,body,hasAttachments,flag,parentFolderId&$expand=attachments($select=id,name,contentType,size,isInline,contentId)`); + const m = await graph(`/me/messages/${id(mid)}?$select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,isRead,body,hasAttachments,flag,parentFolderId&$expand=attachments($select=id,name,contentType,size,isInline)`); let html = m.body && m.body.contentType === 'html' ? m.body.content || '' : textToHtml((m.body && m.body.content) || ''); html = await withInline(html, m.attachments, mid); return json(res, 200, { id: m.id, subject: m.subject || '(No subject)', from: addr(m.from), to: (m.toRecipients || []).map(addr), cc: (m.ccRecipients || []).map(addr), date: m.receivedDateTime, read: !!m.isRead, flagged: !!(m.flag && m.flag.flagStatus === 'flagged'), - html, attachments: (m.attachments || []).filter(a => !(a.isInline && a.contentId)).map(a => ({ id: a.id, name: a.name, type: a.contentType, size: a.size })) }); + html, attachments: (m.attachments || []).filter(a => !a.isInline).map(a => ({ id: a.id, name: a.name, type: a.contentType, size: a.size })) }); } if (p === '/api/mail/attachment') { const mid = q.get('id'), aid = q.get('aid'); diff --git a/tools/mail-test.js b/tools/mail-test.js index 20bd0d5..1f1dd02 100644 --- a/tools/mail-test.js +++ b/tools/mail-test.js @@ -47,14 +47,15 @@ const SECRET = 'mail-test-secret-'.repeat(3); if ((m = /^\/me\/mailFolders\/(\w+)$/.exec(g))) { const f = { inbox: folder('inbox', 'Inbox', 3, 10), drafts: folder('drafts', 'Drafts', 0, 1), sentitems: folder('sentitems', 'Sent Items', 0, 4), archive: folder('archive', 'Archive', 0, 0), junkemail: folder('junkemail', 'Junk Email', 0, 0), deleteditems: folder('deleteditems', 'Deleted Items', 0, 2) }[m[1]]; return f ? out(rs, 200, f) : out(rs, 404, { error: { message: 'no folder' } }); } if (g === '/me/mailFolders') return out(rs, 200, { value: [folder('inbox', 'Inbox', 3, 10), { id: 'CUSTOM1', displayName: 'Receipts', unreadItemCount: 1, totalItemCount: 5 }, { id: 'CUSTOM2', displayName: 'Outbox', unreadItemCount: 0, totalItemCount: 0 }] }); if ((m = /^\/me\/mailFolders\/([\w=-]+)\/messages$/.exec(g))) { const all = u.searchParams.get('$search') ? msgs.filter(x => x.subject.toLowerCase().includes(u.searchParams.get('$search').replace(/"/g, '').toLowerCase())) : msgs; return out(rs, 200, { value: all, ...(u.searchParams.get('$skip') === '0' && all.length >= 2 ? { '@odata.nextLink': 'x' } : {}) }); } + if (/\$expand=attachments\([^)]*contentId/.test(decodeURIComponent(u.search))) return out(rs, 400, { error: { message: "Parsing OData Select and Expand failed: Could not find a property named 'contentId' on type 'microsoft.graph.attachment'." } }); if ((m = /^\/me\/messages\/([\w=-]+)$/.exec(g))) { if (rq.method === 'PATCH') { ms.patched = JSON.parse(b); return out(rs, 200, {}); } if (rq.method === 'DELETE') { ms.deleted = m[1]; return out(rs, 204); } const x = msgs.find(y => y.id === m[1]); if (!x) return out(rs, 404, { error: { message: 'gone' } }); return out(rs, 200, { ...x, toRecipients: [{ emailAddress: { name: 'Nate', address: 'nate@outlook.com' } }], ccRecipients: [], body: { contentType: 'html', content: '
Hi