From 3d80c05423d0866b8ea7fc4ad6e6f021182acb71 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Mon, 20 Nov 2023 21:36:40 +0100 Subject: [PATCH] webmail: for long to/cc/bcc address list (>5) show the first 4 and a button to show the rest for issue #98 by mattfbacon, thanks --- webmail/lib.ts | 33 +++++++++++++++++++++++++++++---- webmail/msg.js | 19 ++++++++++++++++--- webmail/msg.ts | 2 +- webmail/text.js | 17 +++++++++++++++-- webmail/webmail.js | 21 +++++++++++++++++---- webmail/webmail.ts | 4 ++-- 6 files changed, 80 insertions(+), 16 deletions(-) diff --git a/webmail/lib.ts b/webmail/lib.ts index 132fb8e..917294b 100644 --- a/webmail/lib.ts +++ b/webmail/lib.ts @@ -334,10 +334,35 @@ const equalAddress = (a: api.MessageAddress, b: api.MessageAddress) => { return (!a.User || !b.User || a.User === b.User) && a.Domain.ASCII === b.Domain.ASCII } +const addressList = (allAddrs: boolean, l: api.MessageAddress[]) => { + if (l.length <= 5 || allAddrs) { + return dom.span(join(l.map(a => formatAddressFull(a)), () => ', ')) + } + let elem = dom.span( + join( + l.slice(0, 4).map(a => formatAddressFull(a)), + () => ', ' + ), + ' ', + dom.clickbutton('More...', attr.title('More addresses:\n'+l.slice(4).map(a => formatAddressFull(a)).join(',\n')), function click() { + const nelem = dom.span( + join(l.map(a => formatAddressFull(a)), () => ', '), + ' ', + dom.clickbutton('Less...', function click() { + elem.replaceWith(addressList(allAddrs, l)) + }), + ) + elem.replaceWith(nelem) + elem = nelem + }) + ) + return elem +} + // loadMsgheaderView loads the common message headers into msgheaderelem. // if refineKeyword is set, labels are shown and a click causes a call to // refineKeyword. -const loadMsgheaderView = (msgheaderelem: HTMLElement, mi: api.MessageItem, moreHeaders: string[], refineKeyword: null | ((kw: string) => Promise)) => { +const loadMsgheaderView = (msgheaderelem: HTMLElement, mi: api.MessageItem, moreHeaders: string[], refineKeyword: null | ((kw: string) => Promise), allAddrs: boolean) => { const msgenv = mi.Envelope const received = mi.Message.Received const receivedlocal = new Date(received.getTime()) @@ -362,15 +387,15 @@ const loadMsgheaderView = (msgheaderelem: HTMLElement, mi: api.MessageItem, more ), dom.tr( dom.td('To:', style({textAlign: 'right', color: '#555', whiteSpace: 'nowrap'})), - dom.td(join((msgenv.To || []).map(a => formatAddressFull(a)), () => ', ')), + dom.td(addressList(allAddrs, msgenv.To || [])), ), (msgenv.CC || []).length === 0 ? [] : dom.tr( dom.td('Cc:', style({textAlign: 'right', color: '#555', whiteSpace: 'nowrap'})), - dom.td(join((msgenv.CC || []).map(a => formatAddressFull(a)), () => ', ')), + dom.td(addressList(allAddrs, msgenv.CC || [])), ), (msgenv.BCC || []).length === 0 ? [] : dom.tr( dom.td('Bcc:', style({textAlign: 'right', color: '#555', whiteSpace: 'nowrap'})), - dom.td(join((msgenv.BCC || []).map(a => formatAddressFull(a)), () => ', ')), + dom.td(addressList(allAddrs, msgenv.BCC || [])), ), dom.tr( dom.td('Subject:', style({textAlign: 'right', color: '#555', whiteSpace: 'nowrap'})), diff --git a/webmail/msg.js b/webmail/msg.js index 4e035ec..34c9a2b 100644 --- a/webmail/msg.js +++ b/webmail/msg.js @@ -974,16 +974,29 @@ const formatEmailASCII = (a) => { const equalAddress = (a, b) => { return (!a.User || !b.User || a.User === b.User) && a.Domain.ASCII === b.Domain.ASCII; }; +const addressList = (allAddrs, l) => { + if (l.length <= 5 || allAddrs) { + return dom.span(join(l.map(a => formatAddressFull(a)), () => ', ')); + } + let elem = dom.span(join(l.slice(0, 4).map(a => formatAddressFull(a)), () => ', '), ' ', dom.clickbutton('More...', attr.title('More addresses:\n' + l.slice(4).map(a => formatAddressFull(a)).join(',\n')), function click() { + const nelem = dom.span(join(l.map(a => formatAddressFull(a)), () => ', '), ' ', dom.clickbutton('Less...', function click() { + elem.replaceWith(addressList(allAddrs, l)); + })); + elem.replaceWith(nelem); + elem = nelem; + })); + return elem; +}; // loadMsgheaderView loads the common message headers into msgheaderelem. // if refineKeyword is set, labels are shown and a click causes a call to // refineKeyword. -const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword) => { +const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAddrs) => { const msgenv = mi.Envelope; const received = mi.Message.Received; const receivedlocal = new Date(received.getTime()); dom._kids(msgheaderelem, // todo: make addresses clickable, start search (keep current mailbox if any) - dom.tr(dom.td('From:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(style({ width: '100%' }), dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(join((msgenv.From || []).map(a => formatAddressFull(a)), () => ', ')), dom.div(attr.title('Received: ' + received.toString() + ';\nDate header in message: ' + (msgenv.Date ? msgenv.Date.toString() : '(missing/invalid)')), receivedlocal.toDateString() + ' ' + receivedlocal.toTimeString().split(' ')[0])))), (msgenv.ReplyTo || []).length === 0 ? [] : dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.ReplyTo || []).map(a => formatAddressFull(a)), () => ', '))), dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.To || []).map(a => formatAddressFull(a)), () => ', '))), (msgenv.CC || []).length === 0 ? [] : dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.CC || []).map(a => formatAddressFull(a)), () => ', '))), (msgenv.BCC || []).length === 0 ? [] : dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.BCC || []).map(a => formatAddressFull(a)), () => ', '))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(msgenv.Subject || ''), dom.div(mi.Message.IsForward ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Forwarded', attr.title('Message came in from a forwarded address. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.IsMailingList ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Mailing list', attr.title('Message was received from a mailing list. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.ReceivedTLSVersion === 1 ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #e15d1c' }), 'Without TLS', attr.title('Message received (last hop) without TLS.')) : [], mi.Message.ReceivedTLSVersion > 1 && !mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #50c40f' }), 'With TLS', attr.title('Message received (last hop) with TLS.')) : [], mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '.1em .3em', fontSize: '.9em', backgroundColor: '#d2f791', border: '1px solid #ccc', borderRadius: '3px' }), 'With RequireTLS', attr.title('Transported with RequireTLS, ensuring TLS along the entire delivery path from sender to recipient, with TLS certificate verification through MTA-STS and/or DANE.')) : [], mi.IsSigned ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message has a signature') : [], mi.IsEncrypted ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message is encrypted') : [], refineKeyword ? (mi.Message.Keywords || []).map(kw => dom.clickbutton(dom._class('keyword'), kw, async function click() { + dom.tr(dom.td('From:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(style({ width: '100%' }), dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(join((msgenv.From || []).map(a => formatAddressFull(a)), () => ', ')), dom.div(attr.title('Received: ' + received.toString() + ';\nDate header in message: ' + (msgenv.Date ? msgenv.Date.toString() : '(missing/invalid)')), receivedlocal.toDateString() + ' ' + receivedlocal.toTimeString().split(' ')[0])))), (msgenv.ReplyTo || []).length === 0 ? [] : dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.ReplyTo || []).map(a => formatAddressFull(a)), () => ', '))), dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(addressList(allAddrs, msgenv.To || []))), (msgenv.CC || []).length === 0 ? [] : dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(addressList(allAddrs, msgenv.CC || []))), (msgenv.BCC || []).length === 0 ? [] : dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(addressList(allAddrs, msgenv.BCC || []))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(msgenv.Subject || ''), dom.div(mi.Message.IsForward ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Forwarded', attr.title('Message came in from a forwarded address. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.IsMailingList ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Mailing list', attr.title('Message was received from a mailing list. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.ReceivedTLSVersion === 1 ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #e15d1c' }), 'Without TLS', attr.title('Message received (last hop) without TLS.')) : [], mi.Message.ReceivedTLSVersion > 1 && !mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #50c40f' }), 'With TLS', attr.title('Message received (last hop) with TLS.')) : [], mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '.1em .3em', fontSize: '.9em', backgroundColor: '#d2f791', border: '1px solid #ccc', borderRadius: '3px' }), 'With RequireTLS', attr.title('Transported with RequireTLS, ensuring TLS along the entire delivery path from sender to recipient, with TLS certificate verification through MTA-STS and/or DANE.')) : [], mi.IsSigned ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message has a signature') : [], mi.IsEncrypted ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message is encrypted') : [], refineKeyword ? (mi.Message.Keywords || []).map(kw => dom.clickbutton(dom._class('keyword'), kw, async function click() { await refineKeyword(kw); })) : [])))), moreHeaders.map(k => dom.tr(dom.td(k + ':', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td()))); }; @@ -995,7 +1008,7 @@ const init = () => { dom._kids(msgattachmentview, dom.div(style({ borderTop: '1px solid #ccc' }), dom.div(dom._class('pad'), 'Attachments: ', join(mi.Attachments.map(a => a.Filename || '(unnamed)'), () => ', ')))); } const msgheaderview = dom.table(dom._class('msgheaders'), style({ marginBottom: '1ex', width: '100%' })); - loadMsgheaderView(msgheaderview, mi, [], null); + loadMsgheaderView(msgheaderview, mi, [], null, true); const l = window.location.pathname.split('/'); const w = l[l.length - 1]; let iframepath; diff --git a/webmail/msg.ts b/webmail/msg.ts index 1344fac..e18314d 100644 --- a/webmail/msg.ts +++ b/webmail/msg.ts @@ -20,7 +20,7 @@ const init = () => { } const msgheaderview = dom.table(dom._class('msgheaders'), style({marginBottom: '1ex', width: '100%'})) - loadMsgheaderView(msgheaderview, mi, [], null) + loadMsgheaderView(msgheaderview, mi, [], null, true) const l = window.location.pathname.split('/') const w = l[l.length-1] diff --git a/webmail/text.js b/webmail/text.js index 087622f..189071b 100644 --- a/webmail/text.js +++ b/webmail/text.js @@ -974,16 +974,29 @@ const formatEmailASCII = (a) => { const equalAddress = (a, b) => { return (!a.User || !b.User || a.User === b.User) && a.Domain.ASCII === b.Domain.ASCII; }; +const addressList = (allAddrs, l) => { + if (l.length <= 5 || allAddrs) { + return dom.span(join(l.map(a => formatAddressFull(a)), () => ', ')); + } + let elem = dom.span(join(l.slice(0, 4).map(a => formatAddressFull(a)), () => ', '), ' ', dom.clickbutton('More...', attr.title('More addresses:\n' + l.slice(4).map(a => formatAddressFull(a)).join(',\n')), function click() { + const nelem = dom.span(join(l.map(a => formatAddressFull(a)), () => ', '), ' ', dom.clickbutton('Less...', function click() { + elem.replaceWith(addressList(allAddrs, l)); + })); + elem.replaceWith(nelem); + elem = nelem; + })); + return elem; +}; // loadMsgheaderView loads the common message headers into msgheaderelem. // if refineKeyword is set, labels are shown and a click causes a call to // refineKeyword. -const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword) => { +const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAddrs) => { const msgenv = mi.Envelope; const received = mi.Message.Received; const receivedlocal = new Date(received.getTime()); dom._kids(msgheaderelem, // todo: make addresses clickable, start search (keep current mailbox if any) - dom.tr(dom.td('From:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(style({ width: '100%' }), dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(join((msgenv.From || []).map(a => formatAddressFull(a)), () => ', ')), dom.div(attr.title('Received: ' + received.toString() + ';\nDate header in message: ' + (msgenv.Date ? msgenv.Date.toString() : '(missing/invalid)')), receivedlocal.toDateString() + ' ' + receivedlocal.toTimeString().split(' ')[0])))), (msgenv.ReplyTo || []).length === 0 ? [] : dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.ReplyTo || []).map(a => formatAddressFull(a)), () => ', '))), dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.To || []).map(a => formatAddressFull(a)), () => ', '))), (msgenv.CC || []).length === 0 ? [] : dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.CC || []).map(a => formatAddressFull(a)), () => ', '))), (msgenv.BCC || []).length === 0 ? [] : dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.BCC || []).map(a => formatAddressFull(a)), () => ', '))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(msgenv.Subject || ''), dom.div(mi.Message.IsForward ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Forwarded', attr.title('Message came in from a forwarded address. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.IsMailingList ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Mailing list', attr.title('Message was received from a mailing list. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.ReceivedTLSVersion === 1 ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #e15d1c' }), 'Without TLS', attr.title('Message received (last hop) without TLS.')) : [], mi.Message.ReceivedTLSVersion > 1 && !mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #50c40f' }), 'With TLS', attr.title('Message received (last hop) with TLS.')) : [], mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '.1em .3em', fontSize: '.9em', backgroundColor: '#d2f791', border: '1px solid #ccc', borderRadius: '3px' }), 'With RequireTLS', attr.title('Transported with RequireTLS, ensuring TLS along the entire delivery path from sender to recipient, with TLS certificate verification through MTA-STS and/or DANE.')) : [], mi.IsSigned ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message has a signature') : [], mi.IsEncrypted ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message is encrypted') : [], refineKeyword ? (mi.Message.Keywords || []).map(kw => dom.clickbutton(dom._class('keyword'), kw, async function click() { + dom.tr(dom.td('From:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(style({ width: '100%' }), dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(join((msgenv.From || []).map(a => formatAddressFull(a)), () => ', ')), dom.div(attr.title('Received: ' + received.toString() + ';\nDate header in message: ' + (msgenv.Date ? msgenv.Date.toString() : '(missing/invalid)')), receivedlocal.toDateString() + ' ' + receivedlocal.toTimeString().split(' ')[0])))), (msgenv.ReplyTo || []).length === 0 ? [] : dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.ReplyTo || []).map(a => formatAddressFull(a)), () => ', '))), dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(addressList(allAddrs, msgenv.To || []))), (msgenv.CC || []).length === 0 ? [] : dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(addressList(allAddrs, msgenv.CC || []))), (msgenv.BCC || []).length === 0 ? [] : dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(addressList(allAddrs, msgenv.BCC || []))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(msgenv.Subject || ''), dom.div(mi.Message.IsForward ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Forwarded', attr.title('Message came in from a forwarded address. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.IsMailingList ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Mailing list', attr.title('Message was received from a mailing list. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.ReceivedTLSVersion === 1 ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #e15d1c' }), 'Without TLS', attr.title('Message received (last hop) without TLS.')) : [], mi.Message.ReceivedTLSVersion > 1 && !mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #50c40f' }), 'With TLS', attr.title('Message received (last hop) with TLS.')) : [], mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '.1em .3em', fontSize: '.9em', backgroundColor: '#d2f791', border: '1px solid #ccc', borderRadius: '3px' }), 'With RequireTLS', attr.title('Transported with RequireTLS, ensuring TLS along the entire delivery path from sender to recipient, with TLS certificate verification through MTA-STS and/or DANE.')) : [], mi.IsSigned ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message has a signature') : [], mi.IsEncrypted ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message is encrypted') : [], refineKeyword ? (mi.Message.Keywords || []).map(kw => dom.clickbutton(dom._class('keyword'), kw, async function click() { await refineKeyword(kw); })) : [])))), moreHeaders.map(k => dom.tr(dom.td(k + ':', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td()))); }; diff --git a/webmail/webmail.js b/webmail/webmail.js index 05bf01d..effe624 100644 --- a/webmail/webmail.js +++ b/webmail/webmail.js @@ -974,16 +974,29 @@ const formatEmailASCII = (a) => { const equalAddress = (a, b) => { return (!a.User || !b.User || a.User === b.User) && a.Domain.ASCII === b.Domain.ASCII; }; +const addressList = (allAddrs, l) => { + if (l.length <= 5 || allAddrs) { + return dom.span(join(l.map(a => formatAddressFull(a)), () => ', ')); + } + let elem = dom.span(join(l.slice(0, 4).map(a => formatAddressFull(a)), () => ', '), ' ', dom.clickbutton('More...', attr.title('More addresses:\n' + l.slice(4).map(a => formatAddressFull(a)).join(',\n')), function click() { + const nelem = dom.span(join(l.map(a => formatAddressFull(a)), () => ', '), ' ', dom.clickbutton('Less...', function click() { + elem.replaceWith(addressList(allAddrs, l)); + })); + elem.replaceWith(nelem); + elem = nelem; + })); + return elem; +}; // loadMsgheaderView loads the common message headers into msgheaderelem. // if refineKeyword is set, labels are shown and a click causes a call to // refineKeyword. -const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword) => { +const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAddrs) => { const msgenv = mi.Envelope; const received = mi.Message.Received; const receivedlocal = new Date(received.getTime()); dom._kids(msgheaderelem, // todo: make addresses clickable, start search (keep current mailbox if any) - dom.tr(dom.td('From:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(style({ width: '100%' }), dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(join((msgenv.From || []).map(a => formatAddressFull(a)), () => ', ')), dom.div(attr.title('Received: ' + received.toString() + ';\nDate header in message: ' + (msgenv.Date ? msgenv.Date.toString() : '(missing/invalid)')), receivedlocal.toDateString() + ' ' + receivedlocal.toTimeString().split(' ')[0])))), (msgenv.ReplyTo || []).length === 0 ? [] : dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.ReplyTo || []).map(a => formatAddressFull(a)), () => ', '))), dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.To || []).map(a => formatAddressFull(a)), () => ', '))), (msgenv.CC || []).length === 0 ? [] : dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.CC || []).map(a => formatAddressFull(a)), () => ', '))), (msgenv.BCC || []).length === 0 ? [] : dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.BCC || []).map(a => formatAddressFull(a)), () => ', '))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(msgenv.Subject || ''), dom.div(mi.Message.IsForward ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Forwarded', attr.title('Message came in from a forwarded address. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.IsMailingList ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Mailing list', attr.title('Message was received from a mailing list. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.ReceivedTLSVersion === 1 ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #e15d1c' }), 'Without TLS', attr.title('Message received (last hop) without TLS.')) : [], mi.Message.ReceivedTLSVersion > 1 && !mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #50c40f' }), 'With TLS', attr.title('Message received (last hop) with TLS.')) : [], mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '.1em .3em', fontSize: '.9em', backgroundColor: '#d2f791', border: '1px solid #ccc', borderRadius: '3px' }), 'With RequireTLS', attr.title('Transported with RequireTLS, ensuring TLS along the entire delivery path from sender to recipient, with TLS certificate verification through MTA-STS and/or DANE.')) : [], mi.IsSigned ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message has a signature') : [], mi.IsEncrypted ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message is encrypted') : [], refineKeyword ? (mi.Message.Keywords || []).map(kw => dom.clickbutton(dom._class('keyword'), kw, async function click() { + dom.tr(dom.td('From:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(style({ width: '100%' }), dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(join((msgenv.From || []).map(a => formatAddressFull(a)), () => ', ')), dom.div(attr.title('Received: ' + received.toString() + ';\nDate header in message: ' + (msgenv.Date ? msgenv.Date.toString() : '(missing/invalid)')), receivedlocal.toDateString() + ' ' + receivedlocal.toTimeString().split(' ')[0])))), (msgenv.ReplyTo || []).length === 0 ? [] : dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(join((msgenv.ReplyTo || []).map(a => formatAddressFull(a)), () => ', '))), dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(addressList(allAddrs, msgenv.To || []))), (msgenv.CC || []).length === 0 ? [] : dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(addressList(allAddrs, msgenv.CC || []))), (msgenv.BCC || []).length === 0 ? [] : dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(addressList(allAddrs, msgenv.BCC || []))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td(dom.div(style({ display: 'flex', justifyContent: 'space-between' }), dom.div(msgenv.Subject || ''), dom.div(mi.Message.IsForward ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Forwarded', attr.title('Message came in from a forwarded address. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.IsMailingList ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em' }), 'Mailing list', attr.title('Message was received from a mailing list. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.ReceivedTLSVersion === 1 ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #e15d1c' }), 'Without TLS', attr.title('Message received (last hop) without TLS.')) : [], mi.Message.ReceivedTLSVersion > 1 && !mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '0px 0.15em', fontSize: '.9em', borderBottom: '1.5px solid #50c40f' }), 'With TLS', attr.title('Message received (last hop) with TLS.')) : [], mi.Message.ReceivedRequireTLS ? dom.span(style({ padding: '.1em .3em', fontSize: '.9em', backgroundColor: '#d2f791', border: '1px solid #ccc', borderRadius: '3px' }), 'With RequireTLS', attr.title('Transported with RequireTLS, ensuring TLS along the entire delivery path from sender to recipient, with TLS certificate verification through MTA-STS and/or DANE.')) : [], mi.IsSigned ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message has a signature') : [], mi.IsEncrypted ? dom.span(style({ backgroundColor: '#666', padding: '0px 0.15em', fontSize: '.9em', color: 'white', borderRadius: '.15em' }), 'Message is encrypted') : [], refineKeyword ? (mi.Message.Keywords || []).map(kw => dom.clickbutton(dom._class('keyword'), kw, async function click() { await refineKeyword(kw); })) : [])))), moreHeaders.map(k => dom.tr(dom.td(k + ':', style({ textAlign: 'right', color: '#555', whiteSpace: 'nowrap' })), dom.td()))); }; @@ -2899,7 +2912,7 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad }))); }; loadButtons(parsedMessageOpt || null); - loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword); + loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword, false); const loadHeaderDetails = (pm) => { if (msgheaderdetailsElem) { msgheaderdetailsElem.remove(); @@ -3051,7 +3064,7 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad updateKeywords: async (modseq, keywords) => { mi.Message.ModSeq = modseq; mi.Message.Keywords = keywords; - loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword); + loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword, false); loadMoreHeaders(await parsedMessagePromise); }, }; diff --git a/webmail/webmail.ts b/webmail/webmail.ts index ef58868..e886d6c 100644 --- a/webmail/webmail.ts +++ b/webmail/webmail.ts @@ -2436,7 +2436,7 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l } loadButtons(parsedMessageOpt || null) - loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword) + loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword, false) const loadHeaderDetails = (pm: api.ParsedMessage) => { if (msgheaderdetailsElem) { @@ -2705,7 +2705,7 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l updateKeywords: async (modseq: number, keywords: string[]) => { mi.Message.ModSeq = modseq mi.Message.Keywords = keywords - loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword) + loadMsgheaderView(msgheaderElem, miv.messageitem, settings.showHeaders, refineKeyword, false) loadMoreHeaders(await parsedMessagePromise) }, }