mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 17:44:35 +03:00
webmail: when moving a single message out of/to the inbox, ask if user wants to create a rule to automatically do that server-side for future deliveries
if the message has a list-id header, we assume this is a (mailing) list message, and we require a dkim/spf-verified domain (we prefer the shortest that is a suffix of the list-id value). the rule we would add will mark such messages as from a mailing list, changing filtering rules on incoming messages (not enforcing dmarc policies). messages will be matched on list-id header and will only match if they have the same dkim/spf-verified domain. if the message doesn't have a list-id header, we'll ask to match based on "message from" address. we don't ask the user in several cases: - if the destination/source mailbox is a special-use mailbox (e.g. trash,archive,sent,junk; inbox isn't included) - if the rule already exist (no point in adding it again). - if the user said "no, not for this list-id/from-address" in the past. - if the user said "no, not for messages moved to this mailbox" in the past. we'll add the rule if the message was moved out of the inbox. if the message was moved to the inbox, we check if there is a matching rule that we can remove. we now remember the "no" answers (for list-id, msg-from-addr and mailbox) in the account database. to implement the msgfrom rules, this adds support to rulesets for matching on message "from" address. before, we could match on smtp from address (and other fields). rulesets now also have a field for comments. webmail adds a note that it created the rule, with the date. manual editing of the rulesets is still in the webaccount page. this webmail functionality is just a convenient way to add/remove common rules.
This commit is contained in:
264
webmail/api.go
264
webmail/api.go
@ -16,8 +16,10 @@ import (
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -31,10 +33,12 @@ import (
|
||||
"github.com/mjl-/sherpadoc"
|
||||
"github.com/mjl-/sherpaprom"
|
||||
|
||||
"github.com/mjl-/mox/config"
|
||||
"github.com/mjl-/mox/dkim"
|
||||
"github.com/mjl-/mox/dns"
|
||||
"github.com/mjl-/mox/message"
|
||||
"github.com/mjl-/mox/metrics"
|
||||
"github.com/mjl-/mox/mlog"
|
||||
"github.com/mjl-/mox/mox-"
|
||||
"github.com/mjl-/mox/moxio"
|
||||
"github.com/mjl-/mox/moxvar"
|
||||
@ -1904,6 +1908,254 @@ func (Webmail) SettingsSave(ctx context.Context, settings store.Settings) {
|
||||
xcheckf(ctx, err, "save settings")
|
||||
}
|
||||
|
||||
func (Webmail) RulesetSuggestMove(ctx context.Context, msgID, mbSrcID, mbDstID int64) (listID string, msgFrom string, isRemove bool, rcptTo string, ruleset *config.Ruleset) {
|
||||
withAccount(ctx, func(log mlog.Log, acc *store.Account) {
|
||||
xdbread(ctx, acc, func(tx *bstore.Tx) {
|
||||
m := xmessageID(ctx, tx, msgID)
|
||||
mbSrc := xmailboxID(ctx, tx, mbSrcID)
|
||||
mbDst := xmailboxID(ctx, tx, mbDstID)
|
||||
|
||||
if m.RcptToLocalpart == "" && m.RcptToDomain == "" {
|
||||
return
|
||||
}
|
||||
rcptTo = m.RcptToLocalpart.String() + "@" + m.RcptToDomain
|
||||
|
||||
conf, _ := acc.Conf()
|
||||
dest := conf.Destinations[rcptTo] // May not be present.
|
||||
defaultMailbox := "Inbox"
|
||||
if dest.Mailbox != "" {
|
||||
defaultMailbox = dest.Mailbox
|
||||
}
|
||||
|
||||
// Only suggest rules for messages moved into/out of the default mailbox (Inbox).
|
||||
if mbSrc.Name != defaultMailbox && mbDst.Name != defaultMailbox {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have a previous answer "No" answer for moving from/to mailbox.
|
||||
exists, err := bstore.QueryTx[store.RulesetNoMailbox](tx).FilterNonzero(store.RulesetNoMailbox{MailboxID: mbSrcID}).FilterEqual("ToMailbox", false).Exists()
|
||||
xcheckf(ctx, err, "looking up previous response for source mailbox")
|
||||
if exists {
|
||||
return
|
||||
}
|
||||
exists, err = bstore.QueryTx[store.RulesetNoMailbox](tx).FilterNonzero(store.RulesetNoMailbox{MailboxID: mbDstID}).FilterEqual("ToMailbox", true).Exists()
|
||||
xcheckf(ctx, err, "looking up previous response for destination mailbox")
|
||||
if exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse message for List-Id header.
|
||||
state := msgState{acc: acc}
|
||||
defer state.clear()
|
||||
pm, err := parsedMessage(log, m, &state, true, false)
|
||||
xcheckf(ctx, err, "parsing message")
|
||||
|
||||
// The suggested ruleset. Once all is checked, we'll return it.
|
||||
var nrs *config.Ruleset
|
||||
|
||||
// If List-Id header is present, we'll treat it as a (mailing) list message.
|
||||
if l, ok := pm.Headers["List-Id"]; ok {
|
||||
if len(l) != 1 {
|
||||
log.Debug("not exactly one list-id header", slog.Any("listid", l))
|
||||
return
|
||||
}
|
||||
var listIDDom dns.Domain
|
||||
listID, listIDDom = parseListID(l[0])
|
||||
if listID == "" {
|
||||
log.Debug("invalid list-id header", slog.String("listid", l[0]))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have a previous "No" answer for this list-id.
|
||||
no := store.RulesetNoListID{
|
||||
RcptToAddress: rcptTo,
|
||||
ListID: listID,
|
||||
ToInbox: mbDst.Name == "Inbox",
|
||||
}
|
||||
exists, err = bstore.QueryTx[store.RulesetNoListID](tx).FilterNonzero(no).Exists()
|
||||
xcheckf(ctx, err, "looking up previous response for list-id")
|
||||
if exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the "ListAllowDomain" to use. We only match and move messages with verified
|
||||
// SPF/DKIM. Otherwise spammers could add a list-id headers for mailing lists you
|
||||
// are subscribed to, and take advantage of any reduced junk filtering.
|
||||
listIDDomStr := listIDDom.Name()
|
||||
|
||||
doms := m.DKIMDomains
|
||||
if m.MailFromValidated {
|
||||
doms = append(doms, m.MailFromDomain)
|
||||
}
|
||||
// Sort, we prefer the shortest name, e.g. DKIM signature on whole domain instead
|
||||
// of SPF verification of one host.
|
||||
sort.Slice(doms, func(i, j int) bool {
|
||||
return len(doms[i]) < len(doms[j])
|
||||
})
|
||||
var listAllowDom string
|
||||
for _, dom := range doms {
|
||||
if dom == listIDDomStr || strings.HasSuffix(listIDDomStr, "."+dom) {
|
||||
listAllowDom = dom
|
||||
break
|
||||
}
|
||||
}
|
||||
if listAllowDom == "" {
|
||||
return
|
||||
}
|
||||
|
||||
listIDRegExp := regexp.QuoteMeta(fmt.Sprintf("<%s>", listID)) + "$"
|
||||
nrs = &config.Ruleset{
|
||||
HeadersRegexp: map[string]string{"^list-id$": listIDRegExp},
|
||||
ListAllowDomain: listAllowDom,
|
||||
Mailbox: mbDst.Name,
|
||||
}
|
||||
} else {
|
||||
// Otherwise, try to make a rule based on message "From" address.
|
||||
if m.MsgFromLocalpart == "" && m.MsgFromDomain == "" {
|
||||
return
|
||||
}
|
||||
msgFrom = m.MsgFromLocalpart.String() + "@" + m.MsgFromDomain
|
||||
|
||||
no := store.RulesetNoMsgFrom{
|
||||
RcptToAddress: rcptTo,
|
||||
MsgFromAddress: msgFrom,
|
||||
ToInbox: mbDst.Name == "Inbox",
|
||||
}
|
||||
exists, err = bstore.QueryTx[store.RulesetNoMsgFrom](tx).FilterNonzero(no).Exists()
|
||||
xcheckf(ctx, err, "looking up previous response for message from address")
|
||||
if exists {
|
||||
return
|
||||
}
|
||||
|
||||
nrs = &config.Ruleset{
|
||||
MsgFromRegexp: "^" + regexp.QuoteMeta(msgFrom) + "$",
|
||||
Mailbox: mbDst.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// Only suggest adding/removing rule if it isn't/is present.
|
||||
var have bool
|
||||
for _, rs := range dest.Rulesets {
|
||||
xrs := config.Ruleset{
|
||||
MsgFromRegexp: rs.MsgFromRegexp,
|
||||
HeadersRegexp: rs.HeadersRegexp,
|
||||
ListAllowDomain: rs.ListAllowDomain,
|
||||
Mailbox: nrs.Mailbox,
|
||||
}
|
||||
if xrs.Equal(*nrs) {
|
||||
have = true
|
||||
break
|
||||
}
|
||||
}
|
||||
isRemove = mbDst.Name == defaultMailbox
|
||||
if isRemove {
|
||||
nrs.Mailbox = mbSrc.Name
|
||||
}
|
||||
if isRemove && !have || !isRemove && have {
|
||||
return
|
||||
}
|
||||
|
||||
// We'll be returning a suggested ruleset.
|
||||
nrs.Comment = "by webmail on " + time.Now().Format("2006-01-02")
|
||||
ruleset = nrs
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the list-id value (the value between <>) from a list-id header.
|
||||
// Returns an empty string if it couldn't be parsed.
|
||||
func parseListID(s string) (listID string, dom dns.Domain) {
|
||||
// ../rfc/2919:198
|
||||
s = strings.TrimRight(s, " \t")
|
||||
if !strings.HasSuffix(s, ">") {
|
||||
return "", dns.Domain{}
|
||||
}
|
||||
s = s[:len(s)-1]
|
||||
t := strings.Split(s, "<")
|
||||
if len(t) == 1 {
|
||||
return "", dns.Domain{}
|
||||
}
|
||||
s = t[len(t)-1]
|
||||
dom, err := dns.ParseDomain(s)
|
||||
if err != nil {
|
||||
return "", dom
|
||||
}
|
||||
return s, dom
|
||||
}
|
||||
|
||||
func (Webmail) RulesetAdd(ctx context.Context, rcptTo string, ruleset config.Ruleset) {
|
||||
reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
|
||||
|
||||
err := mox.AccountSave(ctx, reqInfo.AccountName, func(acc *config.Account) {
|
||||
dest, ok := acc.Destinations[rcptTo]
|
||||
if !ok {
|
||||
// todo: we could find the catchall address and add the rule, or add the address explicitly.
|
||||
xcheckuserf(ctx, errors.New("destination address not found in account (hint: if this is a catchall address, configure the address explicitly to configure rulesets)"), "looking up address")
|
||||
}
|
||||
|
||||
nd := map[string]config.Destination{}
|
||||
for addr, d := range acc.Destinations {
|
||||
nd[addr] = d
|
||||
}
|
||||
dest.Rulesets = append(slices.Clone(dest.Rulesets), ruleset)
|
||||
nd[rcptTo] = dest
|
||||
acc.Destinations = nd
|
||||
})
|
||||
xcheckf(ctx, err, "saving account with new ruleset")
|
||||
}
|
||||
|
||||
func (Webmail) RulesetRemove(ctx context.Context, rcptTo string, ruleset config.Ruleset) {
|
||||
reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
|
||||
|
||||
err := mox.AccountSave(ctx, reqInfo.AccountName, func(acc *config.Account) {
|
||||
dest, ok := acc.Destinations[rcptTo]
|
||||
if !ok {
|
||||
xcheckuserf(ctx, errors.New("destination address not found in account"), "looking up address")
|
||||
}
|
||||
|
||||
nd := map[string]config.Destination{}
|
||||
for addr, d := range acc.Destinations {
|
||||
nd[addr] = d
|
||||
}
|
||||
var l []config.Ruleset
|
||||
skipped := 0
|
||||
for _, rs := range dest.Rulesets {
|
||||
if rs.Equal(ruleset) {
|
||||
skipped++
|
||||
} else {
|
||||
l = append(l, rs)
|
||||
}
|
||||
}
|
||||
if skipped != 1 {
|
||||
xcheckuserf(ctx, fmt.Errorf("affected %d configured rulesets, expected 1", skipped), "changing rulesets")
|
||||
}
|
||||
dest.Rulesets = l
|
||||
nd[rcptTo] = dest
|
||||
acc.Destinations = nd
|
||||
})
|
||||
xcheckf(ctx, err, "saving account with new ruleset")
|
||||
}
|
||||
|
||||
func (Webmail) RulesetMessageNever(ctx context.Context, rcptTo, listID, msgFrom string, toInbox bool) {
|
||||
withAccount(ctx, func(log mlog.Log, acc *store.Account) {
|
||||
var err error
|
||||
if listID != "" {
|
||||
err = acc.DB.Insert(ctx, &store.RulesetNoListID{RcptToAddress: rcptTo, ListID: listID, ToInbox: toInbox})
|
||||
} else {
|
||||
err = acc.DB.Insert(ctx, &store.RulesetNoMsgFrom{RcptToAddress: rcptTo, MsgFromAddress: msgFrom, ToInbox: toInbox})
|
||||
}
|
||||
xcheckf(ctx, err, "storing user response")
|
||||
})
|
||||
}
|
||||
|
||||
func (Webmail) RulesetMailboxNever(ctx context.Context, mailboxID int64, toMailbox bool) {
|
||||
withAccount(ctx, func(log mlog.Log, acc *store.Account) {
|
||||
err := acc.DB.Insert(ctx, &store.RulesetNoMailbox{MailboxID: mailboxID, ToMailbox: toMailbox})
|
||||
xcheckf(ctx, err, "storing user response")
|
||||
})
|
||||
}
|
||||
|
||||
func slicesAny[T any](l []T) []any {
|
||||
r := make([]any, len(l))
|
||||
for i, v := range l {
|
||||
@ -1912,6 +2164,18 @@ func slicesAny[T any](l []T) []any {
|
||||
return r
|
||||
}
|
||||
|
||||
func withAccount(ctx context.Context, fn func(log mlog.Log, acc *store.Account)) {
|
||||
log := pkglog.WithContext(ctx)
|
||||
reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
|
||||
acc, err := store.OpenAccount(log, reqInfo.AccountName)
|
||||
xcheckf(ctx, err, "open account")
|
||||
defer func() {
|
||||
err := acc.Close()
|
||||
log.Check(err, "closing account")
|
||||
}()
|
||||
fn(log, acc)
|
||||
}
|
||||
|
||||
// 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, msgThread ChangeMsgThread, mailboxRemove ChangeMailboxRemove, mailboxAdd ChangeMailboxAdd, mailboxRename ChangeMailboxRename, mailboxCounts ChangeMailboxCounts, mailboxSpecialUse ChangeMailboxSpecialUse, mailboxKeywords ChangeMailboxKeywords, flags store.Flags) {
|
||||
return
|
||||
|
229
webmail/api.json
229
webmail/api.json
@ -438,6 +438,151 @@
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "RulesetSuggestMove",
|
||||
"Docs": "",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "msgID",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "mbSrcID",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "mbDstID",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "listID",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "msgFrom",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "isRemove",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "rcptTo",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ruleset",
|
||||
"Typewords": [
|
||||
"nullable",
|
||||
"Ruleset"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "RulesetAdd",
|
||||
"Docs": "",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "rcptTo",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ruleset",
|
||||
"Typewords": [
|
||||
"Ruleset"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "RulesetRemove",
|
||||
"Docs": "",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "rcptTo",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ruleset",
|
||||
"Typewords": [
|
||||
"Ruleset"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "RulesetMessageNever",
|
||||
"Docs": "",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "rcptTo",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "listID",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "msgFrom",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "toInbox",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "RulesetMailboxNever",
|
||||
"Docs": "",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "mailboxID",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "toMailbox",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "SSETypes",
|
||||
"Docs": "SSETypes exists to ensure the generated API contains the types, for use in SSE events.",
|
||||
@ -1601,6 +1746,90 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Ruleset",
|
||||
"Docs": "",
|
||||
"Fields": [
|
||||
{
|
||||
"Name": "SMTPMailFromRegexp",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "MsgFromRegexp",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "VerifiedDomain",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "HeadersRegexp",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"{}",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "IsForward",
|
||||
"Docs": "todo: once we implement ARC, we can use dkim domains that we cannot verify but that the arc-verified forwarding mail server was able to verify.",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ListAllowDomain",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "AcceptRejectsToMailbox",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Mailbox",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Comment",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "VerifiedDNSDomain",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"Domain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ListAllowDNSDomain",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"Domain"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "EventStart",
|
||||
"Docs": "EventStart is the first message sent on an SSE connection, giving the client\nbasic data to populate its UI. After this event, messages will follow quickly in\nan EventViewMsgs event.",
|
||||
|
@ -220,6 +220,20 @@ export interface Settings {
|
||||
ShowAddressSecurity: boolean // Whether to show the bars underneath the address input fields indicating starttls/dnssec/dane/mtasts/requiretls support by address.
|
||||
}
|
||||
|
||||
export interface Ruleset {
|
||||
SMTPMailFromRegexp: string
|
||||
MsgFromRegexp: string
|
||||
VerifiedDomain: string
|
||||
HeadersRegexp?: { [key: string]: string }
|
||||
IsForward: boolean // todo: once we implement ARC, we can use dkim domains that we cannot verify but that the arc-verified forwarding mail server was able to verify.
|
||||
ListAllowDomain: string
|
||||
AcceptRejectsToMailbox: string
|
||||
Mailbox: string
|
||||
Comment: string
|
||||
VerifiedDNSDomain: Domain
|
||||
ListAllowDNSDomain: Domain
|
||||
}
|
||||
|
||||
// EventStart is the first message sent on an SSE connection, giving the client
|
||||
// basic data to populate its UI. After this event, messages will follow quickly in
|
||||
// an EventViewMsgs event.
|
||||
@ -567,7 +581,7 @@ export enum Quoting {
|
||||
// Localparts are in Unicode NFC.
|
||||
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,"ChangeMsgThread":true,"ComposeMessage":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,"FromAddressSettings":true,"Mailbox":true,"Message":true,"MessageAddress":true,"MessageEnvelope":true,"MessageItem":true,"NotFilter":true,"Page":true,"ParsedMessage":true,"Part":true,"Query":true,"RecipientSecurity":true,"Request":true,"Settings":true,"SpecialUse":true,"SubmitMessage":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,"ComposeMessage":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,"FromAddressSettings":true,"Mailbox":true,"Message":true,"MessageAddress":true,"MessageEnvelope":true,"MessageItem":true,"NotFilter":true,"Page":true,"ParsedMessage":true,"Part":true,"Query":true,"RecipientSecurity":true,"Request":true,"Ruleset":true,"Settings":true,"SpecialUse":true,"SubmitMessage":true}
|
||||
export const stringsTypes: {[typename: string]: boolean} = {"AttachmentType":true,"CSRFToken":true,"Localpart":true,"Quoting":true,"SecurityResult":true,"ThreadMode":true,"ViewMode":true}
|
||||
export const intsTypes: {[typename: string]: boolean} = {"ModSeq":true,"UID":true,"Validation":true}
|
||||
export const types: TypenameMap = {
|
||||
@ -590,6 +604,7 @@ export const types: TypenameMap = {
|
||||
"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"]}]},
|
||||
"RecipientSecurity": {"Name":"RecipientSecurity","Docs":"","Fields":[{"Name":"STARTTLS","Docs":"","Typewords":["SecurityResult"]},{"Name":"MTASTS","Docs":"","Typewords":["SecurityResult"]},{"Name":"DNSSEC","Docs":"","Typewords":["SecurityResult"]},{"Name":"DANE","Docs":"","Typewords":["SecurityResult"]},{"Name":"RequireTLS","Docs":"","Typewords":["SecurityResult"]}]},
|
||||
"Settings": {"Name":"Settings","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["uint8"]},{"Name":"Signature","Docs":"","Typewords":["string"]},{"Name":"Quoting","Docs":"","Typewords":["Quoting"]},{"Name":"ShowAddressSecurity","Docs":"","Typewords":["bool"]}]},
|
||||
"Ruleset": {"Name":"Ruleset","Docs":"","Fields":[{"Name":"SMTPMailFromRegexp","Docs":"","Typewords":["string"]},{"Name":"MsgFromRegexp","Docs":"","Typewords":["string"]},{"Name":"VerifiedDomain","Docs":"","Typewords":["string"]},{"Name":"HeadersRegexp","Docs":"","Typewords":["{}","string"]},{"Name":"IsForward","Docs":"","Typewords":["bool"]},{"Name":"ListAllowDomain","Docs":"","Typewords":["string"]},{"Name":"AcceptRejectsToMailbox","Docs":"","Typewords":["string"]},{"Name":"Mailbox","Docs":"","Typewords":["string"]},{"Name":"Comment","Docs":"","Typewords":["string"]},{"Name":"VerifiedDNSDomain","Docs":"","Typewords":["Domain"]},{"Name":"ListAllowDNSDomain","Docs":"","Typewords":["Domain"]}]},
|
||||
"EventStart": {"Name":"EventStart","Docs":"","Fields":[{"Name":"SSEID","Docs":"","Typewords":["int64"]},{"Name":"LoginAddress","Docs":"","Typewords":["MessageAddress"]},{"Name":"Addresses","Docs":"","Typewords":["[]","MessageAddress"]},{"Name":"DomainAddressConfigs","Docs":"","Typewords":["{}","DomainAddressConfig"]},{"Name":"MailboxName","Docs":"","Typewords":["string"]},{"Name":"Mailboxes","Docs":"","Typewords":["[]","Mailbox"]},{"Name":"RejectsMailbox","Docs":"","Typewords":["string"]},{"Name":"Settings","Docs":"","Typewords":["Settings"]},{"Name":"AccountPath","Docs":"","Typewords":["string"]},{"Name":"Version","Docs":"","Typewords":["string"]}]},
|
||||
"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"]}]},
|
||||
@ -644,6 +659,7 @@ export const parser = {
|
||||
Mailbox: (v: any) => parse("Mailbox", v) as Mailbox,
|
||||
RecipientSecurity: (v: any) => parse("RecipientSecurity", v) as RecipientSecurity,
|
||||
Settings: (v: any) => parse("Settings", v) as Settings,
|
||||
Ruleset: (v: any) => parse("Ruleset", v) as Ruleset,
|
||||
EventStart: (v: any) => parse("EventStart", v) as EventStart,
|
||||
DomainAddressConfig: (v: any) => parse("DomainAddressConfig", v) as DomainAddressConfig,
|
||||
EventViewErr: (v: any) => parse("EventViewErr", v) as EventViewErr,
|
||||
@ -959,6 +975,46 @@ export class Client {
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
|
||||
async RulesetSuggestMove(msgID: number, mbSrcID: number, mbDstID: number): Promise<[string, string, boolean, string, Ruleset | null]> {
|
||||
const fn: string = "RulesetSuggestMove"
|
||||
const paramTypes: string[][] = [["int64"],["int64"],["int64"]]
|
||||
const returnTypes: string[][] = [["string"],["string"],["bool"],["string"],["nullable","Ruleset"]]
|
||||
const params: any[] = [msgID, mbSrcID, mbDstID]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as [string, string, boolean, string, Ruleset | null]
|
||||
}
|
||||
|
||||
async RulesetAdd(rcptTo: string, ruleset: Ruleset): Promise<void> {
|
||||
const fn: string = "RulesetAdd"
|
||||
const paramTypes: string[][] = [["string"],["Ruleset"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [rcptTo, ruleset]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
|
||||
async RulesetRemove(rcptTo: string, ruleset: Ruleset): Promise<void> {
|
||||
const fn: string = "RulesetRemove"
|
||||
const paramTypes: string[][] = [["string"],["Ruleset"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [rcptTo, ruleset]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
|
||||
async RulesetMessageNever(rcptTo: string, listID: string, msgFrom: string, toInbox: boolean): Promise<void> {
|
||||
const fn: string = "RulesetMessageNever"
|
||||
const paramTypes: string[][] = [["string"],["string"],["string"],["bool"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [rcptTo, listID, msgFrom, toInbox]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
|
||||
async RulesetMailboxNever(mailboxID: number, toMailbox: boolean): Promise<void> {
|
||||
const fn: string = "RulesetMailboxNever"
|
||||
const paramTypes: string[][] = [["int64"],["bool"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [mailboxID, toMailbox]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...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, ChangeMsgThread, ChangeMailboxRemove, ChangeMailboxAdd, ChangeMailboxRename, ChangeMailboxCounts, ChangeMailboxSpecialUse, ChangeMailboxKeywords, Flags]> {
|
||||
const fn: string = "SSETypes"
|
||||
|
@ -53,6 +53,7 @@ func TestAPI(t *testing.T) {
|
||||
os.RemoveAll("../testdata/webmail/data")
|
||||
mox.Context = ctxbg
|
||||
mox.ConfigStaticPath = filepath.FromSlash("../testdata/webmail/mox.conf")
|
||||
mox.ConfigDynamicPath = filepath.FromSlash("../testdata/webmail/domains.conf")
|
||||
mox.MustLoadConfig(true, false)
|
||||
defer store.Switchboard()()
|
||||
|
||||
@ -469,4 +470,65 @@ func TestAPI(t *testing.T) {
|
||||
rs, err = recipientSecurity(ctx, resolver, "mjl@a.mox.example")
|
||||
tcompare(t, err, nil)
|
||||
tcompare(t, rs, RecipientSecurity{SecurityResultYes, SecurityResultNo, SecurityResultNo, SecurityResultNo, SecurityResultNo})
|
||||
|
||||
// Suggesting/adding/removing rulesets.
|
||||
|
||||
testSuggest := func(msgID int64, expListID string, expMsgFrom string) {
|
||||
listID, msgFrom, isRemove, rcptTo, ruleset := api.RulesetSuggestMove(ctx, msgID, inbox.ID, testbox1.ID)
|
||||
tcompare(t, listID, expListID)
|
||||
tcompare(t, msgFrom, expMsgFrom)
|
||||
tcompare(t, isRemove, false)
|
||||
tcompare(t, rcptTo, "mox@other.example")
|
||||
tcompare(t, ruleset == nil, false)
|
||||
|
||||
// Moving in opposite direction doesn't get a suggestion without the rule present.
|
||||
_, _, _, _, rs0 := api.RulesetSuggestMove(ctx, msgID, testbox1.ID, inbox.ID)
|
||||
tcompare(t, rs0 == nil, true)
|
||||
|
||||
api.RulesetAdd(ctx, rcptTo, *ruleset)
|
||||
|
||||
// Ruleset that exists won't get a suggestion again.
|
||||
_, _, _, _, ruleset = api.RulesetSuggestMove(ctx, msgID, inbox.ID, testbox1.ID)
|
||||
tcompare(t, ruleset == nil, true)
|
||||
|
||||
// Moving in oppositive direction, with rule present, gets the suggestion to remove.
|
||||
_, _, _, _, ruleset = api.RulesetSuggestMove(ctx, msgID, testbox1.ID, inbox.ID)
|
||||
tcompare(t, ruleset == nil, false)
|
||||
|
||||
api.RulesetRemove(ctx, rcptTo, *ruleset)
|
||||
|
||||
// If ListID/MsgFrom is marked as never, we won't get a suggestion.
|
||||
api.RulesetMessageNever(ctx, rcptTo, expListID, expMsgFrom, false)
|
||||
_, _, _, _, ruleset = api.RulesetSuggestMove(ctx, msgID, inbox.ID, testbox1.ID)
|
||||
tcompare(t, ruleset == nil, true)
|
||||
|
||||
var n int
|
||||
if expListID != "" {
|
||||
n, err = bstore.QueryDB[store.RulesetNoListID](ctx, acc.DB).Delete()
|
||||
} else {
|
||||
n, err = bstore.QueryDB[store.RulesetNoMsgFrom](ctx, acc.DB).Delete()
|
||||
}
|
||||
tcheck(t, err, "remove never-answer for listid/msgfrom")
|
||||
tcompare(t, n, 1)
|
||||
_, _, _, _, ruleset = api.RulesetSuggestMove(ctx, msgID, inbox.ID, testbox1.ID)
|
||||
tcompare(t, ruleset == nil, false)
|
||||
|
||||
// If Mailbox is marked as never, we won't get a suggestion.
|
||||
api.RulesetMailboxNever(ctx, testbox1.ID, true)
|
||||
_, _, _, _, ruleset = api.RulesetSuggestMove(ctx, msgID, inbox.ID, testbox1.ID)
|
||||
tcompare(t, ruleset == nil, true)
|
||||
|
||||
n, err = bstore.QueryDB[store.RulesetNoMailbox](ctx, acc.DB).Delete()
|
||||
tcheck(t, err, "remove never-answer for mailbox")
|
||||
tcompare(t, n, 1)
|
||||
|
||||
}
|
||||
|
||||
// For MsgFrom.
|
||||
tdeliver(t, acc, inboxText)
|
||||
testSuggest(inboxText.ID, "", "mjl@mox.example")
|
||||
|
||||
// For List-Id.
|
||||
tdeliver(t, acc, inboxHTML)
|
||||
testSuggest(inboxHTML.ID, "list.mox.example", "")
|
||||
}
|
||||
|
@ -290,7 +290,7 @@ var api;
|
||||
Quoting["Bottom"] = "bottom";
|
||||
Quoting["Top"] = "top";
|
||||
})(Quoting = api.Quoting || (api.Quoting = {}));
|
||||
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, "ComposeMessage": 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, "FromAddressSettings": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "RecipientSecurity": true, "Request": true, "Settings": true, "SpecialUse": true, "SubmitMessage": 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, "ComposeMessage": 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, "FromAddressSettings": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "RecipientSecurity": true, "Request": true, "Ruleset": true, "Settings": true, "SpecialUse": true, "SubmitMessage": true };
|
||||
api.stringsTypes = { "AttachmentType": true, "CSRFToken": true, "Localpart": true, "Quoting": true, "SecurityResult": true, "ThreadMode": true, "ViewMode": true };
|
||||
api.intsTypes = { "ModSeq": true, "UID": true, "Validation": true };
|
||||
api.types = {
|
||||
@ -313,6 +313,7 @@ var api;
|
||||
"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"] }] },
|
||||
"RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
||||
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Settings", "Docs": "", "Typewords": ["Settings"] }, { "Name": "AccountPath", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }] },
|
||||
"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"] }] },
|
||||
@ -366,6 +367,7 @@ var api;
|
||||
Mailbox: (v) => api.parse("Mailbox", v),
|
||||
RecipientSecurity: (v) => api.parse("RecipientSecurity", v),
|
||||
Settings: (v) => api.parse("Settings", v),
|
||||
Ruleset: (v) => api.parse("Ruleset", v),
|
||||
EventStart: (v) => api.parse("EventStart", v),
|
||||
DomainAddressConfig: (v) => api.parse("DomainAddressConfig", v),
|
||||
EventViewErr: (v) => api.parse("EventViewErr", v),
|
||||
@ -650,6 +652,41 @@ var api;
|
||||
const params = [settings];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetSuggestMove(msgID, mbSrcID, mbDstID) {
|
||||
const fn = "RulesetSuggestMove";
|
||||
const paramTypes = [["int64"], ["int64"], ["int64"]];
|
||||
const returnTypes = [["string"], ["string"], ["bool"], ["string"], ["nullable", "Ruleset"]];
|
||||
const params = [msgID, mbSrcID, mbDstID];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetAdd(rcptTo, ruleset) {
|
||||
const fn = "RulesetAdd";
|
||||
const paramTypes = [["string"], ["Ruleset"]];
|
||||
const returnTypes = [];
|
||||
const params = [rcptTo, ruleset];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetRemove(rcptTo, ruleset) {
|
||||
const fn = "RulesetRemove";
|
||||
const paramTypes = [["string"], ["Ruleset"]];
|
||||
const returnTypes = [];
|
||||
const params = [rcptTo, ruleset];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetMessageNever(rcptTo, listID, msgFrom, toInbox) {
|
||||
const fn = "RulesetMessageNever";
|
||||
const paramTypes = [["string"], ["string"], ["string"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [rcptTo, listID, msgFrom, toInbox];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetMailboxNever(mailboxID, toMailbox) {
|
||||
const fn = "RulesetMailboxNever";
|
||||
const paramTypes = [["int64"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [mailboxID, toMailbox];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...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";
|
||||
|
@ -290,7 +290,7 @@ var api;
|
||||
Quoting["Bottom"] = "bottom";
|
||||
Quoting["Top"] = "top";
|
||||
})(Quoting = api.Quoting || (api.Quoting = {}));
|
||||
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, "ComposeMessage": 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, "FromAddressSettings": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "RecipientSecurity": true, "Request": true, "Settings": true, "SpecialUse": true, "SubmitMessage": 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, "ComposeMessage": 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, "FromAddressSettings": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "RecipientSecurity": true, "Request": true, "Ruleset": true, "Settings": true, "SpecialUse": true, "SubmitMessage": true };
|
||||
api.stringsTypes = { "AttachmentType": true, "CSRFToken": true, "Localpart": true, "Quoting": true, "SecurityResult": true, "ThreadMode": true, "ViewMode": true };
|
||||
api.intsTypes = { "ModSeq": true, "UID": true, "Validation": true };
|
||||
api.types = {
|
||||
@ -313,6 +313,7 @@ var api;
|
||||
"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"] }] },
|
||||
"RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
||||
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Settings", "Docs": "", "Typewords": ["Settings"] }, { "Name": "AccountPath", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }] },
|
||||
"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"] }] },
|
||||
@ -366,6 +367,7 @@ var api;
|
||||
Mailbox: (v) => api.parse("Mailbox", v),
|
||||
RecipientSecurity: (v) => api.parse("RecipientSecurity", v),
|
||||
Settings: (v) => api.parse("Settings", v),
|
||||
Ruleset: (v) => api.parse("Ruleset", v),
|
||||
EventStart: (v) => api.parse("EventStart", v),
|
||||
DomainAddressConfig: (v) => api.parse("DomainAddressConfig", v),
|
||||
EventViewErr: (v) => api.parse("EventViewErr", v),
|
||||
@ -650,6 +652,41 @@ var api;
|
||||
const params = [settings];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetSuggestMove(msgID, mbSrcID, mbDstID) {
|
||||
const fn = "RulesetSuggestMove";
|
||||
const paramTypes = [["int64"], ["int64"], ["int64"]];
|
||||
const returnTypes = [["string"], ["string"], ["bool"], ["string"], ["nullable", "Ruleset"]];
|
||||
const params = [msgID, mbSrcID, mbDstID];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetAdd(rcptTo, ruleset) {
|
||||
const fn = "RulesetAdd";
|
||||
const paramTypes = [["string"], ["Ruleset"]];
|
||||
const returnTypes = [];
|
||||
const params = [rcptTo, ruleset];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetRemove(rcptTo, ruleset) {
|
||||
const fn = "RulesetRemove";
|
||||
const paramTypes = [["string"], ["Ruleset"]];
|
||||
const returnTypes = [];
|
||||
const params = [rcptTo, ruleset];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetMessageNever(rcptTo, listID, msgFrom, toInbox) {
|
||||
const fn = "RulesetMessageNever";
|
||||
const paramTypes = [["string"], ["string"], ["string"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [rcptTo, listID, msgFrom, toInbox];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetMailboxNever(mailboxID, toMailbox) {
|
||||
const fn = "RulesetMailboxNever";
|
||||
const paramTypes = [["int64"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [mailboxID, toMailbox];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...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";
|
||||
|
@ -290,7 +290,7 @@ var api;
|
||||
Quoting["Bottom"] = "bottom";
|
||||
Quoting["Top"] = "top";
|
||||
})(Quoting = api.Quoting || (api.Quoting = {}));
|
||||
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, "ComposeMessage": 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, "FromAddressSettings": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "RecipientSecurity": true, "Request": true, "Settings": true, "SpecialUse": true, "SubmitMessage": 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, "ComposeMessage": 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, "FromAddressSettings": true, "Mailbox": true, "Message": true, "MessageAddress": true, "MessageEnvelope": true, "MessageItem": true, "NotFilter": true, "Page": true, "ParsedMessage": true, "Part": true, "Query": true, "RecipientSecurity": true, "Request": true, "Ruleset": true, "Settings": true, "SpecialUse": true, "SubmitMessage": true };
|
||||
api.stringsTypes = { "AttachmentType": true, "CSRFToken": true, "Localpart": true, "Quoting": true, "SecurityResult": true, "ThreadMode": true, "ViewMode": true };
|
||||
api.intsTypes = { "ModSeq": true, "UID": true, "Validation": true };
|
||||
api.types = {
|
||||
@ -313,6 +313,7 @@ var api;
|
||||
"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"] }] },
|
||||
"RecipientSecurity": { "Name": "RecipientSecurity", "Docs": "", "Fields": [{ "Name": "STARTTLS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "MTASTS", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DNSSEC", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "DANE", "Docs": "", "Typewords": ["SecurityResult"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["SecurityResult"] }] },
|
||||
"Settings": { "Name": "Settings", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["uint8"] }, { "Name": "Signature", "Docs": "", "Typewords": ["string"] }, { "Name": "Quoting", "Docs": "", "Typewords": ["Quoting"] }, { "Name": "ShowAddressSecurity", "Docs": "", "Typewords": ["bool"] }] },
|
||||
"Ruleset": { "Name": "Ruleset", "Docs": "", "Fields": [{ "Name": "SMTPMailFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgFromRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "HeadersRegexp", "Docs": "", "Typewords": ["{}", "string"] }, { "Name": "IsForward", "Docs": "", "Typewords": ["bool"] }, { "Name": "ListAllowDomain", "Docs": "", "Typewords": ["string"] }, { "Name": "AcceptRejectsToMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Comment", "Docs": "", "Typewords": ["string"] }, { "Name": "VerifiedDNSDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "ListAllowDNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"EventStart": { "Name": "EventStart", "Docs": "", "Fields": [{ "Name": "SSEID", "Docs": "", "Typewords": ["int64"] }, { "Name": "LoginAddress", "Docs": "", "Typewords": ["MessageAddress"] }, { "Name": "Addresses", "Docs": "", "Typewords": ["[]", "MessageAddress"] }, { "Name": "DomainAddressConfigs", "Docs": "", "Typewords": ["{}", "DomainAddressConfig"] }, { "Name": "MailboxName", "Docs": "", "Typewords": ["string"] }, { "Name": "Mailboxes", "Docs": "", "Typewords": ["[]", "Mailbox"] }, { "Name": "RejectsMailbox", "Docs": "", "Typewords": ["string"] }, { "Name": "Settings", "Docs": "", "Typewords": ["Settings"] }, { "Name": "AccountPath", "Docs": "", "Typewords": ["string"] }, { "Name": "Version", "Docs": "", "Typewords": ["string"] }] },
|
||||
"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"] }] },
|
||||
@ -366,6 +367,7 @@ var api;
|
||||
Mailbox: (v) => api.parse("Mailbox", v),
|
||||
RecipientSecurity: (v) => api.parse("RecipientSecurity", v),
|
||||
Settings: (v) => api.parse("Settings", v),
|
||||
Ruleset: (v) => api.parse("Ruleset", v),
|
||||
EventStart: (v) => api.parse("EventStart", v),
|
||||
DomainAddressConfig: (v) => api.parse("DomainAddressConfig", v),
|
||||
EventViewErr: (v) => api.parse("EventViewErr", v),
|
||||
@ -650,6 +652,41 @@ var api;
|
||||
const params = [settings];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetSuggestMove(msgID, mbSrcID, mbDstID) {
|
||||
const fn = "RulesetSuggestMove";
|
||||
const paramTypes = [["int64"], ["int64"], ["int64"]];
|
||||
const returnTypes = [["string"], ["string"], ["bool"], ["string"], ["nullable", "Ruleset"]];
|
||||
const params = [msgID, mbSrcID, mbDstID];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetAdd(rcptTo, ruleset) {
|
||||
const fn = "RulesetAdd";
|
||||
const paramTypes = [["string"], ["Ruleset"]];
|
||||
const returnTypes = [];
|
||||
const params = [rcptTo, ruleset];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetRemove(rcptTo, ruleset) {
|
||||
const fn = "RulesetRemove";
|
||||
const paramTypes = [["string"], ["Ruleset"]];
|
||||
const returnTypes = [];
|
||||
const params = [rcptTo, ruleset];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetMessageNever(rcptTo, listID, msgFrom, toInbox) {
|
||||
const fn = "RulesetMessageNever";
|
||||
const paramTypes = [["string"], ["string"], ["string"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [rcptTo, listID, msgFrom, toInbox];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
async RulesetMailboxNever(mailboxID, toMailbox) {
|
||||
const fn = "RulesetMailboxNever";
|
||||
const paramTypes = [["int64"], ["bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [mailboxID, toMailbox];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...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";
|
||||
@ -1297,7 +1334,7 @@ Enable consistency checking in UI updates:
|
||||
- todo: buttons/mechanism to operate on all messages in a mailbox/search query, without having to list and select all messages. e.g. clearing flags/labels.
|
||||
- todo: can we detect if browser supports proper CSP? if not, refuse to load html messages?
|
||||
- todo: more search criteria? Date header field (instead of time received), text vs html (only, either or both), attachment filenames and sizes
|
||||
- todo: integrate more of the account page into webmail? importing/exporting messages, configuring delivery rules (possibly with sieve). for messages moved out of inbox to non-special-use mailbox, show button that helps make an automatic rule to move such messages again (e.g. based on message From address, message From domain or List-ID header).
|
||||
- todo: integrate more of the account page into webmail? importing/exporting messages, configuring delivery rules (possibly with sieve).
|
||||
- todo: configurable keyboard shortcuts? we use strings like "ctrl p" which we already generate and match on, add a mapping from command name to cmd* functions, and have a map of keys to command names. the commands for up/down with shift/ctrl modifiers may need special attention.
|
||||
- todo: nicer address input fields like other mail clients do. with tab to autocomplete and turn input into a box and delete removing of the entire address.
|
||||
- todo: consider composing messages with bcc headers that are kept as message Bcc headers, optionally with checkbox.
|
||||
@ -2937,11 +2974,61 @@ const movePopover = (e, mailboxes, msgs) => {
|
||||
}
|
||||
let msgsMailboxID = (msgs[0].MailboxID && msgs.filter(m => m.MailboxID === msgs[0].MailboxID).length === msgs.length) ? msgs[0].MailboxID : 0;
|
||||
const remove = popover(e.target, {}, dom.div(style({ display: 'flex', flexDirection: 'column', gap: '.25em' }), mailboxes.map(mb => dom.div(dom.clickbutton(mb.Name, mb.ID === msgsMailboxID ? attr.disabled('') : [], async function click() {
|
||||
const msgIDs = msgs.filter(m => m.MailboxID !== mb.ID).map(m => m.ID);
|
||||
const moveMsgs = msgs.filter(m => m.MailboxID !== mb.ID);
|
||||
const msgIDs = moveMsgs.map(m => m.ID);
|
||||
await withStatus('Moving to mailbox', client.MessageMove(msgIDs, mb.ID));
|
||||
if (moveMsgs.length === 1) {
|
||||
await moveAskRuleset(moveMsgs[0].ID, moveMsgs[0].MailboxID, mb, mailboxes);
|
||||
}
|
||||
remove();
|
||||
})))));
|
||||
};
|
||||
// We've moved a single message. If the source or destination mailbox is not a
|
||||
// "special-use" mailbox (other than inbox), and there isn't a rule yet or there is
|
||||
// one we may want to delete, and we haven't asked about adding/removing this
|
||||
// ruleset before, ask the user to add/remove a ruleset for moving. If the message
|
||||
// has a list-id header, we ask to create a ruleset treating it as a mailing list
|
||||
// message matching on future list-id header and spf/dkim verified domain,
|
||||
// otherwise we make a rule based on message "from" address.
|
||||
const moveAskRuleset = async (msgID, mbSrcID, mbDst, mailboxes) => {
|
||||
const mbSrc = mailboxes.find(mb => mb.ID === mbSrcID);
|
||||
if (!mbSrc || isSpecialUse(mbDst) || isSpecialUse(mbSrc)) {
|
||||
return;
|
||||
}
|
||||
const [listID, msgFrom, isRemove, rcptTo, ruleset] = await withStatus('Checking rulesets', client.RulesetSuggestMove(msgID, mbSrc.ID, mbDst.ID));
|
||||
if (!ruleset) {
|
||||
return;
|
||||
}
|
||||
const what = listID ? ['list with id "', listID, '"'] : ['address "', msgFrom, '"'];
|
||||
if (isRemove) {
|
||||
const remove = popup(dom.h1('Remove rule?'), dom.p(style({ maxWidth: '30em' }), 'Would you like to remove the server-side rule that automatically delivers messages from ', what, ' to mailbox "', mbDst.Name, '"?'), dom.br(), dom.div(dom.clickbutton('Yes, remove rule', async function click() {
|
||||
await withStatus('Remove ruleset', client.RulesetRemove(rcptTo, ruleset));
|
||||
remove();
|
||||
}), ' ', dom.clickbutton('Not now', async function click() {
|
||||
remove();
|
||||
})), dom.br(), dom.div(style({ marginBottom: '1ex' }), dom.clickbutton("No, and don't ask again for ", what, async function click() {
|
||||
await withStatus('Store ruleset response', client.RulesetMessageNever(rcptTo, listID, msgFrom, true));
|
||||
remove();
|
||||
})), dom.div(dom.clickbutton("No, and don't ask again when moving messages out of \"", mbSrc.Name, '"', async function click() {
|
||||
await withStatus('Store ruleset response', client.RulesetMailboxNever(mbSrc.ID, false));
|
||||
remove();
|
||||
})));
|
||||
return;
|
||||
}
|
||||
const remove = popup(dom.h1('Add rule?'), dom.p(style({ maxWidth: '30em' }), 'Would you like to create a server-side ruleset that automatically delivers future messages from ', what, ' to mailbox "', mbDst.Name, '"?'), dom.br(), dom.div(dom.clickbutton('Yes, add rule', async function click() {
|
||||
await withStatus('Add ruleset', client.RulesetAdd(rcptTo, ruleset));
|
||||
remove();
|
||||
}), ' ', dom.clickbutton('Not now', async function click() {
|
||||
remove();
|
||||
})), dom.br(), dom.div(style({ marginBottom: '1ex' }), dom.clickbutton("No, and don't ask again for ", what, async function click() {
|
||||
await withStatus('Store ruleset response', client.RulesetMessageNever(rcptTo, listID, msgFrom, false));
|
||||
remove();
|
||||
})), dom.div(dom.clickbutton("No, and don't ask again when moving messages to \"", mbDst.Name, '"', async function click() {
|
||||
await withStatus('Store ruleset response', client.RulesetMailboxNever(mbDst.ID, true));
|
||||
remove();
|
||||
})));
|
||||
};
|
||||
const isSpecialUse = (mb) => mb.Archive || mb.Draft || mb.Junk || mb.Sent || mb.Trash;
|
||||
// Make new MsgitemView, to be added to the list.
|
||||
const newMsgitemView = (mi, msglistView, otherMailbox, listMailboxes, receivedTime, initialCollapsed) => {
|
||||
// note: mi may be replaced.
|
||||
@ -5237,6 +5324,11 @@ const newMailboxView = (xmb, mailboxlistView, otherMailbox) => {
|
||||
.filter(mbMsgID => mailboxMsgIDs.length === 1 || !sentMailboxID || mbMsgID[0] !== sentMailboxID || !otherMailbox(sentMailboxID))
|
||||
.map(mbMsgID => mbMsgID[1]);
|
||||
await withStatus('Moving to ' + xmb.Name, client.MessageMove(msgIDs, xmb.ID));
|
||||
if (msgIDs.length === 1) {
|
||||
const msgID = msgIDs[0];
|
||||
const mbSrcID = mailboxMsgIDs.find(mbMsgID => mbMsgID[1] === msgID)[0];
|
||||
await moveAskRuleset(msgID, mbSrcID, xmb, mailboxlistView.mailboxes());
|
||||
}
|
||||
}, dom.div(dom._class('mailbox'), style({ display: 'flex', justifyContent: 'space-between' }), name = dom.div(style({ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' })), dom.div(style({ whiteSpace: 'nowrap' }), actionBtn = dom.clickbutton(dom._class('mailboxhoveronly'), '...', attr.tabindex('-1'), // Without, tab breaks because this disappears when mailbox loses focus.
|
||||
attr.arialabel('Mailbox actions'), attr.title('Actions on mailbox, like deleting, emptying, renaming.'), function click(e) {
|
||||
e.stopPropagation();
|
||||
|
@ -81,7 +81,7 @@ Enable consistency checking in UI updates:
|
||||
- todo: buttons/mechanism to operate on all messages in a mailbox/search query, without having to list and select all messages. e.g. clearing flags/labels.
|
||||
- todo: can we detect if browser supports proper CSP? if not, refuse to load html messages?
|
||||
- todo: more search criteria? Date header field (instead of time received), text vs html (only, either or both), attachment filenames and sizes
|
||||
- todo: integrate more of the account page into webmail? importing/exporting messages, configuring delivery rules (possibly with sieve). for messages moved out of inbox to non-special-use mailbox, show button that helps make an automatic rule to move such messages again (e.g. based on message From address, message From domain or List-ID header).
|
||||
- todo: integrate more of the account page into webmail? importing/exporting messages, configuring delivery rules (possibly with sieve).
|
||||
- todo: configurable keyboard shortcuts? we use strings like "ctrl p" which we already generate and match on, add a mapping from command name to cmd* functions, and have a map of keys to command names. the commands for up/down with shift/ctrl modifiers may need special attention.
|
||||
- todo: nicer address input fields like other mail clients do. with tab to autocomplete and turn input into a box and delete removing of the entire address.
|
||||
- todo: consider composing messages with bcc headers that are kept as message Bcc headers, optionally with checkbox.
|
||||
@ -2208,8 +2208,12 @@ const movePopover = (e: MouseEvent, mailboxes: api.Mailbox[], msgs: api.Message[
|
||||
mb.Name,
|
||||
mb.ID === msgsMailboxID ? attr.disabled('') : [],
|
||||
async function click() {
|
||||
const msgIDs = msgs.filter(m => m.MailboxID !== mb.ID).map(m => m.ID)
|
||||
const moveMsgs = msgs.filter(m => m.MailboxID !== mb.ID)
|
||||
const msgIDs = moveMsgs.map(m => m.ID)
|
||||
await withStatus('Moving to mailbox', client.MessageMove(msgIDs, mb.ID))
|
||||
if (moveMsgs.length === 1) {
|
||||
await moveAskRuleset(moveMsgs[0].ID, moveMsgs[0].MailboxID, mb, mailboxes)
|
||||
}
|
||||
remove()
|
||||
}
|
||||
),
|
||||
@ -2219,6 +2223,95 @@ const movePopover = (e: MouseEvent, mailboxes: api.Mailbox[], msgs: api.Message[
|
||||
)
|
||||
}
|
||||
|
||||
// We've moved a single message. If the source or destination mailbox is not a
|
||||
// "special-use" mailbox (other than inbox), and there isn't a rule yet or there is
|
||||
// one we may want to delete, and we haven't asked about adding/removing this
|
||||
// ruleset before, ask the user to add/remove a ruleset for moving. If the message
|
||||
// has a list-id header, we ask to create a ruleset treating it as a mailing list
|
||||
// message matching on future list-id header and spf/dkim verified domain,
|
||||
// otherwise we make a rule based on message "from" address.
|
||||
const moveAskRuleset = async (msgID: number, mbSrcID: number, mbDst: api.Mailbox, mailboxes: api.Mailbox[]) => {
|
||||
const mbSrc = mailboxes.find(mb => mb.ID === mbSrcID)
|
||||
if (!mbSrc || isSpecialUse(mbDst) || isSpecialUse(mbSrc)) {
|
||||
return
|
||||
}
|
||||
|
||||
const [listID, msgFrom, isRemove, rcptTo, ruleset] = await withStatus('Checking rulesets', client.RulesetSuggestMove(msgID, mbSrc.ID, mbDst.ID))
|
||||
if (!ruleset) {
|
||||
return
|
||||
}
|
||||
|
||||
const what = listID ? ['list with id "', listID, '"'] : ['address "', msgFrom, '"']
|
||||
|
||||
if (isRemove) {
|
||||
const remove = popup(
|
||||
dom.h1('Remove rule?'),
|
||||
dom.p(
|
||||
style({maxWidth: '30em'}),
|
||||
'Would you like to remove the server-side rule that automatically delivers messages from ', what, ' to mailbox "', mbDst.Name, '"?',
|
||||
),
|
||||
dom.br(),
|
||||
dom.div(
|
||||
dom.clickbutton('Yes, remove rule', async function click() {
|
||||
await withStatus('Remove ruleset', client.RulesetRemove(rcptTo, ruleset))
|
||||
remove()
|
||||
}), ' ',
|
||||
dom.clickbutton('Not now', async function click() {
|
||||
remove()
|
||||
}),
|
||||
),
|
||||
dom.br(),
|
||||
dom.div(
|
||||
style({marginBottom: '1ex'}),
|
||||
dom.clickbutton("No, and don't ask again for ", what, async function click() {
|
||||
await withStatus('Store ruleset response', client.RulesetMessageNever(rcptTo, listID, msgFrom, true))
|
||||
remove()
|
||||
}),
|
||||
),
|
||||
dom.div(
|
||||
dom.clickbutton("No, and don't ask again when moving messages out of \"", mbSrc.Name, '"', async function click() {
|
||||
await withStatus('Store ruleset response', client.RulesetMailboxNever(mbSrc.ID, false))
|
||||
remove()
|
||||
}),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
const remove = popup(
|
||||
dom.h1('Add rule?'),
|
||||
dom.p(
|
||||
style({maxWidth: '30em'}),
|
||||
'Would you like to create a server-side ruleset that automatically delivers future messages from ', what, ' to mailbox "', mbDst.Name, '"?',
|
||||
),
|
||||
dom.br(),
|
||||
dom.div(
|
||||
dom.clickbutton('Yes, add rule', async function click() {
|
||||
await withStatus('Add ruleset', client.RulesetAdd(rcptTo, ruleset))
|
||||
remove()
|
||||
}), ' ',
|
||||
dom.clickbutton('Not now', async function click() {
|
||||
remove()
|
||||
}),
|
||||
),
|
||||
dom.br(),
|
||||
dom.div(
|
||||
style({marginBottom: '1ex'}),
|
||||
dom.clickbutton("No, and don't ask again for ", what, async function click() {
|
||||
await withStatus('Store ruleset response', client.RulesetMessageNever(rcptTo, listID, msgFrom, false))
|
||||
remove()
|
||||
}),
|
||||
),
|
||||
dom.div(
|
||||
dom.clickbutton("No, and don't ask again when moving messages to \"", mbDst.Name, '"', async function click() {
|
||||
await withStatus('Store ruleset response', client.RulesetMailboxNever(mbDst.ID, true))
|
||||
remove()
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const isSpecialUse = (mb: api.Mailbox) => mb.Archive || mb.Draft || mb.Junk || mb.Sent || mb.Trash
|
||||
|
||||
// MsgitemView is a message-line in the list of messages. Selecting it loads and displays the message, a MsgView.
|
||||
interface MsgitemView {
|
||||
root: HTMLElement // MsglistView toggles active/focus classes on the root element.
|
||||
@ -5047,6 +5140,11 @@ const newMailboxView = (xmb: api.Mailbox, mailboxlistView: MailboxlistView, othe
|
||||
.filter(mbMsgID => mailboxMsgIDs.length === 1 || !sentMailboxID || mbMsgID[0] !== sentMailboxID || !otherMailbox(sentMailboxID))
|
||||
.map(mbMsgID => mbMsgID[1])
|
||||
await withStatus('Moving to '+xmb.Name, client.MessageMove(msgIDs, xmb.ID))
|
||||
if (msgIDs.length === 1) {
|
||||
const msgID = msgIDs[0]
|
||||
const mbSrcID = mailboxMsgIDs.find(mbMsgID => mbMsgID[1] === msgID)![0]
|
||||
await moveAskRuleset(msgID, mbSrcID, xmb, mailboxlistView.mailboxes())
|
||||
}
|
||||
},
|
||||
dom.div(dom._class('mailbox'),
|
||||
style({display: 'flex', justifyContent: 'space-between'}),
|
||||
|
@ -188,6 +188,7 @@ var (
|
||||
From: "mjl <mjl@mox.example>",
|
||||
To: "mox <mox@other.example>",
|
||||
Subject: "html message",
|
||||
Headers: [][2]string{{"List-Id", "test <list.mox.example>"}},
|
||||
Part: Part{Type: "text/html", Content: `<html>the body <img src="cid:img1@mox.example" /></html>`},
|
||||
}
|
||||
msgAlt = Message{
|
||||
@ -265,7 +266,16 @@ func tdeliver(t *testing.T, acc *store.Account, tm *testmsg) {
|
||||
defer msgFile.Close()
|
||||
size, err := msgFile.Write(tm.msg.Marshal(t))
|
||||
tcheck(t, err, "write message temp")
|
||||
m := store.Message{Flags: tm.Flags, Keywords: tm.Keywords, Size: int64(size)}
|
||||
m := store.Message{
|
||||
Flags: tm.Flags,
|
||||
RcptToLocalpart: "mox",
|
||||
RcptToDomain: "other.example",
|
||||
MsgFromLocalpart: "mjl",
|
||||
MsgFromDomain: "mox.example",
|
||||
DKIMDomains: []string{"mox.example"},
|
||||
Keywords: tm.Keywords,
|
||||
Size: int64(size),
|
||||
}
|
||||
err = acc.DeliverMailbox(pkglog, tm.Mailbox, &m, msgFile)
|
||||
tcheck(t, err, "deliver test message")
|
||||
err = msgFile.Close()
|
||||
|
Reference in New Issue
Block a user