mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 18:24:35 +03:00
implement message threading in backend and webmail
we match messages to their parents based on the "references" and "in-reply-to" headers (requiring the same base subject), and in absense of those headers we also by only base subject (against messages received max 4 weeks ago). we store a threadid with messages. all messages in a thread have the same threadid. messages also have a "thread parent ids", which holds all id's of parent messages up to the thread root. then there is "thread missing link", which is set when a referenced immediate parent wasn't found (but possibly earlier ancestors can still be found and will be in thread parent ids". threads can be muted: newly delivered messages are automatically marked as read/seen. threads can be marked as collapsed: if set, the webmail collapses the thread to a single item in the basic threading view (default is to expand threads). the muted and collapsed fields are copied from their parent on message delivery. the threading is implemented in the webmail. the non-threading mode still works as before. the new default threading mode "unread" automatically expands only the threads with at least one unread (not seen) meessage. the basic threading mode "on" expands all threads except when explicitly collapsed (as saved in the thread collapsed field). new shortcuts for navigation/interaction threads have been added, e.g. go to previous/next thread root, toggle collapse/expand of thread (or double click), toggle mute of thread. some previous shortcuts have changed, see the help for details. the message threading are added with an explicit account upgrade step, automatically started when an account is opened. the upgrade is done in the background because it will take too long for large mailboxes to block account operations. the upgrade takes two steps: 1. updating all message records in the database to add a normalized message-id and thread base subject (with "re:", "fwd:" and several other schemes stripped). 2. going through all messages in the database again, reading the "references" and "in-reply-to" headers from disk, and matching against their parents. this second step is also done at the end of each import of mbox/maildir mailboxes. new deliveries are matched immediately against other existing messages, currently no attempt is made to rematch previously delivered messages (which could be useful for related messages being delivered out of order). the threading is not yet exposed over imap.
This commit is contained in:
137
webmail/api.go
137
webmail/api.go
@ -748,7 +748,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||
err = tx.Update(&sentmb)
|
||||
xcheckf(ctx, err, "updating sent mailbox for counts")
|
||||
|
||||
err = acc.DeliverMessage(log, tx, &sentm, dataFile, true, true, false)
|
||||
err = acc.DeliverMessage(log, tx, &sentm, dataFile, true, true, false, false)
|
||||
if err != nil {
|
||||
metricSubmission.WithLabelValues("storesenterror").Inc()
|
||||
metricked = true
|
||||
@ -1488,7 +1488,140 @@ func (Webmail) MailboxSetSpecialUse(ctx context.Context, mb store.Mailbox) {
|
||||
})
|
||||
}
|
||||
|
||||
// ThreadCollapse saves the ThreadCollapse field for the messages and its
|
||||
// children. The messageIDs are typically thread roots. But not all roots
|
||||
// (without parent) of a thread need to have the same collapsed state.
|
||||
func (Webmail) ThreadCollapse(ctx context.Context, messageIDs []int64, collapse bool) {
|
||||
log := xlog.WithContext(ctx)
|
||||
reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
|
||||
acc, err := store.OpenAccount(reqInfo.AccountName)
|
||||
xcheckf(ctx, err, "open account")
|
||||
defer func() {
|
||||
err := acc.Close()
|
||||
log.Check(err, "closing account")
|
||||
}()
|
||||
|
||||
if len(messageIDs) == 0 {
|
||||
xcheckuserf(ctx, errors.New("no messages"), "setting collapse")
|
||||
}
|
||||
|
||||
acc.WithWLock(func() {
|
||||
changes := make([]store.Change, 0, len(messageIDs))
|
||||
xdbwrite(ctx, acc, func(tx *bstore.Tx) {
|
||||
// Gather ThreadIDs to list all potential messages, for a way to get all potential
|
||||
// (child) messages. Further refined in FilterFn.
|
||||
threadIDs := map[int64]struct{}{}
|
||||
msgIDs := map[int64]struct{}{}
|
||||
for _, id := range messageIDs {
|
||||
m := store.Message{ID: id}
|
||||
err := tx.Get(&m)
|
||||
if err == bstore.ErrAbsent {
|
||||
xcheckuserf(ctx, err, "get message")
|
||||
}
|
||||
xcheckf(ctx, err, "get message")
|
||||
threadIDs[m.ThreadID] = struct{}{}
|
||||
msgIDs[id] = struct{}{}
|
||||
}
|
||||
|
||||
var updated []store.Message
|
||||
q := bstore.QueryTx[store.Message](tx)
|
||||
q.FilterEqual("ThreadID", slicesAny(maps.Keys(threadIDs))...)
|
||||
q.FilterNotEqual("ThreadCollapsed", collapse)
|
||||
q.FilterFn(func(tm store.Message) bool {
|
||||
for _, id := range tm.ThreadParentIDs {
|
||||
if _, ok := msgIDs[id]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
_, ok := msgIDs[tm.ID]
|
||||
return ok
|
||||
})
|
||||
q.Gather(&updated)
|
||||
q.SortAsc("ID") // Consistent order for testing.
|
||||
_, err = q.UpdateFields(map[string]any{"ThreadCollapsed": collapse})
|
||||
xcheckf(ctx, err, "updating collapse in database")
|
||||
|
||||
for _, m := range updated {
|
||||
changes = append(changes, m.ChangeThread())
|
||||
}
|
||||
})
|
||||
store.BroadcastChanges(acc, changes)
|
||||
})
|
||||
}
|
||||
|
||||
// ThreadMute saves the ThreadMute field for the messages and their children.
|
||||
// If messages are muted, they are also marked collapsed.
|
||||
func (Webmail) ThreadMute(ctx context.Context, messageIDs []int64, mute bool) {
|
||||
log := xlog.WithContext(ctx)
|
||||
reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
|
||||
acc, err := store.OpenAccount(reqInfo.AccountName)
|
||||
xcheckf(ctx, err, "open account")
|
||||
defer func() {
|
||||
err := acc.Close()
|
||||
log.Check(err, "closing account")
|
||||
}()
|
||||
|
||||
if len(messageIDs) == 0 {
|
||||
xcheckuserf(ctx, errors.New("no messages"), "setting mute")
|
||||
}
|
||||
|
||||
acc.WithWLock(func() {
|
||||
changes := make([]store.Change, 0, len(messageIDs))
|
||||
xdbwrite(ctx, acc, func(tx *bstore.Tx) {
|
||||
threadIDs := map[int64]struct{}{}
|
||||
msgIDs := map[int64]struct{}{}
|
||||
for _, id := range messageIDs {
|
||||
m := store.Message{ID: id}
|
||||
err := tx.Get(&m)
|
||||
if err == bstore.ErrAbsent {
|
||||
xcheckuserf(ctx, err, "get message")
|
||||
}
|
||||
xcheckf(ctx, err, "get message")
|
||||
threadIDs[m.ThreadID] = struct{}{}
|
||||
msgIDs[id] = struct{}{}
|
||||
}
|
||||
|
||||
var updated []store.Message
|
||||
|
||||
q := bstore.QueryTx[store.Message](tx)
|
||||
q.FilterEqual("ThreadID", slicesAny(maps.Keys(threadIDs))...)
|
||||
q.FilterFn(func(tm store.Message) bool {
|
||||
if tm.ThreadMuted == mute && (!mute || tm.ThreadCollapsed) {
|
||||
return false
|
||||
}
|
||||
for _, id := range tm.ThreadParentIDs {
|
||||
if _, ok := msgIDs[id]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
_, ok := msgIDs[tm.ID]
|
||||
return ok
|
||||
})
|
||||
q.Gather(&updated)
|
||||
fields := map[string]any{"ThreadMuted": mute}
|
||||
if mute {
|
||||
fields["ThreadCollapsed"] = true
|
||||
}
|
||||
_, err = q.UpdateFields(fields)
|
||||
xcheckf(ctx, err, "updating mute in database")
|
||||
|
||||
for _, m := range updated {
|
||||
changes = append(changes, m.ChangeThread())
|
||||
}
|
||||
})
|
||||
store.BroadcastChanges(acc, changes)
|
||||
})
|
||||
}
|
||||
|
||||
func slicesAny[T any](l []T) []any {
|
||||
r := make([]any, len(l))
|
||||
for i, v := range l {
|
||||
r[i] = v
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SSETypes exists to ensure the generated API contains the types, for use in SSE events.
|
||||
func (Webmail) SSETypes() (start EventStart, viewErr EventViewErr, viewReset EventViewReset, viewMsgs EventViewMsgs, viewChanges EventViewChanges, msgAdd ChangeMsgAdd, msgRemove ChangeMsgRemove, msgFlags ChangeMsgFlags, mailboxRemove ChangeMailboxRemove, mailboxAdd ChangeMailboxAdd, mailboxRename ChangeMailboxRename, mailboxCounts ChangeMailboxCounts, mailboxSpecialUse ChangeMailboxSpecialUse, mailboxKeywords ChangeMailboxKeywords, flags store.Flags) {
|
||||
func (Webmail) SSETypes() (start EventStart, viewErr EventViewErr, viewReset EventViewReset, viewMsgs EventViewMsgs, viewChanges EventViewChanges, msgAdd ChangeMsgAdd, msgRemove ChangeMsgRemove, msgFlags ChangeMsgFlags, msgThread ChangeMsgThread, mailboxRemove ChangeMailboxRemove, mailboxAdd ChangeMailboxAdd, mailboxRename ChangeMailboxRename, mailboxCounts ChangeMailboxCounts, mailboxSpecialUse ChangeMailboxSpecialUse, mailboxKeywords ChangeMailboxKeywords, flags store.Flags) {
|
||||
return
|
||||
}
|
||||
|
166
webmail/api.json
166
webmail/api.json
@ -235,6 +235,46 @@
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "ThreadCollapse",
|
||||
"Docs": "ThreadCollapse saves the ThreadCollapse field for the messages and its\nchildren. The messageIDs are typically thread roots. But not all roots\n(without parent) of a thread need to have the same collapsed state.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "messageIDs",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "collapse",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "ThreadMute",
|
||||
"Docs": "ThreadMute saves the ThreadMute field for the messages and their children.\nIf messages are muted, they are also marked collapsed.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "messageIDs",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "mute",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "SSETypes",
|
||||
"Docs": "SSETypes exists to ensure the generated API contains the types, for use in SSE events.",
|
||||
@ -288,6 +328,12 @@
|
||||
"ChangeMsgFlags"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "msgThread",
|
||||
"Typewords": [
|
||||
"ChangeMsgThread"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "mailboxRemove",
|
||||
"Typewords": [
|
||||
@ -394,6 +440,13 @@
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Threading",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"ThreadMode"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Filter",
|
||||
"Docs": "",
|
||||
@ -783,7 +836,7 @@
|
||||
},
|
||||
{
|
||||
"Name": "Subject",
|
||||
"Docs": "",
|
||||
"Docs": "Q/B-word-decoded.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
@ -1319,8 +1372,9 @@
|
||||
},
|
||||
{
|
||||
"Name": "MessageItems",
|
||||
"Docs": "If empty, this was the last message for the request.",
|
||||
"Docs": "If empty, this was the last message for the request. If non-empty, a list of thread messages. Each with the first message being the reason this thread is included and can be used as AnchorID in followup requests. If the threading mode is \"off\" in the query, there will always be only a single message. If a thread is sent, all messages in the thread are sent, including those that don't match the query (e.g. from another mailbox). Threads can be displayed based on the ThreadParentIDs field, with possibly slightly different display based on field ThreadMissingLink.",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"[]",
|
||||
"MessageItem"
|
||||
]
|
||||
@ -1388,6 +1442,13 @@
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "MatchQuery",
|
||||
"Docs": "If message does not match query, it can still be included because of threading.",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -1630,19 +1691,62 @@
|
||||
},
|
||||
{
|
||||
"Name": "MessageID",
|
||||
"Docs": "Value of Message-Id header. Only set for messages that were delivered to the rejects mailbox. For ensuring such messages are delivered only once. Value includes \u003c\u003e.",
|
||||
"Docs": "Canonicalized Message-Id, always lower-case and normalized quoting, without \u003c\u003e's. Empty if missing. Used for matching message threads, and to prevent duplicate reject delivery.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "SubjectBase",
|
||||
"Docs": "For matching threads in case there is no References/In-Reply-To header. It is lower-cased, white-space collapsed, mailing list tags and re/fwd tags removed.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "MessageHash",
|
||||
"Docs": "Hash of message. For rejects delivery, so optional like MessageID.",
|
||||
"Docs": "Hash of message. For rejects delivery in case there is no Message-ID, only set when delivered as reject.",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"uint8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ThreadID",
|
||||
"Docs": "ID of message starting this thread.",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ThreadParentIDs",
|
||||
"Docs": "IDs of parent messages, from closest parent to the root message. Parent messages may be in a different mailbox, or may no longer exist. ThreadParentIDs must never contain the message id itself (a cycle), and parent messages must reference the same ancestors.",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ThreadMissingLink",
|
||||
"Docs": "ThreadMissingLink is true if there is no match with a direct parent. E.g. first ID in ThreadParentIDs is not the direct ancestor (an intermediate message may have been deleted), or subject-based matching was done.",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ThreadMuted",
|
||||
"Docs": "If set, newly delivered child messages are automatically marked as read. This field is copied to new child messages. Changes are propagated to the webmail client.",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ThreadCollapsed",
|
||||
"Docs": "If set, this (sub)thread is collapsed in the webmail client, for threading mode \"on\" (mode \"unread\" ignores it). This field is copied to new child message. Changes are propagated to the webmail client.",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Seen",
|
||||
"Docs": "",
|
||||
@ -1888,7 +1992,7 @@
|
||||
},
|
||||
{
|
||||
"Name": "ChangeMsgAdd",
|
||||
"Docs": "ChangeMsgAdd adds a new message to the view.",
|
||||
"Docs": "ChangeMsgAdd adds a new message and possibly its thread to the view.",
|
||||
"Fields": [
|
||||
{
|
||||
"Name": "MailboxID",
|
||||
@ -1927,9 +2031,10 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "MessageItem",
|
||||
"Name": "MessageItems",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"MessageItem"
|
||||
]
|
||||
}
|
||||
@ -2088,6 +2193,34 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ChangeMsgThread",
|
||||
"Docs": "ChangeMsgThread updates muted/collapsed fields for one message.",
|
||||
"Fields": [
|
||||
{
|
||||
"Name": "MessageIDs",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Muted",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Collapsed",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ChangeMailboxRemove",
|
||||
"Docs": "ChangeMailboxRemove indicates a mailbox was removed, including all its messages.",
|
||||
@ -2382,6 +2515,27 @@
|
||||
}
|
||||
],
|
||||
"Strings": [
|
||||
{
|
||||
"Name": "ThreadMode",
|
||||
"Docs": "",
|
||||
"Values": [
|
||||
{
|
||||
"Name": "ThreadOff",
|
||||
"Value": "off",
|
||||
"Docs": ""
|
||||
},
|
||||
{
|
||||
"Name": "ThreadOn",
|
||||
"Value": "on",
|
||||
"Docs": ""
|
||||
},
|
||||
{
|
||||
"Name": "ThreadUnread",
|
||||
"Value": "unread",
|
||||
"Docs": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "AttachmentType",
|
||||
"Docs": "AttachmentType is for filtering by attachment type.",
|
||||
|
@ -16,6 +16,7 @@ export interface Request {
|
||||
// Query is a request for messages that match filters, in a given order.
|
||||
export interface Query {
|
||||
OrderAsc: boolean // Order by received ascending or desending.
|
||||
Threading: ThreadMode
|
||||
Filter: Filter
|
||||
NotFilter: NotFilter
|
||||
}
|
||||
@ -91,7 +92,7 @@ export interface Part {
|
||||
// Envelope holds the basic/common message headers as used in IMAP4.
|
||||
export interface Envelope {
|
||||
Date: Date
|
||||
Subject: string
|
||||
Subject: string // Q/B-word-decoded.
|
||||
From?: Address[] | null
|
||||
Sender?: Address[] | null
|
||||
ReplyTo?: Address[] | null
|
||||
@ -218,7 +219,7 @@ export interface EventViewReset {
|
||||
export interface EventViewMsgs {
|
||||
ViewID: number
|
||||
RequestID: number
|
||||
MessageItems?: MessageItem[] | null // If empty, this was the last message for the request.
|
||||
MessageItems?: (MessageItem[] | null)[] | null // If empty, this was the last message for the request. If non-empty, a list of thread messages. Each with the first message being the reason this thread is included and can be used as AnchorID in followup requests. If the threading mode is "off" in the query, there will always be only a single message. If a thread is sent, all messages in the thread are sent, including those that don't match the query (e.g. from another mailbox). Threads can be displayed based on the ThreadParentIDs field, with possibly slightly different display based on field ThreadMissingLink.
|
||||
ParsedMessage?: ParsedMessage | null // If set, will match the target page.DestMessageID from the request.
|
||||
ViewEnd: boolean // If set, there are no more messages in this view at this moment. Messages can be added, typically via Change messages, e.g. for new deliveries.
|
||||
}
|
||||
@ -233,6 +234,7 @@ export interface MessageItem {
|
||||
IsSigned: boolean
|
||||
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.
|
||||
}
|
||||
|
||||
// Message stored in database and per-message file on disk.
|
||||
@ -276,8 +278,14 @@ export interface Message {
|
||||
DKIMDomains?: string[] | null // Domains with verified DKIM signatures. Unicode string. For forwarded messages, a DKIM domain that matched a ruleset's verified domain is left out, but included in OrigDKIMDomains.
|
||||
OrigEHLODomain: string // For forwarded messages,
|
||||
OrigDKIMDomains?: string[] | null
|
||||
MessageID: string // Value of Message-Id header. Only set for messages that were delivered to the rejects mailbox. For ensuring such messages are delivered only once. Value includes <>.
|
||||
MessageHash?: string | null // Hash of message. For rejects delivery, so optional like MessageID.
|
||||
MessageID: string // Canonicalized Message-Id, always lower-case and normalized quoting, without <>'s. Empty if missing. Used for matching message threads, and to prevent duplicate reject delivery.
|
||||
SubjectBase: string // For matching threads in case there is no References/In-Reply-To header. It is lower-cased, white-space collapsed, mailing list tags and re/fwd tags removed.
|
||||
MessageHash?: string | null // Hash of message. For rejects delivery in case there is no Message-ID, only set when delivered as reject.
|
||||
ThreadID: number // ID of message starting this thread.
|
||||
ThreadParentIDs?: number[] | null // IDs of parent messages, from closest parent to the root message. Parent messages may be in a different mailbox, or may no longer exist. ThreadParentIDs must never contain the message id itself (a cycle), and parent messages must reference the same ancestors.
|
||||
ThreadMissingLink: boolean // ThreadMissingLink is true if there is no match with a direct parent. E.g. first ID in ThreadParentIDs is not the direct ancestor (an intermediate message may have been deleted), or subject-based matching was done.
|
||||
ThreadMuted: boolean // If set, newly delivered child messages are automatically marked as read. This field is copied to new child messages. Changes are propagated to the webmail client.
|
||||
ThreadCollapsed: boolean // If set, this (sub)thread is collapsed in the webmail client, for threading mode "on" (mode "unread" ignores it). This field is copied to new child message. Changes are propagated to the webmail client.
|
||||
Seen: boolean
|
||||
Answered: boolean
|
||||
Flagged: boolean
|
||||
@ -326,14 +334,14 @@ export interface EventViewChanges {
|
||||
Changes?: (any[] | null)[] | null // The first field of [2]any is a string, the second of the Change types below.
|
||||
}
|
||||
|
||||
// ChangeMsgAdd adds a new message to the view.
|
||||
// ChangeMsgAdd adds a new message and possibly its thread to the view.
|
||||
export interface ChangeMsgAdd {
|
||||
MailboxID: number
|
||||
UID: UID
|
||||
ModSeq: ModSeq
|
||||
Flags: Flags // System flags.
|
||||
Keywords?: string[] | null // Other flags.
|
||||
MessageItem: MessageItem
|
||||
MessageItems?: MessageItem[] | null
|
||||
}
|
||||
|
||||
// Flags for a mail message.
|
||||
@ -367,6 +375,13 @@ export interface ChangeMsgFlags {
|
||||
Keywords?: string[] | null // Non-system/well-known flags/keywords/labels.
|
||||
}
|
||||
|
||||
// ChangeMsgThread updates muted/collapsed fields for one message.
|
||||
export interface ChangeMsgThread {
|
||||
MessageIDs?: number[] | null
|
||||
Muted: boolean
|
||||
Collapsed: boolean
|
||||
}
|
||||
|
||||
// ChangeMailboxRemove indicates a mailbox was removed, including all its messages.
|
||||
export interface ChangeMailboxRemove {
|
||||
MailboxID: number
|
||||
@ -446,6 +461,12 @@ export enum Validation {
|
||||
ValidationNone = 10, // E.g. No records.
|
||||
}
|
||||
|
||||
export enum ThreadMode {
|
||||
ThreadOff = "off",
|
||||
ThreadOn = "on",
|
||||
ThreadUnread = "unread",
|
||||
}
|
||||
|
||||
// AttachmentType is for filtering by attachment type.
|
||||
export enum AttachmentType {
|
||||
AttachmentIndifferent = "",
|
||||
@ -464,12 +485,12 @@ export enum AttachmentType {
|
||||
// An empty string can be a valid localpart.
|
||||
export type Localpart = string
|
||||
|
||||
export const structTypes: {[typename: string]: boolean} = {"Address":true,"Attachment":true,"ChangeMailboxAdd":true,"ChangeMailboxCounts":true,"ChangeMailboxKeywords":true,"ChangeMailboxRemove":true,"ChangeMailboxRename":true,"ChangeMailboxSpecialUse":true,"ChangeMsgAdd":true,"ChangeMsgFlags":true,"ChangeMsgRemove":true,"Domain":true,"DomainAddressConfig":true,"Envelope":true,"EventStart":true,"EventViewChanges":true,"EventViewErr":true,"EventViewMsgs":true,"EventViewReset":true,"File":true,"Filter":true,"Flags":true,"ForwardAttachments":true,"Mailbox":true,"Message":true,"MessageAddress":true,"MessageEnvelope":true,"MessageItem":true,"NotFilter":true,"Page":true,"ParsedMessage":true,"Part":true,"Query":true,"Request":true,"SpecialUse":true,"SubmitMessage":true}
|
||||
export const stringsTypes: {[typename: string]: boolean} = {"AttachmentType":true,"Localpart":true}
|
||||
export const structTypes: {[typename: string]: boolean} = {"Address":true,"Attachment":true,"ChangeMailboxAdd":true,"ChangeMailboxCounts":true,"ChangeMailboxKeywords":true,"ChangeMailboxRemove":true,"ChangeMailboxRename":true,"ChangeMailboxSpecialUse":true,"ChangeMsgAdd":true,"ChangeMsgFlags":true,"ChangeMsgRemove":true,"ChangeMsgThread":true,"Domain":true,"DomainAddressConfig":true,"Envelope":true,"EventStart":true,"EventViewChanges":true,"EventViewErr":true,"EventViewMsgs":true,"EventViewReset":true,"File":true,"Filter":true,"Flags":true,"ForwardAttachments":true,"Mailbox":true,"Message":true,"MessageAddress":true,"MessageEnvelope":true,"MessageItem":true,"NotFilter":true,"Page":true,"ParsedMessage":true,"Part":true,"Query":true,"Request":true,"SpecialUse":true,"SubmitMessage":true}
|
||||
export const stringsTypes: {[typename: string]: boolean} = {"AttachmentType":true,"Localpart":true,"ThreadMode":true}
|
||||
export const intsTypes: {[typename: string]: boolean} = {"ModSeq":true,"UID":true,"Validation":true}
|
||||
export const types: TypenameMap = {
|
||||
"Request": {"Name":"Request","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"SSEID","Docs":"","Typewords":["int64"]},{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"Cancel","Docs":"","Typewords":["bool"]},{"Name":"Query","Docs":"","Typewords":["Query"]},{"Name":"Page","Docs":"","Typewords":["Page"]}]},
|
||||
"Query": {"Name":"Query","Docs":"","Fields":[{"Name":"OrderAsc","Docs":"","Typewords":["bool"]},{"Name":"Filter","Docs":"","Typewords":["Filter"]},{"Name":"NotFilter","Docs":"","Typewords":["NotFilter"]}]},
|
||||
"Query": {"Name":"Query","Docs":"","Fields":[{"Name":"OrderAsc","Docs":"","Typewords":["bool"]},{"Name":"Threading","Docs":"","Typewords":["ThreadMode"]},{"Name":"Filter","Docs":"","Typewords":["Filter"]},{"Name":"NotFilter","Docs":"","Typewords":["NotFilter"]}]},
|
||||
"Filter": {"Name":"Filter","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"MailboxChildrenIncluded","Docs":"","Typewords":["bool"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Words","Docs":"","Typewords":["[]","string"]},{"Name":"From","Docs":"","Typewords":["[]","string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Oldest","Docs":"","Typewords":["nullable","timestamp"]},{"Name":"Newest","Docs":"","Typewords":["nullable","timestamp"]},{"Name":"Subject","Docs":"","Typewords":["[]","string"]},{"Name":"Attachments","Docs":"","Typewords":["AttachmentType"]},{"Name":"Labels","Docs":"","Typewords":["[]","string"]},{"Name":"Headers","Docs":"","Typewords":["[]","[]","string"]},{"Name":"SizeMin","Docs":"","Typewords":["int64"]},{"Name":"SizeMax","Docs":"","Typewords":["int64"]}]},
|
||||
"NotFilter": {"Name":"NotFilter","Docs":"","Fields":[{"Name":"Words","Docs":"","Typewords":["[]","string"]},{"Name":"From","Docs":"","Typewords":["[]","string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Subject","Docs":"","Typewords":["[]","string"]},{"Name":"Attachments","Docs":"","Typewords":["AttachmentType"]},{"Name":"Labels","Docs":"","Typewords":["[]","string"]}]},
|
||||
"Page": {"Name":"Page","Docs":"","Fields":[{"Name":"AnchorMessageID","Docs":"","Typewords":["int64"]},{"Name":"Count","Docs":"","Typewords":["int32"]},{"Name":"DestMessageID","Docs":"","Typewords":["int64"]}]},
|
||||
@ -487,16 +508,17 @@ export const types: TypenameMap = {
|
||||
"DomainAddressConfig": {"Name":"DomainAddressConfig","Docs":"","Fields":[{"Name":"LocalpartCatchallSeparator","Docs":"","Typewords":["string"]},{"Name":"LocalpartCaseSensitive","Docs":"","Typewords":["bool"]}]},
|
||||
"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"]}]},
|
||||
"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":"MessageHash","Docs":"","Typewords":["nullable","string"]},{"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"]}]},
|
||||
"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"]}]},
|
||||
"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":"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"]}]},
|
||||
"EventViewChanges": {"Name":"EventViewChanges","Docs":"","Fields":[{"Name":"ViewID","Docs":"","Typewords":["int64"]},{"Name":"Changes","Docs":"","Typewords":["[]","[]","any"]}]},
|
||||
"ChangeMsgAdd": {"Name":"ChangeMsgAdd","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"UID","Docs":"","Typewords":["UID"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]},{"Name":"Flags","Docs":"","Typewords":["Flags"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"MessageItem","Docs":"","Typewords":["MessageItem"]}]},
|
||||
"ChangeMsgAdd": {"Name":"ChangeMsgAdd","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"UID","Docs":"","Typewords":["UID"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]},{"Name":"Flags","Docs":"","Typewords":["Flags"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"MessageItems","Docs":"","Typewords":["[]","MessageItem"]}]},
|
||||
"Flags": {"Name":"Flags","Docs":"","Fields":[{"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"]}]},
|
||||
"ChangeMsgRemove": {"Name":"ChangeMsgRemove","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"UIDs","Docs":"","Typewords":["[]","UID"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]}]},
|
||||
"ChangeMsgFlags": {"Name":"ChangeMsgFlags","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"UID","Docs":"","Typewords":["UID"]},{"Name":"ModSeq","Docs":"","Typewords":["ModSeq"]},{"Name":"Mask","Docs":"","Typewords":["Flags"]},{"Name":"Flags","Docs":"","Typewords":["Flags"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]}]},
|
||||
"ChangeMsgThread": {"Name":"ChangeMsgThread","Docs":"","Fields":[{"Name":"MessageIDs","Docs":"","Typewords":["[]","int64"]},{"Name":"Muted","Docs":"","Typewords":["bool"]},{"Name":"Collapsed","Docs":"","Typewords":["bool"]}]},
|
||||
"ChangeMailboxRemove": {"Name":"ChangeMailboxRemove","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]}]},
|
||||
"ChangeMailboxAdd": {"Name":"ChangeMailboxAdd","Docs":"","Fields":[{"Name":"Mailbox","Docs":"","Typewords":["Mailbox"]}]},
|
||||
"ChangeMailboxRename": {"Name":"ChangeMailboxRename","Docs":"","Fields":[{"Name":"MailboxID","Docs":"","Typewords":["int64"]},{"Name":"OldName","Docs":"","Typewords":["string"]},{"Name":"NewName","Docs":"","Typewords":["string"]},{"Name":"Flags","Docs":"","Typewords":["[]","string"]}]},
|
||||
@ -507,6 +529,7 @@ export const types: TypenameMap = {
|
||||
"UID": {"Name":"UID","Docs":"","Values":null},
|
||||
"ModSeq": {"Name":"ModSeq","Docs":"","Values":null},
|
||||
"Validation": {"Name":"Validation","Docs":"","Values":[{"Name":"ValidationUnknown","Value":0,"Docs":""},{"Name":"ValidationStrict","Value":1,"Docs":""},{"Name":"ValidationDMARC","Value":2,"Docs":""},{"Name":"ValidationRelaxed","Value":3,"Docs":""},{"Name":"ValidationPass","Value":4,"Docs":""},{"Name":"ValidationNeutral","Value":5,"Docs":""},{"Name":"ValidationTemperror","Value":6,"Docs":""},{"Name":"ValidationPermerror","Value":7,"Docs":""},{"Name":"ValidationFail","Value":8,"Docs":""},{"Name":"ValidationSoftfail","Value":9,"Docs":""},{"Name":"ValidationNone","Value":10,"Docs":""}]},
|
||||
"ThreadMode": {"Name":"ThreadMode","Docs":"","Values":[{"Name":"ThreadOff","Value":"off","Docs":""},{"Name":"ThreadOn","Value":"on","Docs":""},{"Name":"ThreadUnread","Value":"unread","Docs":""}]},
|
||||
"AttachmentType": {"Name":"AttachmentType","Docs":"","Values":[{"Name":"AttachmentIndifferent","Value":"","Docs":""},{"Name":"AttachmentNone","Value":"none","Docs":""},{"Name":"AttachmentAny","Value":"any","Docs":""},{"Name":"AttachmentImage","Value":"image","Docs":""},{"Name":"AttachmentPDF","Value":"pdf","Docs":""},{"Name":"AttachmentArchive","Value":"archive","Docs":""},{"Name":"AttachmentSpreadsheet","Value":"spreadsheet","Docs":""},{"Name":"AttachmentDocument","Value":"document","Docs":""},{"Name":"AttachmentPresentation","Value":"presentation","Docs":""}]},
|
||||
"Localpart": {"Name":"Localpart","Docs":"","Values":null},
|
||||
}
|
||||
@ -541,6 +564,7 @@ export const parser = {
|
||||
Flags: (v: any) => parse("Flags", v) as Flags,
|
||||
ChangeMsgRemove: (v: any) => parse("ChangeMsgRemove", v) as ChangeMsgRemove,
|
||||
ChangeMsgFlags: (v: any) => parse("ChangeMsgFlags", v) as ChangeMsgFlags,
|
||||
ChangeMsgThread: (v: any) => parse("ChangeMsgThread", v) as ChangeMsgThread,
|
||||
ChangeMailboxRemove: (v: any) => parse("ChangeMailboxRemove", v) as ChangeMailboxRemove,
|
||||
ChangeMailboxAdd: (v: any) => parse("ChangeMailboxAdd", v) as ChangeMailboxAdd,
|
||||
ChangeMailboxRename: (v: any) => parse("ChangeMailboxRename", v) as ChangeMailboxRename,
|
||||
@ -551,6 +575,7 @@ export const parser = {
|
||||
UID: (v: any) => parse("UID", v) as UID,
|
||||
ModSeq: (v: any) => parse("ModSeq", v) as ModSeq,
|
||||
Validation: (v: any) => parse("Validation", v) as Validation,
|
||||
ThreadMode: (v: any) => parse("ThreadMode", v) as ThreadMode,
|
||||
AttachmentType: (v: any) => parse("AttachmentType", v) as AttachmentType,
|
||||
Localpart: (v: any) => parse("Localpart", v) as Localpart,
|
||||
}
|
||||
@ -712,13 +737,34 @@ export class Client {
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
|
||||
// ThreadCollapse saves the ThreadCollapse field for the messages and its
|
||||
// children. The messageIDs are typically thread roots. But not all roots
|
||||
// (without parent) of a thread need to have the same collapsed state.
|
||||
async ThreadCollapse(messageIDs: number[] | null, collapse: boolean): Promise<void> {
|
||||
const fn: string = "ThreadCollapse"
|
||||
const paramTypes: string[][] = [["[]","int64"],["bool"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [messageIDs, collapse]
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
|
||||
// ThreadMute saves the ThreadMute field for the messages and their children.
|
||||
// If messages are muted, they are also marked collapsed.
|
||||
async ThreadMute(messageIDs: number[] | null, mute: boolean): Promise<void> {
|
||||
const fn: string = "ThreadMute"
|
||||
const paramTypes: string[][] = [["[]","int64"],["bool"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [messageIDs, mute]
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
|
||||
// SSETypes exists to ensure the generated API contains the types, for use in SSE events.
|
||||
async SSETypes(): Promise<[EventStart, EventViewErr, EventViewReset, EventViewMsgs, EventViewChanges, ChangeMsgAdd, ChangeMsgRemove, ChangeMsgFlags, ChangeMailboxRemove, ChangeMailboxAdd, ChangeMailboxRename, ChangeMailboxCounts, ChangeMailboxSpecialUse, ChangeMailboxKeywords, Flags]> {
|
||||
async SSETypes(): Promise<[EventStart, EventViewErr, EventViewReset, EventViewMsgs, EventViewChanges, ChangeMsgAdd, ChangeMsgRemove, ChangeMsgFlags, ChangeMsgThread, ChangeMailboxRemove, ChangeMailboxAdd, ChangeMailboxRename, ChangeMailboxCounts, ChangeMailboxSpecialUse, ChangeMailboxKeywords, Flags]> {
|
||||
const fn: string = "SSETypes"
|
||||
const paramTypes: string[][] = []
|
||||
const returnTypes: string[][] = [["EventStart"],["EventViewErr"],["EventViewReset"],["EventViewMsgs"],["EventViewChanges"],["ChangeMsgAdd"],["ChangeMsgRemove"],["ChangeMsgFlags"],["ChangeMailboxRemove"],["ChangeMailboxAdd"],["ChangeMailboxRename"],["ChangeMailboxCounts"],["ChangeMailboxSpecialUse"],["ChangeMailboxKeywords"],["Flags"]]
|
||||
const returnTypes: string[][] = [["EventStart"],["EventViewErr"],["EventViewReset"],["EventViewMsgs"],["EventViewChanges"],["ChangeMsgAdd"],["ChangeMsgRemove"],["ChangeMsgFlags"],["ChangeMsgThread"],["ChangeMailboxRemove"],["ChangeMailboxAdd"],["ChangeMailboxRename"],["ChangeMailboxCounts"],["ChangeMailboxSpecialUse"],["ChangeMailboxKeywords"],["Flags"]]
|
||||
const params: any[] = []
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params) as [EventStart, EventViewErr, EventViewReset, EventViewMsgs, EventViewChanges, ChangeMsgAdd, ChangeMsgRemove, ChangeMsgFlags, ChangeMailboxRemove, ChangeMailboxAdd, ChangeMailboxRename, ChangeMailboxCounts, ChangeMailboxSpecialUse, ChangeMailboxKeywords, Flags]
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params) as [EventStart, EventViewErr, EventViewReset, EventViewMsgs, EventViewChanges, ChangeMsgAdd, ChangeMsgRemove, ChangeMsgFlags, ChangeMsgThread, ChangeMailboxRemove, ChangeMailboxAdd, ChangeMailboxRename, ChangeMailboxCounts, ChangeMailboxSpecialUse, ChangeMailboxKeywords, Flags]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ func messageItem(log *mlog.Log, m store.Message, state *msgState) (MessageItem,
|
||||
// Clear largish unused data.
|
||||
m.MsgPrefix = nil
|
||||
m.ParsedBuf = nil
|
||||
return MessageItem{m, pm.envelope, pm.attachments, pm.isSigned, pm.isEncrypted, pm.firstLine}, nil
|
||||
return MessageItem{m, pm.envelope, pm.attachments, pm.isSigned, pm.isEncrypted, pm.firstLine, true}, nil
|
||||
}
|
||||
|
||||
// formatFirstLine returns a line the client can display next to the subject line
|
||||
|
@ -17,6 +17,12 @@ var api;
|
||||
Validation[Validation["ValidationSoftfail"] = 9] = "ValidationSoftfail";
|
||||
Validation[Validation["ValidationNone"] = 10] = "ValidationNone";
|
||||
})(Validation = api.Validation || (api.Validation = {}));
|
||||
let ThreadMode;
|
||||
(function (ThreadMode) {
|
||||
ThreadMode["ThreadOff"] = "off";
|
||||
ThreadMode["ThreadOn"] = "on";
|
||||
ThreadMode["ThreadUnread"] = "unread";
|
||||
})(ThreadMode = api.ThreadMode || (api.ThreadMode = {}));
|
||||
// AttachmentType is for filtering by attachment type.
|
||||
let AttachmentType;
|
||||
(function (AttachmentType) {
|
||||
@ -30,12 +36,12 @@ var api;
|
||||
AttachmentType["AttachmentDocument"] = "document";
|
||||
AttachmentType["AttachmentPresentation"] = "presentation";
|
||||
})(AttachmentType = api.AttachmentType || (api.AttachmentType = {}));
|
||||
api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "Request": true, "SpecialUse": true, "SubmitMessage": true };
|
||||
api.stringsTypes = { "AttachmentType": true, "Localpart": true };
|
||||
api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "ChangeMsgThread": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "Request": true, "SpecialUse": true, "SubmitMessage": true };
|
||||
api.stringsTypes = { "AttachmentType": true, "Localpart": true, "ThreadMode": true };
|
||||
api.intsTypes = { "ModSeq": true, "UID": true, "Validation": true };
|
||||
api.types = {
|
||||
"Request": { "Name": "Request", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Cancel", "Docs": "", "Typewords": ["bool"] }, { "Name": "Query", "Docs": "", "Typewords": ["Query"] }, { "Name": "Page", "Docs": "", "Typewords": ["Page"] }] },
|
||||
"Query": { "Name": "Query", "Docs": "", "Fields": [{ "Name": "OrderAsc", "Docs": "", "Typewords": ["bool"] }, { "Name": "Filter", "Docs": "", "Typewords": ["Filter"] }, { "Name": "NotFilter", "Docs": "", "Typewords": ["NotFilter"] }] },
|
||||
"Query": { "Name": "Query", "Docs": "", "Fields": [{ "Name": "OrderAsc", "Docs": "", "Typewords": ["bool"] }, { "Name": "Threading", "Docs": "", "Typewords": ["ThreadMode"] }, { "Name": "Filter", "Docs": "", "Typewords": ["Filter"] }, { "Name": "NotFilter", "Docs": "", "Typewords": ["NotFilter"] }] },
|
||||
"Filter": { "Name": "Filter", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxChildrenIncluded", "Docs": "", "Typewords": ["bool"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Words", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Oldest", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "Newest", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "Subject", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["AttachmentType"] }, { "Name": "Labels", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Headers", "Docs": "", "Typewords": ["[]", "[]", "string"] }, { "Name": "SizeMin", "Docs": "", "Typewords": ["int64"] }, { "Name": "SizeMax", "Docs": "", "Typewords": ["int64"] }] },
|
||||
"NotFilter": { "Name": "NotFilter", "Docs": "", "Fields": [{ "Name": "Words", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["AttachmentType"] }, { "Name": "Labels", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"Page": { "Name": "Page", "Docs": "", "Fields": [{ "Name": "AnchorMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Count", "Docs": "", "Typewords": ["int32"] }, { "Name": "DestMessageID", "Docs": "", "Typewords": ["int64"] }] },
|
||||
@ -53,16 +59,17 @@ var api;
|
||||
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"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"] }] },
|
||||
"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": "MessageHash", "Docs": "", "Typewords": ["nullable", "string"] }, { "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"] }] },
|
||||
"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"] }] },
|
||||
"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": "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"] }] },
|
||||
"EventViewChanges": { "Name": "EventViewChanges", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Changes", "Docs": "", "Typewords": ["[]", "[]", "any"] }] },
|
||||
"ChangeMsgAdd": { "Name": "ChangeMsgAdd", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Flags", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MessageItem", "Docs": "", "Typewords": ["MessageItem"] }] },
|
||||
"ChangeMsgAdd": { "Name": "ChangeMsgAdd", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Flags", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MessageItems", "Docs": "", "Typewords": ["[]", "MessageItem"] }] },
|
||||
"Flags": { "Name": "Flags", "Docs": "", "Fields": [{ "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"] }] },
|
||||
"ChangeMsgRemove": { "Name": "ChangeMsgRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UIDs", "Docs": "", "Typewords": ["[]", "UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] },
|
||||
"ChangeMsgFlags": { "Name": "ChangeMsgFlags", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Mask", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Flags", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"ChangeMsgThread": { "Name": "ChangeMsgThread", "Docs": "", "Fields": [{ "Name": "MessageIDs", "Docs": "", "Typewords": ["[]", "int64"] }, { "Name": "Muted", "Docs": "", "Typewords": ["bool"] }, { "Name": "Collapsed", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"ChangeMailboxRemove": { "Name": "ChangeMailboxRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }] },
|
||||
"ChangeMailboxAdd": { "Name": "ChangeMailboxAdd", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["Mailbox"] }] },
|
||||
"ChangeMailboxRename": { "Name": "ChangeMailboxRename", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "OldName", "Docs": "", "Typewords": ["string"] }, { "Name": "NewName", "Docs": "", "Typewords": ["string"] }, { "Name": "Flags", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
@ -73,6 +80,7 @@ var api;
|
||||
"UID": { "Name": "UID", "Docs": "", "Values": null },
|
||||
"ModSeq": { "Name": "ModSeq", "Docs": "", "Values": null },
|
||||
"Validation": { "Name": "Validation", "Docs": "", "Values": [{ "Name": "ValidationUnknown", "Value": 0, "Docs": "" }, { "Name": "ValidationStrict", "Value": 1, "Docs": "" }, { "Name": "ValidationDMARC", "Value": 2, "Docs": "" }, { "Name": "ValidationRelaxed", "Value": 3, "Docs": "" }, { "Name": "ValidationPass", "Value": 4, "Docs": "" }, { "Name": "ValidationNeutral", "Value": 5, "Docs": "" }, { "Name": "ValidationTemperror", "Value": 6, "Docs": "" }, { "Name": "ValidationPermerror", "Value": 7, "Docs": "" }, { "Name": "ValidationFail", "Value": 8, "Docs": "" }, { "Name": "ValidationSoftfail", "Value": 9, "Docs": "" }, { "Name": "ValidationNone", "Value": 10, "Docs": "" }] },
|
||||
"ThreadMode": { "Name": "ThreadMode", "Docs": "", "Values": [{ "Name": "ThreadOff", "Value": "off", "Docs": "" }, { "Name": "ThreadOn", "Value": "on", "Docs": "" }, { "Name": "ThreadUnread", "Value": "unread", "Docs": "" }] },
|
||||
"AttachmentType": { "Name": "AttachmentType", "Docs": "", "Values": [{ "Name": "AttachmentIndifferent", "Value": "", "Docs": "" }, { "Name": "AttachmentNone", "Value": "none", "Docs": "" }, { "Name": "AttachmentAny", "Value": "any", "Docs": "" }, { "Name": "AttachmentImage", "Value": "image", "Docs": "" }, { "Name": "AttachmentPDF", "Value": "pdf", "Docs": "" }, { "Name": "AttachmentArchive", "Value": "archive", "Docs": "" }, { "Name": "AttachmentSpreadsheet", "Value": "spreadsheet", "Docs": "" }, { "Name": "AttachmentDocument", "Value": "document", "Docs": "" }, { "Name": "AttachmentPresentation", "Value": "presentation", "Docs": "" }] },
|
||||
"Localpart": { "Name": "Localpart", "Docs": "", "Values": null },
|
||||
};
|
||||
@ -106,6 +114,7 @@ var api;
|
||||
Flags: (v) => api.parse("Flags", v),
|
||||
ChangeMsgRemove: (v) => api.parse("ChangeMsgRemove", v),
|
||||
ChangeMsgFlags: (v) => api.parse("ChangeMsgFlags", v),
|
||||
ChangeMsgThread: (v) => api.parse("ChangeMsgThread", v),
|
||||
ChangeMailboxRemove: (v) => api.parse("ChangeMailboxRemove", v),
|
||||
ChangeMailboxAdd: (v) => api.parse("ChangeMailboxAdd", v),
|
||||
ChangeMailboxRename: (v) => api.parse("ChangeMailboxRename", v),
|
||||
@ -116,6 +125,7 @@ var api;
|
||||
UID: (v) => api.parse("UID", v),
|
||||
ModSeq: (v) => api.parse("ModSeq", v),
|
||||
Validation: (v) => api.parse("Validation", v),
|
||||
ThreadMode: (v) => api.parse("ThreadMode", v),
|
||||
AttachmentType: (v) => api.parse("AttachmentType", v),
|
||||
Localpart: (v) => api.parse("Localpart", v),
|
||||
};
|
||||
@ -261,11 +271,30 @@ var api;
|
||||
const params = [mb];
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// ThreadCollapse saves the ThreadCollapse field for the messages and its
|
||||
// children. The messageIDs are typically thread roots. But not all roots
|
||||
// (without parent) of a thread need to have the same collapsed state.
|
||||
async ThreadCollapse(messageIDs, collapse) {
|
||||
const fn = "ThreadCollapse";
|
||||
const paramTypes = [["[]", "int64"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [messageIDs, collapse];
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// ThreadMute saves the ThreadMute field for the messages and their children.
|
||||
// If messages are muted, they are also marked collapsed.
|
||||
async ThreadMute(messageIDs, mute) {
|
||||
const fn = "ThreadMute";
|
||||
const paramTypes = [["[]", "int64"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [messageIDs, mute];
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// SSETypes exists to ensure the generated API contains the types, for use in SSE events.
|
||||
async SSETypes() {
|
||||
const fn = "SSETypes";
|
||||
const paramTypes = [];
|
||||
const returnTypes = [["EventStart"], ["EventViewErr"], ["EventViewReset"], ["EventViewMsgs"], ["EventViewChanges"], ["ChangeMsgAdd"], ["ChangeMsgRemove"], ["ChangeMsgFlags"], ["ChangeMailboxRemove"], ["ChangeMailboxAdd"], ["ChangeMailboxRename"], ["ChangeMailboxCounts"], ["ChangeMailboxSpecialUse"], ["ChangeMailboxKeywords"], ["Flags"]];
|
||||
const returnTypes = [["EventStart"], ["EventViewErr"], ["EventViewReset"], ["EventViewMsgs"], ["EventViewChanges"], ["ChangeMsgAdd"], ["ChangeMsgRemove"], ["ChangeMsgFlags"], ["ChangeMsgThread"], ["ChangeMailboxRemove"], ["ChangeMailboxAdd"], ["ChangeMailboxRename"], ["ChangeMailboxCounts"], ["ChangeMailboxSpecialUse"], ["ChangeMailboxKeywords"], ["Flags"]];
|
||||
const params = [];
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
|
@ -17,6 +17,12 @@ var api;
|
||||
Validation[Validation["ValidationSoftfail"] = 9] = "ValidationSoftfail";
|
||||
Validation[Validation["ValidationNone"] = 10] = "ValidationNone";
|
||||
})(Validation = api.Validation || (api.Validation = {}));
|
||||
let ThreadMode;
|
||||
(function (ThreadMode) {
|
||||
ThreadMode["ThreadOff"] = "off";
|
||||
ThreadMode["ThreadOn"] = "on";
|
||||
ThreadMode["ThreadUnread"] = "unread";
|
||||
})(ThreadMode = api.ThreadMode || (api.ThreadMode = {}));
|
||||
// AttachmentType is for filtering by attachment type.
|
||||
let AttachmentType;
|
||||
(function (AttachmentType) {
|
||||
@ -30,12 +36,12 @@ var api;
|
||||
AttachmentType["AttachmentDocument"] = "document";
|
||||
AttachmentType["AttachmentPresentation"] = "presentation";
|
||||
})(AttachmentType = api.AttachmentType || (api.AttachmentType = {}));
|
||||
api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "Request": true, "SpecialUse": true, "SubmitMessage": true };
|
||||
api.stringsTypes = { "AttachmentType": true, "Localpart": true };
|
||||
api.structTypes = { "Address": true, "Attachment": true, "ChangeMailboxAdd": true, "ChangeMailboxCounts": true, "ChangeMailboxKeywords": true, "ChangeMailboxRemove": true, "ChangeMailboxRename": true, "ChangeMailboxSpecialUse": true, "ChangeMsgAdd": true, "ChangeMsgFlags": true, "ChangeMsgRemove": true, "ChangeMsgThread": true, "Domain": true, "DomainAddressConfig": true, "Envelope": true, "EventStart": true, "EventViewChanges": true, "EventViewErr": true, "EventViewMsgs": true, "EventViewReset": true, "File": true, "Filter": true, "Flags": true, "ForwardAttachments": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "Request": true, "SpecialUse": true, "SubmitMessage": true };
|
||||
api.stringsTypes = { "AttachmentType": true, "Localpart": true, "ThreadMode": true };
|
||||
api.intsTypes = { "ModSeq": true, "UID": true, "Validation": true };
|
||||
api.types = {
|
||||
"Request": { "Name": "Request", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Cancel", "Docs": "", "Typewords": ["bool"] }, { "Name": "Query", "Docs": "", "Typewords": ["Query"] }, { "Name": "Page", "Docs": "", "Typewords": ["Page"] }] },
|
||||
"Query": { "Name": "Query", "Docs": "", "Fields": [{ "Name": "OrderAsc", "Docs": "", "Typewords": ["bool"] }, { "Name": "Filter", "Docs": "", "Typewords": ["Filter"] }, { "Name": "NotFilter", "Docs": "", "Typewords": ["NotFilter"] }] },
|
||||
"Query": { "Name": "Query", "Docs": "", "Fields": [{ "Name": "OrderAsc", "Docs": "", "Typewords": ["bool"] }, { "Name": "Threading", "Docs": "", "Typewords": ["ThreadMode"] }, { "Name": "Filter", "Docs": "", "Typewords": ["Filter"] }, { "Name": "NotFilter", "Docs": "", "Typewords": ["NotFilter"] }] },
|
||||
"Filter": { "Name": "Filter", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "MailboxChildrenIncluded", "Docs": "", "Typewords": ["bool"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Words", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Oldest", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "Newest", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "Subject", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["AttachmentType"] }, { "Name": "Labels", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Headers", "Docs": "", "Typewords": ["[]", "[]", "string"] }, { "Name": "SizeMin", "Docs": "", "Typewords": ["int64"] }, { "Name": "SizeMax", "Docs": "", "Typewords": ["int64"] }] },
|
||||
"NotFilter": { "Name": "NotFilter", "Docs": "", "Fields": [{ "Name": "Words", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "From", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["AttachmentType"] }, { "Name": "Labels", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"Page": { "Name": "Page", "Docs": "", "Fields": [{ "Name": "AnchorMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Count", "Docs": "", "Typewords": ["int32"] }, { "Name": "DestMessageID", "Docs": "", "Typewords": ["int64"] }] },
|
||||
@ -53,16 +59,17 @@ var api;
|
||||
"DomainAddressConfig": { "Name": "DomainAddressConfig", "Docs": "", "Fields": [{ "Name": "LocalpartCatchallSeparator", "Docs": "", "Typewords": ["string"] }, { "Name": "LocalpartCaseSensitive", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"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"] }] },
|
||||
"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": "MessageHash", "Docs": "", "Typewords": ["nullable", "string"] }, { "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"] }] },
|
||||
"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"] }] },
|
||||
"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": "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"] }] },
|
||||
"EventViewChanges": { "Name": "EventViewChanges", "Docs": "", "Fields": [{ "Name": "ViewID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Changes", "Docs": "", "Typewords": ["[]", "[]", "any"] }] },
|
||||
"ChangeMsgAdd": { "Name": "ChangeMsgAdd", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Flags", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MessageItem", "Docs": "", "Typewords": ["MessageItem"] }] },
|
||||
"ChangeMsgAdd": { "Name": "ChangeMsgAdd", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Flags", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "MessageItems", "Docs": "", "Typewords": ["[]", "MessageItem"] }] },
|
||||
"Flags": { "Name": "Flags", "Docs": "", "Fields": [{ "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"] }] },
|
||||
"ChangeMsgRemove": { "Name": "ChangeMsgRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UIDs", "Docs": "", "Typewords": ["[]", "UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }] },
|
||||
"ChangeMsgFlags": { "Name": "ChangeMsgFlags", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "UID", "Docs": "", "Typewords": ["UID"] }, { "Name": "ModSeq", "Docs": "", "Typewords": ["ModSeq"] }, { "Name": "Mask", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Flags", "Docs": "", "Typewords": ["Flags"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"ChangeMsgThread": { "Name": "ChangeMsgThread", "Docs": "", "Fields": [{ "Name": "MessageIDs", "Docs": "", "Typewords": ["[]", "int64"] }, { "Name": "Muted", "Docs": "", "Typewords": ["bool"] }, { "Name": "Collapsed", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"ChangeMailboxRemove": { "Name": "ChangeMailboxRemove", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }] },
|
||||
"ChangeMailboxAdd": { "Name": "ChangeMailboxAdd", "Docs": "", "Fields": [{ "Name": "Mailbox", "Docs": "", "Typewords": ["Mailbox"] }] },
|
||||
"ChangeMailboxRename": { "Name": "ChangeMailboxRename", "Docs": "", "Fields": [{ "Name": "MailboxID", "Docs": "", "Typewords": ["int64"] }, { "Name": "OldName", "Docs": "", "Typewords": ["string"] }, { "Name": "NewName", "Docs": "", "Typewords": ["string"] }, { "Name": "Flags", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
@ -73,6 +80,7 @@ var api;
|
||||
"UID": { "Name": "UID", "Docs": "", "Values": null },
|
||||
"ModSeq": { "Name": "ModSeq", "Docs": "", "Values": null },
|
||||
"Validation": { "Name": "Validation", "Docs": "", "Values": [{ "Name": "ValidationUnknown", "Value": 0, "Docs": "" }, { "Name": "ValidationStrict", "Value": 1, "Docs": "" }, { "Name": "ValidationDMARC", "Value": 2, "Docs": "" }, { "Name": "ValidationRelaxed", "Value": 3, "Docs": "" }, { "Name": "ValidationPass", "Value": 4, "Docs": "" }, { "Name": "ValidationNeutral", "Value": 5, "Docs": "" }, { "Name": "ValidationTemperror", "Value": 6, "Docs": "" }, { "Name": "ValidationPermerror", "Value": 7, "Docs": "" }, { "Name": "ValidationFail", "Value": 8, "Docs": "" }, { "Name": "ValidationSoftfail", "Value": 9, "Docs": "" }, { "Name": "ValidationNone", "Value": 10, "Docs": "" }] },
|
||||
"ThreadMode": { "Name": "ThreadMode", "Docs": "", "Values": [{ "Name": "ThreadOff", "Value": "off", "Docs": "" }, { "Name": "ThreadOn", "Value": "on", "Docs": "" }, { "Name": "ThreadUnread", "Value": "unread", "Docs": "" }] },
|
||||
"AttachmentType": { "Name": "AttachmentType", "Docs": "", "Values": [{ "Name": "AttachmentIndifferent", "Value": "", "Docs": "" }, { "Name": "AttachmentNone", "Value": "none", "Docs": "" }, { "Name": "AttachmentAny", "Value": "any", "Docs": "" }, { "Name": "AttachmentImage", "Value": "image", "Docs": "" }, { "Name": "AttachmentPDF", "Value": "pdf", "Docs": "" }, { "Name": "AttachmentArchive", "Value": "archive", "Docs": "" }, { "Name": "AttachmentSpreadsheet", "Value": "spreadsheet", "Docs": "" }, { "Name": "AttachmentDocument", "Value": "document", "Docs": "" }, { "Name": "AttachmentPresentation", "Value": "presentation", "Docs": "" }] },
|
||||
"Localpart": { "Name": "Localpart", "Docs": "", "Values": null },
|
||||
};
|
||||
@ -106,6 +114,7 @@ var api;
|
||||
Flags: (v) => api.parse("Flags", v),
|
||||
ChangeMsgRemove: (v) => api.parse("ChangeMsgRemove", v),
|
||||
ChangeMsgFlags: (v) => api.parse("ChangeMsgFlags", v),
|
||||
ChangeMsgThread: (v) => api.parse("ChangeMsgThread", v),
|
||||
ChangeMailboxRemove: (v) => api.parse("ChangeMailboxRemove", v),
|
||||
ChangeMailboxAdd: (v) => api.parse("ChangeMailboxAdd", v),
|
||||
ChangeMailboxRename: (v) => api.parse("ChangeMailboxRename", v),
|
||||
@ -116,6 +125,7 @@ var api;
|
||||
UID: (v) => api.parse("UID", v),
|
||||
ModSeq: (v) => api.parse("ModSeq", v),
|
||||
Validation: (v) => api.parse("Validation", v),
|
||||
ThreadMode: (v) => api.parse("ThreadMode", v),
|
||||
AttachmentType: (v) => api.parse("AttachmentType", v),
|
||||
Localpart: (v) => api.parse("Localpart", v),
|
||||
};
|
||||
@ -261,11 +271,30 @@ var api;
|
||||
const params = [mb];
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// ThreadCollapse saves the ThreadCollapse field for the messages and its
|
||||
// children. The messageIDs are typically thread roots. But not all roots
|
||||
// (without parent) of a thread need to have the same collapsed state.
|
||||
async ThreadCollapse(messageIDs, collapse) {
|
||||
const fn = "ThreadCollapse";
|
||||
const paramTypes = [["[]", "int64"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [messageIDs, collapse];
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// ThreadMute saves the ThreadMute field for the messages and their children.
|
||||
// If messages are muted, they are also marked collapsed.
|
||||
async ThreadMute(messageIDs, mute) {
|
||||
const fn = "ThreadMute";
|
||||
const paramTypes = [["[]", "int64"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [messageIDs, mute];
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// SSETypes exists to ensure the generated API contains the types, for use in SSE events.
|
||||
async SSETypes() {
|
||||
const fn = "SSETypes";
|
||||
const paramTypes = [];
|
||||
const returnTypes = [["EventStart"], ["EventViewErr"], ["EventViewReset"], ["EventViewMsgs"], ["EventViewChanges"], ["ChangeMsgAdd"], ["ChangeMsgRemove"], ["ChangeMsgFlags"], ["ChangeMailboxRemove"], ["ChangeMailboxAdd"], ["ChangeMailboxRename"], ["ChangeMailboxCounts"], ["ChangeMailboxSpecialUse"], ["ChangeMailboxKeywords"], ["Flags"]];
|
||||
const returnTypes = [["EventStart"], ["EventViewErr"], ["EventViewReset"], ["EventViewMsgs"], ["EventViewChanges"], ["ChangeMsgAdd"], ["ChangeMsgRemove"], ["ChangeMsgFlags"], ["ChangeMsgThread"], ["ChangeMailboxRemove"], ["ChangeMailboxAdd"], ["ChangeMailboxRename"], ["ChangeMailboxCounts"], ["ChangeMailboxSpecialUse"], ["ChangeMailboxKeywords"], ["Flags"]];
|
||||
const params = [];
|
||||
return await _sherpaCall(this.baseURL, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
|
229
webmail/view.go
229
webmail/view.go
@ -52,9 +52,18 @@ type Request struct {
|
||||
Page Page
|
||||
}
|
||||
|
||||
type ThreadMode string
|
||||
|
||||
const (
|
||||
ThreadOff ThreadMode = "off"
|
||||
ThreadOn ThreadMode = "on"
|
||||
ThreadUnread ThreadMode = "unread"
|
||||
)
|
||||
|
||||
// Query is a request for messages that match filters, in a given order.
|
||||
type Query struct {
|
||||
OrderAsc bool // Order by received ascending or desending.
|
||||
Threading ThreadMode
|
||||
Filter Filter
|
||||
NotFilter NotFilter
|
||||
}
|
||||
@ -163,6 +172,7 @@ type MessageItem struct {
|
||||
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.
|
||||
}
|
||||
|
||||
// ParsedMessage has more parsed/derived information about a message, intended
|
||||
@ -219,8 +229,18 @@ type EventViewMsgs struct {
|
||||
ViewID int64
|
||||
RequestID int64
|
||||
|
||||
MessageItems []MessageItem // If empty, this was the last message for the request.
|
||||
ParsedMessage *ParsedMessage // If set, will match the target page.DestMessageID from the request.
|
||||
// If empty, this was the last message for the request. If non-empty, a list of
|
||||
// thread messages. Each with the first message being the reason this thread is
|
||||
// included and can be used as AnchorID in followup requests. If the threading mode
|
||||
// is "off" in the query, there will always be only a single message. If a thread
|
||||
// is sent, all messages in the thread are sent, including those that don't match
|
||||
// the query (e.g. from another mailbox). Threads can be displayed based on the
|
||||
// ThreadParentIDs field, with possibly slightly different display based on field
|
||||
// ThreadMissingLink.
|
||||
MessageItems [][]MessageItem
|
||||
|
||||
// If set, will match the target page.DestMessageID from the request.
|
||||
ParsedMessage *ParsedMessage
|
||||
|
||||
// If set, there are no more messages in this view at this moment. Messages can be
|
||||
// added, typically via Change messages, e.g. for new deliveries.
|
||||
@ -253,10 +273,10 @@ type EventViewChanges struct {
|
||||
Changes [][2]any // The first field of [2]any is a string, the second of the Change types below.
|
||||
}
|
||||
|
||||
// ChangeMsgAdd adds a new message to the view.
|
||||
// ChangeMsgAdd adds a new message and possibly its thread to the view.
|
||||
type ChangeMsgAdd struct {
|
||||
store.ChangeAddUID
|
||||
MessageItem MessageItem
|
||||
MessageItems []MessageItem
|
||||
}
|
||||
|
||||
// ChangeMsgRemove removes one or more messages from the view.
|
||||
@ -269,6 +289,11 @@ type ChangeMsgFlags struct {
|
||||
store.ChangeFlags
|
||||
}
|
||||
|
||||
// ChangeMsgThread updates muted/collapsed fields for one message.
|
||||
type ChangeMsgThread struct {
|
||||
store.ChangeThread
|
||||
}
|
||||
|
||||
// ChangeMailboxRemove indicates a mailbox was removed, including all its messages.
|
||||
type ChangeMailboxRemove struct {
|
||||
store.ChangeRemoveMailbox
|
||||
@ -308,9 +333,9 @@ type ChangeMailboxKeywords struct {
|
||||
type view struct {
|
||||
Request Request
|
||||
|
||||
// Last message we sent to the client. We use it to decide if a newly delivered
|
||||
// message is within the view and the client should get a notification.
|
||||
LastMessage store.Message
|
||||
// Received of last message we sent to the client. We use it to decide if a newly
|
||||
// delivered message is within the view and the client should get a notification.
|
||||
LastMessageReceived time.Time
|
||||
|
||||
// If set, the last message in the query view has been sent. There is no need to do
|
||||
// another query, it will not return more data. Used to decide if an event for a
|
||||
@ -322,6 +347,12 @@ type view struct {
|
||||
// Mailboxes to match, can be multiple, for matching children. If empty, there is
|
||||
// no filter on mailboxes.
|
||||
mailboxIDs map[int64]bool
|
||||
|
||||
// Threads sent to client. New messages for this thread are also sent, regardless
|
||||
// of regular query matching, so also for other mailboxes. If the user (re)moved
|
||||
// all messages of a thread, they may still receive events for the thread. Only
|
||||
// filled when query with threading not off.
|
||||
threadIDs map[int64]struct{}
|
||||
}
|
||||
|
||||
// sses tracks all sse connections, and access to them.
|
||||
@ -513,6 +544,9 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
http.Error(w, "400 - bad request - request cannot have Page.Count 0", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Query.Threading == "" {
|
||||
req.Query.Threading = ThreadOff
|
||||
}
|
||||
|
||||
var writer *eventWriter
|
||||
|
||||
@ -699,7 +733,7 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
// Start a view, it determines if we send a change to the client. And start an
|
||||
// implicit query for messages, we'll send the messages to the client which can
|
||||
// fill its ui with messages.
|
||||
v := view{req, store.Message{}, false, matchMailboxes, mailboxIDs}
|
||||
v := view{req, time.Time{}, false, matchMailboxes, mailboxIDs, map[int64]struct{}{}}
|
||||
go viewRequestTx(reqctx, log, acc, qtx, v, viewMsgsc, viewErrc, viewResetc, donec)
|
||||
qtx = nil // viewRequestTx closes qtx
|
||||
|
||||
@ -764,7 +798,7 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
|
||||
// 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) (changedUIDs []store.UID) {
|
||||
xchangedUIDs := func(mailboxID int64, uids []store.UID, isRemove bool) (changedUIDs []store.UID) {
|
||||
uidsAny := make([]any, len(uids))
|
||||
for i, uid := range uids {
|
||||
uidsAny[i] = uid
|
||||
@ -774,8 +808,10 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
q := bstore.QueryTx[store.Message](xtx)
|
||||
q.FilterNonzero(store.Message{MailboxID: mailboxID})
|
||||
q.FilterEqual("UID", uidsAny...)
|
||||
mbOK := v.matchesMailbox(mailboxID)
|
||||
err = q.ForEach(func(m store.Message) error {
|
||||
if v.inRange(m) {
|
||||
_, thread := v.threadIDs[m.ThreadID]
|
||||
if thread || mbOK && (v.inRange(m) || isRemove && m.Expunged) {
|
||||
changedUIDs = append(changedUIDs, m.UID)
|
||||
}
|
||||
return nil
|
||||
@ -788,33 +824,40 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
for _, change := range changes {
|
||||
switch c := change.(type) {
|
||||
case store.ChangeAddUID:
|
||||
if ok, err := v.matches(log, acc, true, 0, c.MailboxID, c.UID, c.Flags, c.Keywords, getmsg); err != nil {
|
||||
xcheckf(ctx, err, "matching new message against view")
|
||||
} else if !ok {
|
||||
continue
|
||||
}
|
||||
ok, err := v.matches(log, acc, true, 0, c.MailboxID, c.UID, c.Flags, c.Keywords, getmsg)
|
||||
xcheckf(ctx, err, "matching new message against view")
|
||||
m, err := getmsg(0, c.MailboxID, c.UID)
|
||||
xcheckf(ctx, err, "get message")
|
||||
_, thread := v.threadIDs[m.ThreadID]
|
||||
if !ok && !thread {
|
||||
continue
|
||||
}
|
||||
state := msgState{acc: acc}
|
||||
mi, err := messageItem(log, m, &state)
|
||||
state.clear()
|
||||
xcheckf(ctx, err, "make messageitem")
|
||||
taggedChanges = append(taggedChanges, [2]any{"ChangeMsgAdd", ChangeMsgAdd{c, mi}})
|
||||
mi.MatchQuery = ok
|
||||
|
||||
mil := []MessageItem{mi}
|
||||
if !thread && req.Query.Threading != ThreadOff {
|
||||
err := ensureTx()
|
||||
xcheckf(ctx, err, "transaction")
|
||||
more, _, err := gatherThread(log, xtx, acc, v, m, 0)
|
||||
xcheckf(ctx, err, "gathering thread messages for id %d, thread %d", m.ID, m.ThreadID)
|
||||
mil = append(mil, more...)
|
||||
v.threadIDs[m.ThreadID] = struct{}{}
|
||||
}
|
||||
|
||||
taggedChanges = append(taggedChanges, [2]any{"ChangeMsgAdd", ChangeMsgAdd{c, mil}})
|
||||
|
||||
// If message extends the view, store it as such.
|
||||
if !v.Request.Query.OrderAsc && m.Received.Before(v.LastMessage.Received) || v.Request.Query.OrderAsc && m.Received.After(v.LastMessage.Received) {
|
||||
v.LastMessage = m
|
||||
if !v.Request.Query.OrderAsc && m.Received.Before(v.LastMessageReceived) || v.Request.Query.OrderAsc && m.Received.After(v.LastMessageReceived) {
|
||||
v.LastMessageReceived = m.Received
|
||||
}
|
||||
|
||||
case store.ChangeRemoveUIDs:
|
||||
// We do a quick filter over changes, not sending UID updates for unselected
|
||||
// mailboxes or when the message is outside the range of the view. But we still may
|
||||
// send messages that don't apply to the filter. If client doesn't recognize the
|
||||
// messages, that's fine.
|
||||
if !v.matchesMailbox(c.MailboxID) {
|
||||
continue
|
||||
}
|
||||
changedUIDs := xchangedUIDs(c.MailboxID, c.UIDs)
|
||||
// We may send changes for uids the client doesn't know, that's fine.
|
||||
changedUIDs := xchangedUIDs(c.MailboxID, c.UIDs, true)
|
||||
if len(changedUIDs) == 0 {
|
||||
continue
|
||||
}
|
||||
@ -823,11 +866,8 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
taggedChanges = append(taggedChanges, [2]any{"ChangeMsgRemove", ch})
|
||||
|
||||
case store.ChangeFlags:
|
||||
// As with ChangeRemoveUIDs above, we send more changes than strictly needed.
|
||||
if !v.matchesMailbox(c.MailboxID) {
|
||||
continue
|
||||
}
|
||||
changedUIDs := xchangedUIDs(c.MailboxID, []store.UID{c.UID})
|
||||
// We may send changes for uids the client doesn't know, that's fine.
|
||||
changedUIDs := xchangedUIDs(c.MailboxID, []store.UID{c.UID}, false)
|
||||
if len(changedUIDs) == 0 {
|
||||
continue
|
||||
}
|
||||
@ -835,6 +875,10 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
ch.UID = changedUIDs[0]
|
||||
taggedChanges = append(taggedChanges, [2]any{"ChangeMsgFlags", ch})
|
||||
|
||||
case store.ChangeThread:
|
||||
// Change in muted/collaped state, just always ship it.
|
||||
taggedChanges = append(taggedChanges, [2]any{"ChangeMsgThread", ChangeMsgThread{c}})
|
||||
|
||||
case store.ChangeRemoveMailbox:
|
||||
taggedChanges = append(taggedChanges, [2]any{"ChangeMailboxRemove", ChangeMailboxRemove{c}})
|
||||
|
||||
@ -910,7 +954,7 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
v.End = true
|
||||
}
|
||||
if len(vm.MessageItems) > 0 {
|
||||
v.LastMessage = vm.MessageItems[len(vm.MessageItems)-1].Message
|
||||
v.LastMessageReceived = vm.MessageItems[len(vm.MessageItems)-1][0].Message.Received
|
||||
}
|
||||
writer.xsendEvent(ctx, log, "viewMsgs", vm)
|
||||
|
||||
@ -948,7 +992,7 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
cancelDrain()
|
||||
}
|
||||
if req.Cancel {
|
||||
v = view{req, store.Message{}, false, false, nil}
|
||||
v = view{req, time.Time{}, false, false, nil, nil}
|
||||
continue
|
||||
}
|
||||
|
||||
@ -987,7 +1031,7 @@ func serveEvents(ctx context.Context, log *mlog.Log, w http.ResponseWriter, r *h
|
||||
if req.Query.Filter.MailboxChildrenIncluded {
|
||||
xgatherMailboxIDs(ctx, rtx, mailboxIDs, mailboxPrefixes)
|
||||
}
|
||||
v = view{req, store.Message{}, false, matchMailboxes, mailboxIDs}
|
||||
v = view{req, time.Time{}, false, matchMailboxes, mailboxIDs, map[int64]struct{}{}}
|
||||
} else {
|
||||
v.Request = req
|
||||
}
|
||||
@ -1067,7 +1111,7 @@ func (v view) matchesMailbox(mailboxID int64) bool {
|
||||
// inRange returns whether m is within the range for the view, whether a change for
|
||||
// this message should be sent to the client so it can update its state.
|
||||
func (v view) inRange(m store.Message) bool {
|
||||
return v.End || !v.Request.Query.OrderAsc && !m.Received.Before(v.LastMessage.Received) || v.Request.Query.OrderAsc && !m.Received.After(v.LastMessage.Received)
|
||||
return v.End || !v.Request.Query.OrderAsc && !m.Received.Before(v.LastMessageReceived) || v.Request.Query.OrderAsc && !m.Received.After(v.LastMessageReceived)
|
||||
}
|
||||
|
||||
// matches checks if the message, identified by either messageID or mailboxID+UID,
|
||||
@ -1152,7 +1196,7 @@ type msgResp struct {
|
||||
err error // If set, an error happened and fields below are not set.
|
||||
reset bool // If set, the anchor message does not exist (anymore?) and we are sending messages from the start, fields below not set.
|
||||
viewEnd bool // If set, the last message for the view was seen, no more should be requested, fields below not set.
|
||||
mi MessageItem // If none of the cases above apply, the message that was found matching the query.
|
||||
mil []MessageItem // If none of the cases above apply, the messages that was found matching the query. First message was reason the thread is returned, for use as AnchorID in followup request.
|
||||
pm *ParsedMessage // If m was the target page.DestMessageID, or this is the first match, this is the parsed message of mi.
|
||||
}
|
||||
|
||||
@ -1176,7 +1220,7 @@ func viewRequestTx(ctx context.Context, log *mlog.Log, acc *store.Account, tx *b
|
||||
}
|
||||
}()
|
||||
|
||||
var msgitems []MessageItem // Gathering for 300ms, then flushing.
|
||||
var msgitems [][]MessageItem // Gathering for 300ms, then flushing.
|
||||
var parsedMessage *ParsedMessage
|
||||
var viewEnd bool
|
||||
|
||||
@ -1225,7 +1269,7 @@ func viewRequestTx(ctx context.Context, log *mlog.Log, acc *store.Account, tx *b
|
||||
return
|
||||
}
|
||||
|
||||
msgitems = append(msgitems, mr.mi)
|
||||
msgitems = append(msgitems, mr.mil)
|
||||
if mr.pm != nil {
|
||||
parsedMessage = mr.pm
|
||||
}
|
||||
@ -1406,6 +1450,12 @@ func queryMessages(ctx context.Context, log *mlog.Log, acc *store.Account, tx *b
|
||||
end = false
|
||||
return bstore.StopForEach
|
||||
}
|
||||
|
||||
if _, ok := v.threadIDs[m.ThreadID]; ok {
|
||||
// Message was already returned as part of a thread.
|
||||
return nil
|
||||
}
|
||||
|
||||
var pm *ParsedMessage
|
||||
if m.ID == page.DestMessageID || page.DestMessageID == 0 && have == 0 && page.AnchorMessageID == 0 {
|
||||
found = true
|
||||
@ -1415,12 +1465,60 @@ func queryMessages(ctx context.Context, log *mlog.Log, acc *store.Account, tx *b
|
||||
}
|
||||
pm = &xpm
|
||||
}
|
||||
|
||||
mi, err := messageItem(log, m, &state)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making messageitem for message %d: %v", m.ID, err)
|
||||
}
|
||||
mrc <- msgResp{mi: mi, pm: pm}
|
||||
have++
|
||||
mil := []MessageItem{mi}
|
||||
if query.Threading != ThreadOff {
|
||||
more, xpm, err := gatherThread(log, tx, acc, v, m, page.DestMessageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gathering thread messages for id %d, thread %d: %v", m.ID, m.ThreadID, err)
|
||||
}
|
||||
if xpm != nil {
|
||||
pm = xpm
|
||||
found = true
|
||||
}
|
||||
mil = append(mil, more...)
|
||||
v.threadIDs[m.ThreadID] = struct{}{}
|
||||
|
||||
// Calculate how many messages the frontend is going to show, and only count those as returned.
|
||||
collapsed := map[int64]bool{}
|
||||
for _, mi := range mil {
|
||||
collapsed[mi.Message.ID] = mi.Message.ThreadCollapsed
|
||||
}
|
||||
unread := map[int64]bool{} // Propagated to thread root.
|
||||
if query.Threading == ThreadUnread {
|
||||
for _, mi := range mil {
|
||||
m := mi.Message
|
||||
if m.Seen {
|
||||
continue
|
||||
}
|
||||
unread[m.ID] = true
|
||||
for _, id := range m.ThreadParentIDs {
|
||||
unread[id] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, mi := range mil {
|
||||
m := mi.Message
|
||||
threadRoot := true
|
||||
rootID := m.ID
|
||||
for _, id := range m.ThreadParentIDs {
|
||||
if _, ok := collapsed[id]; ok {
|
||||
threadRoot = false
|
||||
rootID = id
|
||||
}
|
||||
}
|
||||
if threadRoot || (query.Threading == ThreadOn && !collapsed[rootID] || query.Threading == ThreadUnread && unread[rootID]) {
|
||||
have++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
have++
|
||||
}
|
||||
mrc <- msgResp{mil: mil, pm: pm}
|
||||
return nil
|
||||
})
|
||||
// Check for an error in one of the filters again. Check in ForEach would not
|
||||
@ -1437,6 +1535,57 @@ func queryMessages(ctx context.Context, log *mlog.Log, acc *store.Account, tx *b
|
||||
}
|
||||
}
|
||||
|
||||
func gatherThread(log *mlog.Log, tx *bstore.Tx, acc *store.Account, v view, m store.Message, destMessageID int64) ([]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")
|
||||
}
|
||||
|
||||
// Fetch other messages for this thread.
|
||||
qt := bstore.QueryTx[store.Message](tx)
|
||||
qt.FilterNonzero(store.Message{ThreadID: m.ThreadID})
|
||||
qt.FilterEqual("Expunged", false)
|
||||
qt.FilterNotEqual("ID", m.ID)
|
||||
tml, err := qt.List()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("listing other messages in thread for message %d, thread %d: %v", m.ID, m.ThreadID, err)
|
||||
}
|
||||
|
||||
var mil []MessageItem
|
||||
var pm *ParsedMessage
|
||||
for _, tm := range tml {
|
||||
err := func() error {
|
||||
xstate := msgState{acc: acc}
|
||||
defer xstate.clear()
|
||||
|
||||
mi, err := messageItem(log, tm, &xstate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making messageitem for message %d, for thread %d: %v", tm.ID, m.ThreadID, err)
|
||||
}
|
||||
mi.MatchQuery, err = v.matches(log, acc, false, tm.ID, tm.MailboxID, tm.UID, tm.Flags, tm.Keywords, func(int64, int64, store.UID) (store.Message, error) {
|
||||
return tm, nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("matching thread message %d against view query: %v", tm.ID, err)
|
||||
}
|
||||
mil = append(mil, mi)
|
||||
|
||||
if tm.ID == destMessageID {
|
||||
xpm, err := parsedMessage(log, tm, &xstate, true, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing thread message %d: %v", tm.ID, err)
|
||||
}
|
||||
pm = &xpm
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return mil, pm, nil
|
||||
}
|
||||
|
||||
// While checking the filters on a message, we may need to get more message
|
||||
// details as each filter passes. We check the filters that need the basic
|
||||
// information first, and load and cache more details for the next filters.
|
||||
|
@ -50,8 +50,10 @@ func TestView(t *testing.T) {
|
||||
listsGoNutsMinimal = &testmsg{"Lists/Go/Nuts", store.Flags{}, nil, msgMinimal, zerom, 0}
|
||||
trashMinimal = &testmsg{"Trash", store.Flags{}, nil, msgMinimal, zerom, 0}
|
||||
junkMinimal = &testmsg{"Trash", store.Flags{}, nil, msgMinimal, zerom, 0}
|
||||
trashAlt = &testmsg{"Trash", store.Flags{}, nil, msgAlt, zerom, 0}
|
||||
inboxAltReply = &testmsg{"Inbox", store.Flags{}, nil, msgAltReply, zerom, 0}
|
||||
)
|
||||
var testmsgs = []*testmsg{inboxMinimal, inboxFlags, listsMinimal, listsGoNutsMinimal, trashMinimal, junkMinimal}
|
||||
var testmsgs = []*testmsg{inboxMinimal, inboxFlags, listsMinimal, listsGoNutsMinimal, trashMinimal, junkMinimal, trashAlt, inboxAltReply}
|
||||
for _, tm := range testmsgs {
|
||||
tdeliver(t, acc, tm)
|
||||
}
|
||||
@ -116,10 +118,10 @@ func TestView(t *testing.T) {
|
||||
evr.Get("start", &start)
|
||||
var viewMsgs EventViewMsgs
|
||||
evr.Get("viewMsgs", &viewMsgs)
|
||||
tcompare(t, len(viewMsgs.MessageItems), 2)
|
||||
tcompare(t, len(viewMsgs.MessageItems), 3)
|
||||
tcompare(t, viewMsgs.ViewEnd, true)
|
||||
|
||||
var inbox, archive, lists store.Mailbox
|
||||
var inbox, archive, lists, trash store.Mailbox
|
||||
for _, mb := range start.Mailboxes {
|
||||
if mb.Archive {
|
||||
archive = mb
|
||||
@ -127,6 +129,8 @@ func TestView(t *testing.T) {
|
||||
inbox = mb
|
||||
} else if mb.Name == "Lists" {
|
||||
lists = mb
|
||||
} else if mb.Name == "Trash" {
|
||||
trash = mb
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,7 +165,7 @@ func TestView(t *testing.T) {
|
||||
testConn(api.Token(ctx), "&waitMinMsec=1&waitMaxMsec=2", waitReq, func(start EventStart, evr eventReader) {
|
||||
var vm EventViewMsgs
|
||||
evr.Get("viewMsgs", &vm)
|
||||
tcompare(t, len(vm.MessageItems), 2)
|
||||
tcompare(t, len(vm.MessageItems), 3)
|
||||
})
|
||||
|
||||
// Connection with DestMessageID.
|
||||
@ -174,7 +178,7 @@ func TestView(t *testing.T) {
|
||||
testConn(tokens[len(tokens)-3], "", destMsgReq, func(start EventStart, evr eventReader) {
|
||||
var vm EventViewMsgs
|
||||
evr.Get("viewMsgs", &vm)
|
||||
tcompare(t, len(vm.MessageItems), 2)
|
||||
tcompare(t, len(vm.MessageItems), 3)
|
||||
tcompare(t, vm.ParsedMessage.ID, destMsgReq.Page.DestMessageID)
|
||||
})
|
||||
// todo: destmessageid past count, needs large mailbox
|
||||
@ -189,7 +193,7 @@ func TestView(t *testing.T) {
|
||||
testConn(api.Token(ctx), "", badDestMsgReq, func(start EventStart, evr eventReader) {
|
||||
var vm EventViewMsgs
|
||||
evr.Get("viewMsgs", &vm)
|
||||
tcompare(t, len(vm.MessageItems), 2)
|
||||
tcompare(t, len(vm.MessageItems), 3)
|
||||
})
|
||||
|
||||
// Connection with missing unknown AnchorMessageID, resets view.
|
||||
@ -205,7 +209,7 @@ func TestView(t *testing.T) {
|
||||
|
||||
var vm EventViewMsgs
|
||||
evr.Get("viewMsgs", &vm)
|
||||
tcompare(t, len(vm.MessageItems), 2)
|
||||
tcompare(t, len(vm.MessageItems), 3)
|
||||
})
|
||||
|
||||
// Connection that starts with a filter, without mailbox.
|
||||
@ -219,12 +223,12 @@ func TestView(t *testing.T) {
|
||||
var vm EventViewMsgs
|
||||
evr.Get("viewMsgs", &vm)
|
||||
tcompare(t, len(vm.MessageItems), 1)
|
||||
tcompare(t, vm.MessageItems[0].Message.ID, inboxFlags.ID)
|
||||
tcompare(t, vm.MessageItems[0][0].Message.ID, inboxFlags.ID)
|
||||
})
|
||||
|
||||
// Paginate from previous last element. There is nothing new.
|
||||
var viewID int64 = 1
|
||||
api.Request(ctx, Request{ID: 1, SSEID: start.SSEID, ViewID: viewID, Query: Query{Filter: Filter{MailboxID: inbox.ID}}, Page: Page{Count: 10, AnchorMessageID: viewMsgs.MessageItems[len(viewMsgs.MessageItems)-1].Message.ID}})
|
||||
api.Request(ctx, Request{ID: 1, SSEID: start.SSEID, ViewID: viewID, Query: Query{Filter: Filter{MailboxID: inbox.ID}}, Page: Page{Count: 10, AnchorMessageID: viewMsgs.MessageItems[len(viewMsgs.MessageItems)-1][0].Message.ID}})
|
||||
evr.Get("viewMsgs", &viewMsgs)
|
||||
tcompare(t, len(viewMsgs.MessageItems), 0)
|
||||
|
||||
@ -235,6 +239,36 @@ func TestView(t *testing.T) {
|
||||
tcompare(t, len(viewMsgs.MessageItems), 0)
|
||||
tcompare(t, viewMsgs.ViewEnd, true)
|
||||
|
||||
threadlen := func(mil [][]MessageItem) int {
|
||||
n := 0
|
||||
for _, l := range mil {
|
||||
n += len(l)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Request with threading, should also include parent message from Trash mailbox (trashAlt).
|
||||
viewID++
|
||||
api.Request(ctx, Request{ID: 1, SSEID: start.SSEID, ViewID: viewID, Query: Query{Filter: Filter{MailboxID: inbox.ID}, Threading: "unread"}, Page: Page{Count: 10}})
|
||||
evr.Get("viewMsgs", &viewMsgs)
|
||||
tcompare(t, len(viewMsgs.MessageItems), 3)
|
||||
tcompare(t, threadlen(viewMsgs.MessageItems), 3+1)
|
||||
tcompare(t, viewMsgs.ViewEnd, true)
|
||||
// And likewise when querying Trash, should also include child message in Inbox (inboxAltReply).
|
||||
viewID++
|
||||
api.Request(ctx, Request{ID: 1, SSEID: start.SSEID, ViewID: viewID, Query: Query{Filter: Filter{MailboxID: trash.ID}, Threading: "on"}, Page: Page{Count: 10}})
|
||||
evr.Get("viewMsgs", &viewMsgs)
|
||||
tcompare(t, len(viewMsgs.MessageItems), 3)
|
||||
tcompare(t, threadlen(viewMsgs.MessageItems), 3+1)
|
||||
tcompare(t, viewMsgs.ViewEnd, true)
|
||||
// Without threading, the inbox has just 3 messages.
|
||||
viewID++
|
||||
api.Request(ctx, Request{ID: 1, SSEID: start.SSEID, ViewID: viewID, Query: Query{Filter: Filter{MailboxID: inbox.ID}, Threading: "off"}, Page: Page{Count: 10}})
|
||||
evr.Get("viewMsgs", &viewMsgs)
|
||||
tcompare(t, len(viewMsgs.MessageItems), 3)
|
||||
tcompare(t, threadlen(viewMsgs.MessageItems), 3)
|
||||
tcompare(t, viewMsgs.ViewEnd, true)
|
||||
|
||||
testFilter := func(orderAsc bool, f Filter, nf NotFilter, expIDs []int64) {
|
||||
t.Helper()
|
||||
viewID++
|
||||
@ -242,7 +276,7 @@ func TestView(t *testing.T) {
|
||||
evr.Get("viewMsgs", &viewMsgs)
|
||||
ids := make([]int64, len(viewMsgs.MessageItems))
|
||||
for i, mi := range viewMsgs.MessageItems {
|
||||
ids[i] = mi.Message.ID
|
||||
ids[i] = mi[0].Message.ID
|
||||
}
|
||||
tcompare(t, ids, expIDs)
|
||||
tcompare(t, viewMsgs.ViewEnd, true)
|
||||
@ -250,32 +284,32 @@ func TestView(t *testing.T) {
|
||||
|
||||
// Test filtering.
|
||||
var znf NotFilter
|
||||
testFilter(false, Filter{MailboxID: lists.ID, MailboxChildrenIncluded: true}, znf, []int64{listsGoNutsMinimal.ID, listsMinimal.ID}) // Mailbox and sub mailbox.
|
||||
testFilter(true, Filter{MailboxID: lists.ID, MailboxChildrenIncluded: true}, znf, []int64{listsMinimal.ID, listsGoNutsMinimal.ID}) // Oldest first first.
|
||||
testFilter(false, Filter{MailboxID: -1}, znf, []int64{listsGoNutsMinimal.ID, listsMinimal.ID, inboxFlags.ID, inboxMinimal.ID}) // All except trash/junk/rejects.
|
||||
testFilter(false, Filter{MailboxID: lists.ID, MailboxChildrenIncluded: true}, znf, []int64{listsGoNutsMinimal.ID, listsMinimal.ID}) // Mailbox and sub mailbox.
|
||||
testFilter(true, Filter{MailboxID: lists.ID, MailboxChildrenIncluded: true}, znf, []int64{listsMinimal.ID, listsGoNutsMinimal.ID}) // Oldest first first.
|
||||
testFilter(false, Filter{MailboxID: -1}, znf, []int64{inboxAltReply.ID, listsGoNutsMinimal.ID, listsMinimal.ID, inboxFlags.ID, inboxMinimal.ID}) // All except trash/junk/rejects.
|
||||
testFilter(false, Filter{Labels: []string{`\seen`}}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Labels: []string{`\seen`}}, []int64{inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Labels: []string{`\seen`}}, []int64{inboxAltReply.ID, inboxMinimal.ID})
|
||||
testFilter(false, Filter{Labels: []string{`testlabel`}}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Labels: []string{`testlabel`}}, []int64{inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID, Oldest: &inboxFlags.m.Received}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Labels: []string{`testlabel`}}, []int64{inboxAltReply.ID, inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID, Oldest: &inboxFlags.m.Received}, znf, []int64{inboxAltReply.ID, inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID, Newest: &inboxMinimal.m.Received}, znf, []int64{inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID, SizeMin: inboxFlags.m.Size}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID, SizeMax: inboxMinimal.m.Size}, znf, []int64{inboxMinimal.ID})
|
||||
testFilter(false, Filter{From: []string{"mjl+altrel@mox.example"}}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{From: []string{"mjl+altrel@mox.example"}}, []int64{inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{From: []string{"mjl+altrel@mox.example"}}, []int64{inboxAltReply.ID, inboxMinimal.ID})
|
||||
testFilter(false, Filter{To: []string{"mox+altrel@other.example"}}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{To: []string{"mox+altrel@other.example"}}, []int64{inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{To: []string{"mox+altrel@other.example"}}, []int64{inboxAltReply.ID, inboxMinimal.ID})
|
||||
testFilter(false, Filter{From: []string{"mjl+altrel@mox.example", "bogus"}}, znf, []int64{})
|
||||
testFilter(false, Filter{To: []string{"mox+altrel@other.example", "bogus"}}, znf, []int64{})
|
||||
testFilter(false, Filter{Subject: []string{"test", "alt", "rel"}}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Subject: []string{"alt"}}, []int64{inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Subject: []string{"alt"}}, []int64{inboxAltReply.ID, inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID, Words: []string{"the text body", "body", "the "}}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Words: []string{"the text body"}}, []int64{inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Words: []string{"the text body"}}, []int64{inboxAltReply.ID, inboxMinimal.ID})
|
||||
testFilter(false, Filter{Headers: [][2]string{{"X-Special", ""}}}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{Headers: [][2]string{{"X-Special", "testing"}}}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{Headers: [][2]string{{"X-Special", "other"}}}, znf, []int64{})
|
||||
testFilter(false, Filter{Attachments: AttachmentImage}, znf, []int64{inboxFlags.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Attachments: AttachmentImage}, []int64{inboxMinimal.ID})
|
||||
testFilter(false, Filter{MailboxID: inbox.ID}, NotFilter{Attachments: AttachmentImage}, []int64{inboxAltReply.ID, inboxMinimal.ID})
|
||||
|
||||
// Test changes.
|
||||
getChanges := func(changes ...any) {
|
||||
@ -341,13 +375,13 @@ func TestView(t *testing.T) {
|
||||
var chmbcounts ChangeMailboxCounts
|
||||
getChanges(&chmsgadd, &chmbcounts)
|
||||
tcompare(t, chmsgadd.ChangeAddUID.MailboxID, inbox.ID)
|
||||
tcompare(t, chmsgadd.MessageItem.Message.ID, inboxNew.ID)
|
||||
tcompare(t, chmsgadd.MessageItems[0].Message.ID, inboxNew.ID)
|
||||
chmbcounts.Size = 0
|
||||
tcompare(t, chmbcounts, ChangeMailboxCounts{
|
||||
ChangeMailboxCounts: store.ChangeMailboxCounts{
|
||||
MailboxID: inbox.ID,
|
||||
MailboxName: inbox.Name,
|
||||
MailboxCounts: store.MailboxCounts{Total: 3, Unread: 2, Unseen: 2},
|
||||
MailboxCounts: store.MailboxCounts{Total: 4, Unread: 3, Unseen: 3},
|
||||
},
|
||||
})
|
||||
|
||||
@ -369,7 +403,7 @@ func TestView(t *testing.T) {
|
||||
ChangeMailboxCounts: store.ChangeMailboxCounts{
|
||||
MailboxID: inbox.ID,
|
||||
MailboxName: inbox.Name,
|
||||
MailboxCounts: store.MailboxCounts{Total: 3, Unread: 1, Unseen: 1},
|
||||
MailboxCounts: store.MailboxCounts{Total: 4, Unread: 2, Unseen: 2},
|
||||
},
|
||||
})
|
||||
|
||||
@ -384,10 +418,40 @@ func TestView(t *testing.T) {
|
||||
ChangeMailboxCounts: store.ChangeMailboxCounts{
|
||||
MailboxID: inbox.ID,
|
||||
MailboxName: inbox.Name,
|
||||
MailboxCounts: store.MailboxCounts{Total: 1},
|
||||
MailboxCounts: store.MailboxCounts{Total: 2, Unread: 1, Unseen: 1},
|
||||
},
|
||||
})
|
||||
|
||||
// ChangeMsgThread
|
||||
api.ThreadCollapse(ctx, []int64{inboxAltReply.ID}, true)
|
||||
var chmsgthread ChangeMsgThread
|
||||
getChanges(&chmsgthread)
|
||||
tcompare(t, chmsgthread.ChangeThread, store.ChangeThread{MessageIDs: []int64{inboxAltReply.ID}, Muted: false, Collapsed: true})
|
||||
|
||||
// Now collapsing the thread root, the child is already collapsed so no change.
|
||||
api.ThreadCollapse(ctx, []int64{trashAlt.ID}, true)
|
||||
getChanges(&chmsgthread)
|
||||
tcompare(t, chmsgthread.ChangeThread, store.ChangeThread{MessageIDs: []int64{trashAlt.ID}, Muted: false, Collapsed: true})
|
||||
|
||||
// Expand thread root, including change for child.
|
||||
api.ThreadCollapse(ctx, []int64{trashAlt.ID}, false)
|
||||
var chmsgthread2 ChangeMsgThread
|
||||
getChanges(&chmsgthread, &chmsgthread2)
|
||||
tcompare(t, chmsgthread.ChangeThread, store.ChangeThread{MessageIDs: []int64{trashAlt.ID}, Muted: false, Collapsed: false})
|
||||
tcompare(t, chmsgthread2.ChangeThread, store.ChangeThread{MessageIDs: []int64{inboxAltReply.ID}, Muted: false, Collapsed: false})
|
||||
|
||||
// Mute thread, including child, also collapses.
|
||||
api.ThreadMute(ctx, []int64{trashAlt.ID}, true)
|
||||
getChanges(&chmsgthread, &chmsgthread2)
|
||||
tcompare(t, chmsgthread.ChangeThread, store.ChangeThread{MessageIDs: []int64{trashAlt.ID}, Muted: true, Collapsed: true})
|
||||
tcompare(t, chmsgthread2.ChangeThread, store.ChangeThread{MessageIDs: []int64{inboxAltReply.ID}, Muted: true, Collapsed: true})
|
||||
|
||||
// And unmute Mute thread, including child. Messages are not expanded.
|
||||
api.ThreadMute(ctx, []int64{trashAlt.ID}, false)
|
||||
getChanges(&chmsgthread, &chmsgthread2)
|
||||
tcompare(t, chmsgthread.ChangeThread, store.ChangeThread{MessageIDs: []int64{trashAlt.ID}, Muted: false, Collapsed: true})
|
||||
tcompare(t, chmsgthread2.ChangeThread, store.ChangeThread{MessageIDs: []int64{inboxAltReply.ID}, Muted: false, Collapsed: true})
|
||||
|
||||
// todo: check move operations and their changes, e.g. MailboxDelete, MailboxEmpty, MessageRemove.
|
||||
}
|
||||
|
||||
|
@ -758,7 +758,7 @@ func handle(apiHandler http.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
m.MsgPrefix = nil
|
||||
m.ParsedBuf = nil
|
||||
mi := MessageItem{m, pm.envelope, pm.attachments, pm.isSigned, pm.isEncrypted, pm.firstLine}
|
||||
mi := MessageItem{m, pm.envelope, pm.attachments, pm.isSigned, pm.isEncrypted, pm.firstLine, false}
|
||||
mijson, err := json.Marshal(mi)
|
||||
xcheckf(ctx, err, "marshal messageitem")
|
||||
|
||||
|
@ -34,20 +34,24 @@ iframe { border: 0; }
|
||||
.msgitemcell { padding: 2px 4px; }
|
||||
/* note: we assign widths to .msgitemflags, .msgitemfrom, .msgitemsubject, .msgitemage, and offsets through a stylesheet created in js */
|
||||
.msgitemage { text-align: right; }
|
||||
.msgitemfrom { position: relative; }
|
||||
.msgitemfromtext { white-space: nowrap; overflow: hidden; }
|
||||
.msgitemfromthreadbar { position: absolute; border-right: 2px solid #666; right: 0; top: 0; bottom: 0; /* top or bottom set with inline style for first & last */ }
|
||||
.msgitemsubjecttext { white-space: nowrap; overflow: hidden; }
|
||||
.msgitemsubjectsnippet { font-weight: normal; color: #666; }
|
||||
.msgitemmailbox { background-color: #999; color: white; border: 1px solid #777; padding: 0 .15em; margin-left: .15em; border-radius: .15em; font-weight: normal; font-size: .9em; white-space: nowrap; }
|
||||
.msgitemmailbox { background: #999; color: white; border: 1px solid #777; padding: 0 .15em; margin-left: .15em; border-radius: .15em; font-weight: normal; font-size: .9em; white-space: nowrap; }
|
||||
.msgitemmailbox.msgitemmailboxcollapsed { background: #eee; color: #333; }
|
||||
.msgitemidentity { background-color: #999; color: white; border: 1px solid #777; padding: 0 .15em; margin-left: .15em; border-radius: .15em; font-weight: normal; font-size: .9em; white-space: nowrap; }
|
||||
|
||||
.topbar, .mailboxesbar { background-color: #fdfdf1; }
|
||||
.msglist { background-color: #f5ffff; }
|
||||
table.search td { padding: .25em; }
|
||||
.keyword { background-color: gold; color: black; border: 1px solid #8c7600; padding: 0 .15em; border-radius: .15em; font-weight: normal; font-size: .9em; margin: 0 .15em; white-space: nowrap; }
|
||||
.keyword.keywordcollapsed { background-color: #ffeb7e; color: #333; }
|
||||
.mailbox { padding: .15em .25em; }
|
||||
.mailboxitem { cursor: pointer; border-radius: .15em; }
|
||||
.mailboxitem.dropping { background-color: gold !important; }
|
||||
.mailboxitem:hover { background-color: #eee; }
|
||||
.mailboxitem.dropping { background: gold !important; }
|
||||
.mailboxitem:hover { background: #eee; }
|
||||
.mailboxitem.active { background: linear-gradient(135deg, #ffc7ab 0%, #ffdeab 100%); }
|
||||
.mailboxhoveronly { visibility: hidden; }
|
||||
.mailboxitem:hover .mailboxhoveronly, .mailboxitem:focus .mailboxhoveronly { visibility: visible; }
|
||||
@ -59,6 +63,7 @@ table.search td { padding: .25em; }
|
||||
.msgitem.active { background: linear-gradient(135deg, #8bc8ff 0%, #8ee5ff 100%); }
|
||||
.msgitemsubject { position: relative; }
|
||||
.msgitemflag { margin-right: 1px; font-weight: normal; font-size: .9em; }
|
||||
.msgitemflag.msgitemflagcollapsed { color: #666; }
|
||||
.quoted1 { color: #03828f; }
|
||||
.quoted2 { color: #c7445c; }
|
||||
.quoted3 { color: #417c10; }
|
||||
|
1629
webmail/webmail.js
1629
webmail/webmail.js
File diff suppressed because it is too large
Load Diff
1828
webmail/webmail.ts
1828
webmail/webmail.ts
File diff suppressed because it is too large
Load Diff
@ -45,6 +45,7 @@ type Message struct {
|
||||
From, To, Cc, Bcc, Subject, MessageID string
|
||||
Headers [][2]string
|
||||
Date time.Time
|
||||
References string
|
||||
Part Part
|
||||
}
|
||||
|
||||
@ -84,6 +85,7 @@ func (m Message) Marshal(t *testing.T) []byte {
|
||||
header("Subject", m.Subject)
|
||||
header("Message-Id", m.MessageID)
|
||||
header("Date", m.Date.Format(message.RFC5322Z))
|
||||
header("References", m.References)
|
||||
for _, t := range m.Headers {
|
||||
header(t[0], t[1])
|
||||
}
|
||||
@ -181,10 +183,11 @@ var (
|
||||
Part: Part{Type: "text/html", Content: `<html>the body <img src="cid:img1@mox.example" /></html>`},
|
||||
}
|
||||
msgAlt = Message{
|
||||
From: "mjl <mjl@mox.example>",
|
||||
To: "mox <mox@other.example>",
|
||||
Subject: "test",
|
||||
Headers: [][2]string{{"In-Reply-To", "<previous@host.example>"}},
|
||||
From: "mjl <mjl@mox.example>",
|
||||
To: "mox <mox@other.example>",
|
||||
Subject: "test",
|
||||
MessageID: "<alt@localhost>",
|
||||
Headers: [][2]string{{"In-Reply-To", "<previous@host.example>"}},
|
||||
Part: Part{
|
||||
Type: "multipart/alternative",
|
||||
Parts: []Part{
|
||||
@ -193,6 +196,11 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
msgAltReply = Message{
|
||||
Subject: "Re: test",
|
||||
References: "<alt@localhost>",
|
||||
Part: Part{Type: "text/plain", Content: "reply to alt"},
|
||||
}
|
||||
msgAltRel = Message{
|
||||
From: "mjl <mjl+altrel@mox.example>",
|
||||
To: "mox <mox+altrel@other.example>",
|
||||
|
Reference in New Issue
Block a user