mirror of
https://github.com/mjl-/mox.git
synced 2025-01-15 09:56:27 +03:00
webmail: fix js error rerendering additional headers after updated keywords
i've seen the error a few times: msgheaderElem.children[(msgheaderElem.children.length - 1)] is undefined i've seen it happen after sending a reply (with the "answered" flag added). the updateKeywords callback would render the message again, but the code for rendering the "additional headers" table rows again was making invalid assumptions. the approach is now changed. the backend now just immediately sends the additional headers to the frontend. before, the frontend would first render the base message, then render again once the headers came in for the parsed message. this also prevents a reflow for the (quite common) case that one of the additional headers are present in the message.
This commit is contained in:
parent
f7193bd4c3
commit
1e15a10b66
11 changed files with 167 additions and 119 deletions
|
@ -2102,6 +2102,15 @@
|
|||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "MoreHeaders",
|
||||
"Docs": "All headers from store.Settings.ShowHeaders that are present.",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"[]",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -300,6 +300,7 @@ export interface MessageItem {
|
|||
IsEncrypted: boolean
|
||||
FirstLine: string // Of message body, for showing as preview.
|
||||
MatchQuery: boolean // If message does not match query, it can still be included because of threading.
|
||||
MoreHeaders?: (string[] | null)[] | null // All headers from store.Settings.ShowHeaders that are present.
|
||||
}
|
||||
|
||||
// Message stored in database and per-message file on disk.
|
||||
|
@ -613,7 +614,7 @@ export const types: TypenameMap = {
|
|||
"EventViewErr": {"Name":"EventViewErr","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"RequestID","Docs":"","Typewords":["int64"]},{"Name":"Err","Docs":"","Typewords":["string"]}]},
|
||||
"EventViewReset": {"Name":"EventViewReset","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"RequestID","Docs":"","Typewords":["int64"]}]},
|
||||
"EventViewMsgs": {"Name":"EventViewMsgs","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"RequestID","Docs":"","Typewords":["int64"]},{"Name":"MessageItems","Docs":"","Typewords":["[]","[]","MessageItem"]},{"Name":"ParsedMessage","Docs":"","Typewords":["nullable","ParsedMessage"]},{"Name":"ViewEnd","Docs":"","Typewords":["bool"]}]},
|
||||
"MessageItem": {"Name":"MessageItem","Docs":"","Fields":[{"Name":"Message","Docs":"","Typewords":["Message"]},{"Name":"Envelope","Docs":"","Typewords":["MessageEnvelope"]},{"Name":"Attachments","Docs":"","Typewords":["[]","Attachment"]},{"Name":"IsSigned","Docs":"","Typewords":["bool"]},{"Name":"IsEncrypted","Docs":"","Typewords":["bool"]},{"Name":"FirstLine","Docs":"","Typewords":["string"]},{"Name":"MatchQuery","Docs":"","Typewords":["bool"]}]},
|
||||
"MessageItem": {"Name":"MessageItem","Docs":"","Fields":[{"Name":"Message","Docs":"","Typewords":["Message"]},{"Name":"Envelope","Docs":"","Typewords":["MessageEnvelope"]},{"Name":"Attachments","Docs":"","Typewords":["[]","Attachment"]},{"Name":"IsSigned","Docs":"","Typewords":["bool"]},{"Name":"IsEncrypted","Docs":"","Typewords":["bool"]},{"Name":"FirstLine","Docs":"","Typewords":["string"]},{"Name":"MatchQuery","Docs":"","Typewords":["bool"]},{"Name":"MoreHeaders","Docs":"","Typewords":["[]","[]","string"]}]},
|
||||
"Message": {"Name":"Message","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"UID","Docs":"","Typewords":["UID"]},{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]},{"Name":"CreateSeq","Docs":"","Typewords":["ModSeq"]},{"Name":"Expunged","Docs":"","Typewords":["bool"]},{"Name":"IsReject","Docs":"","Typewords":["bool"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"MailboxOrigID","Docs":"","Typewords":["int64"]},{"Name":"MailboxDestinedID","Docs":"","Typewords":["int64"]},{"Name":"Received","Docs":"","Typewords":["timestamp"]},{"Name":"RemoteIP","Docs":"","Typewords":["string"]},{"Name":"RemoteIPMasked1","Docs":"","Typewords":["string"]},{"Name":"RemoteIPMasked2","Docs":"","Typewords":["string"]},{"Name":"RemoteIPMasked3","Docs":"","Typewords":["string"]},{"Name":"EHLODomain","Docs":"","Typewords":["string"]},{"Name":"MailFrom","Docs":"","Typewords":["string"]},{"Name":"MailFromLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"MailFromDomain","Docs":"","Typewords":["string"]},{"Name":"RcptToLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"RcptToDomain","Docs":"","Typewords":["string"]},{"Name":"MsgFromLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"MsgFromDomain","Docs":"","Typewords":["string"]},{"Name":"MsgFromOrgDomain","Docs":"","Typewords":["string"]},{"Name":"EHLOValidated","Docs":"","Typewords":["bool"]},{"Name":"MailFromValidated","Docs":"","Typewords":["bool"]},{"Name":"MsgFromValidated","Docs":"","Typewords":["bool"]},{"Name":"EHLOValidation","Docs":"","Typewords":["Validation"]},{"Name":"MailFromValidation","Docs":"","Typewords":["Validation"]},{"Name":"MsgFromValidation","Docs":"","Typewords":["Validation"]},{"Name":"DKIMDomains","Docs":"","Typewords":["[]","string"]},{"Name":"OrigEHLODomain","Docs":"","Typewords":["string"]},{"Name":"OrigDKIMDomains","Docs":"","Typewords":["[]","string"]},{"Name":"MessageID","Docs":"","Typewords":["string"]},{"Name":"SubjectBase","Docs":"","Typewords":["string"]},{"Name":"MessageHash","Docs":"","Typewords":["nullable","string"]},{"Name":"ThreadID","Docs":"","Typewords":["int64"]},{"Name":"ThreadParentIDs","Docs":"","Typewords":["[]","int64"]},{"Name":"ThreadMissingLink","Docs":"","Typewords":["bool"]},{"Name":"ThreadMuted","Docs":"","Typewords":["bool"]},{"Name":"ThreadCollapsed","Docs":"","Typewords":["bool"]},{"Name":"IsMailingList","Docs":"","Typewords":["bool"]},{"Name":"DSN","Docs":"","Typewords":["bool"]},{"Name":"ReceivedTLSVersion","Docs":"","Typewords":["uint16"]},{"Name":"ReceivedTLSCipherSuite","Docs":"","Typewords":["uint16"]},{"Name":"ReceivedRequireTLS","Docs":"","Typewords":["bool"]},{"Name":"Seen","Docs":"","Typewords":["bool"]},{"Name":"Answered","Docs":"","Typewords":["bool"]},{"Name":"Flagged","Docs":"","Typewords":["bool"]},{"Name":"Forwarded","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Notjunk","Docs":"","Typewords":["bool"]},{"Name":"Deleted","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Phishing","Docs":"","Typewords":["bool"]},{"Name":"MDNSent","Docs":"","Typewords":["bool"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"Size","Docs":"","Typewords":["int64"]},{"Name":"TrainedJunk","Docs":"","Typewords":["nullable","bool"]},{"Name":"MsgPrefix","Docs":"","Typewords":["nullable","string"]},{"Name":"ParsedBuf","Docs":"","Typewords":["nullable","string"]}]},
|
||||
"MessageEnvelope": {"Name":"MessageEnvelope","Docs":"","Fields":[{"Name":"Date","Docs":"","Typewords":["timestamp"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"From","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"Sender","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"ReplyTo","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"To","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"CC","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"BCC","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"InReplyTo","Docs":"","Typewords":["string"]},{"Name":"MessageID","Docs":"","Typewords":["string"]}]},
|
||||
"Attachment": {"Name":"Attachment","Docs":"","Fields":[{"Name":"Path","Docs":"","Typewords":["[]","int32"]},{"Name":"Filename","Docs":"","Typewords":["string"]},{"Name":"Part","Docs":"","Typewords":["Part"]}]},
|
||||
|
|
|
@ -443,10 +443,11 @@ const addressList = (allAddrs: boolean, l: api.MessageAddress[]) => {
|
|||
// 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<void>), allAddrs: boolean) => {
|
||||
const loadMsgheaderView = (msgheaderelem: HTMLTableSectionElement, mi: api.MessageItem, moreHeaders: string[], refineKeyword: null | ((kw: string) => Promise<void>), allAddrs: boolean) => {
|
||||
const msgenv = mi.Envelope
|
||||
const received = mi.Message.Received
|
||||
const receivedlocal = new Date(received.getTime())
|
||||
// Similar to webmail.ts:/headerTextMildStyle
|
||||
const msgHeaderFieldStyle = css('msgHeaderField', {textAlign: 'right', color: styles.colorMild, whiteSpace: 'nowrap'})
|
||||
const msgAttrStyle = css('msgAttr', {padding: '0px 0.15em', fontSize: '.9em'})
|
||||
dom._kids(msgheaderelem,
|
||||
|
@ -502,11 +503,17 @@ const loadMsgheaderView = (msgheaderelem: HTMLElement, mi: api.MessageItem, more
|
|||
)
|
||||
),
|
||||
),
|
||||
moreHeaders.map(k =>
|
||||
(mi.MoreHeaders || []).map(t =>
|
||||
dom.tr(
|
||||
dom.td(k+':', msgHeaderFieldStyle),
|
||||
dom.td(),
|
||||
)
|
||||
dom.td(t![0]+':', msgHeaderFieldStyle),
|
||||
dom.td(t![1]),
|
||||
),
|
||||
),
|
||||
// Ensure width of all possible additional headers is taken into account, to
|
||||
// prevent different layout between messages when not all headers are present.
|
||||
dom.tr(
|
||||
dom.td(moreHeaders.map(s => dom.div(s+':', msgHeaderFieldStyle, style({visibility: 'hidden', height: 0})))),
|
||||
dom.td(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"io"
|
||||
"log/slog"
|
||||
"mime"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
|
@ -72,15 +73,31 @@ func tryDecodeParam(log mlog.Log, name string) string {
|
|||
|
||||
// todo: mime.FormatMediaType does not wrap long lines. should do it ourselves, and split header into several parts (if commonly supported).
|
||||
|
||||
func messageItem(log mlog.Log, m store.Message, state *msgState) (MessageItem, error) {
|
||||
pm, err := parsedMessage(log, m, state, false, true)
|
||||
func messageItemMoreHeaders(moreHeaders []string, pm ParsedMessage) (l [][2]string) {
|
||||
for _, k := range moreHeaders {
|
||||
k = textproto.CanonicalMIMEHeaderKey(k)
|
||||
for _, v := range pm.Headers[k] {
|
||||
l = append(l, [2]string{k, v})
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func messageItem(log mlog.Log, m store.Message, state *msgState, moreHeaders []string) (MessageItem, error) {
|
||||
full := len(moreHeaders) > 0
|
||||
pm, err := parsedMessage(log, m, state, full, true)
|
||||
if err != nil && errors.Is(err, message.ErrHeader) && full {
|
||||
log.Debugx("load message item without parsing headers after error", err, slog.Int64("msgid", m.ID))
|
||||
pm, err = parsedMessage(log, m, state, false, true)
|
||||
}
|
||||
if err != nil {
|
||||
return MessageItem{}, fmt.Errorf("parsing message %d for item: %v", m.ID, err)
|
||||
}
|
||||
// Clear largish unused data.
|
||||
m.MsgPrefix = nil
|
||||
m.ParsedBuf = nil
|
||||
return MessageItem{m, pm.envelope, pm.attachments, pm.isSigned, pm.isEncrypted, pm.firstLine, true}, nil
|
||||
l := messageItemMoreHeaders(moreHeaders, pm)
|
||||
return MessageItem{m, pm.envelope, pm.attachments, pm.isSigned, pm.isEncrypted, pm.firstLine, true, l}, nil
|
||||
}
|
||||
|
||||
// formatFirstLine returns a line the client can display next to the subject line
|
||||
|
|
|
@ -318,7 +318,7 @@ var api;
|
|||
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
|
||||
"EventViewReset": { "Name": "EventViewReset", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }] },
|
||||
"EventViewMsgs": { "Name": "EventViewMsgs", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageItems", "Docs": "", "Typewords": ["[]", "[]", "MessageItem"] }, { "Name": "ParsedMessage", "Docs": "", "Typewords": ["nullable", "ParsedMessage"] }, { "Name": "ViewEnd", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"MessageItem": { "Name": "MessageItem", "Docs": "", "Fields": [{ "Name": "Message", "Docs": "", "Typewords": ["Message"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["MessageEnvelope"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "Attachment"] }, { "Name": "IsSigned", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsEncrypted", "Docs": "", "Typewords": ["bool"] }, { "Name": "FirstLine", "Docs": "", "Typewords": ["string"] }, { "Name": "MatchQuery", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"MessageItem": { "Name": "MessageItem", "Docs": "", "Fields": [{ "Name": "Message", "Docs": "", "Typewords": ["Message"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["MessageEnvelope"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "Attachment"] }, { "Name": "IsSigned", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsEncrypted", "Docs": "", "Typewords": ["bool"] }, { "Name": "FirstLine", "Docs": "", "Typewords": ["string"] }, { "Name": "MatchQuery", "Docs": "", "Typewords": ["bool"] }, { "Name": "MoreHeaders", "Docs": "", "Typewords": ["[]", "[]", "string"] }] },
|
||||
"Message": { "Name": "Message", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "CreateSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Expunged", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsReject", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "MailboxOrigID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxDestinedID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Received", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "RemoteIP", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPMasked1", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPMasked2", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPMasked3", "Docs": "", "Typewords": ["string"] }, { "Name": "EHLODomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFromLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "MailFromDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "RcptToLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "RcptToDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "MsgFromDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromOrgDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "EHLOValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "MailFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "MsgFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "EHLOValidation", "Docs": "", "Typewords": ["Validation"] }, { "Name": "MailFromValidation", "Docs": "", "Typewords": ["Validation"] }, { "Name": "MsgFromValidation", "Docs": "", "Typewords": ["Validation"] }, { "Name": "DKIMDomains", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "OrigEHLODomain", "Docs": "", "Typewords": ["string"] }, { "Name": "OrigDKIMDomains", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }, { "Name": "SubjectBase", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageHash", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "ThreadID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ThreadParentIDs", "Docs": "", "Typewords": ["[]", "int64"] }, { "Name": "ThreadMissingLink", "Docs": "", "Typewords": ["bool"] }, { "Name": "ThreadMuted", "Docs": "", "Typewords": ["bool"] }, { "Name": "ThreadCollapsed", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsMailingList", "Docs": "", "Typewords": ["bool"] }, { "Name": "DSN", "Docs": "", "Typewords": ["bool"] }, { "Name": "ReceivedTLSVersion", "Docs": "", "Typewords": ["uint16"] }, { "Name": "ReceivedTLSCipherSuite", "Docs": "", "Typewords": ["uint16"] }, { "Name": "ReceivedRequireTLS", "Docs": "", "Typewords": ["bool"] }, { "Name": "Seen", "Docs": "", "Typewords": ["bool"] }, { "Name": "Answered", "Docs": "", "Typewords": ["bool"] }, { "Name": "Flagged", "Docs": "", "Typewords": ["bool"] }, { "Name": "Forwarded", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Notjunk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Phishing", "Docs": "", "Typewords": ["bool"] }, { "Name": "MDNSent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }, { "Name": "TrainedJunk", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "MsgPrefix", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "ParsedBuf", "Docs": "", "Typewords": ["nullable", "string"] }] },
|
||||
"MessageEnvelope": { "Name": "MessageEnvelope", "Docs": "", "Fields": [{ "Name": "Date", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "Sender", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "CC", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "BCC", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "InReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }] },
|
||||
"Attachment": { "Name": "Attachment", "Docs": "", "Fields": [{ "Name": "Path", "Docs": "", "Typewords": ["[]", "int32"] }, { "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "Part", "Docs": "", "Typewords": ["Part"] }] },
|
||||
|
@ -1434,13 +1434,17 @@ const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAdd
|
|||
const msgenv = mi.Envelope;
|
||||
const received = mi.Message.Received;
|
||||
const receivedlocal = new Date(received.getTime());
|
||||
// Similar to webmail.ts:/headerTextMildStyle
|
||||
const msgHeaderFieldStyle = css('msgHeaderField', { textAlign: 'right', color: styles.colorMild, whiteSpace: 'nowrap' });
|
||||
const msgAttrStyle = css('msgAttr', { padding: '0px 0.15em', fontSize: '.9em' });
|
||||
dom._kids(msgheaderelem,
|
||||
// todo: make addresses clickable, start search (keep current mailbox if any)
|
||||
dom.tr(dom.td('From:', msgHeaderFieldStyle), dom.td(style({ width: '100%' }), dom.div(css('msgFromReceivedSpread', { display: 'flex', justifyContent: 'space-between' }), dom.div(join((msgenv.From || []).map(a => formatAddressValidated(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:', msgHeaderFieldStyle), dom.td(join((msgenv.ReplyTo || []).map(a => formatAddressElem(a)), () => ', '))), dom.tr(dom.td('To:', msgHeaderFieldStyle), dom.td(addressList(allAddrs, msgenv.To || []))), (msgenv.CC || []).length === 0 ? [] : dom.tr(dom.td('Cc:', msgHeaderFieldStyle), dom.td(addressList(allAddrs, msgenv.CC || []))), (msgenv.BCC || []).length === 0 ? [] : dom.tr(dom.td('Bcc:', msgHeaderFieldStyle), dom.td(addressList(allAddrs, msgenv.BCC || []))), dom.tr(dom.td('Subject:', msgHeaderFieldStyle), dom.td(dom.div(css('msgSubjectAttrsSpread', { display: 'flex', justifyContent: 'space-between' }), dom.div(msgenv.Subject || ''), dom.div(mi.Message.IsForward ? dom.span(msgAttrStyle, 'Forwarded', attr.title('Message came in from a forwarded address. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.IsMailingList ? dom.span(msgAttrStyle, '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(msgAttrStyle, css('msgAttrNoTLS', { borderBottom: '1.5px solid', borderBottomColor: styles.underlineRed }), 'Without TLS', attr.title('Message received (last hop) without TLS.')) : [], mi.Message.ReceivedTLSVersion > 1 && !mi.Message.ReceivedRequireTLS ? dom.span(msgAttrStyle, css('msgAttrTLS', { borderBottom: '1.5px solid', borderBottomColor: styles.underlineGreen }), 'With TLS', attr.title('Message received (last hop) with TLS.')) : [], mi.Message.ReceivedRequireTLS ? dom.span(css('msgAttrRequireTLS', { padding: '.1em .3em', fontSize: '.9em', backgroundColor: styles.successBackground, border: '1px solid', borderColor: styles.borderColor, 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(msgAttrStyle, css('msgAttrSigned', { backgroundColor: styles.colorMild, color: styles.backgroundColorMild, borderRadius: '.15em' }), 'Message has a signature') : [], mi.IsEncrypted ? dom.span(msgAttrStyle, css('msgAttrEncrypted', { backgroundColor: styles.colorMild, color: styles.backgroundColorMild, borderRadius: '.15em' }), 'Message is encrypted') : [], refineKeyword ? (mi.Message.Keywords || []).map(kw => dom.clickbutton(styleClasses.keyword, dom._class('keywordButton'), kw, async function click() {
|
||||
await refineKeyword(kw);
|
||||
})) : [])))), moreHeaders.map(k => dom.tr(dom.td(k + ':', msgHeaderFieldStyle), dom.td())));
|
||||
})) : [])))), (mi.MoreHeaders || []).map(t => dom.tr(dom.td(t[0] + ':', msgHeaderFieldStyle), dom.td(t[1]))),
|
||||
// Ensure width of all possible additional headers is taken into account, to
|
||||
// prevent different layout between messages when not all headers are present.
|
||||
dom.tr(dom.td(moreHeaders.map(s => dom.div(s + ':', msgHeaderFieldStyle, style({ visibility: 'hidden', height: 0 })))), dom.td()));
|
||||
};
|
||||
// Javascript is generated from typescript, do not modify generated javascript because changes will be overwritten.
|
||||
const init = () => {
|
||||
|
@ -1450,7 +1454,7 @@ const init = () => {
|
|||
if (mi.Attachments && mi.Attachments.length > 0) {
|
||||
dom._kids(msgattachmentview, dom.div(css('msgAttachments', { borderTop: '1px solid', borderTopColor: styles.borderColor }), dom.div(dom._class('pad'), 'Attachments: ', join(mi.Attachments.map(a => a.Filename || '(unnamed)'), () => ', '))));
|
||||
}
|
||||
const msgheaderview = dom.table(styleClasses.msgHeaders);
|
||||
const msgheaderview = dom.tbody();
|
||||
loadMsgheaderView(msgheaderview, mi, [], null, true);
|
||||
const l = window.location.pathname.split('/');
|
||||
const w = l[l.length - 1];
|
||||
|
@ -1471,7 +1475,7 @@ const init = () => {
|
|||
iframepath += '?sameorigin=true';
|
||||
let iframe;
|
||||
const page = document.getElementById('page');
|
||||
const root = dom.div(dom.div(css('msgMeta', { backgroundColor: styles.backgroundColorMild, borderBottom: '1px solid', borderBottomColor: styles.borderColor }), msgheaderview, msgattachmentview), iframe = dom.iframe(attr.title('Message body.'), attr.src(iframepath), css('msgIframe', { width: '100%', height: '100%' }), function load() {
|
||||
const root = dom.div(dom.div(css('msgMeta', { backgroundColor: styles.backgroundColorMild, borderBottom: '1px solid', borderBottomColor: styles.borderColor }), dom.table(styleClasses.msgHeaders, msgheaderview), msgattachmentview), iframe = dom.iframe(attr.title('Message body.'), attr.src(iframepath), css('msgIframe', { width: '100%', height: '100%' }), function load() {
|
||||
// Note: we load the iframe content specifically in a way that fires the load event only when the content is fully rendered.
|
||||
iframe.style.height = iframe.contentDocument.documentElement.scrollHeight + 'px';
|
||||
if (window.location.hash === '#print') {
|
||||
|
|
|
@ -22,7 +22,7 @@ const init = () => {
|
|||
)
|
||||
}
|
||||
|
||||
const msgheaderview = dom.table(styleClasses.msgHeaders)
|
||||
const msgheaderview = dom.tbody()
|
||||
loadMsgheaderView(msgheaderview, mi, [], null, true)
|
||||
|
||||
const l = window.location.pathname.split('/')
|
||||
|
@ -45,7 +45,10 @@ const init = () => {
|
|||
const root = dom.div(
|
||||
dom.div(
|
||||
css('msgMeta', {backgroundColor: styles.backgroundColorMild, borderBottom: '1px solid', borderBottomColor: styles.borderColor}),
|
||||
msgheaderview,
|
||||
dom.table(
|
||||
styleClasses.msgHeaders,
|
||||
msgheaderview,
|
||||
),
|
||||
msgattachmentview,
|
||||
),
|
||||
iframe=dom.iframe(
|
||||
|
|
|
@ -318,7 +318,7 @@ var api;
|
|||
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
|
||||
"EventViewReset": { "Name": "EventViewReset", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }] },
|
||||
"EventViewMsgs": { "Name": "EventViewMsgs", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageItems", "Docs": "", "Typewords": ["[]", "[]", "MessageItem"] }, { "Name": "ParsedMessage", "Docs": "", "Typewords": ["nullable", "ParsedMessage"] }, { "Name": "ViewEnd", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"MessageItem": { "Name": "MessageItem", "Docs": "", "Fields": [{ "Name": "Message", "Docs": "", "Typewords": ["Message"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["MessageEnvelope"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "Attachment"] }, { "Name": "IsSigned", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsEncrypted", "Docs": "", "Typewords": ["bool"] }, { "Name": "FirstLine", "Docs": "", "Typewords": ["string"] }, { "Name": "MatchQuery", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"MessageItem": { "Name": "MessageItem", "Docs": "", "Fields": [{ "Name": "Message", "Docs": "", "Typewords": ["Message"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["MessageEnvelope"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "Attachment"] }, { "Name": "IsSigned", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsEncrypted", "Docs": "", "Typewords": ["bool"] }, { "Name": "FirstLine", "Docs": "", "Typewords": ["string"] }, { "Name": "MatchQuery", "Docs": "", "Typewords": ["bool"] }, { "Name": "MoreHeaders", "Docs": "", "Typewords": ["[]", "[]", "string"] }] },
|
||||
"Message": { "Name": "Message", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "CreateSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Expunged", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsReject", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "MailboxOrigID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxDestinedID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Received", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "RemoteIP", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPMasked1", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPMasked2", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPMasked3", "Docs": "", "Typewords": ["string"] }, { "Name": "EHLODomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFromLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "MailFromDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "RcptToLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "RcptToDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "MsgFromDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromOrgDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "EHLOValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "MailFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "MsgFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "EHLOValidation", "Docs": "", "Typewords": ["Validation"] }, { "Name": "MailFromValidation", "Docs": "", "Typewords": ["Validation"] }, { "Name": "MsgFromValidation", "Docs": "", "Typewords": ["Validation"] }, { "Name": "DKIMDomains", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "OrigEHLODomain", "Docs": "", "Typewords": ["string"] }, { "Name": "OrigDKIMDomains", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }, { "Name": "SubjectBase", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageHash", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "ThreadID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ThreadParentIDs", "Docs": "", "Typewords": ["[]", "int64"] }, { "Name": "ThreadMissingLink", "Docs": "", "Typewords": ["bool"] }, { "Name": "ThreadMuted", "Docs": "", "Typewords": ["bool"] }, { "Name": "ThreadCollapsed", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsMailingList", "Docs": "", "Typewords": ["bool"] }, { "Name": "DSN", "Docs": "", "Typewords": ["bool"] }, { "Name": "ReceivedTLSVersion", "Docs": "", "Typewords": ["uint16"] }, { "Name": "ReceivedTLSCipherSuite", "Docs": "", "Typewords": ["uint16"] }, { "Name": "ReceivedRequireTLS", "Docs": "", "Typewords": ["bool"] }, { "Name": "Seen", "Docs": "", "Typewords": ["bool"] }, { "Name": "Answered", "Docs": "", "Typewords": ["bool"] }, { "Name": "Flagged", "Docs": "", "Typewords": ["bool"] }, { "Name": "Forwarded", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Notjunk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Phishing", "Docs": "", "Typewords": ["bool"] }, { "Name": "MDNSent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }, { "Name": "TrainedJunk", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "MsgPrefix", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "ParsedBuf", "Docs": "", "Typewords": ["nullable", "string"] }] },
|
||||
"MessageEnvelope": { "Name": "MessageEnvelope", "Docs": "", "Fields": [{ "Name": "Date", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "Sender", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "CC", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "BCC", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "InReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }] },
|
||||
"Attachment": { "Name": "Attachment", "Docs": "", "Fields": [{ "Name": "Path", "Docs": "", "Typewords": ["[]", "int32"] }, { "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "Part", "Docs": "", "Typewords": ["Part"] }] },
|
||||
|
@ -1434,13 +1434,17 @@ const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAdd
|
|||
const msgenv = mi.Envelope;
|
||||
const received = mi.Message.Received;
|
||||
const receivedlocal = new Date(received.getTime());
|
||||
// Similar to webmail.ts:/headerTextMildStyle
|
||||
const msgHeaderFieldStyle = css('msgHeaderField', { textAlign: 'right', color: styles.colorMild, whiteSpace: 'nowrap' });
|
||||
const msgAttrStyle = css('msgAttr', { padding: '0px 0.15em', fontSize: '.9em' });
|
||||
dom._kids(msgheaderelem,
|
||||
// todo: make addresses clickable, start search (keep current mailbox if any)
|
||||
dom.tr(dom.td('From:', msgHeaderFieldStyle), dom.td(style({ width: '100%' }), dom.div(css('msgFromReceivedSpread', { display: 'flex', justifyContent: 'space-between' }), dom.div(join((msgenv.From || []).map(a => formatAddressValidated(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:', msgHeaderFieldStyle), dom.td(join((msgenv.ReplyTo || []).map(a => formatAddressElem(a)), () => ', '))), dom.tr(dom.td('To:', msgHeaderFieldStyle), dom.td(addressList(allAddrs, msgenv.To || []))), (msgenv.CC || []).length === 0 ? [] : dom.tr(dom.td('Cc:', msgHeaderFieldStyle), dom.td(addressList(allAddrs, msgenv.CC || []))), (msgenv.BCC || []).length === 0 ? [] : dom.tr(dom.td('Bcc:', msgHeaderFieldStyle), dom.td(addressList(allAddrs, msgenv.BCC || []))), dom.tr(dom.td('Subject:', msgHeaderFieldStyle), dom.td(dom.div(css('msgSubjectAttrsSpread', { display: 'flex', justifyContent: 'space-between' }), dom.div(msgenv.Subject || ''), dom.div(mi.Message.IsForward ? dom.span(msgAttrStyle, 'Forwarded', attr.title('Message came in from a forwarded address. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.IsMailingList ? dom.span(msgAttrStyle, '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(msgAttrStyle, css('msgAttrNoTLS', { borderBottom: '1.5px solid', borderBottomColor: styles.underlineRed }), 'Without TLS', attr.title('Message received (last hop) without TLS.')) : [], mi.Message.ReceivedTLSVersion > 1 && !mi.Message.ReceivedRequireTLS ? dom.span(msgAttrStyle, css('msgAttrTLS', { borderBottom: '1.5px solid', borderBottomColor: styles.underlineGreen }), 'With TLS', attr.title('Message received (last hop) with TLS.')) : [], mi.Message.ReceivedRequireTLS ? dom.span(css('msgAttrRequireTLS', { padding: '.1em .3em', fontSize: '.9em', backgroundColor: styles.successBackground, border: '1px solid', borderColor: styles.borderColor, 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(msgAttrStyle, css('msgAttrSigned', { backgroundColor: styles.colorMild, color: styles.backgroundColorMild, borderRadius: '.15em' }), 'Message has a signature') : [], mi.IsEncrypted ? dom.span(msgAttrStyle, css('msgAttrEncrypted', { backgroundColor: styles.colorMild, color: styles.backgroundColorMild, borderRadius: '.15em' }), 'Message is encrypted') : [], refineKeyword ? (mi.Message.Keywords || []).map(kw => dom.clickbutton(styleClasses.keyword, dom._class('keywordButton'), kw, async function click() {
|
||||
await refineKeyword(kw);
|
||||
})) : [])))), moreHeaders.map(k => dom.tr(dom.td(k + ':', msgHeaderFieldStyle), dom.td())));
|
||||
})) : [])))), (mi.MoreHeaders || []).map(t => dom.tr(dom.td(t[0] + ':', msgHeaderFieldStyle), dom.td(t[1]))),
|
||||
// Ensure width of all possible additional headers is taken into account, to
|
||||
// prevent different layout between messages when not all headers are present.
|
||||
dom.tr(dom.td(moreHeaders.map(s => dom.div(s + ':', msgHeaderFieldStyle, style({ visibility: 'hidden', height: 0 })))), dom.td()));
|
||||
};
|
||||
// Javascript is generated from typescript, do not modify generated javascript because changes will be overwritten.
|
||||
const init = async () => {
|
||||
|
|
|
@ -172,8 +172,9 @@ type MessageItem struct {
|
|||
Attachments []Attachment
|
||||
IsSigned bool
|
||||
IsEncrypted bool
|
||||
FirstLine string // Of message body, for showing as preview.
|
||||
MatchQuery bool // If message does not match query, it can still be included because of threading.
|
||||
FirstLine string // Of message body, for showing as preview.
|
||||
MatchQuery bool // If message does not match query, it can still be included because of threading.
|
||||
MoreHeaders [][2]string // All headers from store.Settings.ShowHeaders that are present.
|
||||
}
|
||||
|
||||
// ParsedMessage has more parsed/derived information about a message, intended
|
||||
|
@ -488,6 +489,23 @@ type ioErr struct {
|
|||
err error
|
||||
}
|
||||
|
||||
// ensure we have a non-nil moreHeaders, taking it from Settings.
|
||||
func ensureMoreHeaders(tx *bstore.Tx, moreHeaders []string) ([]string, error) {
|
||||
if moreHeaders != nil {
|
||||
return moreHeaders, nil
|
||||
}
|
||||
|
||||
s := store.Settings{ID: 1}
|
||||
if err := tx.Get(&s); err != nil {
|
||||
return nil, fmt.Errorf("get settings: %v", err)
|
||||
}
|
||||
moreHeaders = s.ShowHeaders
|
||||
if moreHeaders == nil {
|
||||
moreHeaders = []string{} // Ensure we won't get Settings again next call.
|
||||
}
|
||||
return moreHeaders, nil
|
||||
}
|
||||
|
||||
// serveEvents serves an SSE connection. Authentication is done through a query
|
||||
// string parameter "singleUseToken", a one-time-use token returned by the Token
|
||||
// API call.
|
||||
|
@ -824,6 +842,17 @@ func serveEvents(ctx context.Context, log mlog.Log, accountPath string, w http.R
|
|||
return bstore.QueryTx[store.Message](xtx).FilterEqual("Expunged", false).FilterNonzero(store.Message{MailboxID: mailboxID, UID: uid}).Get()
|
||||
}
|
||||
|
||||
// Additional headers from settings to add to MessageItems.
|
||||
var moreHeaders []string
|
||||
xmoreHeaders := func() []string {
|
||||
err := ensureTx()
|
||||
xcheckf(ctx, err, "transaction")
|
||||
|
||||
moreHeaders, err = ensureMoreHeaders(xtx, moreHeaders)
|
||||
xcheckf(ctx, err, "ensuring more headers")
|
||||
return moreHeaders
|
||||
}
|
||||
|
||||
// Return uids that are within range in view. Because the end has been reached, or
|
||||
// because the UID is not after the last message.
|
||||
xchangedUIDs := func(mailboxID int64, uids []store.UID, isRemove bool) (changedUIDs []store.UID) {
|
||||
|
@ -860,8 +889,9 @@ func serveEvents(ctx context.Context, log mlog.Log, accountPath string, w http.R
|
|||
if !ok && !thread {
|
||||
continue
|
||||
}
|
||||
|
||||
state := msgState{acc: acc}
|
||||
mi, err := messageItem(log, m, &state)
|
||||
mi, err := messageItem(log, m, &state, xmoreHeaders())
|
||||
state.clear()
|
||||
xcheckf(ctx, err, "make messageitem")
|
||||
mi.MatchQuery = ok
|
||||
|
@ -870,7 +900,7 @@ func serveEvents(ctx context.Context, log mlog.Log, accountPath string, w http.R
|
|||
if !thread && req.Query.Threading != ThreadOff {
|
||||
err := ensureTx()
|
||||
xcheckf(ctx, err, "transaction")
|
||||
more, _, err := gatherThread(log, xtx, acc, v, m, 0, false)
|
||||
more, _, err := gatherThread(log, xtx, acc, v, m, 0, false, xmoreHeaders())
|
||||
xcheckf(ctx, err, "gathering thread messages for id %d, thread %d", m.ID, m.ThreadID)
|
||||
mil = append(mil, more...)
|
||||
v.threadIDs[m.ThreadID] = struct{}{}
|
||||
|
@ -1460,6 +1490,8 @@ func queryMessages(ctx context.Context, log mlog.Log, acc *store.Account, tx *bs
|
|||
q.FilterFn(wordsFilter)
|
||||
}
|
||||
|
||||
var moreHeaders []string // From store.Settings.ShowHeaders
|
||||
|
||||
if query.OrderAsc {
|
||||
q.SortAsc("Received")
|
||||
} else {
|
||||
|
@ -1501,13 +1533,19 @@ func queryMessages(ctx context.Context, log mlog.Log, acc *store.Account, tx *bs
|
|||
}
|
||||
}
|
||||
|
||||
mi, err := messageItem(log, m, &state)
|
||||
var err error
|
||||
moreHeaders, err = ensureMoreHeaders(tx, moreHeaders)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ensuring more headers: %v", err)
|
||||
}
|
||||
|
||||
mi, err := messageItem(log, m, &state, moreHeaders)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making messageitem for message %d: %v", m.ID, err)
|
||||
}
|
||||
mil := []MessageItem{mi}
|
||||
if query.Threading != ThreadOff {
|
||||
more, xpm, err := gatherThread(log, tx, acc, v, m, page.DestMessageID, page.AnchorMessageID == 0 && have == 0)
|
||||
more, xpm, err := gatherThread(log, tx, acc, v, m, page.DestMessageID, page.AnchorMessageID == 0 && have == 0, moreHeaders)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gathering thread messages for id %d, thread %d: %v", m.ID, m.ThreadID, err)
|
||||
}
|
||||
|
@ -1576,7 +1614,7 @@ func queryMessages(ctx context.Context, log mlog.Log, acc *store.Account, tx *bs
|
|||
}
|
||||
}
|
||||
|
||||
func gatherThread(log mlog.Log, tx *bstore.Tx, acc *store.Account, v view, m store.Message, destMessageID int64, first bool) ([]MessageItem, *ParsedMessage, error) {
|
||||
func gatherThread(log mlog.Log, tx *bstore.Tx, acc *store.Account, v view, m store.Message, destMessageID int64, first bool, moreHeaders []string) ([]MessageItem, *ParsedMessage, error) {
|
||||
if m.ThreadID == 0 {
|
||||
// If we would continue, FilterNonzero would fail because there are no non-zero fields.
|
||||
return nil, nil, fmt.Errorf("message has threadid 0, account is probably still being upgraded, try turning threading off until the upgrade is done")
|
||||
|
@ -1601,7 +1639,7 @@ func gatherThread(log mlog.Log, tx *bstore.Tx, acc *store.Account, v view, m sto
|
|||
xstate := msgState{acc: acc}
|
||||
defer xstate.clear()
|
||||
|
||||
mi, err := messageItem(log, tm, &xstate)
|
||||
mi, err := messageItem(log, tm, &xstate, moreHeaders)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making messageitem for message %d, for thread %d: %v", tm.ID, m.ThreadID, err)
|
||||
}
|
||||
|
|
|
@ -376,7 +376,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
// Many of the requests need either a message or a parsed part. Make it easy to
|
||||
// fetch/prepare and cleanup. We only do all the work when the request seems legit
|
||||
// (valid HTTP route and method).
|
||||
xprepare := func() (acc *store.Account, m store.Message, msgr *store.MsgReader, p message.Part, cleanup func(), ok bool) {
|
||||
xprepare := func() (acc *store.Account, moreHeaders []string, m store.Message, msgr *store.MsgReader, p message.Part, cleanup func(), ok bool) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "405 - method not allowed - post required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
|
@ -404,7 +404,17 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
xcheckf(ctx, err, "open account")
|
||||
|
||||
m = store.Message{ID: id}
|
||||
err = acc.DB.Get(ctx, &m)
|
||||
err = acc.DB.Read(ctx, func(tx *bstore.Tx) error {
|
||||
if err := tx.Get(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
s := store.Settings{ID: 1}
|
||||
if err := tx.Get(&s); err != nil {
|
||||
return fmt.Errorf("get settings for more headers: %v", err)
|
||||
}
|
||||
moreHeaders = s.ShowHeaders
|
||||
return nil
|
||||
})
|
||||
if err == bstore.ErrAbsent || err == nil && m.Expunged {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
@ -486,7 +496,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
|
||||
switch {
|
||||
case len(t) == 2 && t[1] == "attachments.zip":
|
||||
acc, m, msgr, p, cleanup, ok := xprepare()
|
||||
acc, _, m, msgr, p, cleanup, ok := xprepare()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -494,7 +504,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
state := msgState{acc: acc, m: m, msgr: msgr, part: &p}
|
||||
// note: state is cleared by cleanup
|
||||
|
||||
mi, err := messageItem(log, m, &state)
|
||||
mi, err := messageItem(log, m, &state, nil)
|
||||
xcheckf(ctx, err, "parsing message")
|
||||
|
||||
headers(false, false, false, false)
|
||||
|
@ -605,7 +615,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
|
||||
// Raw display of a message, as text/plain.
|
||||
case len(t) == 2 && t[1] == "raw":
|
||||
_, _, msgr, p, cleanup, ok := xprepare()
|
||||
_, _, _, msgr, p, cleanup, ok := xprepare()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -631,7 +641,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
// msg.html has a javascript tag with message data, and javascript to render the
|
||||
// message header like the regular webmail.html and to load the message body in a
|
||||
// separate iframe with a separate request with stronger CSP.
|
||||
acc, m, msgr, p, cleanup, ok := xprepare()
|
||||
acc, _, m, msgr, p, cleanup, ok := xprepare()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -664,7 +674,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
// This is js with data inside instead so we can load it synchronously, which we do
|
||||
// to get a "loaded" event after the page was actually loaded.
|
||||
|
||||
acc, m, msgr, p, cleanup, ok := xprepare()
|
||||
acc, moreHeaders, m, msgr, p, cleanup, ok := xprepare()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -679,7 +689,8 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
|
||||
m.MsgPrefix = nil
|
||||
m.ParsedBuf = nil
|
||||
mi := MessageItem{m, pm.envelope, pm.attachments, pm.isSigned, pm.isEncrypted, pm.firstLine, false}
|
||||
hl := messageItemMoreHeaders(moreHeaders, pm)
|
||||
mi := MessageItem{m, pm.envelope, pm.attachments, pm.isSigned, pm.isEncrypted, pm.firstLine, false, hl}
|
||||
mijson, err := json.Marshal(mi)
|
||||
xcheckf(ctx, err, "marshal messageitem")
|
||||
|
||||
|
@ -695,7 +706,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
// renders just the text content with the same code as webmail.html. Used by the
|
||||
// iframe in the msgtext endpoint. Not used by the regular webmail viewer, it
|
||||
// renders the text itself, with the same shared js code.
|
||||
acc, m, msgr, p, cleanup, ok := xprepare()
|
||||
acc, _, m, msgr, p, cleanup, ok := xprepare()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -729,7 +740,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
case len(t) == 2 && (t[1] == "html" || t[1] == "htmlexternal"):
|
||||
// Returns the first HTML part, with "cid:" URIs replaced with an inlined datauri
|
||||
// if the referenced Content-ID attachment can be found.
|
||||
_, _, _, p, cleanup, ok := xprepare()
|
||||
_, _, _, _, p, cleanup, ok := xprepare()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -783,7 +794,7 @@ func handle(apiHandler http.Handler, isForwarded bool, accountPath string, w htt
|
|||
// data with a text/plain content-type so the browser will attempt to display it,
|
||||
// and "download" adds a content-disposition header causing the browser the
|
||||
// download the file.
|
||||
_, _, _, p, cleanup, ok := xprepare()
|
||||
_, _, _, _, p, cleanup, ok := xprepare()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -318,7 +318,7 @@ var api;
|
|||
"EventViewErr": { "Name": "EventViewErr", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Err", "Docs": "", "Typewords": ["string"] }] },
|
||||
"EventViewReset": { "Name": "EventViewReset", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }] },
|
||||
"EventViewMsgs": { "Name": "EventViewMsgs", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "RequestID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageItems", "Docs": "", "Typewords": ["[]", "[]", "MessageItem"] }, { "Name": "ParsedMessage", "Docs": "", "Typewords": ["nullable", "ParsedMessage"] }, { "Name": "ViewEnd", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"MessageItem": { "Name": "MessageItem", "Docs": "", "Fields": [{ "Name": "Message", "Docs": "", "Typewords": ["Message"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["MessageEnvelope"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "Attachment"] }, { "Name": "IsSigned", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsEncrypted", "Docs": "", "Typewords": ["bool"] }, { "Name": "FirstLine", "Docs": "", "Typewords": ["string"] }, { "Name": "MatchQuery", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"MessageItem": { "Name": "MessageItem", "Docs": "", "Fields": [{ "Name": "Message", "Docs": "", "Typewords": ["Message"] }, { "Name": "Envelope", "Docs": "", "Typewords": ["MessageEnvelope"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "Attachment"] }, { "Name": "IsSigned", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsEncrypted", "Docs": "", "Typewords": ["bool"] }, { "Name": "FirstLine", "Docs": "", "Typewords": ["string"] }, { "Name": "MatchQuery", "Docs": "", "Typewords": ["bool"] }, { "Name": "MoreHeaders", "Docs": "", "Typewords": ["[]", "[]", "string"] }] },
|
||||
"Message": { "Name": "Message", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "CreateSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Expunged", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsReject", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "MailboxOrigID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxDestinedID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Received", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "RemoteIP", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPMasked1", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPMasked2", "Docs": "", "Typewords": ["string"] }, { "Name": "RemoteIPMasked3", "Docs": "", "Typewords": ["string"] }, { "Name": "EHLODomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFrom", "Docs": "", "Typewords": ["string"] }, { "Name": "MailFromLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "MailFromDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "RcptToLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "RcptToDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "MsgFromDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromOrgDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "EHLOValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "MailFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "MsgFromValidated", "Docs": "", "Typewords": ["bool"] }, { "Name": "EHLOValidation", "Docs": "", "Typewords": ["Validation"] }, { "Name": "MailFromValidation", "Docs": "", "Typewords": ["Validation"] }, { "Name": "MsgFromValidation", "Docs": "", "Typewords": ["Validation"] }, { "Name": "DKIMDomains", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "OrigEHLODomain", "Docs": "", "Typewords": ["string"] }, { "Name": "OrigDKIMDomains", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }, { "Name": "SubjectBase", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageHash", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "ThreadID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ThreadParentIDs", "Docs": "", "Typewords": ["[]", "int64"] }, { "Name": "ThreadMissingLink", "Docs": "", "Typewords": ["bool"] }, { "Name": "ThreadMuted", "Docs": "", "Typewords": ["bool"] }, { "Name": "ThreadCollapsed", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsMailingList", "Docs": "", "Typewords": ["bool"] }, { "Name": "DSN", "Docs": "", "Typewords": ["bool"] }, { "Name": "ReceivedTLSVersion", "Docs": "", "Typewords": ["uint16"] }, { "Name": "ReceivedTLSCipherSuite", "Docs": "", "Typewords": ["uint16"] }, { "Name": "ReceivedRequireTLS", "Docs": "", "Typewords": ["bool"] }, { "Name": "Seen", "Docs": "", "Typewords": ["bool"] }, { "Name": "Answered", "Docs": "", "Typewords": ["bool"] }, { "Name": "Flagged", "Docs": "", "Typewords": ["bool"] }, { "Name": "Forwarded", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Notjunk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Phishing", "Docs": "", "Typewords": ["bool"] }, { "Name": "MDNSent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }, { "Name": "TrainedJunk", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "MsgPrefix", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "ParsedBuf", "Docs": "", "Typewords": ["nullable", "string"] }] },
|
||||
"MessageEnvelope": { "Name": "MessageEnvelope", "Docs": "", "Fields": [{ "Name": "Date", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "Sender", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "CC", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "BCC", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "InReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }] },
|
||||
"Attachment": { "Name": "Attachment", "Docs": "", "Fields": [{ "Name": "Path", "Docs": "", "Typewords": ["[]", "int32"] }, { "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "Part", "Docs": "", "Typewords": ["Part"] }] },
|
||||
|
@ -1434,13 +1434,17 @@ const loadMsgheaderView = (msgheaderelem, mi, moreHeaders, refineKeyword, allAdd
|
|||
const msgenv = mi.Envelope;
|
||||
const received = mi.Message.Received;
|
||||
const receivedlocal = new Date(received.getTime());
|
||||
// Similar to webmail.ts:/headerTextMildStyle
|
||||
const msgHeaderFieldStyle = css('msgHeaderField', { textAlign: 'right', color: styles.colorMild, whiteSpace: 'nowrap' });
|
||||
const msgAttrStyle = css('msgAttr', { padding: '0px 0.15em', fontSize: '.9em' });
|
||||
dom._kids(msgheaderelem,
|
||||
// todo: make addresses clickable, start search (keep current mailbox if any)
|
||||
dom.tr(dom.td('From:', msgHeaderFieldStyle), dom.td(style({ width: '100%' }), dom.div(css('msgFromReceivedSpread', { display: 'flex', justifyContent: 'space-between' }), dom.div(join((msgenv.From || []).map(a => formatAddressValidated(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:', msgHeaderFieldStyle), dom.td(join((msgenv.ReplyTo || []).map(a => formatAddressElem(a)), () => ', '))), dom.tr(dom.td('To:', msgHeaderFieldStyle), dom.td(addressList(allAddrs, msgenv.To || []))), (msgenv.CC || []).length === 0 ? [] : dom.tr(dom.td('Cc:', msgHeaderFieldStyle), dom.td(addressList(allAddrs, msgenv.CC || []))), (msgenv.BCC || []).length === 0 ? [] : dom.tr(dom.td('Bcc:', msgHeaderFieldStyle), dom.td(addressList(allAddrs, msgenv.BCC || []))), dom.tr(dom.td('Subject:', msgHeaderFieldStyle), dom.td(dom.div(css('msgSubjectAttrsSpread', { display: 'flex', justifyContent: 'space-between' }), dom.div(msgenv.Subject || ''), dom.div(mi.Message.IsForward ? dom.span(msgAttrStyle, 'Forwarded', attr.title('Message came in from a forwarded address. Some message authentication policies, like DMARC, were not evaluated.')) : [], mi.Message.IsMailingList ? dom.span(msgAttrStyle, '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(msgAttrStyle, css('msgAttrNoTLS', { borderBottom: '1.5px solid', borderBottomColor: styles.underlineRed }), 'Without TLS', attr.title('Message received (last hop) without TLS.')) : [], mi.Message.ReceivedTLSVersion > 1 && !mi.Message.ReceivedRequireTLS ? dom.span(msgAttrStyle, css('msgAttrTLS', { borderBottom: '1.5px solid', borderBottomColor: styles.underlineGreen }), 'With TLS', attr.title('Message received (last hop) with TLS.')) : [], mi.Message.ReceivedRequireTLS ? dom.span(css('msgAttrRequireTLS', { padding: '.1em .3em', fontSize: '.9em', backgroundColor: styles.successBackground, border: '1px solid', borderColor: styles.borderColor, 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(msgAttrStyle, css('msgAttrSigned', { backgroundColor: styles.colorMild, color: styles.backgroundColorMild, borderRadius: '.15em' }), 'Message has a signature') : [], mi.IsEncrypted ? dom.span(msgAttrStyle, css('msgAttrEncrypted', { backgroundColor: styles.colorMild, color: styles.backgroundColorMild, borderRadius: '.15em' }), 'Message is encrypted') : [], refineKeyword ? (mi.Message.Keywords || []).map(kw => dom.clickbutton(styleClasses.keyword, dom._class('keywordButton'), kw, async function click() {
|
||||
await refineKeyword(kw);
|
||||
})) : [])))), moreHeaders.map(k => dom.tr(dom.td(k + ':', msgHeaderFieldStyle), dom.td())));
|
||||
})) : [])))), (mi.MoreHeaders || []).map(t => dom.tr(dom.td(t[0] + ':', msgHeaderFieldStyle), dom.td(t[1]))),
|
||||
// Ensure width of all possible additional headers is taken into account, to
|
||||
// prevent different layout between messages when not all headers are present.
|
||||
dom.tr(dom.td(moreHeaders.map(s => dom.div(s + ':', msgHeaderFieldStyle, style({ visibility: 'hidden', height: 0 })))), dom.td()));
|
||||
};
|
||||
// Javascript is generated from typescript, do not modify generated javascript because changes will be overwritten.
|
||||
/*
|
||||
|
@ -2437,7 +2441,7 @@ const cmdSettings = async () => {
|
|||
if (!accountSettings) {
|
||||
throw new Error('No account settings fetched yet.');
|
||||
}
|
||||
const remove = popup(css('popupSettings', { padding: '1em 1em 2em 1em', minWidth: '30em' }), dom.h1('Settings'), dom.form(async function submit(e) {
|
||||
const remove = popup(css('popupSettings', { minWidth: '30em' }), style({ maxWidth: '50em' }), dom.h1('Settings'), dom.form(async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const accSet = {
|
||||
|
@ -2452,7 +2456,7 @@ const cmdSettings = async () => {
|
|||
await withDisabled(fieldset, client.SettingsSave(accSet));
|
||||
accountSettings = accSet;
|
||||
remove();
|
||||
}, fieldset = dom.fieldset(dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Signature'), signature = dom.textarea(new String(accountSettings.Signature), style({ width: '100%' }), attr.rows('' + Math.max(3, 1 + accountSettings.Signature.split('\n').length)))), dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Reply above/below original'), attr.title('Auto: If text is selected, only the replied text is quoted and editing starts below. Otherwise, the full message is quoted and editing starts at the top.'), quoting = dom.select(dom.option(attr.value(''), 'Auto'), dom.option(attr.value('bottom'), 'Bottom', accountSettings.Quoting === api.Quoting.Bottom ? attr.selected('') : []), dom.option(attr.value('top'), 'Top', accountSettings.Quoting === api.Quoting.Top ? attr.selected('') : []))), dom.label(style({ margin: '1ex 0', display: 'block' }), showAddressSecurity = dom.input(attr.type('checkbox'), accountSettings.ShowAddressSecurity ? attr.checked('') : []), ' Show address security indications', attr.title('Show bars underneath address input fields, indicating support for STARTTLS/DNSSEC/DANE/MTA-STS/RequireTLS.')), dom.label(style({ margin: '1ex 0', display: 'block' }), showHTML = dom.input(attr.type('checkbox'), accountSettings.ShowHTML ? attr.checked('') : []), ' Show email as HTML instead of text by default for first-time senders', attr.title('Whether to show HTML or text is remembered per sender. This sets the default for unknown correspondents.')), dom.label(style({ margin: '1ex 0', display: 'block' }), showShortcuts = dom.input(attr.type('checkbox'), accountSettings.NoShowShortcuts ? [] : attr.checked('')), ' Show shortcut keys in bottom left after interaction with mouse'), dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Show additional headers'), showHeaders = dom.textarea(new String((accountSettings.ShowHeaders || []).join('\n')), style({ width: '100%' }), attr.rows('' + Math.max(3, 1 + (accountSettings.ShowHeaders || []).length))), dom.div(style({ fontStyle: 'italic' }), 'One header name per line, for example Delivered-To, X-Mox-Reason, User-Agent, ...')), dom.div(style({ marginTop: '2ex' }), 'Register "mailto:" links with the browser/operating system to compose a message in webmail.', dom.br(), dom.clickbutton('Register', attr.title('In most browsers, registering is only allowed on HTTPS URLs. Your browser may ask for confirmation. If nothing appears to happen, the registration may already have been present.'), function click() {
|
||||
}, fieldset = dom.fieldset(dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Signature'), signature = dom.textarea(new String(accountSettings.Signature), style({ width: '100%' }), attr.rows('' + Math.max(3, 1 + accountSettings.Signature.split('\n').length)))), dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Reply above/below original'), attr.title('Auto: If text is selected, only the replied text is quoted and editing starts below. Otherwise, the full message is quoted and editing starts at the top.'), quoting = dom.select(dom.option(attr.value(''), 'Auto'), dom.option(attr.value('bottom'), 'Bottom', accountSettings.Quoting === api.Quoting.Bottom ? attr.selected('') : []), dom.option(attr.value('top'), 'Top', accountSettings.Quoting === api.Quoting.Top ? attr.selected('') : []))), dom.label(style({ margin: '1ex 0', display: 'block' }), showAddressSecurity = dom.input(attr.type('checkbox'), accountSettings.ShowAddressSecurity ? attr.checked('') : []), ' Show address security indications', attr.title('Show bars underneath address input fields, indicating support for STARTTLS/DNSSEC/DANE/MTA-STS/RequireTLS.')), dom.label(style({ margin: '1ex 0', display: 'block' }), showHTML = dom.input(attr.type('checkbox'), accountSettings.ShowHTML ? attr.checked('') : []), ' Show email as HTML instead of text by default for first-time senders', attr.title('Whether to show HTML or text is remembered per sender. This sets the default for unknown correspondents.')), dom.label(style({ margin: '1ex 0', display: 'block' }), showShortcuts = dom.input(attr.type('checkbox'), accountSettings.NoShowShortcuts ? [] : attr.checked('')), ' Show shortcut keys in bottom left after interaction with mouse'), dom.label(style({ margin: '1ex 0', display: 'block' }), dom.div('Show additional headers'), showHeaders = dom.textarea(new String((accountSettings.ShowHeaders || []).join('\n')), style({ width: '100%' }), attr.rows('' + Math.max(3, 1 + (accountSettings.ShowHeaders || []).length))), dom.div(style({ fontStyle: 'italic' }), 'One header name per line, for example Delivered-To, X-Mox-Reason, User-Agent, ...; Refresh mailbox view for changes to take effect.')), dom.div(style({ marginTop: '2ex' }), 'Register "mailto:" links with the browser/operating system to compose a message in webmail.', dom.br(), dom.clickbutton('Register', attr.title('In most browsers, registering is only allowed on HTTPS URLs. Your browser may ask for confirmation. If nothing appears to happen, the registration may already have been present.'), function click() {
|
||||
if (!window.navigator.registerProtocolHandler) {
|
||||
window.alert('Registering a protocol handler ("mailto:") is not supported by your browser.');
|
||||
return;
|
||||
|
@ -3892,8 +3896,8 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
|||
};
|
||||
let urlType; // text, html, htmlexternal; for opening in new tab/print
|
||||
let msgbuttonElem, msgheaderElem, msgattachmentElem, msgmodeElem;
|
||||
let msgheaderdetailsElem = null; // When full headers are visible, or some headers are requested through settings.
|
||||
const msgmetaElem = dom.div(css('msgmeta', { backgroundColor: styles.backgroundColorMild, borderBottom: '5px solid', borderBottomColor: ['white', 'black'], maxHeight: '90%', overflowY: 'auto' }), attr.role('region'), attr.arialabel('Buttons and headers for message'), msgbuttonElem = dom.div(), dom.div(attr.arialive('assertive'), msgheaderElem = dom.table(styleClasses.msgHeaders), msgattachmentElem = dom.div(), msgmodeElem = dom.div()),
|
||||
let msgheaderFullElem; // Full headers, when enabled.
|
||||
const msgmetaElem = dom.div(css('msgmeta', { backgroundColor: styles.backgroundColorMild, borderBottom: '5px solid', borderBottomColor: ['white', 'black'], maxHeight: '90%', overflowY: 'auto' }), attr.role('region'), attr.arialabel('Buttons and headers for message'), msgbuttonElem = dom.div(), dom.div(attr.arialive('assertive'), dom.table(styleClasses.msgHeaders, msgheaderElem = dom.tbody()), msgheaderFullElem = dom.table(), msgattachmentElem = dom.div(), msgmodeElem = dom.div()),
|
||||
// Explicit separator that separates headers from body, to
|
||||
// prevent HTML messages from faking UI elements.
|
||||
dom.div(css('headerBodySeparator', { height: '2px', backgroundColor: styles.borderColor })));
|
||||
|
@ -3926,17 +3930,13 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
|||
};
|
||||
loadButtons(parsedMessageOpt || null);
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, accountSettings.ShowHeaders || [], refineKeyword, false);
|
||||
// Similar to lib.ts:/msgHeaderFieldStyle
|
||||
const headerTextMildStyle = css('headerTextMild', { textAlign: 'right', color: styles.colorMild });
|
||||
const loadHeaderDetails = (pm) => {
|
||||
if (msgheaderdetailsElem) {
|
||||
msgheaderdetailsElem.remove();
|
||||
msgheaderdetailsElem = null;
|
||||
}
|
||||
if (!settings.showAllHeaders) {
|
||||
return;
|
||||
}
|
||||
msgheaderdetailsElem = dom.table(css('msgHeaderDetails', { marginBottom: '1ex', width: '100%' }), Object.entries(pm.Headers || {}).sort().map(t => (t[1] || []).map(v => dom.tr(dom.td(t[0] + ':', headerTextMildStyle), dom.td(v)))));
|
||||
msgattachmentElem.parentNode.insertBefore(msgheaderdetailsElem, msgattachmentElem);
|
||||
const table = dom.table(css('msgHeaderDetails', { width: '100%' }), !settings.showAllHeaders ? [] :
|
||||
Object.entries(pm.Headers || {}).sort().map(t => (t[1] || []).map(v => dom.tr(dom.td(t[0] + ':', headerTextMildStyle), dom.td(v)))));
|
||||
msgheaderFullElem.replaceWith(table);
|
||||
msgheaderFullElem = table;
|
||||
};
|
||||
const isText = (a) => ['text', 'message'].includes(a.Part.MediaType.toLowerCase());
|
||||
const isPDF = (a) => (a.Part.MediaType + '/' + a.Part.MediaSubType).toLowerCase() === 'application/pdf';
|
||||
|
@ -4057,25 +4057,6 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
|||
dom._kids(msgcontentElem, dom.iframe(attr.tabindex('0'), attr.title('HTML version of message with images inlined and with external resources loaded.'), attr.src('msg/' + m.ID + '/' + urlType), css('msgIframeHTML', { position: 'absolute', width: '100%', height: '100%' })));
|
||||
renderAttachments(); // Rerender opaciy on inline images.
|
||||
};
|
||||
const loadMoreHeaders = (pm) => {
|
||||
const hl = accountSettings.ShowHeaders || [];
|
||||
if (hl.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < hl.length; i++) {
|
||||
msgheaderElem.children[msgheaderElem.children.length - 1].remove();
|
||||
}
|
||||
hl.forEach(k => {
|
||||
const vl = pm.Headers?.[k];
|
||||
if (!vl || vl.length === 0) {
|
||||
return;
|
||||
}
|
||||
vl.forEach(v => {
|
||||
const e = dom.tr(dom.td(k + ':', headerTextMildStyle, style({ whiteSpace: 'nowrap' })), dom.td(v));
|
||||
msgheaderElem.appendChild(e);
|
||||
});
|
||||
});
|
||||
};
|
||||
const mv = {
|
||||
root: root,
|
||||
messageitem: mi,
|
||||
|
@ -4085,7 +4066,6 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
|||
mi.Message.ModSeq = modseq;
|
||||
mi.Message.Keywords = keywords;
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, accountSettings.ShowHeaders || [], refineKeyword, false);
|
||||
loadMoreHeaders(await parsedMessagePromise);
|
||||
},
|
||||
};
|
||||
(async () => {
|
||||
|
@ -4112,7 +4092,6 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
|||
}
|
||||
loadButtons(pm);
|
||||
loadHeaderDetails(pm);
|
||||
loadMoreHeaders(pm);
|
||||
const msgHeaderSeparatorStyle = css('msgHeaderSeparator', { borderTop: '1px solid', borderTopColor: styles.borderColor });
|
||||
const msgModeWarningStyle = css('msgModeWarning', { backgroundColor: styles.warningBackgroundColor, padding: '0 .15em' });
|
||||
const htmlNote = 'In the HTML viewer, the following potentially dangerous functionality is disabled: submitting forms, starting a download from a link, navigating away from this page by clicking a link. If a link does not work, try explicitly opening it in a new tab.';
|
||||
|
|
|
@ -1113,7 +1113,8 @@ const cmdSettings = async () => {
|
|||
}
|
||||
|
||||
const remove = popup(
|
||||
css('popupSettings', {padding: '1em 1em 2em 1em', minWidth: '30em'}),
|
||||
css('popupSettings', {minWidth: '30em'}),
|
||||
style({maxWidth: '50em'}),
|
||||
dom.h1('Settings'),
|
||||
dom.form(
|
||||
async function submit(e: SubmitEvent) {
|
||||
|
@ -1179,7 +1180,7 @@ const cmdSettings = async () => {
|
|||
style({width: '100%'}),
|
||||
attr.rows(''+Math.max(3, 1+(accountSettings.ShowHeaders || []).length)),
|
||||
),
|
||||
dom.div(style({fontStyle: 'italic'}), 'One header name per line, for example Delivered-To, X-Mox-Reason, User-Agent, ...'),
|
||||
dom.div(style({fontStyle: 'italic'}), 'One header name per line, for example Delivered-To, X-Mox-Reason, User-Agent, ...; Refresh mailbox view for changes to take effect.'),
|
||||
),
|
||||
|
||||
|
||||
|
@ -3180,8 +3181,8 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
|
||||
let urlType: string // text, html, htmlexternal; for opening in new tab/print
|
||||
|
||||
let msgbuttonElem: HTMLElement, msgheaderElem: HTMLElement, msgattachmentElem: HTMLElement, msgmodeElem: HTMLElement
|
||||
let msgheaderdetailsElem: HTMLElement | null = null // When full headers are visible, or some headers are requested through settings.
|
||||
let msgbuttonElem: HTMLElement, msgheaderElem: HTMLTableSectionElement, msgattachmentElem: HTMLElement, msgmodeElem: HTMLElement
|
||||
let msgheaderFullElem: HTMLTableElement // Full headers, when enabled.
|
||||
|
||||
const msgmetaElem = dom.div(
|
||||
css('msgmeta', {backgroundColor: styles.backgroundColorMild, borderBottom: '5px solid', borderBottomColor: ['white', 'black'], maxHeight: '90%', overflowY: 'auto'}),
|
||||
|
@ -3189,7 +3190,11 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
msgbuttonElem=dom.div(),
|
||||
dom.div(
|
||||
attr.arialive('assertive'),
|
||||
msgheaderElem=dom.table(styleClasses.msgHeaders),
|
||||
dom.table(
|
||||
styleClasses.msgHeaders,
|
||||
msgheaderElem=dom.tbody(),
|
||||
),
|
||||
msgheaderFullElem=dom.table(),
|
||||
msgattachmentElem=dom.div(),
|
||||
msgmodeElem=dom.div(),
|
||||
),
|
||||
|
@ -3256,28 +3261,23 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, accountSettings.ShowHeaders || [], refineKeyword, false)
|
||||
|
||||
// Similar to lib.ts:/msgHeaderFieldStyle
|
||||
const headerTextMildStyle = css('headerTextMild', {textAlign: 'right', color: styles.colorMild})
|
||||
|
||||
const loadHeaderDetails = (pm: api.ParsedMessage) => {
|
||||
if (msgheaderdetailsElem) {
|
||||
msgheaderdetailsElem.remove()
|
||||
msgheaderdetailsElem = null
|
||||
}
|
||||
if (!settings.showAllHeaders) {
|
||||
return
|
||||
}
|
||||
msgheaderdetailsElem = dom.table(
|
||||
css('msgHeaderDetails', {marginBottom: '1ex', width: '100%'}),
|
||||
Object.entries(pm.Headers || {}).sort().map(t =>
|
||||
(t[1] || []).map(v =>
|
||||
dom.tr(
|
||||
dom.td(t[0]+':', headerTextMildStyle),
|
||||
dom.td(v),
|
||||
const table = dom.table(
|
||||
css('msgHeaderDetails', {width: '100%'}),
|
||||
!settings.showAllHeaders ? [] :
|
||||
Object.entries(pm.Headers || {}).sort().map(t =>
|
||||
(t[1] || []).map(v =>
|
||||
dom.tr(
|
||||
dom.td(t[0]+':', headerTextMildStyle),
|
||||
dom.td(v),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
msgattachmentElem.parentNode!.insertBefore(msgheaderdetailsElem, msgattachmentElem)
|
||||
msgheaderFullElem.replaceWith(table)
|
||||
msgheaderFullElem = table
|
||||
}
|
||||
|
||||
const isText = (a: api.Attachment) => ['text', 'message'].includes(a.Part.MediaType.toLowerCase())
|
||||
|
@ -3512,29 +3512,6 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
renderAttachments() // Rerender opaciy on inline images.
|
||||
}
|
||||
|
||||
const loadMoreHeaders = (pm: api.ParsedMessage) => {
|
||||
const hl = accountSettings.ShowHeaders || []
|
||||
if (hl.length === 0) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < hl.length; i++) {
|
||||
msgheaderElem.children[msgheaderElem.children.length-1].remove()
|
||||
}
|
||||
hl.forEach(k => {
|
||||
const vl = pm.Headers?.[k]
|
||||
if (!vl || vl.length === 0) {
|
||||
return
|
||||
}
|
||||
vl.forEach(v => {
|
||||
const e = dom.tr(
|
||||
dom.td(k+':', headerTextMildStyle, style({whiteSpace: 'nowrap'})),
|
||||
dom.td(v),
|
||||
)
|
||||
msgheaderElem.appendChild(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const mv: MsgView = {
|
||||
root: root,
|
||||
messageitem: mi,
|
||||
|
@ -3544,7 +3521,6 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
mi.Message.ModSeq = modseq
|
||||
mi.Message.Keywords = keywords
|
||||
loadMsgheaderView(msgheaderElem, miv.messageitem, accountSettings.ShowHeaders || [], refineKeyword, false)
|
||||
loadMoreHeaders(await parsedMessagePromise)
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -3570,7 +3546,6 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
|||
|
||||
loadButtons(pm)
|
||||
loadHeaderDetails(pm)
|
||||
loadMoreHeaders(pm)
|
||||
|
||||
const msgHeaderSeparatorStyle = css('msgHeaderSeparator', {borderTop: '1px solid', borderTopColor: styles.borderColor})
|
||||
const msgModeWarningStyle = css('msgModeWarning', {backgroundColor: styles.warningBackgroundColor, padding: '0 .15em'})
|
||||
|
|
Loading…
Reference in a new issue