mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
webmail: for replies/forwards, add button "send and archive thread" next to the "send" button, and give it a control+shift+Enter shortcut
the regular send shortcut is control+Enter. the shift enables "archive thread". there is no configuration option, you'll always get the button, but only for reply/forward, not for new compose. we may do "send and move thread to thrash", but let's wait until people want it. for github issue #135 by mattfbacon
This commit is contained in:
parent
b54e903f01
commit
5229d01601
8 changed files with 188 additions and 125 deletions
|
@ -207,6 +207,7 @@ type SubmitMessage struct {
|
||||||
UserAgent string // User-Agent header added if not empty.
|
UserAgent string // User-Agent header added if not empty.
|
||||||
RequireTLS *bool // For "Require TLS" extension during delivery.
|
RequireTLS *bool // For "Require TLS" extension during delivery.
|
||||||
FutureRelease *time.Time // If set, time (in the future) when message should be delivered from queue.
|
FutureRelease *time.Time // If set, time (in the future) when message should be delivered from queue.
|
||||||
|
ArchiveThread bool // If set, thread is archived after sending message.
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForwardAttachments references attachments by a list of message.Part paths.
|
// ForwardAttachments references attachments by a list of message.Part paths.
|
||||||
|
@ -740,6 +741,28 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
err = acc.RetrainMessages(ctx, log, tx, []store.Message{rm}, false)
|
err = acc.RetrainMessages(ctx, log, tx, []store.Message{rm}, false)
|
||||||
xcheckf(ctx, err, "retraining messages after reply/forward")
|
xcheckf(ctx, err, "retraining messages after reply/forward")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move messages from this thread still in this mailbox to the designated Archive
|
||||||
|
// mailbox.
|
||||||
|
if m.ArchiveThread {
|
||||||
|
mbArchive, err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Archive", true).Get()
|
||||||
|
if err == bstore.ErrAbsent {
|
||||||
|
xcheckuserf(ctx, errors.New("not configured"), "looking up designated archive mailbox")
|
||||||
|
}
|
||||||
|
xcheckf(ctx, err, "looking up designated archive mailbox")
|
||||||
|
|
||||||
|
var msgIDs []int64
|
||||||
|
q := bstore.QueryTx[store.Message](tx)
|
||||||
|
q.FilterNonzero(store.Message{ThreadID: rm.ThreadID, MailboxID: rm.MailboxID})
|
||||||
|
q.FilterEqual("Expunged", false)
|
||||||
|
err = q.IDs(&msgIDs)
|
||||||
|
xcheckf(ctx, err, "listing messages in thread to archive")
|
||||||
|
if len(msgIDs) > 0 {
|
||||||
|
var nchanges []store.Change
|
||||||
|
modseq, nchanges = xops.MessageMoveMailbox(ctx, log, acc, tx, msgIDs, mbArchive, modseq)
|
||||||
|
changes = append(changes, nchanges...)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sentmb, err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Sent", true).Get()
|
sentmb, err := bstore.QueryTx[store.Mailbox](tx).FilterEqual("Sent", true).Get()
|
||||||
|
|
|
@ -1190,6 +1190,13 @@
|
||||||
"nullable",
|
"nullable",
|
||||||
"timestamp"
|
"timestamp"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ArchiveThread",
|
||||||
|
"Docs": "If set, thread is archived after sending message.",
|
||||||
|
"Typewords": [
|
||||||
|
"bool"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -145,6 +145,7 @@ export interface SubmitMessage {
|
||||||
UserAgent: string // User-Agent header added if not empty.
|
UserAgent: string // User-Agent header added if not empty.
|
||||||
RequireTLS?: boolean | null // For "Require TLS" extension during delivery.
|
RequireTLS?: boolean | null // For "Require TLS" extension during delivery.
|
||||||
FutureRelease?: Date | null // If set, time (in the future) when message should be delivered from queue.
|
FutureRelease?: Date | null // If set, time (in the future) when message should be delivered from queue.
|
||||||
|
ArchiveThread: boolean // If set, thread is archived after sending message.
|
||||||
}
|
}
|
||||||
|
|
||||||
// File is a new attachment (not from an existing message that is being
|
// File is a new attachment (not from an existing message that is being
|
||||||
|
@ -551,7 +552,7 @@ export const types: TypenameMap = {
|
||||||
"Address": {"Name":"Address","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Host","Docs":"","Typewords":["string"]}]},
|
"Address": {"Name":"Address","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Host","Docs":"","Typewords":["string"]}]},
|
||||||
"MessageAddress": {"Name":"MessageAddress","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
"MessageAddress": {"Name":"MessageAddress","Docs":"","Fields":[{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"User","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
||||||
"Domain": {"Name":"Domain","Docs":"","Fields":[{"Name":"ASCII","Docs":"","Typewords":["string"]},{"Name":"Unicode","Docs":"","Typewords":["string"]}]},
|
"Domain": {"Name":"Domain","Docs":"","Fields":[{"Name":"ASCII","Docs":"","Typewords":["string"]},{"Name":"Unicode","Docs":"","Typewords":["string"]}]},
|
||||||
"SubmitMessage": {"Name":"SubmitMessage","Docs":"","Fields":[{"Name":"From","Docs":"","Typewords":["string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Cc","Docs":"","Typewords":["[]","string"]},{"Name":"Bcc","Docs":"","Typewords":["[]","string"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"TextBody","Docs":"","Typewords":["string"]},{"Name":"Attachments","Docs":"","Typewords":["[]","File"]},{"Name":"ForwardAttachments","Docs":"","Typewords":["ForwardAttachments"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ResponseMessageID","Docs":"","Typewords":["int64"]},{"Name":"ReplyTo","Docs":"","Typewords":["string"]},{"Name":"UserAgent","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]},{"Name":"FutureRelease","Docs":"","Typewords":["nullable","timestamp"]}]},
|
"SubmitMessage": {"Name":"SubmitMessage","Docs":"","Fields":[{"Name":"From","Docs":"","Typewords":["string"]},{"Name":"To","Docs":"","Typewords":["[]","string"]},{"Name":"Cc","Docs":"","Typewords":["[]","string"]},{"Name":"Bcc","Docs":"","Typewords":["[]","string"]},{"Name":"Subject","Docs":"","Typewords":["string"]},{"Name":"TextBody","Docs":"","Typewords":["string"]},{"Name":"Attachments","Docs":"","Typewords":["[]","File"]},{"Name":"ForwardAttachments","Docs":"","Typewords":["ForwardAttachments"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ResponseMessageID","Docs":"","Typewords":["int64"]},{"Name":"ReplyTo","Docs":"","Typewords":["string"]},{"Name":"UserAgent","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]},{"Name":"FutureRelease","Docs":"","Typewords":["nullable","timestamp"]},{"Name":"ArchiveThread","Docs":"","Typewords":["bool"]}]},
|
||||||
"File": {"Name":"File","Docs":"","Fields":[{"Name":"Filename","Docs":"","Typewords":["string"]},{"Name":"DataURI","Docs":"","Typewords":["string"]}]},
|
"File": {"Name":"File","Docs":"","Fields":[{"Name":"Filename","Docs":"","Typewords":["string"]},{"Name":"DataURI","Docs":"","Typewords":["string"]}]},
|
||||||
"ForwardAttachments": {"Name":"ForwardAttachments","Docs":"","Fields":[{"Name":"MessageID","Docs":"","Typewords":["int64"]},{"Name":"Paths","Docs":"","Typewords":["[]","[]","int32"]}]},
|
"ForwardAttachments": {"Name":"ForwardAttachments","Docs":"","Fields":[{"Name":"MessageID","Docs":"","Typewords":["int64"]},{"Name":"Paths","Docs":"","Typewords":["[]","[]","int32"]}]},
|
||||||
"Mailbox": {"Name":"Mailbox","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"UIDValidity","Docs":"","Typewords":["uint32"]},{"Name":"UIDNext","Docs":"","Typewords":["UID"]},{"Name":"Archive","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Sent","Docs":"","Typewords":["bool"]},{"Name":"Trash","Docs":"","Typewords":["bool"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"HaveCounts","Docs":"","Typewords":["bool"]},{"Name":"Total","Docs":"","Typewords":["int64"]},{"Name":"Deleted","Docs":"","Typewords":["int64"]},{"Name":"Unread","Docs":"","Typewords":["int64"]},{"Name":"Unseen","Docs":"","Typewords":["int64"]},{"Name":"Size","Docs":"","Typewords":["int64"]}]},
|
"Mailbox": {"Name":"Mailbox","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"UIDValidity","Docs":"","Typewords":["uint32"]},{"Name":"UIDNext","Docs":"","Typewords":["UID"]},{"Name":"Archive","Docs":"","Typewords":["bool"]},{"Name":"Draft","Docs":"","Typewords":["bool"]},{"Name":"Junk","Docs":"","Typewords":["bool"]},{"Name":"Sent","Docs":"","Typewords":["bool"]},{"Name":"Trash","Docs":"","Typewords":["bool"]},{"Name":"Keywords","Docs":"","Typewords":["[]","string"]},{"Name":"HaveCounts","Docs":"","Typewords":["bool"]},{"Name":"Total","Docs":"","Typewords":["int64"]},{"Name":"Deleted","Docs":"","Typewords":["int64"]},{"Name":"Unread","Docs":"","Typewords":["int64"]},{"Name":"Unseen","Docs":"","Typewords":["int64"]},{"Name":"Size","Docs":"","Typewords":["int64"]}]},
|
||||||
|
|
|
@ -296,7 +296,7 @@ var api;
|
||||||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||||
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }] },
|
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "ArchiveThread", "Docs": "", "Typewords": ["bool"] }] },
|
||||||
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
||||||
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
||||||
|
|
|
@ -296,7 +296,7 @@ var api;
|
||||||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||||
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }] },
|
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "ArchiveThread", "Docs": "", "Typewords": ["bool"] }] },
|
||||||
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
||||||
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
||||||
|
|
|
@ -296,7 +296,7 @@ var api;
|
||||||
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
"Address": { "Name": "Address", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
"MessageAddress": { "Name": "MessageAddress", "Docs": "", "Fields": [{ "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "User", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||||
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
"Domain": { "Name": "Domain", "Docs": "", "Fields": [{ "Name": "ASCII", "Docs": "", "Typewords": ["string"] }, { "Name": "Unicode", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }] },
|
"SubmitMessage": { "Name": "SubmitMessage", "Docs": "", "Fields": [{ "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Cc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Bcc", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "Subject", "Docs": "", "Typewords": ["string"] }, { "Name": "TextBody", "Docs": "", "Typewords": ["string"] }, { "Name": "Attachments", "Docs": "", "Typewords": ["[]", "File"] }, { "Name": "ForwardAttachments", "Docs": "", "Typewords": ["ForwardAttachments"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ResponseMessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "ReplyTo", "Docs": "", "Typewords": ["string"] }, { "Name": "UserAgent", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureRelease", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "ArchiveThread", "Docs": "", "Typewords": ["bool"] }] },
|
||||||
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
"File": { "Name": "File", "Docs": "", "Fields": [{ "Name": "Filename", "Docs": "", "Typewords": ["string"] }, { "Name": "DataURI", "Docs": "", "Typewords": ["string"] }] },
|
||||||
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
"ForwardAttachments": { "Name": "ForwardAttachments", "Docs": "", "Fields": [{ "Name": "MessageID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Paths", "Docs": "", "Typewords": ["[]", "[]", "int32"] }] },
|
||||||
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
"Mailbox": { "Name": "Mailbox", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "UIDValidity", "Docs": "", "Typewords": ["uint32"] }, { "Name": "UIDNext", "Docs": "", "Typewords": ["UID"] }, { "Name": "Archive", "Docs": "", "Typewords": ["bool"] }, { "Name": "Draft", "Docs": "", "Typewords": ["bool"] }, { "Name": "Junk", "Docs": "", "Typewords": ["bool"] }, { "Name": "Sent", "Docs": "", "Typewords": ["bool"] }, { "Name": "Trash", "Docs": "", "Typewords": ["bool"] }, { "Name": "Keywords", "Docs": "", "Typewords": ["[]", "string"] }, { "Name": "HaveCounts", "Docs": "", "Typewords": ["bool"] }, { "Name": "Total", "Docs": "", "Typewords": ["int64"] }, { "Name": "Deleted", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unread", "Docs": "", "Typewords": ["int64"] }, { "Name": "Unseen", "Docs": "", "Typewords": ["int64"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }] },
|
||||||
|
@ -2178,6 +2178,7 @@ const cmdHelp = async () => {
|
||||||
['→', 'expand thread'],
|
['→', 'expand thread'],
|
||||||
].map(t => dom.tr(dom.td(t[0]), dom.td(t[1]))))), dom.div(style({ width: '40em' }), dom.table(dom.tr(dom.td(attr.colspan('2'), dom.h2('Compose', style({ margin: '0' })))), [
|
].map(t => dom.tr(dom.td(t[0]), dom.td(t[1]))))), dom.div(style({ width: '40em' }), dom.table(dom.tr(dom.td(attr.colspan('2'), dom.h2('Compose', style({ margin: '0' })))), [
|
||||||
['ctrl Enter', 'send message'],
|
['ctrl Enter', 'send message'],
|
||||||
|
['ctrl shift Enter', 'send message and archive thread'],
|
||||||
['ctrl w', 'cancel message'],
|
['ctrl w', 'cancel message'],
|
||||||
['ctrl O', 'add To'],
|
['ctrl O', 'add To'],
|
||||||
['ctrl C', 'add Cc'],
|
['ctrl C', 'add Cc'],
|
||||||
|
@ -2283,7 +2284,7 @@ const cmdTooltip = async () => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
let composeView = null;
|
let composeView = null;
|
||||||
const compose = (opts) => {
|
const compose = (opts, listMailboxes) => {
|
||||||
log('compose', opts);
|
log('compose', opts);
|
||||||
if (composeView) {
|
if (composeView) {
|
||||||
// todo: should allow multiple
|
// todo: should allow multiple
|
||||||
|
@ -2307,7 +2308,7 @@ const compose = (opts) => {
|
||||||
composeElem.remove();
|
composeElem.remove();
|
||||||
composeView = null;
|
composeView = null;
|
||||||
};
|
};
|
||||||
const submit = async () => {
|
const submit = async (archive) => {
|
||||||
const files = await new Promise((resolve, reject) => {
|
const files = await new Promise((resolve, reject) => {
|
||||||
const l = [];
|
const l = [];
|
||||||
if (attachments.files && attachments.files.length === 0) {
|
if (attachments.files && attachments.files.length === 0) {
|
||||||
|
@ -2348,12 +2349,16 @@ const compose = (opts) => {
|
||||||
ResponseMessageID: opts.responseMessageID || 0,
|
ResponseMessageID: opts.responseMessageID || 0,
|
||||||
RequireTLS: requiretls.value === '' ? null : requiretls.value === 'yes',
|
RequireTLS: requiretls.value === '' ? null : requiretls.value === 'yes',
|
||||||
FutureRelease: scheduleTime.value ? new Date(scheduleTime.value) : null,
|
FutureRelease: scheduleTime.value ? new Date(scheduleTime.value) : null,
|
||||||
|
ArchiveThread: archive,
|
||||||
};
|
};
|
||||||
await client.MessageSubmit(message);
|
await client.MessageSubmit(message);
|
||||||
cmdCancel();
|
cmdCancel();
|
||||||
};
|
};
|
||||||
const cmdSend = async () => {
|
const cmdSend = async () => {
|
||||||
await withStatus('Sending email', submit(), fieldset);
|
await withStatus('Sending email', submit(false), fieldset);
|
||||||
|
};
|
||||||
|
const cmdSendArchive = async () => {
|
||||||
|
await withStatus('Sending email and archive', submit(true), fieldset);
|
||||||
};
|
};
|
||||||
const cmdAddTo = async () => { newAddrView('', true, toViews, toBtn, toCell, toRow); };
|
const cmdAddTo = async () => { newAddrView('', true, toViews, toBtn, toCell, toRow); };
|
||||||
const cmdAddCc = async () => { newAddrView('', true, ccViews, ccBtn, ccCell, ccRow); };
|
const cmdAddCc = async () => { newAddrView('', true, ccViews, ccBtn, ccCell, ccRow); };
|
||||||
|
@ -2369,6 +2374,7 @@ const compose = (opts) => {
|
||||||
};
|
};
|
||||||
const shortcuts = {
|
const shortcuts = {
|
||||||
'ctrl Enter': cmdSend,
|
'ctrl Enter': cmdSend,
|
||||||
|
'ctrl shift Enter': cmdSendArchive,
|
||||||
'ctrl w': cmdCancel,
|
'ctrl w': cmdCancel,
|
||||||
'ctrl O': cmdAddTo,
|
'ctrl O': cmdAddTo,
|
||||||
'ctrl C': cmdAddCc,
|
'ctrl C': cmdAddCc,
|
||||||
|
@ -2659,7 +2665,7 @@ const compose = (opts) => {
|
||||||
scheduleTime.value = '';
|
scheduleTime.value = '';
|
||||||
}), dom.div(style({ marginTop: '1ex' }), scheduleTime = dom.input(attr.type('datetime-local'), function change() {
|
}), dom.div(style({ marginTop: '1ex' }), scheduleTime = dom.input(attr.type('datetime-local'), function change() {
|
||||||
scheduleTimeChanged();
|
scheduleTimeChanged();
|
||||||
}), ' in local timezone ' + (Intl.DateTimeFormat().resolvedOptions().timeZone || '') + ', ', scheduleWeekday = dom.span()))), dom.div(style({ margin: '3ex 0 1ex 0', display: 'block' }), dom.submitbutton('Send'))), async function submit(e) {
|
}), ' in local timezone ' + (Intl.DateTimeFormat().resolvedOptions().timeZone || '') + ', ', scheduleWeekday = dom.span()))), dom.div(style({ margin: '3ex 0 1ex 0', display: 'block' }), dom.submitbutton('Send'), ' ', opts.responseMessageID && listMailboxes().find(mb => mb.Archive) ? dom.clickbutton('Send and archive thread', clickCmd(cmdSendArchive, shortcuts)) : [])), async function submit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
shortcutCmd(cmdSend, shortcuts);
|
shortcutCmd(cmdSend, shortcuts);
|
||||||
}));
|
}));
|
||||||
|
@ -3174,7 +3180,7 @@ const newMsgView = (miv, msglistView, listMailboxes, possibleLabels, messageLoad
|
||||||
isList: m.IsMailingList,
|
isList: m.IsMailingList,
|
||||||
editOffset: editOffset,
|
editOffset: editOffset,
|
||||||
};
|
};
|
||||||
compose(opts);
|
compose(opts, listMailboxes);
|
||||||
};
|
};
|
||||||
const reply = async (all) => {
|
const reply = async (all) => {
|
||||||
const contains = (l, a) => !!l.find(e => equalAddress(e, a));
|
const contains = (l, a) => !!l.find(e => equalAddress(e, a));
|
||||||
|
@ -5925,7 +5931,7 @@ const init = async () => {
|
||||||
if (sig) {
|
if (sig) {
|
||||||
body += '\n\n' + sig;
|
body += '\n\n' + sig;
|
||||||
}
|
}
|
||||||
compose({ body: body, editOffset: 0 });
|
compose({ body: body, editOffset: 0 }, listMailboxes);
|
||||||
};
|
};
|
||||||
const cmdOpenInbox = async () => {
|
const cmdOpenInbox = async () => {
|
||||||
const mb = mailboxlistView.findMailboxByName('Inbox');
|
const mb = mailboxlistView.findMailboxByName('Inbox');
|
||||||
|
@ -6054,6 +6060,11 @@ const init = async () => {
|
||||||
if (e.metaKey) {
|
if (e.metaKey) {
|
||||||
l.push('meta');
|
l.push('meta');
|
||||||
}
|
}
|
||||||
|
// Assume regular keys generate a 1 character e.key, and others are special for
|
||||||
|
// which we may want to treat shift specially too.
|
||||||
|
if (e.key.length > 1 && e.shiftKey) {
|
||||||
|
l.push('shift');
|
||||||
|
}
|
||||||
l.push(e.key);
|
l.push(e.key);
|
||||||
const k = l.join(' ');
|
const k = l.join(' ');
|
||||||
if (attachmentView) {
|
if (attachmentView) {
|
||||||
|
@ -6199,7 +6210,7 @@ const init = async () => {
|
||||||
if (opts.subject && opts.subject.includes('=?')) {
|
if (opts.subject && opts.subject.includes('=?')) {
|
||||||
opts.subject = await withStatus('Decoding MIME words for subject', client.DecodeMIMEWords(opts.subject));
|
opts.subject = await withStatus('Decoding MIME words for subject', client.DecodeMIMEWords(opts.subject));
|
||||||
}
|
}
|
||||||
compose(opts);
|
compose(opts, listMailboxes);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
@ -6433,7 +6444,7 @@ const init = async () => {
|
||||||
if (openComposeOptions.subject && openComposeOptions.subject.includes('=?')) {
|
if (openComposeOptions.subject && openComposeOptions.subject.includes('=?')) {
|
||||||
openComposeOptions.subject = await withStatus('Decoding MIME words for subject', client.DecodeMIMEWords(openComposeOptions.subject));
|
openComposeOptions.subject = await withStatus('Decoding MIME words for subject', client.DecodeMIMEWords(openComposeOptions.subject));
|
||||||
}
|
}
|
||||||
compose(openComposeOptions);
|
compose(openComposeOptions, listMailboxes);
|
||||||
openComposeOptions = undefined;
|
openComposeOptions = undefined;
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1191,6 +1191,7 @@ const cmdHelp = async () => {
|
||||||
dom.tr(dom.td(attr.colspan('2'), dom.h2('Compose', style({margin: '0'})))),
|
dom.tr(dom.td(attr.colspan('2'), dom.h2('Compose', style({margin: '0'})))),
|
||||||
[
|
[
|
||||||
['ctrl Enter', 'send message'],
|
['ctrl Enter', 'send message'],
|
||||||
|
['ctrl shift Enter', 'send message and archive thread'],
|
||||||
['ctrl w', 'cancel message'],
|
['ctrl w', 'cancel message'],
|
||||||
['ctrl O', 'add To'],
|
['ctrl O', 'add To'],
|
||||||
['ctrl C', 'add Cc'],
|
['ctrl C', 'add Cc'],
|
||||||
|
@ -1359,7 +1360,7 @@ interface ComposeView {
|
||||||
|
|
||||||
let composeView: ComposeView | null = null
|
let composeView: ComposeView | null = null
|
||||||
|
|
||||||
const compose = (opts: ComposeOptions) => {
|
const compose = (opts: ComposeOptions, listMailboxes: listMailboxes) => {
|
||||||
log('compose', opts)
|
log('compose', opts)
|
||||||
|
|
||||||
if (composeView) {
|
if (composeView) {
|
||||||
|
@ -1401,7 +1402,7 @@ const compose = (opts: ComposeOptions) => {
|
||||||
composeView = null
|
composeView = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async (archive: boolean) => {
|
||||||
const files = await new Promise<api.File[]>((resolve, reject) => {
|
const files = await new Promise<api.File[]>((resolve, reject) => {
|
||||||
const l: api.File[] = []
|
const l: api.File[] = []
|
||||||
if (attachments.files && attachments.files.length === 0) {
|
if (attachments.files && attachments.files.length === 0) {
|
||||||
|
@ -1445,13 +1446,17 @@ const compose = (opts: ComposeOptions) => {
|
||||||
ResponseMessageID: opts.responseMessageID || 0,
|
ResponseMessageID: opts.responseMessageID || 0,
|
||||||
RequireTLS: requiretls.value === '' ? null : requiretls.value === 'yes',
|
RequireTLS: requiretls.value === '' ? null : requiretls.value === 'yes',
|
||||||
FutureRelease: scheduleTime.value ? new Date(scheduleTime.value) : null,
|
FutureRelease: scheduleTime.value ? new Date(scheduleTime.value) : null,
|
||||||
|
ArchiveThread: archive,
|
||||||
}
|
}
|
||||||
await client.MessageSubmit(message)
|
await client.MessageSubmit(message)
|
||||||
cmdCancel()
|
cmdCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmdSend = async () => {
|
const cmdSend = async () => {
|
||||||
await withStatus('Sending email', submit(), fieldset)
|
await withStatus('Sending email', submit(false), fieldset)
|
||||||
|
}
|
||||||
|
const cmdSendArchive = async () => {
|
||||||
|
await withStatus('Sending email and archive', submit(true), fieldset)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmdAddTo = async () => { newAddrView('', true, toViews, toBtn, toCell, toRow) }
|
const cmdAddTo = async () => { newAddrView('', true, toViews, toBtn, toCell, toRow) }
|
||||||
|
@ -1469,6 +1474,7 @@ const compose = (opts: ComposeOptions) => {
|
||||||
|
|
||||||
const shortcuts: {[key: string]: command} = {
|
const shortcuts: {[key: string]: command} = {
|
||||||
'ctrl Enter': cmdSend,
|
'ctrl Enter': cmdSend,
|
||||||
|
'ctrl shift Enter': cmdSendArchive,
|
||||||
'ctrl w': cmdCancel,
|
'ctrl w': cmdCancel,
|
||||||
'ctrl O': cmdAddTo,
|
'ctrl O': cmdAddTo,
|
||||||
'ctrl C': cmdAddCc,
|
'ctrl C': cmdAddCc,
|
||||||
|
@ -1896,6 +1902,8 @@ const compose = (opts: ComposeOptions) => {
|
||||||
dom.div(
|
dom.div(
|
||||||
style({margin: '3ex 0 1ex 0', display: 'block'}),
|
style({margin: '3ex 0 1ex 0', display: 'block'}),
|
||||||
dom.submitbutton('Send'),
|
dom.submitbutton('Send'),
|
||||||
|
' ',
|
||||||
|
opts.responseMessageID && listMailboxes().find(mb => mb.Archive) ? dom.clickbutton('Send and archive thread', clickCmd(cmdSendArchive, shortcuts)) : [],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
async function submit(e: SubmitEvent) {
|
async function submit(e: SubmitEvent) {
|
||||||
|
@ -2583,7 +2591,7 @@ const newMsgView = (miv: MsgitemView, msglistView: MsglistView, listMailboxes: l
|
||||||
isList: m.IsMailingList,
|
isList: m.IsMailingList,
|
||||||
editOffset: editOffset,
|
editOffset: editOffset,
|
||||||
}
|
}
|
||||||
compose(opts)
|
compose(opts, listMailboxes)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = async (all: boolean) => {
|
const reply = async (all: boolean) => {
|
||||||
|
@ -6123,7 +6131,7 @@ const init = async () => {
|
||||||
if (sig) {
|
if (sig) {
|
||||||
body += '\n\n' + sig
|
body += '\n\n' + sig
|
||||||
}
|
}
|
||||||
compose({body: body, editOffset: 0})
|
compose({body: body, editOffset: 0}, listMailboxes)
|
||||||
}
|
}
|
||||||
const cmdOpenInbox = async () => {
|
const cmdOpenInbox = async () => {
|
||||||
const mb = mailboxlistView.findMailboxByName('Inbox')
|
const mb = mailboxlistView.findMailboxByName('Inbox')
|
||||||
|
@ -6339,6 +6347,11 @@ const init = async () => {
|
||||||
if (e.metaKey) {
|
if (e.metaKey) {
|
||||||
l.push('meta')
|
l.push('meta')
|
||||||
}
|
}
|
||||||
|
// Assume regular keys generate a 1 character e.key, and others are special for
|
||||||
|
// which we may want to treat shift specially too.
|
||||||
|
if (e.key.length > 1 && e.shiftKey) {
|
||||||
|
l.push('shift')
|
||||||
|
}
|
||||||
l.push(e.key)
|
l.push(e.key)
|
||||||
const k = l.join(' ')
|
const k = l.join(' ')
|
||||||
|
|
||||||
|
@ -6523,7 +6536,7 @@ const init = async () => {
|
||||||
if (opts.subject && opts.subject.includes('=?')) {
|
if (opts.subject && opts.subject.includes('=?')) {
|
||||||
opts.subject = await withStatus('Decoding MIME words for subject', client.DecodeMIMEWords(opts.subject))
|
opts.subject = await withStatus('Decoding MIME words for subject', client.DecodeMIMEWords(opts.subject))
|
||||||
}
|
}
|
||||||
compose(opts)
|
compose(opts, listMailboxes)
|
||||||
})()
|
})()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.alert('Error parsing compose mailto URL: '+errmsg(err))
|
window.alert('Error parsing compose mailto URL: '+errmsg(err))
|
||||||
|
@ -6782,7 +6795,7 @@ const init = async () => {
|
||||||
if (openComposeOptions.subject && openComposeOptions.subject.includes('=?')) {
|
if (openComposeOptions.subject && openComposeOptions.subject.includes('=?')) {
|
||||||
openComposeOptions.subject = await withStatus('Decoding MIME words for subject', client.DecodeMIMEWords(openComposeOptions.subject))
|
openComposeOptions.subject = await withStatus('Decoding MIME words for subject', client.DecodeMIMEWords(openComposeOptions.subject))
|
||||||
}
|
}
|
||||||
compose(openComposeOptions)
|
compose(openComposeOptions, listMailboxes)
|
||||||
openComposeOptions = undefined
|
openComposeOptions = undefined
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
220
webops/xops.go
220
webops/xops.go
|
@ -282,15 +282,9 @@ func (x XOps) MessageFlagsClear(ctx context.Context, log mlog.Log, acc *store.Ac
|
||||||
// MessageMove moves messages to the mailbox represented by mailboxName, or to mailboxID if mailboxName is empty.
|
// MessageMove moves messages to the mailbox represented by mailboxName, or to mailboxID if mailboxName is empty.
|
||||||
func (x XOps) MessageMove(ctx context.Context, log mlog.Log, acc *store.Account, messageIDs []int64, mailboxName string, mailboxID int64) {
|
func (x XOps) MessageMove(ctx context.Context, log mlog.Log, acc *store.Account, messageIDs []int64, mailboxName string, mailboxID int64) {
|
||||||
acc.WithRLock(func() {
|
acc.WithRLock(func() {
|
||||||
retrain := make([]store.Message, 0, len(messageIDs))
|
var changes []store.Change
|
||||||
removeChanges := map[int64]store.ChangeRemoveUIDs{}
|
|
||||||
// n adds, 1 remove, 2 mailboxcounts, optimistic and at least for a single message.
|
|
||||||
changes := make([]store.Change, 0, len(messageIDs)+3)
|
|
||||||
|
|
||||||
x.DBWrite(ctx, acc, func(tx *bstore.Tx) {
|
x.DBWrite(ctx, acc, func(tx *bstore.Tx) {
|
||||||
var mbSrc store.Mailbox
|
|
||||||
var modseq store.ModSeq
|
|
||||||
|
|
||||||
if mailboxName != "" {
|
if mailboxName != "" {
|
||||||
mb, err := acc.MailboxFind(tx, mailboxName)
|
mb, err := acc.MailboxFind(tx, mailboxName)
|
||||||
x.Checkf(ctx, err, "looking up mailbox name")
|
x.Checkf(ctx, err, "looking up mailbox name")
|
||||||
|
@ -307,111 +301,125 @@ func (x XOps) MessageMove(ctx context.Context, log mlog.Log, acc *store.Account,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
keywords := map[string]struct{}{}
|
_, changes = x.MessageMoveMailbox(ctx, log, acc, tx, messageIDs, mbDst, 0)
|
||||||
|
|
||||||
for _, mid := range messageIDs {
|
|
||||||
m := x.messageID(ctx, tx, mid)
|
|
||||||
|
|
||||||
// We may have loaded this mailbox in the previous iteration of this loop.
|
|
||||||
if m.MailboxID != mbSrc.ID {
|
|
||||||
if mbSrc.ID != 0 {
|
|
||||||
err := tx.Update(&mbSrc)
|
|
||||||
x.Checkf(ctx, err, "updating source mailbox counts")
|
|
||||||
changes = append(changes, mbSrc.ChangeCounts())
|
|
||||||
}
|
|
||||||
mbSrc = x.mailboxID(ctx, tx, m.MailboxID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mbSrc.ID == mailboxID {
|
|
||||||
// Client should filter out messages that are already in mailbox.
|
|
||||||
x.Checkuserf(ctx, errors.New("already in destination mailbox"), "moving message")
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if modseq == 0 {
|
|
||||||
modseq, err = acc.NextModSeq(tx)
|
|
||||||
x.Checkf(ctx, err, "assigning next modseq")
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := removeChanges[m.MailboxID]
|
|
||||||
ch.UIDs = append(ch.UIDs, m.UID)
|
|
||||||
ch.ModSeq = modseq
|
|
||||||
ch.MailboxID = m.MailboxID
|
|
||||||
removeChanges[m.MailboxID] = ch
|
|
||||||
|
|
||||||
// Copy of message record that we'll insert when UID is freed up.
|
|
||||||
om := m
|
|
||||||
om.PrepareExpunge()
|
|
||||||
om.ID = 0 // Assign new ID.
|
|
||||||
om.ModSeq = modseq
|
|
||||||
|
|
||||||
mbSrc.Sub(m.MailboxCounts())
|
|
||||||
|
|
||||||
if mbDst.Trash {
|
|
||||||
m.Seen = true
|
|
||||||
}
|
|
||||||
conf, _ := acc.Conf()
|
|
||||||
m.MailboxID = mbDst.ID
|
|
||||||
if m.IsReject && m.MailboxDestinedID != 0 {
|
|
||||||
// Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
|
|
||||||
// is used for reputation calculation during future deliveries.
|
|
||||||
m.MailboxOrigID = m.MailboxDestinedID
|
|
||||||
m.IsReject = false
|
|
||||||
m.Seen = false
|
|
||||||
}
|
|
||||||
m.UID = mbDst.UIDNext
|
|
||||||
m.ModSeq = modseq
|
|
||||||
mbDst.UIDNext++
|
|
||||||
m.JunkFlagsForMailbox(mbDst, conf)
|
|
||||||
err = tx.Update(&m)
|
|
||||||
x.Checkf(ctx, err, "updating moved message in database")
|
|
||||||
|
|
||||||
// Now that UID is unused, we can insert the old record again.
|
|
||||||
err = tx.Insert(&om)
|
|
||||||
x.Checkf(ctx, err, "inserting record for expunge after moving message")
|
|
||||||
|
|
||||||
mbDst.Add(m.MailboxCounts())
|
|
||||||
|
|
||||||
changes = append(changes, m.ChangeAddUID())
|
|
||||||
retrain = append(retrain, m)
|
|
||||||
|
|
||||||
for _, kw := range m.Keywords {
|
|
||||||
keywords[kw] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tx.Update(&mbSrc)
|
|
||||||
x.Checkf(ctx, err, "updating source mailbox counts")
|
|
||||||
|
|
||||||
changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts())
|
|
||||||
|
|
||||||
// Ensure destination mailbox has keywords of the moved messages.
|
|
||||||
var mbKwChanged bool
|
|
||||||
mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(keywords))
|
|
||||||
if mbKwChanged {
|
|
||||||
changes = append(changes, mbDst.ChangeKeywords())
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Update(&mbDst)
|
|
||||||
x.Checkf(ctx, err, "updating mailbox with uidnext")
|
|
||||||
|
|
||||||
err = acc.RetrainMessages(ctx, log, tx, retrain, false)
|
|
||||||
x.Checkf(ctx, err, "retraining messages after move")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Ensure UIDs of the removed message are in increasing order. It is quite common
|
|
||||||
// for all messages to be from a single source mailbox, meaning this is just one
|
|
||||||
// change, for which we preallocated space.
|
|
||||||
for _, ch := range removeChanges {
|
|
||||||
sort.Slice(ch.UIDs, func(i, j int) bool {
|
|
||||||
return ch.UIDs[i] < ch.UIDs[j]
|
|
||||||
})
|
|
||||||
changes = append(changes, ch)
|
|
||||||
}
|
|
||||||
store.BroadcastChanges(acc, changes)
|
store.BroadcastChanges(acc, changes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x XOps) MessageMoveMailbox(ctx context.Context, log mlog.Log, acc *store.Account, tx *bstore.Tx, messageIDs []int64, mbDst store.Mailbox, modseq store.ModSeq) (store.ModSeq, []store.Change) {
|
||||||
|
retrain := make([]store.Message, 0, len(messageIDs))
|
||||||
|
removeChanges := map[int64]store.ChangeRemoveUIDs{}
|
||||||
|
// n adds, 1 remove, 2 mailboxcounts, optimistic and at least for a single message.
|
||||||
|
changes := make([]store.Change, 0, len(messageIDs)+3)
|
||||||
|
|
||||||
|
var mbSrc store.Mailbox
|
||||||
|
|
||||||
|
keywords := map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, mid := range messageIDs {
|
||||||
|
m := x.messageID(ctx, tx, mid)
|
||||||
|
|
||||||
|
// We may have loaded this mailbox in the previous iteration of this loop.
|
||||||
|
if m.MailboxID != mbSrc.ID {
|
||||||
|
if mbSrc.ID != 0 {
|
||||||
|
err := tx.Update(&mbSrc)
|
||||||
|
x.Checkf(ctx, err, "updating source mailbox counts")
|
||||||
|
changes = append(changes, mbSrc.ChangeCounts())
|
||||||
|
}
|
||||||
|
mbSrc = x.mailboxID(ctx, tx, m.MailboxID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mbSrc.ID == mbDst.ID {
|
||||||
|
// Client should filter out messages that are already in mailbox.
|
||||||
|
x.Checkuserf(ctx, errors.New("already in destination mailbox"), "moving message")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if modseq == 0 {
|
||||||
|
modseq, err = acc.NextModSeq(tx)
|
||||||
|
x.Checkf(ctx, err, "assigning next modseq")
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := removeChanges[m.MailboxID]
|
||||||
|
ch.UIDs = append(ch.UIDs, m.UID)
|
||||||
|
ch.ModSeq = modseq
|
||||||
|
ch.MailboxID = m.MailboxID
|
||||||
|
removeChanges[m.MailboxID] = ch
|
||||||
|
|
||||||
|
// Copy of message record that we'll insert when UID is freed up.
|
||||||
|
om := m
|
||||||
|
om.PrepareExpunge()
|
||||||
|
om.ID = 0 // Assign new ID.
|
||||||
|
om.ModSeq = modseq
|
||||||
|
|
||||||
|
mbSrc.Sub(m.MailboxCounts())
|
||||||
|
|
||||||
|
if mbDst.Trash {
|
||||||
|
m.Seen = true
|
||||||
|
}
|
||||||
|
conf, _ := acc.Conf()
|
||||||
|
m.MailboxID = mbDst.ID
|
||||||
|
if m.IsReject && m.MailboxDestinedID != 0 {
|
||||||
|
// Incorrectly delivered to Rejects mailbox. Adjust MailboxOrigID so this message
|
||||||
|
// is used for reputation calculation during future deliveries.
|
||||||
|
m.MailboxOrigID = m.MailboxDestinedID
|
||||||
|
m.IsReject = false
|
||||||
|
m.Seen = false
|
||||||
|
}
|
||||||
|
m.UID = mbDst.UIDNext
|
||||||
|
m.ModSeq = modseq
|
||||||
|
mbDst.UIDNext++
|
||||||
|
m.JunkFlagsForMailbox(mbDst, conf)
|
||||||
|
err = tx.Update(&m)
|
||||||
|
x.Checkf(ctx, err, "updating moved message in database")
|
||||||
|
|
||||||
|
// Now that UID is unused, we can insert the old record again.
|
||||||
|
err = tx.Insert(&om)
|
||||||
|
x.Checkf(ctx, err, "inserting record for expunge after moving message")
|
||||||
|
|
||||||
|
mbDst.Add(m.MailboxCounts())
|
||||||
|
|
||||||
|
changes = append(changes, m.ChangeAddUID())
|
||||||
|
retrain = append(retrain, m)
|
||||||
|
|
||||||
|
for _, kw := range m.Keywords {
|
||||||
|
keywords[kw] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tx.Update(&mbSrc)
|
||||||
|
x.Checkf(ctx, err, "updating source mailbox counts")
|
||||||
|
|
||||||
|
changes = append(changes, mbSrc.ChangeCounts(), mbDst.ChangeCounts())
|
||||||
|
|
||||||
|
// Ensure destination mailbox has keywords of the moved messages.
|
||||||
|
var mbKwChanged bool
|
||||||
|
mbDst.Keywords, mbKwChanged = store.MergeKeywords(mbDst.Keywords, maps.Keys(keywords))
|
||||||
|
if mbKwChanged {
|
||||||
|
changes = append(changes, mbDst.ChangeKeywords())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Update(&mbDst)
|
||||||
|
x.Checkf(ctx, err, "updating mailbox with uidnext")
|
||||||
|
|
||||||
|
err = acc.RetrainMessages(ctx, log, tx, retrain, false)
|
||||||
|
x.Checkf(ctx, err, "retraining messages after move")
|
||||||
|
|
||||||
|
// Ensure UIDs of the removed message are in increasing order. It is quite common
|
||||||
|
// for all messages to be from a single source mailbox, meaning this is just one
|
||||||
|
// change, for which we preallocated space.
|
||||||
|
for _, ch := range removeChanges {
|
||||||
|
sort.Slice(ch.UIDs, func(i, j int) bool {
|
||||||
|
return ch.UIDs[i] < ch.UIDs[j]
|
||||||
|
})
|
||||||
|
changes = append(changes, ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
return modseq, changes
|
||||||
|
}
|
||||||
|
|
||||||
func isText(p message.Part) bool {
|
func isText(p message.Part) bool {
|
||||||
return p.MediaType == "" && p.MediaSubType == "" || p.MediaType == "TEXT" && p.MediaSubType == "PLAIN"
|
return p.MediaType == "" && p.MediaSubType == "" || p.MediaType == "TEXT" && p.MediaSubType == "PLAIN"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue