mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 04:28:15 +03:00
webmail: for domain in From address, show if domain is dmarc(-like) validated
i'm not sure this is good enough. this is based on field MsgFromValidation, but it doesn't hold the full DMARC information. we also don't know mailing list-status for all historic messages. so the red underline can occur too often.
This commit is contained in:
parent
2ff87a0f9c
commit
fb81effe45
@ -212,6 +212,13 @@ const prop = (x: {[k: string]: any}) => { return {_props: x}}
|
||||
return [dom, style, attr, prop]
|
||||
})()
|
||||
|
||||
// For authentication/security results.
|
||||
const underlineGreen = '#50c40f'
|
||||
const underlineRed = '#e15d1c'
|
||||
const underlineBlue = '#09f'
|
||||
const underlineGrey = '#aaa'
|
||||
const underlineYellow = 'yellow'
|
||||
|
||||
// join elements in l with the results of calls to efn. efn can return
|
||||
// HTMLElements, which cannot be inserted into the dom multiple times, hence the
|
||||
// function.
|
||||
@ -316,6 +323,64 @@ const formatAddressFull = (a: api.MessageAddress): string => {
|
||||
return s
|
||||
}
|
||||
|
||||
// like formatAddressFull, but underline domain with dmarc-like validation if appropriate.
|
||||
const formatAddressFullValidated = (a: api.MessageAddress, m: api.Message, use: boolean): (string | HTMLElement)[] => {
|
||||
const domainText = (s: string): HTMLElement | string => {
|
||||
if (!use) {
|
||||
return s
|
||||
}
|
||||
// We want to show how "approved" this message is given the message From's domain.
|
||||
// We have MsgFromValidation available. It's not the greatest, being a mix of
|
||||
// potential strict validations, actual DMARC policy validation, potential relaxed
|
||||
// validation, but no explicit fail or (temporary) errors. We also don't know if
|
||||
// historic messages were from a mailing list. We could add a heuristic based on
|
||||
// List-Id headers, but it would be unreliable...
|
||||
// todo: add field to Message with the exact results.
|
||||
let color = ''
|
||||
let title = ''
|
||||
switch (m.MsgFromValidation) {
|
||||
case api.Validation.ValidationStrict:
|
||||
color = underlineGreen
|
||||
title = 'Message would have matched a strict DMARC policy.'
|
||||
break
|
||||
case api.Validation.ValidationDMARC:
|
||||
color = underlineGreen
|
||||
title = 'Message matched DMARC policy of domain.'
|
||||
break
|
||||
case api.Validation.ValidationRelaxed:
|
||||
color = underlineGreen
|
||||
title = 'Domain did not have a DMARC policy, but message would match a relaxed policy if it had existed.'
|
||||
break;
|
||||
case api.Validation.ValidationNone:
|
||||
if (m.IsForward || m.IsMailingList) {
|
||||
color = underlineBlue
|
||||
title = 'Message would not pass DMARC policy, but came in through a configured mailing list or forwarding address.'
|
||||
} else {
|
||||
color = underlineRed
|
||||
title = 'Either domain did not have a DMARC policy, or message did not adhere to it.'
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Also for zero value, when unknown. E.g. for sent messages added with IMAP.
|
||||
return dom.span(attr.title('Unknown DMARC verification result.'), s)
|
||||
}
|
||||
return dom.span(attr.title(title), style({borderBottom: '1.5px solid '+color, textDecoration: 'none'}), s)
|
||||
}
|
||||
|
||||
let l: (string | HTMLElement)[] = []
|
||||
if (a.Name) {
|
||||
l.push(a.Name + ' ')
|
||||
}
|
||||
l.push('<' + a.User + '@')
|
||||
l.push(domainText(a.Domain.ASCII))
|
||||
l.push('>')
|
||||
if (a.Domain.Unicode) {
|
||||
// Not underlining because unicode domain may already cause underlining.
|
||||
l.push(' (' + a.User + '@' + a.Domain.Unicode+')')
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// format just the name if present and it doesn't look like an address, or otherwise just the email address.
|
||||
const formatAddressShort = (a: api.MessageAddress): string => {
|
||||
const n = a.Name
|
||||
@ -373,7 +438,7 @@ const loadMsgheaderView = (msgheaderelem: HTMLElement, mi: api.MessageItem, more
|
||||
dom.td(
|
||||
style({width: '100%'}),
|
||||
dom.div(style({display: 'flex', justifyContent: 'space-between'}),
|
||||
dom.div(join((msgenv.From || []).map(a => formatAddressFull(a)), () => ', ')),
|
||||
dom.div(join((msgenv.From || []).map(a => formatAddressFullValidated(a, mi.Message, !!msgenv.From && msgenv.From.length === 1)), () => ', ')),
|
||||
dom.div(
|
||||
attr.title('Received: ' + received.toString() + ';\nDate header in message: ' + (msgenv.Date ? msgenv.Date.toString() : '(missing/invalid)')),
|
||||
receivedlocal.toDateString() + ' ' + receivedlocal.toTimeString().split(' ')[0],
|
||||
|
@ -860,6 +860,12 @@ const [dom, style, attr, prop] = (function () {
|
||||
const prop = (x) => { return { _props: x }; };
|
||||
return [dom, style, attr, prop];
|
||||
})();
|
||||
// For authentication/security results.
|
||||
const underlineGreen = '#50c40f';
|
||||
const underlineRed = '#e15d1c';
|
||||
const underlineBlue = '#09f';
|
||||
const underlineGrey = '#aaa';
|
||||
const underlineYellow = 'yellow';
|
||||
// join elements in l with the results of calls to efn. efn can return
|
||||
// HTMLElements, which cannot be inserted into the dom multiple times, hence the
|
||||
// function.
|
||||
@ -959,6 +965,63 @@ const formatAddressFull = (a) => {
|
||||
}
|
||||
return s;
|
||||
};
|
||||
// like formatAddressFull, but underline domain with dmarc-like validation if appropriate.
|
||||
const formatAddressFullValidated = (a, m, use) => {
|
||||
const domainText = (s) => {
|
||||
if (!use) {
|
||||
return s;
|
||||
}
|
||||
// We want to show how "approved" this message is given the message From's domain.
|
||||
// We have MsgFromValidation available. It's not the greatest, being a mix of
|
||||
// potential strict validations, actual DMARC policy validation, potential relaxed
|
||||
// validation, but no explicit fail or (temporary) errors. We also don't know if
|
||||
// historic messages were from a mailing list. We could add a heuristic based on
|
||||
// List-Id headers, but it would be unreliable...
|
||||
// todo: add field to Message with the exact results.
|
||||
let color = '';
|
||||
let title = '';
|
||||
switch (m.MsgFromValidation) {
|
||||
case api.Validation.ValidationStrict:
|
||||
color = underlineGreen;
|
||||
title = 'Message would have matched a strict DMARC policy.';
|
||||
break;
|
||||
case api.Validation.ValidationDMARC:
|
||||
color = underlineGreen;
|
||||
title = 'Message matched DMARC policy of domain.';
|
||||
break;
|
||||
case api.Validation.ValidationRelaxed:
|
||||
color = underlineGreen;
|
||||
title = 'Domain did not have a DMARC policy, but message would match a relaxed policy if it had existed.';
|
||||
break;
|
||||
case api.Validation.ValidationNone:
|
||||
if (m.IsForward || m.IsMailingList) {
|
||||
color = underlineBlue;
|
||||
title = 'Message would not pass DMARC policy, but came in through a configured mailing list or forwarding address.';
|
||||
}
|
||||
else {
|
||||
color = underlineRed;
|
||||
title = 'Either domain did not have a DMARC policy, or message did not adhere to it.';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Also for zero value, when unknown. E.g. for sent messages added with IMAP.
|
||||
return dom.span(attr.title('Unknown DMARC verification result.'), s);
|
||||
}
|
||||
return dom.span(attr.title(title), style({ borderBottom: '1.5px solid ' + color, textDecoration: 'none' }), s);
|
||||
};
|
||||
let l = [];
|
||||
if (a.Name) {
|
||||
l.push(a.Name + ' ');
|
||||
}
|
||||
l.push('<' + a.User + '@');
|
||||
l.push(domainText(a.Domain.ASCII));
|
||||
l.push('>');
|
||||
if (a.Domain.Unicode) {
|
||||
// Not underlining because unicode domain may already cause underlining.
|
||||
l.push(' (' + a.User + '@' + a.Domain.Unicode + ')');
|
||||
}
|
||||
return l;
|
||||
};
|
||||
// format just the name if present and it doesn't look like an address, or otherwise just the email address.
|
||||
const formatAddressShort = (a) => {
|
||||
const n = a.Name;
|
||||
@ -996,7 +1059,7 @@ const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAdd
|
||||
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(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() {
|
||||
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 => formatAddressFullValidated(a, mi.Message, !!msgenv.From && msgenv.From.length === 1)), () => ', ')), 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())));
|
||||
};
|
||||
|
@ -860,6 +860,12 @@ const [dom, style, attr, prop] = (function () {
|
||||
const prop = (x) => { return { _props: x }; };
|
||||
return [dom, style, attr, prop];
|
||||
})();
|
||||
// For authentication/security results.
|
||||
const underlineGreen = '#50c40f';
|
||||
const underlineRed = '#e15d1c';
|
||||
const underlineBlue = '#09f';
|
||||
const underlineGrey = '#aaa';
|
||||
const underlineYellow = 'yellow';
|
||||
// join elements in l with the results of calls to efn. efn can return
|
||||
// HTMLElements, which cannot be inserted into the dom multiple times, hence the
|
||||
// function.
|
||||
@ -959,6 +965,63 @@ const formatAddressFull = (a) => {
|
||||
}
|
||||
return s;
|
||||
};
|
||||
// like formatAddressFull, but underline domain with dmarc-like validation if appropriate.
|
||||
const formatAddressFullValidated = (a, m, use) => {
|
||||
const domainText = (s) => {
|
||||
if (!use) {
|
||||
return s;
|
||||
}
|
||||
// We want to show how "approved" this message is given the message From's domain.
|
||||
// We have MsgFromValidation available. It's not the greatest, being a mix of
|
||||
// potential strict validations, actual DMARC policy validation, potential relaxed
|
||||
// validation, but no explicit fail or (temporary) errors. We also don't know if
|
||||
// historic messages were from a mailing list. We could add a heuristic based on
|
||||
// List-Id headers, but it would be unreliable...
|
||||
// todo: add field to Message with the exact results.
|
||||
let color = '';
|
||||
let title = '';
|
||||
switch (m.MsgFromValidation) {
|
||||
case api.Validation.ValidationStrict:
|
||||
color = underlineGreen;
|
||||
title = 'Message would have matched a strict DMARC policy.';
|
||||
break;
|
||||
case api.Validation.ValidationDMARC:
|
||||
color = underlineGreen;
|
||||
title = 'Message matched DMARC policy of domain.';
|
||||
break;
|
||||
case api.Validation.ValidationRelaxed:
|
||||
color = underlineGreen;
|
||||
title = 'Domain did not have a DMARC policy, but message would match a relaxed policy if it had existed.';
|
||||
break;
|
||||
case api.Validation.ValidationNone:
|
||||
if (m.IsForward || m.IsMailingList) {
|
||||
color = underlineBlue;
|
||||
title = 'Message would not pass DMARC policy, but came in through a configured mailing list or forwarding address.';
|
||||
}
|
||||
else {
|
||||
color = underlineRed;
|
||||
title = 'Either domain did not have a DMARC policy, or message did not adhere to it.';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Also for zero value, when unknown. E.g. for sent messages added with IMAP.
|
||||
return dom.span(attr.title('Unknown DMARC verification result.'), s);
|
||||
}
|
||||
return dom.span(attr.title(title), style({ borderBottom: '1.5px solid ' + color, textDecoration: 'none' }), s);
|
||||
};
|
||||
let l = [];
|
||||
if (a.Name) {
|
||||
l.push(a.Name + ' ');
|
||||
}
|
||||
l.push('<' + a.User + '@');
|
||||
l.push(domainText(a.Domain.ASCII));
|
||||
l.push('>');
|
||||
if (a.Domain.Unicode) {
|
||||
// Not underlining because unicode domain may already cause underlining.
|
||||
l.push(' (' + a.User + '@' + a.Domain.Unicode + ')');
|
||||
}
|
||||
return l;
|
||||
};
|
||||
// format just the name if present and it doesn't look like an address, or otherwise just the email address.
|
||||
const formatAddressShort = (a) => {
|
||||
const n = a.Name;
|
||||
@ -996,7 +1059,7 @@ const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAdd
|
||||
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(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() {
|
||||
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 => formatAddressFullValidated(a, mi.Message, !!msgenv.From && msgenv.From.length === 1)), () => ', ')), 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())));
|
||||
};
|
||||
|
@ -860,6 +860,12 @@ const [dom, style, attr, prop] = (function () {
|
||||
const prop = (x) => { return { _props: x }; };
|
||||
return [dom, style, attr, prop];
|
||||
})();
|
||||
// For authentication/security results.
|
||||
const underlineGreen = '#50c40f';
|
||||
const underlineRed = '#e15d1c';
|
||||
const underlineBlue = '#09f';
|
||||
const underlineGrey = '#aaa';
|
||||
const underlineYellow = 'yellow';
|
||||
// join elements in l with the results of calls to efn. efn can return
|
||||
// HTMLElements, which cannot be inserted into the dom multiple times, hence the
|
||||
// function.
|
||||
@ -959,6 +965,63 @@ const formatAddressFull = (a) => {
|
||||
}
|
||||
return s;
|
||||
};
|
||||
// like formatAddressFull, but underline domain with dmarc-like validation if appropriate.
|
||||
const formatAddressFullValidated = (a, m, use) => {
|
||||
const domainText = (s) => {
|
||||
if (!use) {
|
||||
return s;
|
||||
}
|
||||
// We want to show how "approved" this message is given the message From's domain.
|
||||
// We have MsgFromValidation available. It's not the greatest, being a mix of
|
||||
// potential strict validations, actual DMARC policy validation, potential relaxed
|
||||
// validation, but no explicit fail or (temporary) errors. We also don't know if
|
||||
// historic messages were from a mailing list. We could add a heuristic based on
|
||||
// List-Id headers, but it would be unreliable...
|
||||
// todo: add field to Message with the exact results.
|
||||
let color = '';
|
||||
let title = '';
|
||||
switch (m.MsgFromValidation) {
|
||||
case api.Validation.ValidationStrict:
|
||||
color = underlineGreen;
|
||||
title = 'Message would have matched a strict DMARC policy.';
|
||||
break;
|
||||
case api.Validation.ValidationDMARC:
|
||||
color = underlineGreen;
|
||||
title = 'Message matched DMARC policy of domain.';
|
||||
break;
|
||||
case api.Validation.ValidationRelaxed:
|
||||
color = underlineGreen;
|
||||
title = 'Domain did not have a DMARC policy, but message would match a relaxed policy if it had existed.';
|
||||
break;
|
||||
case api.Validation.ValidationNone:
|
||||
if (m.IsForward || m.IsMailingList) {
|
||||
color = underlineBlue;
|
||||
title = 'Message would not pass DMARC policy, but came in through a configured mailing list or forwarding address.';
|
||||
}
|
||||
else {
|
||||
color = underlineRed;
|
||||
title = 'Either domain did not have a DMARC policy, or message did not adhere to it.';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Also for zero value, when unknown. E.g. for sent messages added with IMAP.
|
||||
return dom.span(attr.title('Unknown DMARC verification result.'), s);
|
||||
}
|
||||
return dom.span(attr.title(title), style({ borderBottom: '1.5px solid ' + color, textDecoration: 'none' }), s);
|
||||
};
|
||||
let l = [];
|
||||
if (a.Name) {
|
||||
l.push(a.Name + ' ');
|
||||
}
|
||||
l.push('<' + a.User + '@');
|
||||
l.push(domainText(a.Domain.ASCII));
|
||||
l.push('>');
|
||||
if (a.Domain.Unicode) {
|
||||
// Not underlining because unicode domain may already cause underlining.
|
||||
l.push(' (' + a.User + '@' + a.Domain.Unicode + ')');
|
||||
}
|
||||
return l;
|
||||
};
|
||||
// format just the name if present and it doesn't look like an address, or otherwise just the email address.
|
||||
const formatAddressShort = (a) => {
|
||||
const n = a.Name;
|
||||
@ -996,7 +1059,7 @@ const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAdd
|
||||
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(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() {
|
||||
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 => formatAddressFullValidated(a, mi.Message, !!msgenv.From && msgenv.From.length === 1)), () => ', ')), 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())));
|
||||
};
|
||||
@ -2106,15 +2169,15 @@ const compose = (opts) => {
|
||||
}
|
||||
const color = (v) => {
|
||||
if (v === api.SecurityResult.SecurityResultYes) {
|
||||
return '#50c40f';
|
||||
return underlineGreen;
|
||||
}
|
||||
else if (v === api.SecurityResult.SecurityResultNo) {
|
||||
return '#e15d1c';
|
||||
return underlineRed;
|
||||
}
|
||||
else if (v === api.SecurityResult.SecurityResultUnknown) {
|
||||
return 'white';
|
||||
}
|
||||
return '#aaa';
|
||||
return underlineGrey;
|
||||
};
|
||||
const setBar = (c0, c1, c2, c3, c4) => {
|
||||
const stops = [
|
||||
|
@ -1289,13 +1289,13 @@ const compose = (opts: ComposeOptions) => {
|
||||
|
||||
const color = (v: api.SecurityResult) => {
|
||||
if (v === api.SecurityResult.SecurityResultYes) {
|
||||
return '#50c40f'
|
||||
return underlineGreen
|
||||
} else if (v === api.SecurityResult.SecurityResultNo) {
|
||||
return '#e15d1c'
|
||||
return underlineRed
|
||||
} else if (v === api.SecurityResult.SecurityResultUnknown) {
|
||||
return 'white'
|
||||
}
|
||||
return '#aaa'
|
||||
return underlineGrey
|
||||
}
|
||||
const setBar = (c0: string, c1: string, c2: string, c3: string, c4: string) => {
|
||||
const stops = [
|
||||
|
Loading…
x
Reference in New Issue
Block a user