mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
improve queue management
- add option to put messages in the queue "on hold", preventing delivery attempts until taken off hold again. - add "hold rules", to automatically mark some/all submitted messages as "on hold", e.g. from a specific account or to a specific domain. - add operation to "fail" a message, causing a DSN to be delivered to the sender. previously we could only drop a message from the queue. - update admin page & add new cli tools for these operations, with new filtering rules for selecting the messages to operate on. in the admin interface, add filtering and checkboxes to select a set of messages to operate on.
This commit is contained in:
parent
79f1054b64
commit
40ade995a5
19 changed files with 2554 additions and 565 deletions
264
ctl.go
264
ctl.go
|
@ -294,6 +294,14 @@ func servectl(ctx context.Context, log mlog.Log, conn net.Conn, shutdown func())
|
|||
}
|
||||
}
|
||||
|
||||
func xparseFilters(ctl *ctl, s string) (f queue.Filter) {
|
||||
dec := json.NewDecoder(strings.NewReader(s))
|
||||
dec.DisallowUnknownFields()
|
||||
err := dec.Decode(&f)
|
||||
ctl.xcheck(err, "parsing filters")
|
||||
return f
|
||||
}
|
||||
|
||||
func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
||||
log := ctl.log
|
||||
cmd := ctl.xread()
|
||||
|
@ -315,10 +323,10 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
*/
|
||||
|
||||
to := ctl.xread()
|
||||
a, addr, err := store.OpenEmail(ctl.log, to)
|
||||
a, addr, err := store.OpenEmail(log, to)
|
||||
ctl.xcheck(err, "lookup destination address")
|
||||
|
||||
msgFile, err := store.CreateMessageTemp(ctl.log, "ctl-deliver")
|
||||
msgFile, err := store.CreateMessageTemp(log, "ctl-deliver")
|
||||
ctl.xcheck(err, "creating temporary message file")
|
||||
defer store.CloseRemoveTempFile(log, msgFile, "deliver message")
|
||||
mw := message.NewWriter(msgFile)
|
||||
|
@ -354,7 +362,7 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
account := ctl.xread()
|
||||
pw := ctl.xread()
|
||||
|
||||
acc, err := store.OpenAccount(ctl.log, account)
|
||||
acc, err := store.OpenAccount(log, account)
|
||||
ctl.xcheck(err, "open account")
|
||||
defer func() {
|
||||
if acc != nil {
|
||||
|
@ -363,25 +371,95 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
}
|
||||
}()
|
||||
|
||||
err = acc.SetPassword(ctl.log, pw)
|
||||
err = acc.SetPassword(log, pw)
|
||||
ctl.xcheck(err, "setting password")
|
||||
err = acc.Close()
|
||||
ctl.xcheck(err, "closing account")
|
||||
acc = nil
|
||||
ctl.xwriteok()
|
||||
|
||||
case "queue":
|
||||
case "queueholdruleslist":
|
||||
/* protocol:
|
||||
> "queue"
|
||||
> "queueholdruleslist"
|
||||
< "ok"
|
||||
< stream
|
||||
*/
|
||||
qmsgs, err := queue.List(ctx)
|
||||
l, err := queue.HoldRuleList(ctx)
|
||||
ctl.xcheck(err, "listing hold rules")
|
||||
ctl.xwriteok()
|
||||
xw := ctl.writer()
|
||||
fmt.Fprintln(xw, "hold rules:")
|
||||
for _, hr := range l {
|
||||
var elems []string
|
||||
if hr.Account != "" {
|
||||
elems = append(elems, fmt.Sprintf("account %q", hr.Account))
|
||||
}
|
||||
var zerodom dns.Domain
|
||||
if hr.SenderDomain != zerodom {
|
||||
elems = append(elems, fmt.Sprintf("sender domain %q", hr.SenderDomain.Name()))
|
||||
}
|
||||
if hr.RecipientDomain != zerodom {
|
||||
elems = append(elems, fmt.Sprintf("sender domain %q", hr.RecipientDomain.Name()))
|
||||
}
|
||||
if len(elems) == 0 {
|
||||
fmt.Fprintf(xw, "id %d: all messages\n", hr.ID)
|
||||
} else {
|
||||
fmt.Fprintf(xw, "id %d: %s\n", hr.ID, strings.Join(elems, ", "))
|
||||
}
|
||||
}
|
||||
if len(l) == 0 {
|
||||
fmt.Fprint(xw, "(none)\n")
|
||||
}
|
||||
xw.xclose()
|
||||
|
||||
case "queueholdrulesadd":
|
||||
/* protocol:
|
||||
> "queueholdrulesadd"
|
||||
> account
|
||||
> senderdomainstr
|
||||
> recipientdomainstr
|
||||
< "ok" or error
|
||||
*/
|
||||
var hr queue.HoldRule
|
||||
hr.Account = ctl.xread()
|
||||
senderdomstr := ctl.xread()
|
||||
rcptdomstr := ctl.xread()
|
||||
var err error
|
||||
hr.SenderDomain, err = dns.ParseDomain(senderdomstr)
|
||||
ctl.xcheck(err, "parsing sender domain")
|
||||
hr.RecipientDomain, err = dns.ParseDomain(rcptdomstr)
|
||||
ctl.xcheck(err, "parsing recipient domain")
|
||||
hr, err = queue.HoldRuleAdd(ctx, log, hr)
|
||||
ctl.xcheck(err, "add hold rule")
|
||||
ctl.xwriteok()
|
||||
|
||||
case "queueholdrulesremove":
|
||||
/* protocol:
|
||||
> "queueholdrulesremove"
|
||||
> id
|
||||
< "ok" or error
|
||||
*/
|
||||
id, err := strconv.ParseInt(ctl.xread(), 10, 64)
|
||||
ctl.xcheck(err, "parsing id")
|
||||
err = queue.HoldRuleRemove(ctx, log, id)
|
||||
ctl.xcheck(err, "remove hold rule")
|
||||
ctl.xwriteok()
|
||||
|
||||
case "queuelist":
|
||||
/* protocol:
|
||||
> "queue"
|
||||
> queuefilters as json
|
||||
< "ok"
|
||||
< stream
|
||||
*/
|
||||
fs := ctl.xread()
|
||||
f := xparseFilters(ctl, fs)
|
||||
qmsgs, err := queue.List(ctx, f)
|
||||
ctl.xcheck(err, "listing queue")
|
||||
ctl.xwriteok()
|
||||
|
||||
xw := ctl.writer()
|
||||
fmt.Fprintln(xw, "queue:")
|
||||
fmt.Fprintln(xw, "messages:")
|
||||
for _, qm := range qmsgs {
|
||||
var lastAttempt string
|
||||
if qm.LastAttempt != nil {
|
||||
|
@ -390,63 +468,127 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
fmt.Fprintf(xw, "%5d %s from:%s to:%s next %s last %s error %q\n", qm.ID, qm.Queued.Format(time.RFC3339), qm.Sender().LogString(), qm.Recipient().LogString(), -time.Since(qm.NextAttempt).Round(time.Second), lastAttempt, qm.LastError)
|
||||
}
|
||||
if len(qmsgs) == 0 {
|
||||
fmt.Fprint(xw, "(empty)\n")
|
||||
fmt.Fprint(xw, "(none)\n")
|
||||
}
|
||||
xw.xclose()
|
||||
|
||||
case "queuekick":
|
||||
case "queueholdset":
|
||||
/* protocol:
|
||||
> "queuekick"
|
||||
> id
|
||||
> todomain
|
||||
> recipient
|
||||
> transport // if empty, transport is left unchanged; in future, we may want to differtiate between "leave unchanged" and "set to empty string".
|
||||
< count
|
||||
> "queueholdset"
|
||||
> queuefilters as json
|
||||
> "true" or "false"
|
||||
< "ok" or error
|
||||
< count
|
||||
*/
|
||||
|
||||
idstr := ctl.xread()
|
||||
todomain := ctl.xread()
|
||||
recipient := ctl.xread()
|
||||
transport := ctl.xread()
|
||||
id, err := strconv.ParseInt(idstr, 10, 64)
|
||||
if err != nil {
|
||||
ctl.xwrite("0")
|
||||
ctl.xcheck(err, "parsing id")
|
||||
}
|
||||
|
||||
var xtransport *string
|
||||
if transport != "" {
|
||||
xtransport = &transport
|
||||
}
|
||||
count, err := queue.Kick(ctx, id, todomain, recipient, xtransport)
|
||||
ctl.xcheck(err, "kicking queue")
|
||||
ctl.xwrite(fmt.Sprintf("%d", count))
|
||||
fs := ctl.xread()
|
||||
f := xparseFilters(ctl, fs)
|
||||
hold := ctl.xread() == "true"
|
||||
count, err := queue.HoldSet(ctx, f, hold)
|
||||
ctl.xcheck(err, "setting on hold status for messages")
|
||||
ctl.xwriteok()
|
||||
ctl.xwrite(fmt.Sprintf("%d", count))
|
||||
|
||||
case "queueschedule":
|
||||
/* protocol:
|
||||
> "queueschedule"
|
||||
> queuefilters as json
|
||||
> relative to now
|
||||
> duration
|
||||
< "ok" or error
|
||||
< count
|
||||
*/
|
||||
|
||||
fs := ctl.xread()
|
||||
f := xparseFilters(ctl, fs)
|
||||
relnow := ctl.xread()
|
||||
d, err := time.ParseDuration(ctl.xread())
|
||||
ctl.xcheck(err, "parsing duration for next delivery attempt")
|
||||
var count int
|
||||
if relnow == "" {
|
||||
count, err = queue.NextAttemptAdd(ctx, f, d)
|
||||
} else {
|
||||
count, err = queue.NextAttemptSet(ctx, f, time.Now().Add(d))
|
||||
}
|
||||
ctl.xcheck(err, "setting next delivery attempts in queue")
|
||||
ctl.xwriteok()
|
||||
ctl.xwrite(fmt.Sprintf("%d", count))
|
||||
|
||||
case "queuetransport":
|
||||
/* protocol:
|
||||
> "queuetransport"
|
||||
> queuefilters as json
|
||||
> transport
|
||||
< "ok" or error
|
||||
< count
|
||||
*/
|
||||
|
||||
fs := ctl.xread()
|
||||
f := xparseFilters(ctl, fs)
|
||||
transport := ctl.xread()
|
||||
count, err := queue.TransportSet(ctx, f, transport)
|
||||
ctl.xcheck(err, "adding to next delivery attempts in queue")
|
||||
ctl.xwriteok()
|
||||
ctl.xwrite(fmt.Sprintf("%d", count))
|
||||
|
||||
case "queuerequiretls":
|
||||
/* protocol:
|
||||
> "queuerequiretls"
|
||||
> queuefilters as json
|
||||
> reqtls (empty string, "true" or "false")
|
||||
< "ok" or error
|
||||
< count
|
||||
*/
|
||||
|
||||
fs := ctl.xread()
|
||||
f := xparseFilters(ctl, fs)
|
||||
reqtls := ctl.xread()
|
||||
var req *bool
|
||||
switch reqtls {
|
||||
case "":
|
||||
case "true":
|
||||
v := true
|
||||
req = &v
|
||||
case "false":
|
||||
v := false
|
||||
req = &v
|
||||
default:
|
||||
ctl.xcheck(fmt.Errorf("unknown value %q", reqtls), "parsing value")
|
||||
}
|
||||
count, err := queue.RequireTLSSet(ctx, f, req)
|
||||
ctl.xcheck(err, "setting tls requirements on messages in queue")
|
||||
ctl.xwriteok()
|
||||
ctl.xwrite(fmt.Sprintf("%d", count))
|
||||
|
||||
case "queuefail":
|
||||
/* protocol:
|
||||
> "queuefail"
|
||||
> queuefilters as json
|
||||
< "ok" or error
|
||||
< count
|
||||
*/
|
||||
|
||||
fs := ctl.xread()
|
||||
f := xparseFilters(ctl, fs)
|
||||
count, err := queue.Fail(ctx, log, f)
|
||||
ctl.xcheck(err, "marking messages from queue as failed")
|
||||
ctl.xwriteok()
|
||||
ctl.xwrite(fmt.Sprintf("%d", count))
|
||||
|
||||
case "queuedrop":
|
||||
/* protocol:
|
||||
> "queuedrop"
|
||||
> id
|
||||
> todomain
|
||||
> recipient
|
||||
< count
|
||||
> queuefilters as json
|
||||
< "ok" or error
|
||||
< count
|
||||
*/
|
||||
|
||||
idstr := ctl.xread()
|
||||
todomain := ctl.xread()
|
||||
recipient := ctl.xread()
|
||||
id, err := strconv.ParseInt(idstr, 10, 64)
|
||||
if err != nil {
|
||||
ctl.xwrite("0")
|
||||
ctl.xcheck(err, "parsing id")
|
||||
}
|
||||
|
||||
count, err := queue.Drop(ctx, ctl.log, id, todomain, recipient)
|
||||
fs := ctl.xread()
|
||||
f := xparseFilters(ctl, fs)
|
||||
count, err := queue.Drop(ctx, log, f)
|
||||
ctl.xcheck(err, "dropping messages from queue")
|
||||
ctl.xwrite(fmt.Sprintf("%d", count))
|
||||
ctl.xwriteok()
|
||||
ctl.xwrite(fmt.Sprintf("%d", count))
|
||||
|
||||
case "queuedump":
|
||||
/* protocol:
|
||||
|
@ -587,13 +729,13 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
pkg := ctl.xread()
|
||||
levelstr := ctl.xread()
|
||||
if levelstr == "" {
|
||||
mox.Conf.LogLevelRemove(ctl.log, pkg)
|
||||
mox.Conf.LogLevelRemove(log, pkg)
|
||||
} else {
|
||||
level, ok := mlog.Levels[levelstr]
|
||||
if !ok {
|
||||
ctl.xerror("bad level")
|
||||
}
|
||||
mox.Conf.LogLevelSet(ctl.log, pkg, level)
|
||||
mox.Conf.LogLevelSet(log, pkg, level)
|
||||
}
|
||||
ctl.xwriteok()
|
||||
|
||||
|
@ -604,7 +746,7 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
< "ok" or error
|
||||
*/
|
||||
account := ctl.xread()
|
||||
acc, err := store.OpenAccount(ctl.log, account)
|
||||
acc, err := store.OpenAccount(log, account)
|
||||
ctl.xcheck(err, "open account")
|
||||
defer func() {
|
||||
if acc != nil {
|
||||
|
@ -629,7 +771,7 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
log.Check(err, "removing old junkfilter bloom filter file", slog.String("path", bloomPath))
|
||||
|
||||
// Open junk filter, this creates new files.
|
||||
jf, _, err := acc.OpenJunkFilter(ctx, ctl.log)
|
||||
jf, _, err := acc.OpenJunkFilter(ctx, log)
|
||||
ctl.xcheck(err, "open new junk filter")
|
||||
defer func() {
|
||||
if jf == nil {
|
||||
|
@ -645,14 +787,14 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
q.FilterEqual("Expunged", false)
|
||||
err = q.ForEach(func(m store.Message) error {
|
||||
total++
|
||||
ok, err := acc.TrainMessage(ctx, ctl.log, jf, m)
|
||||
ok, err := acc.TrainMessage(ctx, log, jf, m)
|
||||
if ok {
|
||||
trained++
|
||||
}
|
||||
return err
|
||||
})
|
||||
ctl.xcheck(err, "training messages")
|
||||
ctl.log.Info("retrained messages", slog.Int("total", total), slog.Int("trained", trained))
|
||||
log.Info("retrained messages", slog.Int("total", total), slog.Int("trained", trained))
|
||||
|
||||
// Close junk filter, marking success.
|
||||
err = jf.Close()
|
||||
|
@ -669,7 +811,7 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
< stream
|
||||
*/
|
||||
account := ctl.xread()
|
||||
acc, err := store.OpenAccount(ctl.log, account)
|
||||
acc, err := store.OpenAccount(log, account)
|
||||
ctl.xcheck(err, "open account")
|
||||
defer func() {
|
||||
if acc != nil {
|
||||
|
@ -744,7 +886,7 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
const batchSize = 10000
|
||||
|
||||
xfixmsgsize := func(accName string) {
|
||||
acc, err := store.OpenAccount(ctl.log, accName)
|
||||
acc, err := store.OpenAccount(log, accName)
|
||||
ctl.xcheck(err, "open account")
|
||||
defer func() {
|
||||
err := acc.Close()
|
||||
|
@ -879,7 +1021,7 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
const batchSize = 100
|
||||
|
||||
xreparseAccount := func(accName string) {
|
||||
acc, err := store.OpenAccount(ctl.log, accName)
|
||||
acc, err := store.OpenAccount(log, accName)
|
||||
ctl.xcheck(err, "open account")
|
||||
defer func() {
|
||||
err := acc.Close()
|
||||
|
@ -955,7 +1097,7 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
w := ctl.writer()
|
||||
|
||||
xreassignThreads := func(accName string) {
|
||||
acc, err := store.OpenAccount(ctl.log, accName)
|
||||
acc, err := store.OpenAccount(log, accName)
|
||||
ctl.xcheck(err, "open account")
|
||||
defer func() {
|
||||
err := acc.Close()
|
||||
|
@ -963,20 +1105,20 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
|||
}()
|
||||
|
||||
// We don't want to step on an existing upgrade process.
|
||||
err = acc.ThreadingWait(ctl.log)
|
||||
err = acc.ThreadingWait(log)
|
||||
ctl.xcheck(err, "waiting for threading upgrade to finish")
|
||||
// todo: should we try to continue if the threading upgrade failed? only if there is a chance it will succeed this time...
|
||||
|
||||
// todo: reassigning isn't atomic (in a single transaction), ideally it would be (bstore would need to be able to handle large updates).
|
||||
const batchSize = 50000
|
||||
total, err := acc.ResetThreading(ctx, ctl.log, batchSize, true)
|
||||
total, err := acc.ResetThreading(ctx, log, batchSize, true)
|
||||
ctl.xcheck(err, "resetting threading fields")
|
||||
_, err = fmt.Fprintf(w, "New thread base subject assigned to %d message(s), starting to reassign threads...\n", total)
|
||||
ctl.xcheck(err, "write")
|
||||
|
||||
// Assign threads again. Ideally we would do this in a single transaction, but
|
||||
// bstore/boltdb cannot handle so many pending changes, so we set a high batchsize.
|
||||
err = acc.AssignThreads(ctx, ctl.log, nil, 0, 50000, w)
|
||||
err = acc.AssignThreads(ctx, log, nil, 0, 50000, w)
|
||||
ctl.xcheck(err, "reassign threads")
|
||||
|
||||
_, err = fmt.Fprintf(w, "Threads reassigned. You should invalidate messages stored at imap clients with the \"mox bumpuidvalidity account [mailbox]\" command.\n")
|
||||
|
|
58
ctl_test.go
58
ctl_test.go
|
@ -9,6 +9,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mjl-/mox/dmarcdb"
|
||||
"github.com/mjl-/mox/dns"
|
||||
|
@ -67,19 +68,66 @@ func TestCtl(t *testing.T) {
|
|||
err := queue.Init()
|
||||
tcheck(t, err, "queue init")
|
||||
|
||||
// "queue"
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueList(ctl)
|
||||
ctlcmdQueueHoldrulesList(ctl)
|
||||
})
|
||||
|
||||
// "queuekick"
|
||||
// All messages.
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueKick(ctl, 0, "", "", "")
|
||||
ctlcmdQueueHoldrulesAdd(ctl, "", "", "")
|
||||
})
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueHoldrulesAdd(ctl, "mjl", "", "")
|
||||
})
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueHoldrulesAdd(ctl, "", "☺.mox.example", "")
|
||||
})
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueHoldrulesAdd(ctl, "mox", "☺.mox.example", "example.com")
|
||||
})
|
||||
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueHoldrulesRemove(ctl, 1)
|
||||
})
|
||||
|
||||
// Has entries now.
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueHoldrulesList(ctl)
|
||||
})
|
||||
|
||||
// "queuelist"
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueList(ctl, queue.Filter{})
|
||||
})
|
||||
|
||||
// "queueholdset"
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueHoldSet(ctl, queue.Filter{}, true)
|
||||
})
|
||||
|
||||
// "queueschedule"
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueSchedule(ctl, queue.Filter{}, true, time.Minute)
|
||||
})
|
||||
|
||||
// "queuetransport"
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueTransport(ctl, queue.Filter{}, "socks")
|
||||
})
|
||||
|
||||
// "queuerequiretls"
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueRequireTLS(ctl, queue.Filter{}, nil)
|
||||
})
|
||||
|
||||
// "queuefail"
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueFail(ctl, queue.Filter{})
|
||||
})
|
||||
|
||||
// "queuedrop"
|
||||
testctl(func(ctl *ctl) {
|
||||
ctlcmdQueueDrop(ctl, 0, "", "")
|
||||
ctlcmdQueueDrop(ctl, queue.Filter{})
|
||||
})
|
||||
|
||||
// no "queuedump", we don't have a message to dump, and the commands exits without a message.
|
||||
|
|
272
doc.go
272
doc.go
|
@ -25,9 +25,17 @@ any parameters. Followed by the help and usage information for each command.
|
|||
mox setaccountpassword account
|
||||
mox setadminpassword
|
||||
mox loglevels [level [pkg]]
|
||||
mox queue list
|
||||
mox queue kick [-id id] [-todomain domain] [-recipient address] [-transport transport]
|
||||
mox queue drop [-id id] [-todomain domain] [-recipient address]
|
||||
mox queue holdrules list
|
||||
mox queue holdrules add [ruleflags]
|
||||
mox queue holdrules remove ruleid
|
||||
mox queue list [filterflags]
|
||||
mox queue hold [filterflags]
|
||||
mox queue unhold [filterflags]
|
||||
mox queue schedule [filterflags] duration
|
||||
mox queue transport [filterflags] transport
|
||||
mox queue requiretls [filterflags] {yes | no | default}
|
||||
mox queue fail [filterflags]
|
||||
mox queue drop [filterflags]
|
||||
mox queue dump id
|
||||
mox import maildir accountname mailboxname maildir
|
||||
mox import mbox accountname mailboxname mbox
|
||||
|
@ -195,37 +203,227 @@ Valid labels: error, info, debug, trace, traceauth, tracedata.
|
|||
|
||||
usage: mox loglevels [level [pkg]]
|
||||
|
||||
# mox queue holdrules list
|
||||
|
||||
List hold rules for the delivery queue.
|
||||
|
||||
Messages submitted to the queue that match a hold rule will be marked as on hold
|
||||
and not scheduled for delivery.
|
||||
|
||||
usage: mox queue holdrules list
|
||||
|
||||
# mox queue holdrules add
|
||||
|
||||
Add hold rule for the delivery queue.
|
||||
|
||||
Add a hold rule to mark matching newly submitted messages as on hold. Set the
|
||||
matching rules with the flags. Don't specify any flags to match all submitted
|
||||
messages.
|
||||
|
||||
usage: mox queue holdrules add [ruleflags]
|
||||
-account string
|
||||
account submitting the message
|
||||
-recipientdom string
|
||||
recipient domain
|
||||
-senderdom string
|
||||
sender domain
|
||||
|
||||
# mox queue holdrules remove
|
||||
|
||||
Remove hold rule for the delivery queue.
|
||||
|
||||
Remove a hold rule by its id.
|
||||
|
||||
usage: mox queue holdrules remove ruleid
|
||||
|
||||
# mox queue list
|
||||
|
||||
List messages in the delivery queue.
|
||||
List matching messages in the delivery queue.
|
||||
|
||||
This prints the message with its ID, last and next delivery attempts, last
|
||||
error.
|
||||
Prints the message with its ID, last and next delivery attempts, last error.
|
||||
|
||||
usage: mox queue list
|
||||
usage: mox queue list [filterflags]
|
||||
-account string
|
||||
account that queued the message
|
||||
-from string
|
||||
from address of message, use "@example.com" to match all messages for a domain
|
||||
-hold value
|
||||
true or false, whether to match only messages that are (not) on hold
|
||||
-ids value
|
||||
comma-separated list of message IDs
|
||||
-nextattempt string
|
||||
filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-submitted string
|
||||
filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-to string
|
||||
recipient address of message, use "@example.com" to match all messages for a domain
|
||||
-transport value
|
||||
transport to use for messages, empty string sets the default behaviour
|
||||
|
||||
# mox queue kick
|
||||
# mox queue hold
|
||||
|
||||
Schedule matching messages in the queue for immediate delivery.
|
||||
Mark matching messages on hold.
|
||||
|
||||
Messages deliveries are normally attempted with exponential backoff. The first
|
||||
retry after 7.5 minutes, and doubling each time. Kicking messages sets their
|
||||
next scheduled attempt to now, it can cause delivery to fail earlier than
|
||||
without rescheduling.
|
||||
Messages that are on hold are not delivered until marked as off hold again, or
|
||||
otherwise handled by the admin.
|
||||
|
||||
With the -transport flag, future delivery attempts are done using the specified
|
||||
transport. Transports can be configured in mox.conf, e.g. to submit to a remote
|
||||
queue over SMTP.
|
||||
usage: mox queue hold [filterflags]
|
||||
-account string
|
||||
account that queued the message
|
||||
-from string
|
||||
from address of message, use "@example.com" to match all messages for a domain
|
||||
-hold value
|
||||
true or false, whether to match only messages that are (not) on hold
|
||||
-ids value
|
||||
comma-separated list of message IDs
|
||||
-nextattempt string
|
||||
filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-submitted string
|
||||
filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-to string
|
||||
recipient address of message, use "@example.com" to match all messages for a domain
|
||||
-transport value
|
||||
transport to use for messages, empty string sets the default behaviour
|
||||
|
||||
usage: mox queue kick [-id id] [-todomain domain] [-recipient address] [-transport transport]
|
||||
-id int
|
||||
id of message in queue
|
||||
-recipient string
|
||||
recipient email address
|
||||
-todomain string
|
||||
destination domain of messages
|
||||
-transport string
|
||||
transport to use for the next delivery
|
||||
# mox queue unhold
|
||||
|
||||
Mark matching messages off hold.
|
||||
|
||||
Once off hold, messages can be delivered according to their current next
|
||||
delivery attempt. See the "queue schedule" command.
|
||||
|
||||
usage: mox queue unhold [filterflags]
|
||||
-account string
|
||||
account that queued the message
|
||||
-from string
|
||||
from address of message, use "@example.com" to match all messages for a domain
|
||||
-hold value
|
||||
true or false, whether to match only messages that are (not) on hold
|
||||
-ids value
|
||||
comma-separated list of message IDs
|
||||
-nextattempt string
|
||||
filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-submitted string
|
||||
filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-to string
|
||||
recipient address of message, use "@example.com" to match all messages for a domain
|
||||
-transport value
|
||||
transport to use for messages, empty string sets the default behaviour
|
||||
|
||||
# mox queue schedule
|
||||
|
||||
Change next delivery attempt for matching messages.
|
||||
|
||||
The next delivery attempt is adjusted by the duration parameter. If the -now
|
||||
flag is set, the new delivery attempt is set to the duration added to the
|
||||
current time, instead of added to the current scheduled time.
|
||||
|
||||
Schedule immediate delivery with "mox queue schedule -now 0".
|
||||
|
||||
usage: mox queue schedule [filterflags] duration
|
||||
-account string
|
||||
account that queued the message
|
||||
-from string
|
||||
from address of message, use "@example.com" to match all messages for a domain
|
||||
-hold value
|
||||
true or false, whether to match only messages that are (not) on hold
|
||||
-ids value
|
||||
comma-separated list of message IDs
|
||||
-nextattempt string
|
||||
filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-now
|
||||
schedule for duration relative to current time instead of relative to current next delivery attempt for messages
|
||||
-submitted string
|
||||
filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-to string
|
||||
recipient address of message, use "@example.com" to match all messages for a domain
|
||||
-transport value
|
||||
transport to use for messages, empty string sets the default behaviour
|
||||
|
||||
# mox queue transport
|
||||
|
||||
Set transport for matching messages.
|
||||
|
||||
By default, the routing rules determine how a message is delivered. The default
|
||||
and common case is direct delivery with SMTP. Messages can get a previously
|
||||
configured transport assigned to use for delivery, e.g. using submission to
|
||||
another mail server or with connections over a SOCKS proxy.
|
||||
|
||||
usage: mox queue transport [filterflags] transport
|
||||
-account string
|
||||
account that queued the message
|
||||
-from string
|
||||
from address of message, use "@example.com" to match all messages for a domain
|
||||
-hold value
|
||||
true or false, whether to match only messages that are (not) on hold
|
||||
-ids value
|
||||
comma-separated list of message IDs
|
||||
-nextattempt string
|
||||
filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-submitted string
|
||||
filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-to string
|
||||
recipient address of message, use "@example.com" to match all messages for a domain
|
||||
-transport value
|
||||
transport to use for messages, empty string sets the default behaviour
|
||||
|
||||
# mox queue requiretls
|
||||
|
||||
Set TLS requirements for delivery of matching messages.
|
||||
|
||||
Value "yes" is handled as if the RequireTLS extension was specified during
|
||||
submission.
|
||||
|
||||
Value "no" is handled as if the message has a header "TLS-Required: No". This
|
||||
header is not added by the queue. If messages without this header are relayed
|
||||
through other mail servers they will apply their own default TLS policy.
|
||||
|
||||
Value "default" is the default behaviour, currently for unverified opportunistic
|
||||
TLS.
|
||||
|
||||
usage: mox queue requiretls [filterflags] {yes | no | default}
|
||||
-account string
|
||||
account that queued the message
|
||||
-from string
|
||||
from address of message, use "@example.com" to match all messages for a domain
|
||||
-hold value
|
||||
true or false, whether to match only messages that are (not) on hold
|
||||
-ids value
|
||||
comma-separated list of message IDs
|
||||
-nextattempt string
|
||||
filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-submitted string
|
||||
filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-to string
|
||||
recipient address of message, use "@example.com" to match all messages for a domain
|
||||
-transport value
|
||||
transport to use for messages, empty string sets the default behaviour
|
||||
|
||||
# mox queue fail
|
||||
|
||||
Fail delivery of matching messages, delivering DSNs.
|
||||
|
||||
Failing a message is handled similar to how delivery is given up after all
|
||||
delivery attempts failed. The DSN (delivery status notification) message
|
||||
contains a line saying the message was canceled by the admin.
|
||||
|
||||
usage: mox queue fail [filterflags]
|
||||
-account string
|
||||
account that queued the message
|
||||
-from string
|
||||
from address of message, use "@example.com" to match all messages for a domain
|
||||
-hold value
|
||||
true or false, whether to match only messages that are (not) on hold
|
||||
-ids value
|
||||
comma-separated list of message IDs
|
||||
-nextattempt string
|
||||
filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-submitted string
|
||||
filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-to string
|
||||
recipient address of message, use "@example.com" to match all messages for a domain
|
||||
-transport value
|
||||
transport to use for messages, empty string sets the default behaviour
|
||||
|
||||
# mox queue drop
|
||||
|
||||
|
@ -234,13 +432,23 @@ Remove matching messages from the queue.
|
|||
Dangerous operation, this completely removes the message. If you want to store
|
||||
the message, use "queue dump" before removing.
|
||||
|
||||
usage: mox queue drop [-id id] [-todomain domain] [-recipient address]
|
||||
-id int
|
||||
id of message in queue
|
||||
-recipient string
|
||||
recipient email address
|
||||
-todomain string
|
||||
destination domain of messages
|
||||
usage: mox queue drop [filterflags]
|
||||
-account string
|
||||
account that queued the message
|
||||
-from string
|
||||
from address of message, use "@example.com" to match all messages for a domain
|
||||
-hold value
|
||||
true or false, whether to match only messages that are (not) on hold
|
||||
-ids value
|
||||
comma-separated list of message IDs
|
||||
-nextattempt string
|
||||
filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-submitted string
|
||||
filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)
|
||||
-to string
|
||||
recipient address of message, use "@example.com" to match all messages for a domain
|
||||
-transport value
|
||||
transport to use for messages, empty string sets the default behaviour
|
||||
|
||||
# mox queue dump
|
||||
|
||||
|
|
1
lib.ts
1
lib.ts
|
@ -215,6 +215,7 @@ const attr = {
|
|||
method: (s: string) => _attr('method', s),
|
||||
autocomplete: (s: string) => _attr('autocomplete', s),
|
||||
list: (s: string) => _attr('list', s),
|
||||
form: (s: string) => _attr('form', s),
|
||||
}
|
||||
const style = (x: {[k: string]: string | number}) => { return {_styles: x}}
|
||||
const prop = (x: {[k: string]: any}) => { return {_props: x}}
|
||||
|
|
128
main.go
128
main.go
|
@ -99,8 +99,16 @@ var commands = []struct {
|
|||
{"setaccountpassword", cmdSetaccountpassword},
|
||||
{"setadminpassword", cmdSetadminpassword},
|
||||
{"loglevels", cmdLoglevels},
|
||||
{"queue holdrules list", cmdQueueHoldrulesList},
|
||||
{"queue holdrules add", cmdQueueHoldrulesAdd},
|
||||
{"queue holdrules remove", cmdQueueHoldrulesRemove},
|
||||
{"queue list", cmdQueueList},
|
||||
{"queue kick", cmdQueueKick},
|
||||
{"queue hold", cmdQueueHold},
|
||||
{"queue unhold", cmdQueueUnhold},
|
||||
{"queue schedule", cmdQueueSchedule},
|
||||
{"queue transport", cmdQueueTransport},
|
||||
{"queue requiretls", cmdQueueRequireTLS},
|
||||
{"queue fail", cmdQueueFail},
|
||||
{"queue drop", cmdQueueDrop},
|
||||
{"queue dump", cmdQueueDump},
|
||||
{"import maildir", cmdImportMaildir},
|
||||
|
@ -1334,124 +1342,6 @@ func ctlcmdDeliver(ctl *ctl, address string) {
|
|||
}
|
||||
}
|
||||
|
||||
func cmdQueueList(c *cmd) {
|
||||
c.help = `List messages in the delivery queue.
|
||||
|
||||
This prints the message with its ID, last and next delivery attempts, last
|
||||
error.
|
||||
`
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueList(xctl())
|
||||
}
|
||||
|
||||
func ctlcmdQueueList(ctl *ctl) {
|
||||
ctl.xwrite("queue")
|
||||
ctl.xreadok()
|
||||
if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueKick(c *cmd) {
|
||||
c.params = "[-id id] [-todomain domain] [-recipient address] [-transport transport]"
|
||||
c.help = `Schedule matching messages in the queue for immediate delivery.
|
||||
|
||||
Messages deliveries are normally attempted with exponential backoff. The first
|
||||
retry after 7.5 minutes, and doubling each time. Kicking messages sets their
|
||||
next scheduled attempt to now, it can cause delivery to fail earlier than
|
||||
without rescheduling.
|
||||
|
||||
With the -transport flag, future delivery attempts are done using the specified
|
||||
transport. Transports can be configured in mox.conf, e.g. to submit to a remote
|
||||
queue over SMTP.
|
||||
`
|
||||
var id int64
|
||||
var todomain, recipient, transport string
|
||||
c.flag.Int64Var(&id, "id", 0, "id of message in queue")
|
||||
c.flag.StringVar(&todomain, "todomain", "", "destination domain of messages")
|
||||
c.flag.StringVar(&recipient, "recipient", "", "recipient email address")
|
||||
c.flag.StringVar(&transport, "transport", "", "transport to use for the next delivery")
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueKick(xctl(), id, todomain, recipient, transport)
|
||||
}
|
||||
|
||||
func ctlcmdQueueKick(ctl *ctl, id int64, todomain, recipient, transport string) {
|
||||
ctl.xwrite("queuekick")
|
||||
ctl.xwrite(fmt.Sprintf("%d", id))
|
||||
ctl.xwrite(todomain)
|
||||
ctl.xwrite(recipient)
|
||||
ctl.xwrite(transport)
|
||||
count := ctl.xread()
|
||||
line := ctl.xread()
|
||||
if line == "ok" {
|
||||
fmt.Printf("%s messages scheduled\n", count)
|
||||
} else {
|
||||
log.Fatalf("scheduling messages for immediate delivery: %s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueDrop(c *cmd) {
|
||||
c.params = "[-id id] [-todomain domain] [-recipient address]"
|
||||
c.help = `Remove matching messages from the queue.
|
||||
|
||||
Dangerous operation, this completely removes the message. If you want to store
|
||||
the message, use "queue dump" before removing.
|
||||
`
|
||||
var id int64
|
||||
var todomain, recipient string
|
||||
c.flag.Int64Var(&id, "id", 0, "id of message in queue")
|
||||
c.flag.StringVar(&todomain, "todomain", "", "destination domain of messages")
|
||||
c.flag.StringVar(&recipient, "recipient", "", "recipient email address")
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueDrop(xctl(), id, todomain, recipient)
|
||||
}
|
||||
|
||||
func ctlcmdQueueDrop(ctl *ctl, id int64, todomain, recipient string) {
|
||||
ctl.xwrite("queuedrop")
|
||||
ctl.xwrite(fmt.Sprintf("%d", id))
|
||||
ctl.xwrite(todomain)
|
||||
ctl.xwrite(recipient)
|
||||
count := ctl.xread()
|
||||
line := ctl.xread()
|
||||
if line == "ok" {
|
||||
fmt.Printf("%s messages dropped\n", count)
|
||||
} else {
|
||||
log.Fatalf("scheduling messages for immediate delivery: %s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueDump(c *cmd) {
|
||||
c.params = "id"
|
||||
c.help = `Dump a message from the queue.
|
||||
|
||||
The message is printed to stdout and is in standard internet mail format.
|
||||
`
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueDump(xctl(), args[0])
|
||||
}
|
||||
|
||||
func ctlcmdQueueDump(ctl *ctl, id string) {
|
||||
ctl.xwrite("queuedump")
|
||||
ctl.xwrite(id)
|
||||
ctl.xreadok()
|
||||
if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdDKIMGenrsa(c *cmd) {
|
||||
c.params = ">$selector._domainkey.$domain.rsa2048.privatekey.pkcs8.pem"
|
||||
c.help = `Generate a new 2048 bit RSA private key for use with DKIM.
|
||||
|
|
|
@ -28,6 +28,12 @@ groups:
|
|||
annotations:
|
||||
summary: http 5xx responses from webserver
|
||||
|
||||
- alert: mox-queue-hold
|
||||
expr: mox_queue_hold > 0
|
||||
for: 2h
|
||||
annotations:
|
||||
summary: messages on hold in queue for at least two hours
|
||||
|
||||
- alert: mox-submission-errors
|
||||
expr: increase(mox_smtpserver_submission_total{result=~".*error"}[1h]) > 0
|
||||
annotations:
|
||||
|
|
405
queue.go
Normal file
405
queue.go
Normal file
|
@ -0,0 +1,405 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mjl-/mox/queue"
|
||||
)
|
||||
|
||||
func cmdQueueHoldrulesList(c *cmd) {
|
||||
c.help = `List hold rules for the delivery queue.
|
||||
|
||||
Messages submitted to the queue that match a hold rule will be marked as on hold
|
||||
and not scheduled for delivery.
|
||||
`
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueHoldrulesList(xctl())
|
||||
}
|
||||
|
||||
func ctlcmdQueueHoldrulesList(ctl *ctl) {
|
||||
ctl.xwrite("queueholdruleslist")
|
||||
ctl.xreadok()
|
||||
if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueHoldrulesAdd(c *cmd) {
|
||||
c.params = "[ruleflags]"
|
||||
c.help = `Add hold rule for the delivery queue.
|
||||
|
||||
Add a hold rule to mark matching newly submitted messages as on hold. Set the
|
||||
matching rules with the flags. Don't specify any flags to match all submitted
|
||||
messages.
|
||||
`
|
||||
var account, senderDomain, recipientDomain string
|
||||
c.flag.StringVar(&account, "account", "", "account submitting the message")
|
||||
c.flag.StringVar(&senderDomain, "senderdom", "", "sender domain")
|
||||
c.flag.StringVar(&recipientDomain, "recipientdom", "", "recipient domain")
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueHoldrulesAdd(xctl(), account, senderDomain, recipientDomain)
|
||||
}
|
||||
|
||||
func ctlcmdQueueHoldrulesAdd(ctl *ctl, account, senderDomain, recipientDomain string) {
|
||||
ctl.xwrite("queueholdrulesadd")
|
||||
ctl.xwrite(account)
|
||||
ctl.xwrite(senderDomain)
|
||||
ctl.xwrite(recipientDomain)
|
||||
ctl.xreadok()
|
||||
}
|
||||
|
||||
func cmdQueueHoldrulesRemove(c *cmd) {
|
||||
c.params = "ruleid"
|
||||
c.help = `Remove hold rule for the delivery queue.
|
||||
|
||||
Remove a hold rule by its id.
|
||||
`
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
id, err := strconv.ParseInt(args[0], 10, 64)
|
||||
xcheckf(err, "parsing id")
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueHoldrulesRemove(xctl(), id)
|
||||
}
|
||||
|
||||
func ctlcmdQueueHoldrulesRemove(ctl *ctl, id int64) {
|
||||
ctl.xwrite("queueholdrulesremove")
|
||||
ctl.xwrite(fmt.Sprintf("%d", id))
|
||||
ctl.xreadok()
|
||||
}
|
||||
|
||||
// flagFilter is used by many of the queue commands to accept flags for filtering
|
||||
// the messages the operation applies to.
|
||||
func flagFilter(fs *flag.FlagSet, f *queue.Filter) {
|
||||
fs.Func("ids", "comma-separated list of message IDs", func(v string) error {
|
||||
for _, s := range strings.Split(v, ",") {
|
||||
id, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.IDs = append(f.IDs, id)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
fs.StringVar(&f.Account, "account", "", "account that queued the message")
|
||||
fs.StringVar(&f.From, "from", "", `from address of message, use "@example.com" to match all messages for a domain`)
|
||||
fs.StringVar(&f.To, "to", "", `recipient address of message, use "@example.com" to match all messages for a domain`)
|
||||
fs.StringVar(&f.Submitted, "submitted", "", `filter by time of submission relative to now, value must start with "<" (before now) or ">" (after now)`)
|
||||
fs.StringVar(&f.NextAttempt, "nextattempt", "", `filter by time of next delivery attempt relative to now, value must start with "<" (before now) or ">" (after now)`)
|
||||
fs.Func("transport", "transport to use for messages, empty string sets the default behaviour", func(v string) error {
|
||||
f.Transport = &v
|
||||
return nil
|
||||
})
|
||||
fs.Func("hold", "true or false, whether to match only messages that are (not) on hold", func(v string) error {
|
||||
var hold bool
|
||||
if v == "true" {
|
||||
hold = true
|
||||
} else if v == "false" {
|
||||
hold = false
|
||||
} else {
|
||||
return fmt.Errorf("bad value %q", v)
|
||||
}
|
||||
f.Hold = &hold
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func cmdQueueList(c *cmd) {
|
||||
c.params = "[filterflags]"
|
||||
c.help = `List matching messages in the delivery queue.
|
||||
|
||||
Prints the message with its ID, last and next delivery attempts, last error.
|
||||
`
|
||||
var f queue.Filter
|
||||
flagFilter(c.flag, &f)
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueList(xctl(), f)
|
||||
}
|
||||
|
||||
func xctlwritequeuefilter(ctl *ctl, f queue.Filter) {
|
||||
fbuf, err := json.Marshal(f)
|
||||
xcheckf(err, "marshal filter")
|
||||
ctl.xwrite(string(fbuf))
|
||||
}
|
||||
|
||||
func ctlcmdQueueList(ctl *ctl, f queue.Filter) {
|
||||
ctl.xwrite("queuelist")
|
||||
xctlwritequeuefilter(ctl, f)
|
||||
ctl.xreadok()
|
||||
if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueHold(c *cmd) {
|
||||
c.params = "[filterflags]"
|
||||
c.help = `Mark matching messages on hold.
|
||||
|
||||
Messages that are on hold are not delivered until marked as off hold again, or
|
||||
otherwise handled by the admin.
|
||||
`
|
||||
var f queue.Filter
|
||||
flagFilter(c.flag, &f)
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueHoldSet(xctl(), f, true)
|
||||
}
|
||||
|
||||
func cmdQueueUnhold(c *cmd) {
|
||||
c.params = "[filterflags]"
|
||||
c.help = `Mark matching messages off hold.
|
||||
|
||||
Once off hold, messages can be delivered according to their current next
|
||||
delivery attempt. See the "queue schedule" command.
|
||||
`
|
||||
var f queue.Filter
|
||||
flagFilter(c.flag, &f)
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueHoldSet(xctl(), f, false)
|
||||
}
|
||||
|
||||
func ctlcmdQueueHoldSet(ctl *ctl, f queue.Filter, hold bool) {
|
||||
ctl.xwrite("queueholdset")
|
||||
xctlwritequeuefilter(ctl, f)
|
||||
if hold {
|
||||
ctl.xwrite("true")
|
||||
} else {
|
||||
ctl.xwrite("false")
|
||||
}
|
||||
line := ctl.xread()
|
||||
if line == "ok" {
|
||||
fmt.Printf("%s messages changed\n", ctl.xread())
|
||||
} else {
|
||||
log.Fatalf("%s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueSchedule(c *cmd) {
|
||||
c.params = "[filterflags] duration"
|
||||
c.help = `Change next delivery attempt for matching messages.
|
||||
|
||||
The next delivery attempt is adjusted by the duration parameter. If the -now
|
||||
flag is set, the new delivery attempt is set to the duration added to the
|
||||
current time, instead of added to the current scheduled time.
|
||||
|
||||
Schedule immediate delivery with "mox queue schedule -now 0".
|
||||
`
|
||||
var fromNow bool
|
||||
c.flag.BoolVar(&fromNow, "now", false, "schedule for duration relative to current time instead of relative to current next delivery attempt for messages")
|
||||
var f queue.Filter
|
||||
flagFilter(c.flag, &f)
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
d, err := time.ParseDuration(args[0])
|
||||
xcheckf(err, "parsing duration %q", args[0])
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueSchedule(xctl(), f, fromNow, d)
|
||||
}
|
||||
|
||||
func ctlcmdQueueSchedule(ctl *ctl, f queue.Filter, fromNow bool, d time.Duration) {
|
||||
ctl.xwrite("queueschedule")
|
||||
xctlwritequeuefilter(ctl, f)
|
||||
if fromNow {
|
||||
ctl.xwrite("yes")
|
||||
} else {
|
||||
ctl.xwrite("")
|
||||
}
|
||||
ctl.xwrite(d.String())
|
||||
line := ctl.xread()
|
||||
if line == "ok" {
|
||||
fmt.Printf("%s messages rescheduled\n", ctl.xread())
|
||||
} else {
|
||||
log.Fatalf("%s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueTransport(c *cmd) {
|
||||
c.params = "[filterflags] transport"
|
||||
c.help = `Set transport for matching messages.
|
||||
|
||||
By default, the routing rules determine how a message is delivered. The default
|
||||
and common case is direct delivery with SMTP. Messages can get a previously
|
||||
configured transport assigned to use for delivery, e.g. using submission to
|
||||
another mail server or with connections over a SOCKS proxy.
|
||||
`
|
||||
var f queue.Filter
|
||||
flagFilter(c.flag, &f)
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueTransport(xctl(), f, args[0])
|
||||
}
|
||||
|
||||
func ctlcmdQueueTransport(ctl *ctl, f queue.Filter, transport string) {
|
||||
ctl.xwrite("queuetransport")
|
||||
xctlwritequeuefilter(ctl, f)
|
||||
ctl.xwrite(transport)
|
||||
line := ctl.xread()
|
||||
if line == "ok" {
|
||||
fmt.Printf("%s messages changed\n", ctl.xread())
|
||||
} else {
|
||||
log.Fatalf("%s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueRequireTLS(c *cmd) {
|
||||
c.params = "[filterflags] {yes | no | default}"
|
||||
c.help = `Set TLS requirements for delivery of matching messages.
|
||||
|
||||
Value "yes" is handled as if the RequireTLS extension was specified during
|
||||
submission.
|
||||
|
||||
Value "no" is handled as if the message has a header "TLS-Required: No". This
|
||||
header is not added by the queue. If messages without this header are relayed
|
||||
through other mail servers they will apply their own default TLS policy.
|
||||
|
||||
Value "default" is the default behaviour, currently for unverified opportunistic
|
||||
TLS.
|
||||
`
|
||||
var f queue.Filter
|
||||
flagFilter(c.flag, &f)
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
var tlsreq *bool
|
||||
switch args[0] {
|
||||
case "yes":
|
||||
v := true
|
||||
tlsreq = &v
|
||||
case "no":
|
||||
v := false
|
||||
tlsreq = &v
|
||||
case "default":
|
||||
default:
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueRequireTLS(xctl(), f, tlsreq)
|
||||
}
|
||||
|
||||
func ctlcmdQueueRequireTLS(ctl *ctl, f queue.Filter, tlsreq *bool) {
|
||||
ctl.xwrite("queuerequiretls")
|
||||
xctlwritequeuefilter(ctl, f)
|
||||
var req string
|
||||
if tlsreq == nil {
|
||||
req = ""
|
||||
} else if *tlsreq {
|
||||
req = "true"
|
||||
} else {
|
||||
req = "false"
|
||||
}
|
||||
ctl.xwrite(req)
|
||||
line := ctl.xread()
|
||||
if line == "ok" {
|
||||
fmt.Printf("%s messages changed\n", ctl.xread())
|
||||
} else {
|
||||
log.Fatalf("%s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueFail(c *cmd) {
|
||||
c.params = "[filterflags]"
|
||||
c.help = `Fail delivery of matching messages, delivering DSNs.
|
||||
|
||||
Failing a message is handled similar to how delivery is given up after all
|
||||
delivery attempts failed. The DSN (delivery status notification) message
|
||||
contains a line saying the message was canceled by the admin.
|
||||
`
|
||||
var f queue.Filter
|
||||
flagFilter(c.flag, &f)
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueFail(xctl(), f)
|
||||
}
|
||||
|
||||
func ctlcmdQueueFail(ctl *ctl, f queue.Filter) {
|
||||
ctl.xwrite("queuefail")
|
||||
xctlwritequeuefilter(ctl, f)
|
||||
line := ctl.xread()
|
||||
if line == "ok" {
|
||||
fmt.Printf("%s messages marked as failed\n", ctl.xread())
|
||||
} else {
|
||||
log.Fatalf("%s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueDrop(c *cmd) {
|
||||
c.params = "[filterflags]"
|
||||
c.help = `Remove matching messages from the queue.
|
||||
|
||||
Dangerous operation, this completely removes the message. If you want to store
|
||||
the message, use "queue dump" before removing.
|
||||
`
|
||||
var f queue.Filter
|
||||
flagFilter(c.flag, &f)
|
||||
if len(c.Parse()) != 0 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueDrop(xctl(), f)
|
||||
}
|
||||
|
||||
func ctlcmdQueueDrop(ctl *ctl, f queue.Filter) {
|
||||
ctl.xwrite("queuedrop")
|
||||
xctlwritequeuefilter(ctl, f)
|
||||
line := ctl.xread()
|
||||
if line == "ok" {
|
||||
fmt.Printf("%s messages dropped\n", ctl.xread())
|
||||
} else {
|
||||
log.Fatalf("%s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func cmdQueueDump(c *cmd) {
|
||||
c.params = "id"
|
||||
c.help = `Dump a message from the queue.
|
||||
|
||||
The message is printed to stdout and is in standard internet mail format.
|
||||
`
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
mustLoadConfig()
|
||||
ctlcmdQueueDump(xctl(), args[0])
|
||||
}
|
||||
|
||||
func ctlcmdQueueDump(ctl *ctl, id string) {
|
||||
ctl.xwrite("queuedump")
|
||||
ctl.xwrite(id)
|
||||
ctl.xreadok()
|
||||
if _, err := io.Copy(os.Stdout, ctl.reader()); err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
}
|
364
queue/queue.go
364
queue/queue.go
|
@ -61,11 +61,17 @@ var (
|
|||
"result", // ok, timeout, canceled, temperror, permerror, error
|
||||
},
|
||||
)
|
||||
metricHold = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "mox_queue_hold",
|
||||
Help: "Messages in queue that are on hold.",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
var jitter = mox.NewPseudoRand()
|
||||
|
||||
var DBTypes = []any{Msg{}} // Types stored in DB.
|
||||
var DBTypes = []any{Msg{}, HoldRule{}} // Types stored in DB.
|
||||
var DB *bstore.DB // Exported for making backups.
|
||||
|
||||
// Allow requesting delivery starting from up to this interval from time of submission.
|
||||
|
@ -74,6 +80,27 @@ const FutureReleaseIntervalMax = 60 * 24 * time.Hour
|
|||
// Set for mox localserve, to prevent queueing.
|
||||
var Localserve bool
|
||||
|
||||
// HoldRule is a set of conditions that cause a matching message to be marked as on
|
||||
// hold when it is queued. All-empty conditions matches all messages, effectively
|
||||
// pausing the entire queue.
|
||||
type HoldRule struct {
|
||||
ID int64
|
||||
Account string
|
||||
SenderDomain dns.Domain
|
||||
RecipientDomain dns.Domain
|
||||
SenderDomainStr string // Unicode.
|
||||
RecipientDomainStr string // Unicode.
|
||||
}
|
||||
|
||||
func (pr HoldRule) All() bool {
|
||||
pr.ID = 0
|
||||
return pr == HoldRule{}
|
||||
}
|
||||
|
||||
func (pr HoldRule) matches(m Msg) bool {
|
||||
return pr.All() || pr.Account == m.SenderAccount || pr.SenderDomainStr == m.SenderDomainStr || pr.RecipientDomainStr == m.RecipientDomainStr
|
||||
}
|
||||
|
||||
// Msg is a message in the queue.
|
||||
//
|
||||
// Use MakeMsg to make a message with fields that Add needs. Add will further set
|
||||
|
@ -89,12 +116,14 @@ type Msg struct {
|
|||
BaseID int64 `bstore:"index"`
|
||||
|
||||
Queued time.Time `bstore:"default now"`
|
||||
Hold bool // If set, delivery won't be attempted.
|
||||
SenderAccount string // Failures are delivered back to this local account. Also used for routing.
|
||||
SenderLocalpart smtp.Localpart // Should be a local user and domain.
|
||||
SenderDomain dns.IPDomain
|
||||
SenderDomainStr string // For filtering, unicode.
|
||||
RecipientLocalpart smtp.Localpart // Typically a remote user and domain.
|
||||
RecipientDomain dns.IPDomain
|
||||
RecipientDomainStr string // For filtering.
|
||||
RecipientDomainStr string // For filtering, unicode.
|
||||
Attempts int // Next attempt is based on last attempt and exponential back off based on attempts.
|
||||
MaxAttempts int // Max number of attempts before giving up. If 0, then the default of 8 attempts is used instead.
|
||||
DialedIPs map[string][]net.IP // For each host, the IPs that were dialed. Used for IP selection for later attempts.
|
||||
|
@ -174,9 +203,20 @@ func Init() error {
|
|||
}
|
||||
return fmt.Errorf("open queue database: %s", err)
|
||||
}
|
||||
metricHoldUpdate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// When we update the gauge, we just get the full current value, not try to account
|
||||
// for adds/removes.
|
||||
func metricHoldUpdate() {
|
||||
count, err := bstore.QueryDB[Msg](context.Background(), DB).FilterNonzero(Msg{Hold: true}).Count()
|
||||
if err != nil {
|
||||
mlog.New("queue", nil).Errorx("querying number of queued messages that are on hold", err)
|
||||
}
|
||||
metricHold.Set(float64(count))
|
||||
}
|
||||
|
||||
// Shutdown closes the queue database. The delivery process isn't stopped. For tests only.
|
||||
func Shutdown() {
|
||||
err := DB.Close()
|
||||
|
@ -186,10 +226,85 @@ func Shutdown() {
|
|||
DB = nil
|
||||
}
|
||||
|
||||
// Filter filters messages to list or operate on. Used by admin web interface
|
||||
// and cli.
|
||||
//
|
||||
// Only non-empty/non-zero values are applied to the filter. Leaving all fields
|
||||
// empty/zero matches all messages.
|
||||
type Filter struct {
|
||||
IDs []int64
|
||||
Account string
|
||||
From string
|
||||
To string
|
||||
Hold *bool
|
||||
Submitted string // Whether submitted before/after a time relative to now. ">$duration" or "<$duration", also with "now" for duration.
|
||||
NextAttempt string // ">$duration" or "<$duration", also with "now" for duration.
|
||||
Transport *string
|
||||
}
|
||||
|
||||
func (f Filter) apply(q *bstore.Query[Msg]) error {
|
||||
if len(f.IDs) > 0 {
|
||||
q.FilterIDs(f.IDs)
|
||||
}
|
||||
applyTime := func(field string, s string) error {
|
||||
orig := s
|
||||
var before bool
|
||||
if strings.HasPrefix(s, "<") {
|
||||
before = true
|
||||
} else if !strings.HasPrefix(s, ">") {
|
||||
return fmt.Errorf(`must start with "<" for before or ">" for after a duration`)
|
||||
}
|
||||
s = s[1:]
|
||||
var t time.Time
|
||||
if s == "now" {
|
||||
t = time.Now()
|
||||
} else if d, err := time.ParseDuration(s); err != nil {
|
||||
return fmt.Errorf("parsing duration %q: %v", orig, err)
|
||||
} else {
|
||||
t = time.Now().Add(d)
|
||||
}
|
||||
if before {
|
||||
q.FilterLess(field, t)
|
||||
} else {
|
||||
q.FilterGreater(field, t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if f.Hold != nil {
|
||||
q.FilterEqual("Hold", *f.Hold)
|
||||
}
|
||||
if f.Submitted != "" {
|
||||
if err := applyTime("Queued", f.Submitted); err != nil {
|
||||
return fmt.Errorf("applying filter for submitted: %v", err)
|
||||
}
|
||||
}
|
||||
if f.NextAttempt != "" {
|
||||
if err := applyTime("NextAttempt", f.NextAttempt); err != nil {
|
||||
return fmt.Errorf("applying filter for next attempt: %v", err)
|
||||
}
|
||||
}
|
||||
if f.Account != "" {
|
||||
q.FilterNonzero(Msg{SenderAccount: f.Account})
|
||||
}
|
||||
if f.Transport != nil {
|
||||
q.FilterEqual("Transport", *f.Transport)
|
||||
}
|
||||
if f.From != "" || f.To != "" {
|
||||
q.FilterFn(func(m Msg) bool {
|
||||
return f.From != "" && strings.Contains(m.Sender().XString(true), f.From) || f.To != "" && strings.Contains(m.Recipient().XString(true), f.To)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns all messages in the delivery queue.
|
||||
// Ordered by earliest delivery attempt first.
|
||||
func List(ctx context.Context) ([]Msg, error) {
|
||||
qmsgs, err := bstore.QueryDB[Msg](ctx, DB).List()
|
||||
func List(ctx context.Context, f Filter) ([]Msg, error) {
|
||||
q := bstore.QueryDB[Msg](ctx, DB)
|
||||
if err := f.apply(q); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qmsgs, err := q.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -216,6 +331,59 @@ func Count(ctx context.Context) (int, error) {
|
|||
return bstore.QueryDB[Msg](ctx, DB).Count()
|
||||
}
|
||||
|
||||
// HoldRuleList returns all hold rules.
|
||||
func HoldRuleList(ctx context.Context) ([]HoldRule, error) {
|
||||
return bstore.QueryDB[HoldRule](ctx, DB).List()
|
||||
}
|
||||
|
||||
// HoldRuleAdd adds a new hold rule causing newly submitted messages to be marked
|
||||
// as "on hold", and existing matching messages too.
|
||||
func HoldRuleAdd(ctx context.Context, log mlog.Log, hr HoldRule) (HoldRule, error) {
|
||||
err := DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||
hr.ID = 0
|
||||
hr.SenderDomainStr = hr.SenderDomain.Name()
|
||||
hr.RecipientDomainStr = hr.RecipientDomain.Name()
|
||||
if err := tx.Insert(&hr); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("adding hold rule", slog.Any("holdrule", hr))
|
||||
|
||||
q := bstore.QueryTx[Msg](tx)
|
||||
if !hr.All() {
|
||||
q.FilterNonzero(Msg{
|
||||
SenderAccount: hr.Account,
|
||||
SenderDomainStr: hr.SenderDomainStr,
|
||||
RecipientDomainStr: hr.RecipientDomainStr,
|
||||
})
|
||||
}
|
||||
n, err := q.UpdateField("Hold", true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marking existing matching messages in queue on hold: %v", err)
|
||||
}
|
||||
log.Info("marked messages in queue as on hold", slog.Int("messages", n))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return HoldRule{}, err
|
||||
}
|
||||
queuekick()
|
||||
metricHoldUpdate()
|
||||
return hr, nil
|
||||
}
|
||||
|
||||
// HoldRuleRemove removes a hold rule. The Hold field of existing messages are not
|
||||
// changed.
|
||||
func HoldRuleRemove(ctx context.Context, log mlog.Log, holdRuleID int64) error {
|
||||
return DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||
hr := HoldRule{ID: holdRuleID}
|
||||
if err := tx.Get(&hr); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("removing hold rule", slog.Any("holdrule", hr))
|
||||
return tx.Delete(HoldRule{ID: holdRuleID})
|
||||
})
|
||||
}
|
||||
|
||||
// MakeMsg is a convenience function that sets the commonly used fields for a Msg.
|
||||
func MakeMsg(sender, recipient smtp.Path, has8bit, smtputf8 bool, size int64, messageID string, prefix []byte, requireTLS *bool, next time.Time) Msg {
|
||||
return Msg{
|
||||
|
@ -223,7 +391,6 @@ func MakeMsg(sender, recipient smtp.Path, has8bit, smtputf8 bool, size int64, me
|
|||
SenderDomain: sender.IPDomain,
|
||||
RecipientLocalpart: recipient.Localpart,
|
||||
RecipientDomain: recipient.IPDomain,
|
||||
RecipientDomainStr: formatIPDomain(recipient.IPDomain),
|
||||
Has8bit: has8bit,
|
||||
SMTPUTF8: smtputf8,
|
||||
Size: size,
|
||||
|
@ -242,25 +409,20 @@ func MakeMsg(sender, recipient smtp.Path, has8bit, smtputf8 bool, size int64, me
|
|||
//
|
||||
// ID of the messagse must be 0 and will be set after inserting in the queue.
|
||||
//
|
||||
// Add sets derived fields like RecipientDomainStr, and fields related to queueing,
|
||||
// such as Queued, NextAttempt, LastAttempt, LastError.
|
||||
// Add sets derived fields like SenderDomainStr and RecipientDomainStr, and fields
|
||||
// related to queueing, such as Queued, NextAttempt, LastAttempt, LastError.
|
||||
func Add(ctx context.Context, log mlog.Log, senderAccount string, msgFile *os.File, qml ...Msg) error {
|
||||
if len(qml) == 0 {
|
||||
return fmt.Errorf("must queue at least one message")
|
||||
}
|
||||
|
||||
for _, qm := range qml {
|
||||
for i, qm := range qml {
|
||||
if qm.ID != 0 {
|
||||
return fmt.Errorf("id of queued messages must be 0")
|
||||
}
|
||||
if qm.RecipientDomainStr == "" {
|
||||
return fmt.Errorf("recipient domain cannot be empty")
|
||||
}
|
||||
// Sanity check, internal consistency.
|
||||
rcptDom := formatIPDomain(qm.RecipientDomain)
|
||||
if qm.RecipientDomainStr != rcptDom {
|
||||
return fmt.Errorf("mismatch between recipient domain and string form of domain")
|
||||
}
|
||||
qml[i].SenderDomainStr = formatIPDomain(qm.SenderDomain)
|
||||
qml[i].RecipientDomainStr = formatIPDomain(qm.RecipientDomain)
|
||||
}
|
||||
|
||||
if Localserve {
|
||||
|
@ -307,12 +469,24 @@ func Add(ctx context.Context, log mlog.Log, senderAccount string, msgFile *os.Fi
|
|||
}
|
||||
}()
|
||||
|
||||
// Mark messages Hold if they match a hold rule.
|
||||
holdRules, err := bstore.QueryTx[HoldRule](tx).List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting queue hold rules")
|
||||
}
|
||||
|
||||
// Insert messages into queue. If there are multiple messages, they all get a
|
||||
// non-zero BaseID that is the Msg.ID of the first message inserted.
|
||||
var baseID int64
|
||||
for i := range qml {
|
||||
qml[i].SenderAccount = senderAccount
|
||||
qml[i].BaseID = baseID
|
||||
for _, hr := range holdRules {
|
||||
if hr.matches(qml[i]) {
|
||||
qml[i].Hold = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := tx.Insert(&qml[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -351,7 +525,15 @@ func Add(ctx context.Context, log mlog.Log, senderAccount string, msgFile *os.Fi
|
|||
tx = nil
|
||||
paths = nil
|
||||
|
||||
for _, m := range qml {
|
||||
if m.Hold {
|
||||
metricHoldUpdate()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
queuekick()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -374,36 +556,43 @@ func queuekick() {
|
|||
}
|
||||
}
|
||||
|
||||
// Kick sets the NextAttempt for messages matching all filter parameters (ID,
|
||||
// toDomain, recipient) that are nonzero, and kicks the queue, attempting delivery
|
||||
// of those messages. If all parameters are zero, all messages are kicked. If
|
||||
// transport is set, the delivery attempts for the matching messages will use the
|
||||
// transport. An empty string is the default transport, i.e. direct delivery.
|
||||
// Returns number of messages queued for immediate delivery.
|
||||
func Kick(ctx context.Context, ID int64, toDomain, recipient string, transport *string) (int, error) {
|
||||
// NextAttemptAdd adds a duration to the NextAttempt for all matching messages, and
|
||||
// kicks the queue.
|
||||
func NextAttemptAdd(ctx context.Context, f Filter, d time.Duration) (affected int, err error) {
|
||||
err = DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||
q := bstore.QueryDB[Msg](ctx, DB)
|
||||
if ID > 0 {
|
||||
q.FilterID(ID)
|
||||
if err := f.apply(q); err != nil {
|
||||
return err
|
||||
}
|
||||
if toDomain != "" {
|
||||
q.FilterEqual("RecipientDomainStr", toDomain)
|
||||
var msgs []Msg
|
||||
msgs, err := q.List()
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing matching messages: %v", err)
|
||||
}
|
||||
if recipient != "" {
|
||||
q.FilterFn(func(qm Msg) bool {
|
||||
return qm.Recipient().XString(true) == recipient
|
||||
for _, m := range msgs {
|
||||
m.NextAttempt = m.NextAttempt.Add(d)
|
||||
if err := tx.Update(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
affected = len(msgs)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
up := map[string]any{"NextAttempt": time.Now()}
|
||||
if transport != nil {
|
||||
if *transport != "" {
|
||||
_, ok := mox.Conf.Static.Transports[*transport]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unknown transport %q", *transport)
|
||||
queuekick()
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
// NextAttemptSet sets NextAttempt for all matching messages to a new time, and
|
||||
// kicks the queue.
|
||||
func NextAttemptSet(ctx context.Context, f Filter, t time.Time) (affected int, err error) {
|
||||
q := bstore.QueryDB[Msg](ctx, DB)
|
||||
if err := f.apply(q); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
up["Transport"] = *transport
|
||||
}
|
||||
n, err := q.UpdateFields(up)
|
||||
n, err := q.UpdateNonzero(Msg{NextAttempt: t})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("selecting and updating messages in queue: %v", err)
|
||||
}
|
||||
|
@ -411,21 +600,74 @@ func Kick(ctx context.Context, ID int64, toDomain, recipient string, transport *
|
|||
return n, nil
|
||||
}
|
||||
|
||||
// Drop removes messages from the queue that match all nonzero parameters.
|
||||
// If all parameters are zero, all messages are removed.
|
||||
// Returns number of messages removed.
|
||||
func Drop(ctx context.Context, log mlog.Log, ID int64, toDomain string, recipient string) (int, error) {
|
||||
// HoldSet sets Hold for all matching messages and kicks the queue.
|
||||
func HoldSet(ctx context.Context, f Filter, hold bool) (affected int, err error) {
|
||||
q := bstore.QueryDB[Msg](ctx, DB)
|
||||
if ID > 0 {
|
||||
q.FilterID(ID)
|
||||
if err := f.apply(q); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if toDomain != "" {
|
||||
q.FilterEqual("RecipientDomainStr", toDomain)
|
||||
n, err := q.UpdateFields(map[string]any{"Hold": hold})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("selecting and updating messages in queue: %v", err)
|
||||
}
|
||||
if recipient != "" {
|
||||
q.FilterFn(func(qm Msg) bool {
|
||||
return qm.Recipient().XString(true) == recipient
|
||||
queuekick()
|
||||
metricHoldUpdate()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// TransportSet changes the transport to use for the matching messages.
|
||||
func TransportSet(ctx context.Context, f Filter, transport string) (affected int, err error) {
|
||||
q := bstore.QueryDB[Msg](ctx, DB)
|
||||
if err := f.apply(q); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err := q.UpdateFields(map[string]any{"Transport": transport})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("selecting and updating messages in queue: %v", err)
|
||||
}
|
||||
queuekick()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Fail marks matching messages as failed for delivery and delivers DSNs to the sender.
|
||||
func Fail(ctx context.Context, log mlog.Log, f Filter) (affected int, err error) {
|
||||
err = DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||
q := bstore.QueryTx[Msg](tx)
|
||||
if err := f.apply(q); err != nil {
|
||||
return err
|
||||
}
|
||||
var msgs []Msg
|
||||
q.Gather(&msgs)
|
||||
n, err := q.Delete()
|
||||
if err != nil {
|
||||
return fmt.Errorf("selecting and deleting messages from queue: %v", err)
|
||||
}
|
||||
|
||||
var remoteMTA dsn.NameIP
|
||||
for _, m := range msgs {
|
||||
if m.LastAttempt == nil {
|
||||
now := time.Now()
|
||||
m.LastAttempt = &now
|
||||
}
|
||||
deliverDSNFailure(ctx, log, m, remoteMTA, "", "delivery canceled by admin", nil)
|
||||
}
|
||||
affected = n
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("selecting and updating messages in queue: %v", err)
|
||||
}
|
||||
queuekick()
|
||||
metricHoldUpdate()
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
// Drop removes matching messages from the queue.
|
||||
// Returns number of messages removed.
|
||||
func Drop(ctx context.Context, log mlog.Log, f Filter) (affected int, err error) {
|
||||
q := bstore.QueryDB[Msg](ctx, DB)
|
||||
if err := f.apply(q); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var msgs []Msg
|
||||
q.Gather(&msgs)
|
||||
|
@ -439,19 +681,20 @@ func Drop(ctx context.Context, log mlog.Log, ID int64, toDomain string, recipien
|
|||
log.Errorx("removing queue message from file system", err, slog.Int64("queuemsgid", m.ID), slog.String("path", p))
|
||||
}
|
||||
}
|
||||
queuekick()
|
||||
metricHoldUpdate()
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// SaveRequireTLS updates the RequireTLS field of the message with id.
|
||||
func SaveRequireTLS(ctx context.Context, id int64, requireTLS *bool) error {
|
||||
return DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||
m := Msg{ID: id}
|
||||
if err := tx.Get(&m); err != nil {
|
||||
return fmt.Errorf("get message: %w", err)
|
||||
// RequireTLSSet updates the RequireTLS field of matching messages.
|
||||
func RequireTLSSet(ctx context.Context, f Filter, requireTLS *bool) (affected int, err error) {
|
||||
q := bstore.QueryDB[Msg](ctx, DB)
|
||||
if err := f.apply(q); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
m.RequireTLS = requireTLS
|
||||
return tx.Update(&m)
|
||||
})
|
||||
n, err := q.UpdateFields(map[string]any{"RequireTLS": requireTLS})
|
||||
queuekick()
|
||||
return n, err
|
||||
}
|
||||
|
||||
type ReadReaderAtCloser interface {
|
||||
|
@ -522,6 +765,7 @@ func nextWork(ctx context.Context, log mlog.Log, busyDomains map[string]struct{}
|
|||
}
|
||||
q.FilterNotEqual("RecipientDomainStr", doms...)
|
||||
}
|
||||
q.FilterEqual("Hold", false)
|
||||
q.SortAsc("NextAttempt")
|
||||
q.Limit(1)
|
||||
qm, err := q.Get()
|
||||
|
@ -537,6 +781,7 @@ func nextWork(ctx context.Context, log mlog.Log, busyDomains map[string]struct{}
|
|||
func launchWork(log mlog.Log, resolver dns.Resolver, busyDomains map[string]struct{}) int {
|
||||
q := bstore.QueryDB[Msg](mox.Shutdown, DB)
|
||||
q.FilterLessEqual("NextAttempt", time.Now())
|
||||
q.FilterEqual("Hold", false)
|
||||
q.SortAsc("NextAttempt")
|
||||
q.Limit(maxConcurrentDeliveries)
|
||||
if len(busyDomains) > 0 {
|
||||
|
@ -679,6 +924,7 @@ func deliver(log mlog.Log, resolver dns.Resolver, m Msg) {
|
|||
q.FilterNonzero(Msg{BaseID: m.BaseID, RecipientDomainStr: m.RecipientDomainStr, Attempts: m.Attempts - 1})
|
||||
q.FilterNotEqual("ID", m.ID)
|
||||
q.FilterLessEqual("NextAttempt", origNextAttempt)
|
||||
q.FilterEqual("Hold", false)
|
||||
err := q.ForEach(func(xm Msg) error {
|
||||
mrtls := m.RequireTLS != nil
|
||||
xmrtls := xm.RequireTLS != nil
|
||||
|
|
|
@ -104,7 +104,20 @@ func TestQueue(t *testing.T) {
|
|||
err := Init()
|
||||
tcheck(t, err, "queue init")
|
||||
|
||||
msgs, err := List(ctxbg)
|
||||
idfilter := func(msgID int64) Filter {
|
||||
return Filter{IDs: []int64{msgID}}
|
||||
}
|
||||
|
||||
kick := func(expn int, id int64) {
|
||||
t.Helper()
|
||||
n, err := NextAttemptSet(ctxbg, idfilter(id), time.Now())
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != expn {
|
||||
t.Fatalf("kick changed %d messages, expected %d", n, expn)
|
||||
}
|
||||
}
|
||||
|
||||
msgs, err := List(ctxbg, Filter{})
|
||||
tcheck(t, err, "listing messages in queue")
|
||||
if len(msgs) != 0 {
|
||||
t.Fatalf("got %d messages in queue, expected 0", len(msgs))
|
||||
|
@ -125,16 +138,26 @@ func TestQueue(t *testing.T) {
|
|||
err = Add(ctxbg, pkglog, "mjl", mf, qm)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
|
||||
msgs, err = List(ctxbg)
|
||||
qm = MakeMsg(path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, nil, time.Now())
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qm)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
|
||||
msgs, err = List(ctxbg, Filter{})
|
||||
tcheck(t, err, "listing queue")
|
||||
if len(msgs) != 2 {
|
||||
if len(msgs) != 3 {
|
||||
t.Fatalf("got msgs %v, expected 1", msgs)
|
||||
}
|
||||
|
||||
yes := true
|
||||
n, err := RequireTLSSet(ctxbg, Filter{IDs: []int64{msgs[2].ID}}, &yes)
|
||||
tcheck(t, err, "requiretlsset")
|
||||
tcompare(t, n, 1)
|
||||
|
||||
msg := msgs[0]
|
||||
if msg.Attempts != 0 {
|
||||
t.Fatalf("msg attempts %d, expected 0", msg.Attempts)
|
||||
}
|
||||
n, err := Drop(ctxbg, pkglog, msgs[1].ID, "", "")
|
||||
n, err = Drop(ctxbg, pkglog, Filter{IDs: []int64{msgs[1].ID}})
|
||||
tcheck(t, err, "drop")
|
||||
if n != 1 {
|
||||
t.Fatalf("dropped %d, expected 1", n)
|
||||
|
@ -143,6 +166,48 @@ func TestQueue(t *testing.T) {
|
|||
t.Fatalf("dropped message not removed from file system")
|
||||
}
|
||||
|
||||
// Fail a message, check the account has a message afterwards, the DSN.
|
||||
n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
|
||||
tcheck(t, err, "count messages in account")
|
||||
tcompare(t, n, 0)
|
||||
n, err = Fail(ctxbg, pkglog, Filter{IDs: []int64{msgs[2].ID}})
|
||||
tcheck(t, err, "fail")
|
||||
if n != 1 {
|
||||
t.Fatalf("failed %d, expected 1", n)
|
||||
}
|
||||
n, err = bstore.QueryDB[store.Message](ctxbg, acc.DB).Count()
|
||||
tcheck(t, err, "count messages in account")
|
||||
tcompare(t, n, 1)
|
||||
|
||||
// Check filter through various List calls. Other code uses the same filtering function.
|
||||
filter := func(f Filter, expn int) {
|
||||
t.Helper()
|
||||
l, err := List(ctxbg, f)
|
||||
tcheck(t, err, "list messages")
|
||||
tcompare(t, len(l), expn)
|
||||
}
|
||||
filter(Filter{}, 1)
|
||||
filter(Filter{Account: "mjl"}, 1)
|
||||
filter(Filter{Account: "bogus"}, 0)
|
||||
filter(Filter{IDs: []int64{msgs[0].ID}}, 1)
|
||||
filter(Filter{IDs: []int64{msgs[2].ID}}, 0) // Removed.
|
||||
filter(Filter{IDs: []int64{msgs[2].ID + 1}}, 0) // Never existed.
|
||||
filter(Filter{From: "mjl@"}, 1)
|
||||
filter(Filter{From: "bogus@"}, 0)
|
||||
filter(Filter{To: "mjl@"}, 1)
|
||||
filter(Filter{To: "bogus@"}, 0)
|
||||
filter(Filter{Hold: &yes}, 0)
|
||||
no := false
|
||||
filter(Filter{Hold: &no}, 1)
|
||||
filter(Filter{Submitted: "<now"}, 1)
|
||||
filter(Filter{Submitted: ">now"}, 0)
|
||||
filter(Filter{NextAttempt: "<1m"}, 1)
|
||||
filter(Filter{NextAttempt: ">1m"}, 0)
|
||||
var empty string
|
||||
bogus := "bogus"
|
||||
filter(Filter{Transport: &empty}, 1)
|
||||
filter(Filter{Transport: &bogus}, 0)
|
||||
|
||||
next := nextWork(ctxbg, pkglog, nil)
|
||||
if next > 0 {
|
||||
t.Fatalf("nextWork in %s, should be now", next)
|
||||
|
@ -217,12 +282,13 @@ func TestQueue(t *testing.T) {
|
|||
t.Fatalf("message mismatch, got %q, expected %q", string(msgbuf), testmsg)
|
||||
}
|
||||
|
||||
n, err = Kick(ctxbg, msg.ID+1, "", "", nil)
|
||||
// Reduce by more than first attempt interval of 7.5 minutes.
|
||||
n, err = NextAttemptAdd(ctxbg, idfilter(msg.ID+1), -10*time.Minute)
|
||||
tcheck(t, err, "kick")
|
||||
if n != 0 {
|
||||
t.Fatalf("kick %d, expected 0", n)
|
||||
}
|
||||
n, err = Kick(ctxbg, msg.ID, "", "", nil)
|
||||
n, err = NextAttemptAdd(ctxbg, idfilter(msg.ID), -10*time.Minute)
|
||||
tcheck(t, err, "kick")
|
||||
if n != 1 {
|
||||
t.Fatalf("kicked %d, expected 1", n)
|
||||
|
@ -485,7 +551,7 @@ func TestQueue(t *testing.T) {
|
|||
case <-smtpdone:
|
||||
i := 0
|
||||
for {
|
||||
xmsgs, err := List(ctxbg)
|
||||
xmsgs, err := List(ctxbg, Filter{})
|
||||
tcheck(t, err, "list queue")
|
||||
if len(xmsgs) == 0 {
|
||||
ninbox, err := bstore.QueryDB[store.Message](ctxbg, acc.DB).FilterNonzero(store.Message{MailboxID: inbox.ID}).Count()
|
||||
|
@ -595,10 +661,10 @@ func TestQueue(t *testing.T) {
|
|||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
transportSubmitTLS := "submittls"
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", &transportSubmitTLS)
|
||||
tcheck(t, err, "kick queue")
|
||||
n, err = TransportSet(ctxbg, Filter{IDs: []int64{qml[0].ID}}, transportSubmitTLS)
|
||||
tcheck(t, err, "set transport")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
t.Fatalf("TransportSet changed %d messages, expected 1", n)
|
||||
}
|
||||
// Make fake cert, and make it trusted.
|
||||
cert := fakeCert(t, "submission.example", false)
|
||||
|
@ -643,12 +709,12 @@ func TestQueue(t *testing.T) {
|
|||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<socks@localhost>", nil, nil, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
transportSocks := "socks"
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", &transportSocks)
|
||||
tcheck(t, err, "kick queue")
|
||||
n, err = TransportSet(ctxbg, idfilter(qml[0].ID), "socks")
|
||||
tcheck(t, err, "TransportSet")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
t.Fatalf("TransportSet changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
wasNetDialer = testDeliver(fakeSMTPServer)
|
||||
if wasNetDialer {
|
||||
t.Fatalf("expected non-net.Dialer as dialer") // SOCKS5 dialer is a private type, we cannot check for it.
|
||||
|
@ -659,11 +725,7 @@ func TestQueue(t *testing.T) {
|
|||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<opportunistictls@localhost>", nil, nil, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
testDeliver(fakeSMTPSTARTTLSServer)
|
||||
checkTLSResults(t, "mox.example", "mox.example", false, addCounts(1, 0, tlsrpt.MakeResult(tlsrpt.NoPolicyFound, mailDomain, fdNotTrusted)))
|
||||
checkTLSResults(t, "mail.mox.example", "mox.example", true, addCounts(1, 0, tlsrpt.MakeResult(tlsrpt.NoPolicyFound, mailHost)))
|
||||
|
@ -673,11 +735,7 @@ func TestQueue(t *testing.T) {
|
|||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<badtls@localhost>", nil, nil, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
testDeliver(makeBadFakeSMTPSTARTTLSServer(true))
|
||||
checkTLSResults(t, "mox.example", "mox.example", false, addCounts(0, 1, tlsrpt.MakeResult(tlsrpt.NoPolicyFound, mailDomain, fdBadProtocol)))
|
||||
checkTLSResults(t, "mail.mox.example", "mox.example", true, addCounts(0, 1, tlsrpt.MakeResult(tlsrpt.NoPolicyFound, mailHost, fdBadProtocol)))
|
||||
|
@ -693,11 +751,7 @@ func TestQueue(t *testing.T) {
|
|||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<dane@localhost>", nil, nil, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
testDeliver(fakeSMTPSTARTTLSServer)
|
||||
checkTLSResults(t, "mox.example", "mox.example", false, addCounts(1, 0, tlsrpt.MakeResult(tlsrpt.NoPolicyFound, mailDomain, fdNotTrusted)))
|
||||
checkTLSResults(t, "mail.mox.example", "mox.example", true, addCounts(1, 0, tlsrpt.Result{Policy: tlsrpt.TLSAPolicy(resolver.TLSA["_25._tcp.mail.mox.example."], mailHost), FailureDetails: []tlsrpt.FailureDetails{}}))
|
||||
|
@ -710,15 +764,10 @@ func TestQueue(t *testing.T) {
|
|||
tcompare(t, rdt.RequireTLS, true)
|
||||
|
||||
// Add message to be delivered with verified TLS and REQUIRETLS.
|
||||
yes := true
|
||||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<opportunistictls@localhost>", nil, &yes, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
testDeliver(fakeSMTPSTARTTLSServer)
|
||||
|
||||
// Check that message is delivered with all unusable DANE records.
|
||||
|
@ -731,11 +780,7 @@ func TestQueue(t *testing.T) {
|
|||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<daneunusable@localhost>", nil, nil, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
testDeliver(fakeSMTPSTARTTLSServer)
|
||||
checkTLSResults(t, "mox.example", "mox.example", false, addCounts(1, 0, tlsrpt.MakeResult(tlsrpt.NoPolicyFound, mailDomain, fdNotTrusted)))
|
||||
checkTLSResults(t, "mail.mox.example", "mox.example", true, addCounts(1, 0, tlsrpt.Result{Policy: tlsrpt.TLSAPolicy([]adns.TLSA{}, mailHost), FailureDetails: []tlsrpt.FailureDetails{fdTLSAUnusable}}))
|
||||
|
@ -752,11 +797,7 @@ func TestQueue(t *testing.T) {
|
|||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<daneinsecure@localhost>", nil, nil, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
testDeliver(makeBadFakeSMTPSTARTTLSServer(true))
|
||||
resolver.Inauthentic = nil
|
||||
checkTLSResults(t, "mox.example", "mox.example", false, addCounts(0, 1, tlsrpt.MakeResult(tlsrpt.NoPolicyFound, mailDomain, fdBadProtocol)))
|
||||
|
@ -770,37 +811,24 @@ func TestQueue(t *testing.T) {
|
|||
tcompare(t, rdt.RequireTLS, false)
|
||||
|
||||
// Check that message is delivered with TLS-Required: No and non-matching DANE record.
|
||||
no := false
|
||||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<tlsrequirednostarttls@localhost>", nil, &no, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
testDeliver(fakeSMTPSTARTTLSServer)
|
||||
|
||||
// Check that message is delivered with TLS-Required: No and bad TLS, falling back to plain text.
|
||||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<tlsrequirednoplaintext@localhost>", nil, &no, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
testDeliver(makeBadFakeSMTPSTARTTLSServer(true))
|
||||
|
||||
// Add message with requiretls that fails immediately due to no REQUIRETLS support in all servers.
|
||||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<tlsrequiredunsupported@localhost>", nil, &yes, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
testDSN(makeBadFakeSMTPSTARTTLSServer(false))
|
||||
|
||||
// Restore pre-DANE behaviour.
|
||||
|
@ -811,11 +839,7 @@ func TestQueue(t *testing.T) {
|
|||
qml = []Msg{MakeMsg(path, path, false, false, int64(len(testmsg)), "<tlsrequirednopolicy@localhost>", nil, &yes, time.Now())}
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
n, err = Kick(ctxbg, qml[0].ID, "", "", nil)
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
kick(1, qml[0].ID)
|
||||
// Based on DNS lookups, there won't be any dialing or SMTP connection.
|
||||
dialed <- struct{}{}
|
||||
testDSN(func(conn net.Conn) {
|
||||
|
@ -827,7 +851,7 @@ func TestQueue(t *testing.T) {
|
|||
err = Add(ctxbg, pkglog, "mjl", mf, qm)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
|
||||
msgs, err = List(ctxbg)
|
||||
msgs, err = List(ctxbg, Filter{})
|
||||
tcheck(t, err, "list queue")
|
||||
if len(msgs) != 1 {
|
||||
t.Fatalf("queue has %d messages, expected 1", len(msgs))
|
||||
|
@ -1010,6 +1034,14 @@ func TestQueueStart(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// HoldRule prevents delivery.
|
||||
hr, err := HoldRuleAdd(ctxbg, pkglog, HoldRule{})
|
||||
tcheck(t, err, "add hold rule")
|
||||
|
||||
hrl, err := HoldRuleList(ctxbg)
|
||||
tcheck(t, err, "listing hold rules")
|
||||
tcompare(t, hrl, []HoldRule{hr})
|
||||
|
||||
path := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mox.example"}}}
|
||||
mf := prepareFile(t)
|
||||
defer os.Remove(mf.Name())
|
||||
|
@ -1017,20 +1049,50 @@ func TestQueueStart(t *testing.T) {
|
|||
qm := MakeMsg(path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, nil, time.Now())
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qm)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
checkDialed(false) // No delivery attempt yet.
|
||||
|
||||
n, err := Count(ctxbg)
|
||||
tcheck(t, err, "count messages in queue")
|
||||
tcompare(t, n, 1)
|
||||
|
||||
// Take message off hold.
|
||||
n, err = HoldSet(ctxbg, Filter{}, false)
|
||||
tcheck(t, err, "taking message off hold")
|
||||
tcompare(t, n, 1)
|
||||
checkDialed(true)
|
||||
|
||||
// Remove hold rule.
|
||||
err = HoldRuleRemove(ctxbg, pkglog, hr.ID)
|
||||
tcheck(t, err, "removing hold rule")
|
||||
// Check it is gone.
|
||||
hrl, err = HoldRuleList(ctxbg)
|
||||
tcheck(t, err, "listing hold rules")
|
||||
tcompare(t, len(hrl), 0)
|
||||
|
||||
// Don't change message nextattempt time, but kick queue. Message should not be delivered.
|
||||
queuekick()
|
||||
checkDialed(false)
|
||||
|
||||
// Kick for real, should see another attempt.
|
||||
n, err := Kick(ctxbg, 0, "mox.example", "", nil)
|
||||
// Set new next attempt, should see another attempt.
|
||||
n, err = NextAttemptSet(ctxbg, Filter{From: "@mox.example"}, time.Now())
|
||||
tcheck(t, err, "kick queue")
|
||||
if n != 1 {
|
||||
t.Fatalf("kick changed %d messages, expected 1", n)
|
||||
}
|
||||
checkDialed(true)
|
||||
time.Sleep(100 * time.Millisecond) // Racy... we won't get notified when work is done...
|
||||
|
||||
// Submit another, should be delivered immediately without HoldRule.
|
||||
path = smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mox.example"}}}
|
||||
mf = prepareFile(t)
|
||||
defer os.Remove(mf.Name())
|
||||
defer mf.Close()
|
||||
qm = MakeMsg(path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, nil, time.Now())
|
||||
err = Add(ctxbg, pkglog, "mjl", mf, qm)
|
||||
tcheck(t, err, "add message to queue for delivery")
|
||||
checkDialed(true) // Immediate.
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Racy... give time to finish.
|
||||
}
|
||||
|
||||
// Just a cert that appears valid.
|
||||
|
|
|
@ -1443,7 +1443,7 @@ test email
|
|||
}
|
||||
tcheck(t, err, "deliver")
|
||||
|
||||
msgs, err := queue.List(ctxbg)
|
||||
msgs, err := queue.List(ctxbg, queue.Filter{})
|
||||
tcheck(t, err, "listing queue")
|
||||
n++
|
||||
tcompare(t, len(msgs), n)
|
||||
|
@ -1592,11 +1592,11 @@ test email
|
|||
}
|
||||
tcheck(t, err, "deliver")
|
||||
|
||||
msgs, err := queue.List(ctxbg)
|
||||
msgs, err := queue.List(ctxbg, queue.Filter{})
|
||||
tcheck(t, err, "listing queue")
|
||||
tcompare(t, len(msgs), 1)
|
||||
tcompare(t, msgs[0].RequireTLS, expRequireTLS)
|
||||
_, err = queue.Drop(ctxbg, pkglog, msgs[0].ID, "", "")
|
||||
_, err = queue.Drop(ctxbg, pkglog, queue.Filter{IDs: []int64{msgs[0].ID}})
|
||||
tcheck(t, err, "deleting message from queue")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -219,6 +219,7 @@ const [dom, style, attr, prop] = (function () {
|
|||
method: (s) => _attr('method', s),
|
||||
autocomplete: (s) => _attr('autocomplete', s),
|
||||
list: (s) => _attr('list', s),
|
||||
form: (s) => _attr('form', s),
|
||||
};
|
||||
const style = (x) => { return { _styles: x }; };
|
||||
const prop = (x) => { return { _props: x }; };
|
||||
|
|
|
@ -1974,13 +1974,6 @@ func (Admin) ClientConfigsDomain(ctx context.Context, domain string) mox.ClientC
|
|||
return cc
|
||||
}
|
||||
|
||||
// QueueList returns the messages currently in the outgoing queue.
|
||||
func (Admin) QueueList(ctx context.Context) []queue.Msg {
|
||||
l, err := queue.List(ctx)
|
||||
xcheckf(ctx, err, "listing messages in queue")
|
||||
return l
|
||||
}
|
||||
|
||||
// QueueSize returns the number of messages currently in the outgoing queue.
|
||||
func (Admin) QueueSize(ctx context.Context) int {
|
||||
n, err := queue.Count(ctx)
|
||||
|
@ -1988,31 +1981,96 @@ func (Admin) QueueSize(ctx context.Context) int {
|
|||
return n
|
||||
}
|
||||
|
||||
// QueueKick initiates delivery of a message from the queue and sets the transport
|
||||
// to use for delivery.
|
||||
func (Admin) QueueKick(ctx context.Context, id int64, transport string) {
|
||||
n, err := queue.Kick(ctx, id, "", "", &transport)
|
||||
if err == nil && n == 0 {
|
||||
err = errors.New("message not found")
|
||||
}
|
||||
xcheckf(ctx, err, "kick message in queue")
|
||||
// QueueHoldRuleList lists the hold rules.
|
||||
func (Admin) QueueHoldRuleList(ctx context.Context) []queue.HoldRule {
|
||||
l, err := queue.HoldRuleList(ctx)
|
||||
xcheckf(ctx, err, "listing queue hold rules")
|
||||
return l
|
||||
}
|
||||
|
||||
// QueueDrop removes a message from the queue.
|
||||
func (Admin) QueueDrop(ctx context.Context, id int64) {
|
||||
// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
|
||||
// matching the hold rule will be marked "on hold".
|
||||
func (Admin) QueueHoldRuleAdd(ctx context.Context, hr queue.HoldRule) queue.HoldRule {
|
||||
var err error
|
||||
hr.SenderDomain, err = dns.ParseDomain(hr.SenderDomainStr)
|
||||
xcheckuserf(ctx, err, "parsing sender domain %q", hr.SenderDomainStr)
|
||||
hr.RecipientDomain, err = dns.ParseDomain(hr.RecipientDomainStr)
|
||||
xcheckuserf(ctx, err, "parsing recipient domain %q", hr.RecipientDomainStr)
|
||||
|
||||
log := pkglog.WithContext(ctx)
|
||||
n, err := queue.Drop(ctx, log, id, "", "")
|
||||
if err == nil && n == 0 {
|
||||
err = errors.New("message not found")
|
||||
}
|
||||
xcheckf(ctx, err, "drop message from queue")
|
||||
hr, err = queue.HoldRuleAdd(ctx, log, hr)
|
||||
xcheckf(ctx, err, "adding queue hold rule")
|
||||
return hr
|
||||
}
|
||||
|
||||
// QueueSaveRequireTLS updates the requiretls field for a message in the queue,
|
||||
// to be used for the next delivery.
|
||||
func (Admin) QueueSaveRequireTLS(ctx context.Context, id int64, requireTLS *bool) {
|
||||
err := queue.SaveRequireTLS(ctx, id, requireTLS)
|
||||
xcheckf(ctx, err, "update requiretls for message in queue")
|
||||
// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
|
||||
// the queue are not changed.
|
||||
func (Admin) QueueHoldRuleRemove(ctx context.Context, holdRuleID int64) {
|
||||
log := pkglog.WithContext(ctx)
|
||||
err := queue.HoldRuleRemove(ctx, log, holdRuleID)
|
||||
xcheckf(ctx, err, "removing queue hold rule")
|
||||
}
|
||||
|
||||
// QueueList returns the messages currently in the outgoing queue.
|
||||
func (Admin) QueueList(ctx context.Context, filter queue.Filter) []queue.Msg {
|
||||
l, err := queue.List(ctx, filter)
|
||||
xcheckf(ctx, err, "listing messages in queue")
|
||||
return l
|
||||
}
|
||||
|
||||
// QueueNextAttemptSet sets a new time for next delivery attempt of matching
|
||||
// messages from the queue.
|
||||
func (Admin) QueueNextAttemptSet(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
|
||||
n, err := queue.NextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
|
||||
xcheckf(ctx, err, "setting new next delivery attempt time for matching messages in queue")
|
||||
return n
|
||||
}
|
||||
|
||||
// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
|
||||
// matching messages from the queue.
|
||||
func (Admin) QueueNextAttemptAdd(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
|
||||
n, err := queue.NextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
|
||||
xcheckf(ctx, err, "adding duration to next delivery attempt for matching messages in queue")
|
||||
return n
|
||||
}
|
||||
|
||||
// QueueHoldSet sets the Hold field of matching messages in the queue.
|
||||
func (Admin) QueueHoldSet(ctx context.Context, filter queue.Filter, onHold bool) (affected int) {
|
||||
n, err := queue.HoldSet(ctx, filter, onHold)
|
||||
xcheckf(ctx, err, "changing onhold for matching messages in queue")
|
||||
return n
|
||||
}
|
||||
|
||||
// QueueFail fails delivery for matching messages, causing DSNs to be sent.
|
||||
func (Admin) QueueFail(ctx context.Context, filter queue.Filter) (affected int) {
|
||||
log := pkglog.WithContext(ctx)
|
||||
n, err := queue.Fail(ctx, log, filter)
|
||||
xcheckf(ctx, err, "drop messages from queue")
|
||||
return n
|
||||
}
|
||||
|
||||
// QueueDrop removes matching messages from the queue.
|
||||
func (Admin) QueueDrop(ctx context.Context, filter queue.Filter) (affected int) {
|
||||
log := pkglog.WithContext(ctx)
|
||||
n, err := queue.Drop(ctx, log, filter)
|
||||
xcheckf(ctx, err, "drop messages from queue")
|
||||
return n
|
||||
}
|
||||
|
||||
// QueueRequireTLSSet updates the requiretls field for matching messages in the
|
||||
// queue, to be used for the next delivery.
|
||||
func (Admin) QueueRequireTLSSet(ctx context.Context, filter queue.Filter, requireTLS *bool) (affected int) {
|
||||
n, err := queue.RequireTLSSet(ctx, filter, requireTLS)
|
||||
xcheckf(ctx, err, "update requiretls for messages in queue")
|
||||
return n
|
||||
}
|
||||
|
||||
// QueueTransportSet initiates delivery of a message from the queue and sets the transport
|
||||
// to use for delivery.
|
||||
func (Admin) QueueTransportSet(ctx context.Context, filter queue.Filter, transport string) (affected int) {
|
||||
n, err := queue.TransportSet(ctx, filter, transport)
|
||||
xcheckf(ctx, err, "changing transport for messages in queue")
|
||||
return n
|
||||
}
|
||||
|
||||
// LogLevels returns the current log levels.
|
||||
|
|
|
@ -219,6 +219,7 @@ const [dom, style, attr, prop] = (function () {
|
|||
method: (s) => _attr('method', s),
|
||||
autocomplete: (s) => _attr('autocomplete', s),
|
||||
list: (s) => _attr('list', s),
|
||||
form: (s) => _attr('form', s),
|
||||
};
|
||||
const style = (x) => { return { _styles: x }; };
|
||||
const prop = (x) => { return { _props: x }; };
|
||||
|
@ -335,7 +336,7 @@ var api;
|
|||
SPFResult["SPFTemperror"] = "temperror";
|
||||
SPFResult["SPFPermerror"] = "permerror";
|
||||
})(SPFResult = api.SPFResult || (api.SPFResult = {}));
|
||||
api.structTypes = { "AuthResults": true, "AutoconfCheckResult": true, "AutodiscoverCheckResult": true, "AutodiscoverSRV": true, "CheckResult": true, "ClientConfigs": true, "ClientConfigsEntry": true, "DANECheckResult": true, "DKIMAuthResult": true, "DKIMCheckResult": true, "DKIMRecord": true, "DMARCCheckResult": true, "DMARCRecord": true, "DMARCSummary": true, "DNSSECResult": true, "DateRange": true, "Directive": true, "Domain": true, "DomainFeedback": true, "Evaluation": true, "EvaluationStat": true, "Extension": true, "FailureDetails": true, "IPDomain": true, "IPRevCheckResult": true, "Identifiers": true, "MTASTSCheckResult": true, "MTASTSRecord": true, "MX": true, "MXCheckResult": true, "Modifier": true, "Msg": true, "Pair": true, "Policy": true, "PolicyEvaluated": true, "PolicyOverrideReason": true, "PolicyPublished": true, "PolicyRecord": true, "Record": true, "Report": true, "ReportMetadata": true, "ReportRecord": true, "Result": true, "ResultPolicy": true, "Reverse": true, "Row": true, "SMTPAuth": true, "SPFAuthResult": true, "SPFCheckResult": true, "SPFRecord": true, "SRV": true, "SRVConfCheckResult": true, "STSMX": true, "Summary": true, "SuppressAddress": true, "TLSCheckResult": true, "TLSRPTCheckResult": true, "TLSRPTDateRange": true, "TLSRPTRecord": true, "TLSRPTSummary": true, "TLSRPTSuppressAddress": true, "TLSReportRecord": true, "TLSResult": true, "Transport": true, "TransportSMTP": true, "TransportSocks": true, "URI": true, "WebForward": true, "WebHandler": true, "WebRedirect": true, "WebStatic": true, "WebserverConfig": true };
|
||||
api.structTypes = { "AuthResults": true, "AutoconfCheckResult": true, "AutodiscoverCheckResult": true, "AutodiscoverSRV": true, "CheckResult": true, "ClientConfigs": true, "ClientConfigsEntry": true, "DANECheckResult": true, "DKIMAuthResult": true, "DKIMCheckResult": true, "DKIMRecord": true, "DMARCCheckResult": true, "DMARCRecord": true, "DMARCSummary": true, "DNSSECResult": true, "DateRange": true, "Directive": true, "Domain": true, "DomainFeedback": true, "Evaluation": true, "EvaluationStat": true, "Extension": true, "FailureDetails": true, "Filter": true, "HoldRule": true, "IPDomain": true, "IPRevCheckResult": true, "Identifiers": true, "MTASTSCheckResult": true, "MTASTSRecord": true, "MX": true, "MXCheckResult": true, "Modifier": true, "Msg": true, "Pair": true, "Policy": true, "PolicyEvaluated": true, "PolicyOverrideReason": true, "PolicyPublished": true, "PolicyRecord": true, "Record": true, "Report": true, "ReportMetadata": true, "ReportRecord": true, "Result": true, "ResultPolicy": true, "Reverse": true, "Row": true, "SMTPAuth": true, "SPFAuthResult": true, "SPFCheckResult": true, "SPFRecord": true, "SRV": true, "SRVConfCheckResult": true, "STSMX": true, "Summary": true, "SuppressAddress": true, "TLSCheckResult": true, "TLSRPTCheckResult": true, "TLSRPTDateRange": true, "TLSRPTRecord": true, "TLSRPTSummary": true, "TLSRPTSuppressAddress": true, "TLSReportRecord": true, "TLSResult": true, "Transport": true, "TransportSMTP": true, "TransportSocks": true, "URI": true, "WebForward": true, "WebHandler": true, "WebRedirect": true, "WebStatic": true, "WebserverConfig": true };
|
||||
api.stringsTypes = { "Align": true, "Alignment": true, "CSRFToken": true, "DKIMResult": true, "DMARCPolicy": true, "DMARCResult": true, "Disposition": true, "IP": true, "Localpart": true, "Mode": true, "PolicyOverride": true, "PolicyType": true, "RUA": true, "ResultType": true, "SPFDomainScope": true, "SPFResult": true };
|
||||
api.intsTypes = {};
|
||||
api.types = {
|
||||
|
@ -395,7 +396,9 @@ var api;
|
|||
"Reverse": { "Name": "Reverse", "Docs": "", "Fields": [{ "Name": "Hostnames", "Docs": "", "Typewords": ["[]", "string"] }] },
|
||||
"ClientConfigs": { "Name": "ClientConfigs", "Docs": "", "Fields": [{ "Name": "Entries", "Docs": "", "Typewords": ["[]", "ClientConfigsEntry"] }] },
|
||||
"ClientConfigsEntry": { "Name": "ClientConfigsEntry", "Docs": "", "Fields": [{ "Name": "Protocol", "Docs": "", "Typewords": ["string"] }, { "Name": "Host", "Docs": "", "Typewords": ["Domain"] }, { "Name": "Port", "Docs": "", "Typewords": ["int32"] }, { "Name": "Listener", "Docs": "", "Typewords": ["string"] }, { "Name": "Note", "Docs": "", "Typewords": ["string"] }] },
|
||||
"Msg": { "Name": "Msg", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "BaseID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Queued", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "SenderAccount", "Docs": "", "Typewords": ["string"] }, { "Name": "SenderLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "SenderDomain", "Docs": "", "Typewords": ["IPDomain"] }, { "Name": "RecipientLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "RecipientDomain", "Docs": "", "Typewords": ["IPDomain"] }, { "Name": "RecipientDomainStr", "Docs": "", "Typewords": ["string"] }, { "Name": "Attempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxAttempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "DialedIPs", "Docs": "", "Typewords": ["{}", "[]", "IP"] }, { "Name": "NextAttempt", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "LastAttempt", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "LastError", "Docs": "", "Typewords": ["string"] }, { "Name": "Has8bit", "Docs": "", "Typewords": ["bool"] }, { "Name": "SMTPUTF8", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsDMARCReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsTLSReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgPrefix", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "DSNUTF8", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "Transport", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureReleaseRequest", "Docs": "", "Typewords": ["string"] }] },
|
||||
"HoldRule": { "Name": "HoldRule", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Account", "Docs": "", "Typewords": ["string"] }, { "Name": "SenderDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "RecipientDomain", "Docs": "", "Typewords": ["Domain"] }, { "Name": "SenderDomainStr", "Docs": "", "Typewords": ["string"] }, { "Name": "RecipientDomainStr", "Docs": "", "Typewords": ["string"] }] },
|
||||
"Filter": { "Name": "Filter", "Docs": "", "Fields": [{ "Name": "IDs", "Docs": "", "Typewords": ["[]", "int64"] }, { "Name": "Account", "Docs": "", "Typewords": ["string"] }, { "Name": "From", "Docs": "", "Typewords": ["string"] }, { "Name": "To", "Docs": "", "Typewords": ["string"] }, { "Name": "Hold", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "Submitted", "Docs": "", "Typewords": ["string"] }, { "Name": "NextAttempt", "Docs": "", "Typewords": ["string"] }, { "Name": "Transport", "Docs": "", "Typewords": ["nullable", "string"] }] },
|
||||
"Msg": { "Name": "Msg", "Docs": "", "Fields": [{ "Name": "ID", "Docs": "", "Typewords": ["int64"] }, { "Name": "BaseID", "Docs": "", "Typewords": ["int64"] }, { "Name": "Queued", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "Hold", "Docs": "", "Typewords": ["bool"] }, { "Name": "SenderAccount", "Docs": "", "Typewords": ["string"] }, { "Name": "SenderLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "SenderDomain", "Docs": "", "Typewords": ["IPDomain"] }, { "Name": "SenderDomainStr", "Docs": "", "Typewords": ["string"] }, { "Name": "RecipientLocalpart", "Docs": "", "Typewords": ["Localpart"] }, { "Name": "RecipientDomain", "Docs": "", "Typewords": ["IPDomain"] }, { "Name": "RecipientDomainStr", "Docs": "", "Typewords": ["string"] }, { "Name": "Attempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "MaxAttempts", "Docs": "", "Typewords": ["int32"] }, { "Name": "DialedIPs", "Docs": "", "Typewords": ["{}", "[]", "IP"] }, { "Name": "NextAttempt", "Docs": "", "Typewords": ["timestamp"] }, { "Name": "LastAttempt", "Docs": "", "Typewords": ["nullable", "timestamp"] }, { "Name": "LastError", "Docs": "", "Typewords": ["string"] }, { "Name": "Has8bit", "Docs": "", "Typewords": ["bool"] }, { "Name": "SMTPUTF8", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsDMARCReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "IsTLSReport", "Docs": "", "Typewords": ["bool"] }, { "Name": "Size", "Docs": "", "Typewords": ["int64"] }, { "Name": "MessageID", "Docs": "", "Typewords": ["string"] }, { "Name": "MsgPrefix", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "DSNUTF8", "Docs": "", "Typewords": ["nullable", "string"] }, { "Name": "Transport", "Docs": "", "Typewords": ["string"] }, { "Name": "RequireTLS", "Docs": "", "Typewords": ["nullable", "bool"] }, { "Name": "FutureReleaseRequest", "Docs": "", "Typewords": ["string"] }] },
|
||||
"IPDomain": { "Name": "IPDomain", "Docs": "", "Fields": [{ "Name": "IP", "Docs": "", "Typewords": ["IP"] }, { "Name": "Domain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
"WebserverConfig": { "Name": "WebserverConfig", "Docs": "", "Fields": [{ "Name": "WebDNSDomainRedirects", "Docs": "", "Typewords": ["[]", "[]", "Domain"] }, { "Name": "WebDomainRedirects", "Docs": "", "Typewords": ["[]", "[]", "string"] }, { "Name": "WebHandlers", "Docs": "", "Typewords": ["[]", "WebHandler"] }] },
|
||||
"WebHandler": { "Name": "WebHandler", "Docs": "", "Fields": [{ "Name": "LogName", "Docs": "", "Typewords": ["string"] }, { "Name": "Domain", "Docs": "", "Typewords": ["string"] }, { "Name": "PathRegexp", "Docs": "", "Typewords": ["string"] }, { "Name": "DontRedirectPlainHTTP", "Docs": "", "Typewords": ["bool"] }, { "Name": "Compress", "Docs": "", "Typewords": ["bool"] }, { "Name": "WebStatic", "Docs": "", "Typewords": ["nullable", "WebStatic"] }, { "Name": "WebRedirect", "Docs": "", "Typewords": ["nullable", "WebRedirect"] }, { "Name": "WebForward", "Docs": "", "Typewords": ["nullable", "WebForward"] }, { "Name": "Name", "Docs": "", "Typewords": ["string"] }, { "Name": "DNSDomain", "Docs": "", "Typewords": ["Domain"] }] },
|
||||
|
@ -485,6 +488,8 @@ var api;
|
|||
Reverse: (v) => api.parse("Reverse", v),
|
||||
ClientConfigs: (v) => api.parse("ClientConfigs", v),
|
||||
ClientConfigsEntry: (v) => api.parse("ClientConfigsEntry", v),
|
||||
HoldRule: (v) => api.parse("HoldRule", v),
|
||||
Filter: (v) => api.parse("Filter", v),
|
||||
Msg: (v) => api.parse("Msg", v),
|
||||
IPDomain: (v) => api.parse("IPDomain", v),
|
||||
WebserverConfig: (v) => api.parse("WebserverConfig", v),
|
||||
|
@ -812,14 +817,6 @@ var api;
|
|||
const params = [domain];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueList returns the messages currently in the outgoing queue.
|
||||
async QueueList() {
|
||||
const fn = "QueueList";
|
||||
const paramTypes = [];
|
||||
const returnTypes = [["[]", "Msg"]];
|
||||
const params = [];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueSize returns the number of messages currently in the outgoing queue.
|
||||
async QueueSize() {
|
||||
const fn = "QueueSize";
|
||||
|
@ -828,30 +825,98 @@ var api;
|
|||
const params = [];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueKick initiates delivery of a message from the queue and sets the transport
|
||||
// to use for delivery.
|
||||
async QueueKick(id, transport) {
|
||||
const fn = "QueueKick";
|
||||
const paramTypes = [["int64"], ["string"]];
|
||||
const returnTypes = [];
|
||||
const params = [id, transport];
|
||||
// QueueHoldRuleList lists the hold rules.
|
||||
async QueueHoldRuleList() {
|
||||
const fn = "QueueHoldRuleList";
|
||||
const paramTypes = [];
|
||||
const returnTypes = [["[]", "HoldRule"]];
|
||||
const params = [];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueDrop removes a message from the queue.
|
||||
async QueueDrop(id) {
|
||||
const fn = "QueueDrop";
|
||||
// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
|
||||
// matching the hold rule will be marked "on hold".
|
||||
async QueueHoldRuleAdd(hr) {
|
||||
const fn = "QueueHoldRuleAdd";
|
||||
const paramTypes = [["HoldRule"]];
|
||||
const returnTypes = [["HoldRule"]];
|
||||
const params = [hr];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
|
||||
// the queue are not changed.
|
||||
async QueueHoldRuleRemove(holdRuleID) {
|
||||
const fn = "QueueHoldRuleRemove";
|
||||
const paramTypes = [["int64"]];
|
||||
const returnTypes = [];
|
||||
const params = [id];
|
||||
const params = [holdRuleID];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueSaveRequireTLS updates the requiretls field for a message in the queue,
|
||||
// to be used for the next delivery.
|
||||
async QueueSaveRequireTLS(id, requireTLS) {
|
||||
const fn = "QueueSaveRequireTLS";
|
||||
const paramTypes = [["int64"], ["nullable", "bool"]];
|
||||
const returnTypes = [];
|
||||
const params = [id, requireTLS];
|
||||
// QueueList returns the messages currently in the outgoing queue.
|
||||
async QueueList(filter) {
|
||||
const fn = "QueueList";
|
||||
const paramTypes = [["Filter"]];
|
||||
const returnTypes = [["[]", "Msg"]];
|
||||
const params = [filter];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueNextAttemptSet sets a new time for next delivery attempt of matching
|
||||
// messages from the queue.
|
||||
async QueueNextAttemptSet(filter, minutes) {
|
||||
const fn = "QueueNextAttemptSet";
|
||||
const paramTypes = [["Filter"], ["int32"]];
|
||||
const returnTypes = [["int32"]];
|
||||
const params = [filter, minutes];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
|
||||
// matching messages from the queue.
|
||||
async QueueNextAttemptAdd(filter, minutes) {
|
||||
const fn = "QueueNextAttemptAdd";
|
||||
const paramTypes = [["Filter"], ["int32"]];
|
||||
const returnTypes = [["int32"]];
|
||||
const params = [filter, minutes];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueHoldSet sets the Hold field of matching messages in the queue.
|
||||
async QueueHoldSet(filter, onHold) {
|
||||
const fn = "QueueHoldSet";
|
||||
const paramTypes = [["Filter"], ["bool"]];
|
||||
const returnTypes = [["int32"]];
|
||||
const params = [filter, onHold];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueFail fails delivery for matching messages, causing DSNs to be sent.
|
||||
async QueueFail(filter) {
|
||||
const fn = "QueueFail";
|
||||
const paramTypes = [["Filter"]];
|
||||
const returnTypes = [["int32"]];
|
||||
const params = [filter];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueDrop removes matching messages from the queue.
|
||||
async QueueDrop(filter) {
|
||||
const fn = "QueueDrop";
|
||||
const paramTypes = [["Filter"]];
|
||||
const returnTypes = [["int32"]];
|
||||
const params = [filter];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueRequireTLSSet updates the requiretls field for matching messages in the
|
||||
// queue, to be used for the next delivery.
|
||||
async QueueRequireTLSSet(filter, requireTLS) {
|
||||
const fn = "QueueRequireTLSSet";
|
||||
const paramTypes = [["Filter"], ["nullable", "bool"]];
|
||||
const returnTypes = [["int32"]];
|
||||
const params = [filter, requireTLS];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// QueueTransportSet initiates delivery of a message from the queue and sets the transport
|
||||
// to use for delivery.
|
||||
async QueueTransportSet(filter, transport) {
|
||||
const fn = "QueueTransportSet";
|
||||
const paramTypes = [["Filter"], ["string"]];
|
||||
const returnTypes = [["int32"]];
|
||||
const params = [filter, transport];
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
|
||||
}
|
||||
// LogLevels returns the current log levels.
|
||||
|
@ -2453,36 +2518,187 @@ const dnsbl = async () => {
|
|||
}, fieldset = dom.fieldset(dom.div('One per line'), dom.div(style({ marginBottom: '.5ex' }), monitorTextarea = dom.textarea(style({ width: '20rem' }), attr.rows('' + Math.max(5, 1 + (monitorZones || []).length)), new String((monitorZones || []).map(zone => domainName(zone)).join('\n'))), dom.div('Examples: sbl.spamhaus.org or bl.spamcop.net')), dom.div(dom.submitbutton('Save')))));
|
||||
};
|
||||
const queueList = async () => {
|
||||
const [msgs, transports] = await Promise.all([
|
||||
client.QueueList(),
|
||||
let [holdRules, msgs, transports] = await Promise.all([
|
||||
client.QueueHoldRuleList(),
|
||||
client.QueueList({ IDs: [], Account: '', From: '', To: '', Hold: null, Submitted: '', NextAttempt: '', Transport: null }),
|
||||
client.Transports(),
|
||||
]);
|
||||
// todo: sorting by address/timestamps/attempts.
|
||||
// todo: after making changes, don't reload entire page. probably best to fetch messages by id and rerender. also report on which messages weren't affected (e.g. no longer in queue).
|
||||
// todo: display which transport will be used for a message according to routing rules (in case none is explicitly configured).
|
||||
// todo: live updates with SSE connections
|
||||
// todo: keep updating times/age.
|
||||
const nowSecs = new Date().getTime() / 1000;
|
||||
dom._kids(page, crumbs(crumblink('Mox Admin', '#'), 'Queue'),
|
||||
// todo: sorting by address/timestamps/attempts. perhaps filtering.
|
||||
dom.table(dom._class('hover'), dom.thead(dom.tr(dom.th('ID'), dom.th('Submitted'), dom.th('From'), dom.th('To'), dom.th('Size'), dom.th('Attempts'), dom.th('Next attempt'), dom.th('Last attempt'), dom.th('Last error'), dom.th('Require TLS'), dom.th('Transport/Retry'), dom.th('Remove'))), dom.tbody((msgs || []).length === 0 ? dom.tr(dom.td(attr.colspan('12'), 'Currently no messages in the queue.')) : [], (msgs || []).map(m => {
|
||||
let holdRuleAccount;
|
||||
let holdRuleSenderDomain;
|
||||
let holdRuleRecipientDomain;
|
||||
let holdRuleSubmit;
|
||||
let filterForm;
|
||||
let filterAccount;
|
||||
let filterFrom;
|
||||
let filterTo;
|
||||
let filterSubmitted;
|
||||
let filterHold;
|
||||
let filterNextAttempt;
|
||||
let filterTransport;
|
||||
let requiretlsFieldset;
|
||||
let requiretls;
|
||||
let transport;
|
||||
return dom.tr(dom.td('' + m.ID + (m.BaseID > 0 ? '/' + m.BaseID : '')), dom.td(age(new Date(m.Queued), false, nowSecs)), dom.td(m.SenderLocalpart + "@" + ipdomainString(m.SenderDomain)), // todo: escaping of localpart
|
||||
// Message ID to checkbox.
|
||||
let toggles = new Map();
|
||||
// We operate on what the user has selected, not what the filters would currently
|
||||
// evaluate to. This function can throw an error, which is why we have awkward
|
||||
// syntax when calling this as parameter in api client calls below.
|
||||
const gatherIDs = () => {
|
||||
const f = {
|
||||
IDs: Array.from(toggles.entries()).filter(t => t[1].checked).map(t => t[0]),
|
||||
Account: '',
|
||||
From: '',
|
||||
To: '',
|
||||
Hold: null,
|
||||
Submitted: '',
|
||||
NextAttempt: '',
|
||||
Transport: null,
|
||||
};
|
||||
// Don't want to accidentally operate on all messages.
|
||||
if ((f.IDs || []).length === 0) {
|
||||
throw new Error('No messages selected.');
|
||||
}
|
||||
return f;
|
||||
};
|
||||
const tbody = dom.tbody();
|
||||
const render = () => {
|
||||
toggles = new Map();
|
||||
for (const m of (msgs || [])) {
|
||||
toggles.set(m.ID, dom.input(attr.type('checkbox'), attr.checked('')));
|
||||
}
|
||||
dom._kids(tbody, (msgs || []).length === 0 ? dom.tr(dom.td(attr.colspan('14'), 'No messages.')) : [], (msgs || []).map(m => {
|
||||
return dom.tr(dom.td(toggles.get(m.ID)), dom.td('' + m.ID + (m.BaseID > 0 ? '/' + m.BaseID : '')), dom.td(age(new Date(m.Queued), false, nowSecs)), dom.td(m.SenderAccount || '-'), dom.td(m.SenderLocalpart + "@" + ipdomainString(m.SenderDomain)), // todo: escaping of localpart
|
||||
dom.td(m.RecipientLocalpart + "@" + ipdomainString(m.RecipientDomain)), // todo: escaping of localpart
|
||||
dom.td(formatSize(m.Size)), dom.td('' + m.Attempts), dom.td(age(new Date(m.NextAttempt), true, nowSecs)), dom.td(m.LastAttempt ? age(new Date(m.LastAttempt), false, nowSecs) : '-'), dom.td(m.LastError || '-'), dom.td(dom.form(requiretlsFieldset = dom.fieldset(requiretls = dom.select(attr.title('How to use TLS for message delivery over SMTP:\n\nDefault: Delivery attempts follow the policies published by the recipient domain: Verification with MTA-STS and/or DANE, or optional opportunistic unverified STARTTLS if the domain does not specify a policy.\n\nWith RequireTLS: For sensitive messages, you may want to require verified TLS. The recipient destination domain SMTP server must support the REQUIRETLS SMTP extension for delivery to succeed. It is automatically chosen when the destination domain mail servers of all recipients are known to support it.\n\nFallback to insecure: If delivery fails due to MTA-STS and/or DANE policies specified by the recipient domain, and the content is not sensitive, you may choose to ignore the recipient domain TLS policies so delivery can succeed.'), dom.option('Default', attr.value('')), dom.option('With RequireTLS', attr.value('yes'), m.RequireTLS === true ? attr.selected('') : []), dom.option('Fallback to insecure', attr.value('no'), m.RequireTLS === false ? attr.selected('') : [])), ' ', dom.submitbutton('Save')), async function submit(e) {
|
||||
dom.td(formatSize(m.Size)), dom.td('' + m.Attempts), dom.td(m.Hold ? 'Hold' : ''), dom.td(age(new Date(m.NextAttempt), true, nowSecs)), dom.td(m.LastAttempt ? age(new Date(m.LastAttempt), false, nowSecs) : '-'), dom.td(m.LastError || '-'), dom.td(m.RequireTLS === true ? 'Yes' : (m.RequireTLS === false ? 'No' : 'Default')), dom.td(m.Transport || '(default)'));
|
||||
}));
|
||||
};
|
||||
render();
|
||||
const buttonNextAttemptSet = (text, minutes) => dom.clickbutton(text, async function click(e) {
|
||||
// note: awkward client call because gatherIDs() can throw an exception.
|
||||
const n = await check(e.target, (async () => client.QueueNextAttemptSet(gatherIDs(), minutes))());
|
||||
window.alert('' + n + ' message(s) updated');
|
||||
window.location.reload(); // todo: reload less
|
||||
});
|
||||
const buttonNextAttemptAdd = (text, minutes) => dom.clickbutton(text, async function click(e) {
|
||||
const n = await check(e.target, (async () => client.QueueNextAttemptAdd(gatherIDs(), minutes))());
|
||||
window.alert('' + n + ' message(s) updated');
|
||||
window.location.reload(); // todo: reload less
|
||||
});
|
||||
dom._kids(page, crumbs(crumblink('Mox Admin', '#'), 'Queue'), dom.h2('Hold rules', attr.title('Messages submitted to the queue that match a hold rule are automatically marked as "on hold", preventing delivery until explicitly taken off hold again.')), dom.form(attr.id('holdRuleForm'), async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await check(requiretlsFieldset, client.QueueSaveRequireTLS(m.ID, requiretls.value === '' ? null : requiretls.value === 'yes'));
|
||||
})), dom.td(dom.form(transport = dom.select(attr.title('Transport to use for delivery attempts. The default is direct delivery, connecting to the MX hosts of the domain.'), dom.option('(default)', attr.value('')), Object.keys(transports || []).sort().map(t => dom.option(t, m.Transport === t ? attr.checked('') : []))), ' ', dom.submitbutton('Retry now'), async function submit(e) {
|
||||
const pr = {
|
||||
ID: 0,
|
||||
Account: holdRuleAccount.value,
|
||||
SenderDomainStr: holdRuleSenderDomain.value,
|
||||
RecipientDomainStr: holdRuleRecipientDomain.value,
|
||||
// Filled in by backend, we provide dummy values.
|
||||
SenderDomain: { ASCII: '', Unicode: '' },
|
||||
RecipientDomain: { ASCII: '', Unicode: '' },
|
||||
};
|
||||
await check(holdRuleSubmit, client.QueueHoldRuleAdd(pr));
|
||||
window.location.reload(); // todo: reload less
|
||||
}), (function () {
|
||||
// We don't show the full form until asked. Too much visual clutter.
|
||||
let show = (holdRules || []).length > 0;
|
||||
const box = dom.div();
|
||||
const renderHoldRules = () => {
|
||||
dom._kids(box, !show ?
|
||||
dom.div('No hold rules. ', dom.clickbutton('Add', function click() {
|
||||
show = true;
|
||||
renderHoldRules();
|
||||
})) : [
|
||||
dom.p('Newly submitted messages matching a hold rule will be marked as "on hold" and not be delivered until further action by the admin. To create a rule matching all messages, leave all fields empty.'),
|
||||
dom.table(dom.thead(dom.tr(dom.th('Account'), dom.th('Sender domain'), dom.th('Recipient domain'), dom.th('Action'))), dom.tbody((holdRules || []).length === 0 ? dom.tr(dom.td(attr.colspan('4'), 'No hold rules.')) : [], (holdRules || []).map(pr => dom.tr(!pr.Account && !pr.SenderDomainStr && !pr.RecipientDomainStr ?
|
||||
dom.td(attr.colspan('3'), '(Match all messages)') : [
|
||||
dom.td(pr.Account),
|
||||
dom.td(domainString(pr.SenderDomain)),
|
||||
dom.td(domainString(pr.RecipientDomain)),
|
||||
], dom.td(dom.clickbutton('Remove', attr.title('Removing a hold rule does not modify the "on hold" status of messages in the queue.'), async function click(e) {
|
||||
await check(e.target, client.QueueHoldRuleRemove(pr.ID));
|
||||
window.location.reload(); // todo: reload less
|
||||
})))), dom.tr(dom.td(holdRuleAccount = dom.input(attr.form('holdRuleForm'))), dom.td(holdRuleSenderDomain = dom.input(attr.form('holdRuleForm'))), dom.td(holdRuleRecipientDomain = dom.input(attr.form('holdRuleForm'))), dom.td(holdRuleSubmit = dom.submitbutton('Add hold rule', attr.form('holdRuleForm'), attr.title('When adding a new hold rule, existing messages in queue matching the new rule will be marked as on hold.'))))))
|
||||
]);
|
||||
};
|
||||
renderHoldRules();
|
||||
return box;
|
||||
})(), dom.br(),
|
||||
// Filtering.
|
||||
filterForm = dom.form(attr.id('queuefilter'), // Referenced by input elements in table row.
|
||||
async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await check(e.target, client.QueueKick(m.ID, transport.value));
|
||||
const filter = {
|
||||
IDs: [],
|
||||
Account: filterAccount.value,
|
||||
From: filterFrom.value,
|
||||
To: filterTo.value,
|
||||
Hold: filterHold.value === 'Yes' ? true : (filterHold.value === 'No' ? false : null),
|
||||
Submitted: filterSubmitted.value,
|
||||
NextAttempt: filterNextAttempt.value,
|
||||
Transport: !filterTransport.value ? null : (filterTransport.value === '(default)' ? '' : filterTransport.value),
|
||||
};
|
||||
dom._kids(tbody);
|
||||
msgs = await check({ disabled: false }, client.QueueList(filter));
|
||||
render();
|
||||
}), dom.h2('Messages'), dom.table(dom._class('hover'), dom.thead(dom.tr(dom.th(), dom.th('ID'), dom.th('Submitted'), dom.th('Account'), dom.th('From'), dom.th('To'), dom.th('Size'), dom.th('Attempts'), dom.th('Hold'), dom.th('Next attempt'), dom.th('Last attempt'), dom.th('Last error'), dom.th('Require TLS'), dom.th('Transport'), dom.th()), dom.tr(dom.td(dom.input(attr.type('checkbox'), attr.checked(''), attr.form('queuefilter'), function change(e) {
|
||||
const elem = e.target;
|
||||
for (const [_, toggle] of toggles) {
|
||||
toggle.checked = elem.checked;
|
||||
}
|
||||
})), dom.td(), dom.td(filterSubmitted = dom.input(attr.form('queuefilter'), style({ width: '7em' }), attr.title('Example: "<1h" for filtering messages submitted more than 1 minute ago.'))), dom.td(filterAccount = dom.input(attr.form('queuefilter'))), dom.td(filterFrom = dom.input(attr.form('queuefilter')), attr.title('Example: "@sender.example" to filter by domain of sender.')), dom.td(filterTo = dom.input(attr.form('queuefilter')), attr.title('Example: "@recipient.example" to filter by domain of recipient.')), dom.td(), // todo: add filter by size?
|
||||
dom.td(), // todo: add filter by attempts?
|
||||
dom.td(filterHold = dom.select(attr.form('queuefilter'), dom.option('', attr.value('')), dom.option('Yes'), dom.option('No'), function change() {
|
||||
filterForm.requestSubmit();
|
||||
})), dom.td(filterNextAttempt = dom.input(attr.form('queuefilter'), style({ width: '7em' }), attr.title('Example: ">1h" for filtering messages to be delivered in more than 1 hour, or "<now" for messages to be delivered as soon as possible.'))), dom.td(), dom.td(), dom.td(), dom.td(filterTransport = dom.select(Object.keys(transports || []).length === 0 ? style({ display: 'none' }) : [], attr.form('queuefilter'), function change() {
|
||||
filterForm.requestSubmit();
|
||||
}, dom.option(''), dom.option('(default)'), Object.keys(transports || []).sort().map(t => dom.option(t)))), dom.td(dom.submitbutton('Filter', attr.form('queuefilter')), ' ', dom.clickbutton('Reset', attr.form('queuefilter'), function click() {
|
||||
filterForm.reset();
|
||||
filterForm.requestSubmit();
|
||||
})))), tbody), dom.br(), dom.br(), dom.h2('Change selected messages'), dom.div(style({ display: 'flex', gap: '2em' }), dom.div(dom.div('Hold'), dom.div(dom.clickbutton('On', async function click(e) {
|
||||
const n = await check(e.target, (async () => await client.QueueHoldSet(gatherIDs(), true))());
|
||||
window.alert('' + n + ' message(s) updated');
|
||||
window.location.reload(); // todo: reload less
|
||||
}), ' ', dom.clickbutton('Off', async function click(e) {
|
||||
const n = await check(e.target, (async () => await client.QueueHoldSet(gatherIDs(), false))());
|
||||
window.alert('' + n + ' message(s) updated');
|
||||
window.location.reload(); // todo: reload less
|
||||
}))), dom.div(dom.div('Schedule next delivery attempt'), buttonNextAttemptSet('Now', 0), ' ', dom.clickbutton('More...', function click(e) {
|
||||
e.target.replaceWith(dom.div(dom.br(), dom.div('Scheduled time plus'), dom.div(buttonNextAttemptAdd('1m', 1), ' ', buttonNextAttemptAdd('5m', 5), ' ', buttonNextAttemptAdd('30m', 30), ' ', buttonNextAttemptAdd('1h', 60), ' ', buttonNextAttemptAdd('2h', 2 * 60), ' ', buttonNextAttemptAdd('4h', 4 * 60), ' ', buttonNextAttemptAdd('8h', 8 * 60), ' ', buttonNextAttemptAdd('16h', 16 * 60), ' '), dom.br(), dom.div('Now plus'), dom.div(buttonNextAttemptSet('1m', 1), ' ', buttonNextAttemptSet('5m', 5), ' ', buttonNextAttemptSet('30m', 30), ' ', buttonNextAttemptSet('1h', 60), ' ', buttonNextAttemptSet('2h', 2 * 60), ' ', buttonNextAttemptSet('4h', 4 * 60), ' ', buttonNextAttemptSet('8h', 8 * 60), ' ', buttonNextAttemptSet('16h', 16 * 60), ' ')));
|
||||
})), dom.div(dom.form(dom.label('Require TLS'), requiretlsFieldset = dom.fieldset(requiretls = dom.select(attr.title('How to use TLS for message delivery over SMTP:\n\nDefault: Delivery attempts follow the policies published by the recipient domain: Verification with MTA-STS and/or DANE, or optional opportunistic unverified STARTTLS if the domain does not specify a policy.\n\nWith RequireTLS: For sensitive messages, you may want to require verified TLS. The recipient destination domain SMTP server must support the REQUIRETLS SMTP extension for delivery to succeed. It is automatically chosen when the destination domain mail servers of all recipients are known to support it.\n\nFallback to insecure: If delivery fails due to MTA-STS and/or DANE policies specified by the recipient domain, and the content is not sensitive, you may choose to ignore the recipient domain TLS policies so delivery can succeed.'), dom.option('Default', attr.value('')), dom.option('With RequireTLS', attr.value('yes')), dom.option('Fallback to insecure', attr.value('no'))), ' ', dom.submitbutton('Change')), async function submit(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const n = await check(requiretlsFieldset, (async () => await client.QueueRequireTLSSet(gatherIDs(), requiretls.value === '' ? null : requiretls.value === 'yes'))());
|
||||
window.alert('' + n + ' message(s) updated');
|
||||
window.location.reload(); // todo: only refresh the list
|
||||
})), dom.td(dom.clickbutton('Remove', async function click(e) {
|
||||
})), dom.div(dom.form(dom.label('Transport'), dom.fieldset(transport = dom.select(attr.title('Transport to use for delivery attempts. The default is direct delivery, connecting to the MX hosts of the domain.'), dom.option('(default)', attr.value('')), Object.keys(transports || []).sort().map(t => dom.option(t))), ' ', dom.submitbutton('Change')), async function submit(e) {
|
||||
e.preventDefault();
|
||||
if (!window.confirm('Are you sure you want to remove this message? It will be removed completely.')) {
|
||||
e.stopPropagation();
|
||||
const n = await check(e.target, (async () => await client.QueueTransportSet(gatherIDs(), transport.value))());
|
||||
window.alert('' + n + ' message(s) updated');
|
||||
window.location.reload(); // todo: only refresh the list
|
||||
})), dom.div(dom.div('Delivery'), dom.clickbutton('Fail delivery', attr.title('Cause delivery to fail, sending a DSN to the sender.'), async function click(e) {
|
||||
e.preventDefault();
|
||||
if (!window.confirm('Are you sure you want to remove this message? Notifications of delivery failure will be sent (DSNs).')) {
|
||||
return;
|
||||
}
|
||||
await check(e.target, client.QueueDrop(m.ID));
|
||||
const n = await check(e.target, (async () => await client.QueueFail(gatherIDs()))());
|
||||
window.alert('' + n + ' message(s) updated');
|
||||
window.location.reload(); // todo: only refresh the list
|
||||
})), dom.div(dom.div('Messages'), dom.clickbutton('Remove', attr.title('Completely remove messages from queue, not sending a DSN.'), async function click(e) {
|
||||
e.preventDefault();
|
||||
if (!window.confirm('Are you sure you want to remove this message? It will be removed completely, no DSN about failure to deliver will be sent.')) {
|
||||
return;
|
||||
}
|
||||
const n = await check(e.target, (async () => await client.QueueDrop(gatherIDs()))());
|
||||
window.alert('' + n + ' message(s) updated');
|
||||
window.location.reload(); // todo: only refresh the list
|
||||
})));
|
||||
}))));
|
||||
};
|
||||
const webserver = async () => {
|
||||
|
|
|
@ -2136,101 +2136,403 @@ const dnsbl = async () => {
|
|||
}
|
||||
|
||||
const queueList = async () => {
|
||||
const [msgs, transports] = await Promise.all([
|
||||
client.QueueList(),
|
||||
let [holdRules, msgs, transports] = await Promise.all([
|
||||
client.QueueHoldRuleList(),
|
||||
client.QueueList({IDs: [], Account: '', From: '', To: '', Hold: null, Submitted: '', NextAttempt: '', Transport: null}),
|
||||
client.Transports(),
|
||||
])
|
||||
|
||||
// todo: sorting by address/timestamps/attempts.
|
||||
// todo: after making changes, don't reload entire page. probably best to fetch messages by id and rerender. also report on which messages weren't affected (e.g. no longer in queue).
|
||||
// todo: display which transport will be used for a message according to routing rules (in case none is explicitly configured).
|
||||
// todo: live updates with SSE connections
|
||||
// todo: keep updating times/age.
|
||||
|
||||
const nowSecs = new Date().getTime()/1000
|
||||
|
||||
let holdRuleAccount: HTMLInputElement
|
||||
let holdRuleSenderDomain: HTMLInputElement
|
||||
let holdRuleRecipientDomain: HTMLInputElement
|
||||
let holdRuleSubmit: HTMLButtonElement
|
||||
|
||||
let filterForm: HTMLFormElement
|
||||
let filterAccount: HTMLInputElement
|
||||
let filterFrom: HTMLInputElement
|
||||
let filterTo: HTMLInputElement
|
||||
let filterSubmitted: HTMLInputElement
|
||||
let filterHold: HTMLSelectElement
|
||||
let filterNextAttempt: HTMLInputElement
|
||||
let filterTransport: HTMLSelectElement
|
||||
|
||||
let requiretlsFieldset: HTMLFieldSetElement
|
||||
let requiretls: HTMLSelectElement
|
||||
let transport: HTMLSelectElement
|
||||
|
||||
// Message ID to checkbox.
|
||||
let toggles = new Map<number, HTMLInputElement>()
|
||||
// We operate on what the user has selected, not what the filters would currently
|
||||
// evaluate to. This function can throw an error, which is why we have awkward
|
||||
// syntax when calling this as parameter in api client calls below.
|
||||
const gatherIDs = () => {
|
||||
const f: api.Filter = {
|
||||
IDs: Array.from(toggles.entries()).filter(t => t[1].checked).map(t => t[0]),
|
||||
Account: '',
|
||||
From: '',
|
||||
To: '',
|
||||
Hold: null,
|
||||
Submitted: '',
|
||||
NextAttempt: '',
|
||||
Transport: null,
|
||||
}
|
||||
// Don't want to accidentally operate on all messages.
|
||||
if ((f.IDs || []).length === 0) {
|
||||
throw new Error('No messages selected.')
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
const tbody = dom.tbody()
|
||||
|
||||
const render = () => {
|
||||
toggles = new Map<number, HTMLInputElement>()
|
||||
for (const m of (msgs || [])) {
|
||||
toggles.set(m.ID, dom.input(attr.type('checkbox'), attr.checked(''), ))
|
||||
}
|
||||
|
||||
dom._kids(tbody,
|
||||
(msgs || []).length === 0 ? dom.tr(dom.td(attr.colspan('14'), 'No messages.')) : [],
|
||||
(msgs || []).map(m => {
|
||||
return dom.tr(
|
||||
dom.td(toggles.get(m.ID)!),
|
||||
dom.td(''+m.ID + (m.BaseID > 0 ? '/'+m.BaseID : '')),
|
||||
dom.td(age(new Date(m.Queued), false, nowSecs)),
|
||||
dom.td(m.SenderAccount || '-'),
|
||||
dom.td(m.SenderLocalpart+"@"+ipdomainString(m.SenderDomain)), // todo: escaping of localpart
|
||||
dom.td(m.RecipientLocalpart+"@"+ipdomainString(m.RecipientDomain)), // todo: escaping of localpart
|
||||
dom.td(formatSize(m.Size)),
|
||||
dom.td(''+m.Attempts),
|
||||
dom.td(m.Hold ? 'Hold' : ''),
|
||||
dom.td(age(new Date(m.NextAttempt), true, nowSecs)),
|
||||
dom.td(m.LastAttempt ? age(new Date(m.LastAttempt), false, nowSecs) : '-'),
|
||||
dom.td(m.LastError || '-'),
|
||||
dom.td(m.RequireTLS === true ? 'Yes' : (m.RequireTLS === false ? 'No' : 'Default')),
|
||||
dom.td(m.Transport || '(default)'),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
render()
|
||||
|
||||
const buttonNextAttemptSet = (text: string, minutes: number) => dom.clickbutton(text, async function click(e: MouseEvent) {
|
||||
// note: awkward client call because gatherIDs() can throw an exception.
|
||||
const n = await check(e.target! as HTMLButtonElement, (async () => client.QueueNextAttemptSet(gatherIDs(), minutes))())
|
||||
window.alert(''+n+' message(s) updated')
|
||||
window.location.reload() // todo: reload less
|
||||
})
|
||||
const buttonNextAttemptAdd = (text: string, minutes: number) => dom.clickbutton(text, async function click(e: MouseEvent) {
|
||||
const n = await check(e.target! as HTMLButtonElement, (async () => client.QueueNextAttemptAdd(gatherIDs(), minutes))())
|
||||
window.alert(''+n+' message(s) updated')
|
||||
window.location.reload() // todo: reload less
|
||||
})
|
||||
|
||||
dom._kids(page,
|
||||
crumbs(
|
||||
crumblink('Mox Admin', '#'),
|
||||
'Queue',
|
||||
),
|
||||
|
||||
// todo: sorting by address/timestamps/attempts. perhaps filtering.
|
||||
dom.h2('Hold rules', attr.title('Messages submitted to the queue that match a hold rule are automatically marked as "on hold", preventing delivery until explicitly taken off hold again.')),
|
||||
dom.form(
|
||||
attr.id('holdRuleForm'),
|
||||
async function submit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const pr: api.HoldRule = {
|
||||
ID: 0,
|
||||
Account: holdRuleAccount.value,
|
||||
SenderDomainStr: holdRuleSenderDomain.value,
|
||||
RecipientDomainStr: holdRuleRecipientDomain.value,
|
||||
// Filled in by backend, we provide dummy values.
|
||||
SenderDomain: {ASCII: '', Unicode: ''},
|
||||
RecipientDomain: {ASCII: '', Unicode: ''},
|
||||
}
|
||||
await check(holdRuleSubmit, client.QueueHoldRuleAdd(pr))
|
||||
window.location.reload() // todo: reload less
|
||||
},
|
||||
),
|
||||
(function() {
|
||||
// We don't show the full form until asked. Too much visual clutter.
|
||||
let show = (holdRules || []).length > 0
|
||||
const box = dom.div()
|
||||
const renderHoldRules = () => {
|
||||
dom._kids(box, !show ?
|
||||
dom.div('No hold rules. ',
|
||||
dom.clickbutton('Add', function click() {
|
||||
show = true
|
||||
renderHoldRules()
|
||||
}),
|
||||
) : [
|
||||
dom.p('Newly submitted messages matching a hold rule will be marked as "on hold" and not be delivered until further action by the admin. To create a rule matching all messages, leave all fields empty.'),
|
||||
dom.table(
|
||||
dom.thead(
|
||||
dom.tr(
|
||||
dom.th('Account'),
|
||||
dom.th('Sender domain'),
|
||||
dom.th('Recipient domain'),
|
||||
dom.th('Action'),
|
||||
),
|
||||
),
|
||||
dom.tbody(
|
||||
(holdRules || []).length === 0 ? dom.tr(dom.td(attr.colspan('4'), 'No hold rules.')) : [],
|
||||
(holdRules || []).map(pr =>
|
||||
dom.tr(
|
||||
!pr.Account && !pr.SenderDomainStr && !pr.RecipientDomainStr ?
|
||||
dom.td(attr.colspan('3'), '(Match all messages)') : [
|
||||
dom.td(pr.Account),
|
||||
dom.td(domainString(pr.SenderDomain)),
|
||||
dom.td(domainString(pr.RecipientDomain)),
|
||||
],
|
||||
dom.td(
|
||||
dom.clickbutton('Remove', attr.title('Removing a hold rule does not modify the "on hold" status of messages in the queue.'), async function click(e: MouseEvent) {
|
||||
await check(e.target! as HTMLButtonElement, client.QueueHoldRuleRemove(pr.ID))
|
||||
window.location.reload() // todo: reload less
|
||||
})
|
||||
),
|
||||
)
|
||||
),
|
||||
dom.tr(
|
||||
dom.td(holdRuleAccount=dom.input(attr.form('holdRuleForm'))),
|
||||
dom.td(holdRuleSenderDomain=dom.input(attr.form('holdRuleForm'))),
|
||||
dom.td(holdRuleRecipientDomain=dom.input(attr.form('holdRuleForm'))),
|
||||
dom.td(holdRuleSubmit=dom.submitbutton('Add hold rule', attr.form('holdRuleForm'), attr.title('When adding a new hold rule, existing messages in queue matching the new rule will be marked as on hold.'))),
|
||||
),
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
renderHoldRules()
|
||||
return box
|
||||
})(),
|
||||
dom.br(),
|
||||
|
||||
// Filtering.
|
||||
filterForm=dom.form(
|
||||
attr.id('queuefilter'), // Referenced by input elements in table row.
|
||||
async function submit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const filter: api.Filter = {
|
||||
IDs: [],
|
||||
Account: filterAccount.value,
|
||||
From: filterFrom.value,
|
||||
To: filterTo.value,
|
||||
Hold: filterHold.value === 'Yes' ? true : (filterHold.value === 'No' ? false : null),
|
||||
Submitted: filterSubmitted.value,
|
||||
NextAttempt: filterNextAttempt.value,
|
||||
Transport: !filterTransport.value ? null : (filterTransport.value === '(default)' ? '' : filterTransport.value),
|
||||
}
|
||||
dom._kids(tbody)
|
||||
msgs = await check({disabled: false}, client.QueueList(filter))
|
||||
render()
|
||||
},
|
||||
),
|
||||
|
||||
dom.h2('Messages'),
|
||||
dom.table(dom._class('hover'),
|
||||
dom.thead(
|
||||
dom.tr(
|
||||
dom.th(),
|
||||
dom.th('ID'),
|
||||
dom.th('Submitted'),
|
||||
dom.th('Account'),
|
||||
dom.th('From'),
|
||||
dom.th('To'),
|
||||
dom.th('Size'),
|
||||
dom.th('Attempts'),
|
||||
dom.th('Hold'),
|
||||
dom.th('Next attempt'),
|
||||
dom.th('Last attempt'),
|
||||
dom.th('Last error'),
|
||||
dom.th('Require TLS'),
|
||||
dom.th('Transport/Retry'),
|
||||
dom.th('Remove'),
|
||||
dom.th('Transport'),
|
||||
dom.th(),
|
||||
),
|
||||
),
|
||||
dom.tbody(
|
||||
(msgs || []).length === 0 ? dom.tr(dom.td(attr.colspan('12'), 'Currently no messages in the queue.')) : [],
|
||||
(msgs || []).map(m => {
|
||||
let requiretlsFieldset: HTMLFieldSetElement
|
||||
let requiretls: HTMLSelectElement
|
||||
let transport: HTMLSelectElement
|
||||
return dom.tr(
|
||||
dom.td(''+m.ID + (m.BaseID > 0 ? '/'+m.BaseID : '')),
|
||||
dom.td(age(new Date(m.Queued), false, nowSecs)),
|
||||
dom.td(m.SenderLocalpart+"@"+ipdomainString(m.SenderDomain)), // todo: escaping of localpart
|
||||
dom.td(m.RecipientLocalpart+"@"+ipdomainString(m.RecipientDomain)), // todo: escaping of localpart
|
||||
dom.td(formatSize(m.Size)),
|
||||
dom.td(''+m.Attempts),
|
||||
dom.td(age(new Date(m.NextAttempt), true, nowSecs)),
|
||||
dom.td(m.LastAttempt ? age(new Date(m.LastAttempt), false, nowSecs) : '-'),
|
||||
dom.td(m.LastError || '-'),
|
||||
dom.tr(
|
||||
dom.td(
|
||||
dom.input(attr.type('checkbox'), attr.checked(''), attr.form('queuefilter'), function change(e: MouseEvent) {
|
||||
const elem = e.target! as HTMLInputElement
|
||||
for (const [_, toggle] of toggles) {
|
||||
toggle.checked = elem.checked
|
||||
}
|
||||
}),
|
||||
),
|
||||
dom.td(),
|
||||
dom.td(filterSubmitted=dom.input(attr.form('queuefilter'), style({width: '7em'}), attr.title('Example: "<1h" for filtering messages submitted more than 1 minute ago.'))),
|
||||
dom.td(filterAccount=dom.input(attr.form('queuefilter'))),
|
||||
dom.td(filterFrom=dom.input(attr.form('queuefilter')), attr.title('Example: "@sender.example" to filter by domain of sender.')),
|
||||
dom.td(filterTo=dom.input(attr.form('queuefilter')), attr.title('Example: "@recipient.example" to filter by domain of recipient.')),
|
||||
dom.td(), // todo: add filter by size?
|
||||
dom.td(), // todo: add filter by attempts?
|
||||
dom.td(
|
||||
filterHold=dom.select(
|
||||
attr.form('queuefilter'),
|
||||
dom.option('', attr.value('')),
|
||||
dom.option('Yes'),
|
||||
dom.option('No'),
|
||||
function change() {
|
||||
filterForm.requestSubmit()
|
||||
},
|
||||
),
|
||||
),
|
||||
dom.td(filterNextAttempt=dom.input(attr.form('queuefilter'), style({width: '7em'}), attr.title('Example: ">1h" for filtering messages to be delivered in more than 1 hour, or "<now" for messages to be delivered as soon as possible.'))),
|
||||
dom.td(),
|
||||
dom.td(),
|
||||
dom.td(),
|
||||
dom.td(
|
||||
filterTransport=dom.select(
|
||||
Object.keys(transports || []).length === 0 ? style({display: 'none'}) : [],
|
||||
attr.form('queuefilter'),
|
||||
function change() {
|
||||
filterForm.requestSubmit()
|
||||
},
|
||||
dom.option(''),
|
||||
dom.option('(default)'),
|
||||
Object.keys(transports || []).sort().map(t => dom.option(t))
|
||||
),
|
||||
),
|
||||
dom.td(
|
||||
dom.submitbutton('Filter', attr.form('queuefilter')), ' ',
|
||||
dom.clickbutton('Reset', attr.form('queuefilter'), function click() {
|
||||
filterForm.reset()
|
||||
filterForm.requestSubmit()
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
tbody,
|
||||
),
|
||||
dom.br(),
|
||||
dom.br(),
|
||||
dom.h2('Change selected messages'),
|
||||
dom.div(
|
||||
style({display: 'flex', gap: '2em'}),
|
||||
dom.div(
|
||||
dom.div('Hold'),
|
||||
dom.div(
|
||||
dom.clickbutton('On', async function click(e: MouseEvent) {
|
||||
const n = await check(e.target! as HTMLButtonElement, (async () => await client.QueueHoldSet(gatherIDs(), true))())
|
||||
window.alert(''+n+' message(s) updated')
|
||||
window.location.reload() // todo: reload less
|
||||
}), ' ',
|
||||
dom.clickbutton('Off', async function click(e: MouseEvent) {
|
||||
const n = await check(e.target! as HTMLButtonElement, (async () => await client.QueueHoldSet(gatherIDs(), false))())
|
||||
window.alert(''+n+' message(s) updated')
|
||||
window.location.reload() // todo: reload less
|
||||
}),
|
||||
),
|
||||
),
|
||||
dom.div(
|
||||
dom.div('Schedule next delivery attempt'),
|
||||
buttonNextAttemptSet('Now', 0), ' ',
|
||||
dom.clickbutton('More...', function click(e: MouseEvent) {
|
||||
(e.target! as HTMLButtonElement).replaceWith(
|
||||
dom.div(
|
||||
dom.br(),
|
||||
dom.div('Scheduled time plus'),
|
||||
dom.div(
|
||||
buttonNextAttemptAdd('1m', 1), ' ',
|
||||
buttonNextAttemptAdd('5m', 5), ' ',
|
||||
buttonNextAttemptAdd('30m', 30), ' ',
|
||||
buttonNextAttemptAdd('1h', 60), ' ',
|
||||
buttonNextAttemptAdd('2h', 2*60), ' ',
|
||||
buttonNextAttemptAdd('4h', 4*60), ' ',
|
||||
buttonNextAttemptAdd('8h', 8*60), ' ',
|
||||
buttonNextAttemptAdd('16h', 16*60), ' ',
|
||||
),
|
||||
dom.br(),
|
||||
dom.div('Now plus'),
|
||||
dom.div(
|
||||
buttonNextAttemptSet('1m', 1), ' ',
|
||||
buttonNextAttemptSet('5m', 5), ' ',
|
||||
buttonNextAttemptSet('30m', 30), ' ',
|
||||
buttonNextAttemptSet('1h', 60), ' ',
|
||||
buttonNextAttemptSet('2h', 2*60), ' ',
|
||||
buttonNextAttemptSet('4h', 4*60), ' ',
|
||||
buttonNextAttemptSet('8h', 8*60), ' ',
|
||||
buttonNextAttemptSet('16h', 16*60), ' ',
|
||||
)
|
||||
)
|
||||
)
|
||||
}),
|
||||
),
|
||||
dom.div(
|
||||
dom.form(
|
||||
dom.label('Require TLS'),
|
||||
requiretlsFieldset=dom.fieldset(
|
||||
requiretls=dom.select(
|
||||
attr.title('How to use TLS for message delivery over SMTP:\n\nDefault: Delivery attempts follow the policies published by the recipient domain: Verification with MTA-STS and/or DANE, or optional opportunistic unverified STARTTLS if the domain does not specify a policy.\n\nWith RequireTLS: For sensitive messages, you may want to require verified TLS. The recipient destination domain SMTP server must support the REQUIRETLS SMTP extension for delivery to succeed. It is automatically chosen when the destination domain mail servers of all recipients are known to support it.\n\nFallback to insecure: If delivery fails due to MTA-STS and/or DANE policies specified by the recipient domain, and the content is not sensitive, you may choose to ignore the recipient domain TLS policies so delivery can succeed.'),
|
||||
dom.option('Default', attr.value('')),
|
||||
dom.option('With RequireTLS', attr.value('yes'), m.RequireTLS === true ? attr.selected('') : []),
|
||||
dom.option('Fallback to insecure', attr.value('no'), m.RequireTLS === false ? attr.selected('') : []),
|
||||
dom.option('With RequireTLS', attr.value('yes')),
|
||||
dom.option('Fallback to insecure', attr.value('no')),
|
||||
),
|
||||
' ',
|
||||
dom.submitbutton('Save'),
|
||||
dom.submitbutton('Change'),
|
||||
),
|
||||
async function submit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
await check(requiretlsFieldset, client.QueueSaveRequireTLS(m.ID, requiretls.value === '' ? null : requiretls.value === 'yes'))
|
||||
const n = await check(requiretlsFieldset, (async () => await client.QueueRequireTLSSet(gatherIDs(), requiretls.value === '' ? null : requiretls.value === 'yes'))())
|
||||
window.alert(''+n+' message(s) updated')
|
||||
window.location.reload() // todo: only refresh the list
|
||||
}
|
||||
),
|
||||
),
|
||||
dom.td(
|
||||
dom.div(
|
||||
dom.form(
|
||||
dom.label('Transport'),
|
||||
dom.fieldset(
|
||||
transport=dom.select(
|
||||
attr.title('Transport to use for delivery attempts. The default is direct delivery, connecting to the MX hosts of the domain.'),
|
||||
dom.option('(default)', attr.value('')),
|
||||
Object.keys(transports || []).sort().map(t => dom.option(t, m.Transport === t ? attr.checked('') : [])),
|
||||
Object.keys(transports || []).sort().map(t => dom.option(t)),
|
||||
),
|
||||
' ',
|
||||
dom.submitbutton('Retry now'),
|
||||
dom.submitbutton('Change'),
|
||||
),
|
||||
async function submit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
await check(e.target! as HTMLButtonElement, client.QueueKick(m.ID, transport.value))
|
||||
const n = await check(e.target! as HTMLButtonElement, (async () => await client.QueueTransportSet(gatherIDs(), transport.value))())
|
||||
window.alert(''+n+' message(s) updated')
|
||||
window.location.reload() // todo: only refresh the list
|
||||
}
|
||||
),
|
||||
),
|
||||
dom.td(
|
||||
dom.clickbutton('Remove', async function click(e: MouseEvent) {
|
||||
dom.div(
|
||||
dom.div('Delivery'),
|
||||
dom.clickbutton('Fail delivery', attr.title('Cause delivery to fail, sending a DSN to the sender.'), async function click(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
if (!window.confirm('Are you sure you want to remove this message? It will be removed completely.')) {
|
||||
if (!window.confirm('Are you sure you want to remove this message? Notifications of delivery failure will be sent (DSNs).')) {
|
||||
return
|
||||
}
|
||||
await check(e.target! as HTMLButtonElement, client.QueueDrop(m.ID))
|
||||
const n = await check(e.target! as HTMLButtonElement, (async () => await client.QueueFail(gatherIDs()))())
|
||||
window.alert(''+n+' message(s) updated')
|
||||
window.location.reload() // todo: only refresh the list
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
dom.div(
|
||||
dom.div('Messages'),
|
||||
dom.clickbutton('Remove', attr.title('Completely remove messages from queue, not sending a DSN.'), async function click(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
if (!window.confirm('Are you sure you want to remove this message? It will be removed completely, no DSN about failure to deliver will be sent.')) {
|
||||
return
|
||||
}
|
||||
const n = await check(e.target! as HTMLButtonElement, (async () => await client.QueueDrop(gatherIDs()))())
|
||||
window.alert(''+n+' message(s) updated')
|
||||
window.location.reload() // todo: only refresh the list
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -673,20 +673,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueList",
|
||||
"Docs": "QueueList returns the messages currently in the outgoing queue.",
|
||||
"Params": [],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "r0",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"Msg"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueSize",
|
||||
"Docs": "QueueSize returns the number of messages currently in the outgoing queue.",
|
||||
|
@ -701,45 +687,199 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueKick",
|
||||
"Docs": "QueueKick initiates delivery of a message from the queue and sets the transport\nto use for delivery.",
|
||||
"Params": [
|
||||
"Name": "QueueHoldRuleList",
|
||||
"Docs": "QueueHoldRuleList lists the hold rules.",
|
||||
"Params": [],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "id",
|
||||
"Name": "r0",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
"[]",
|
||||
"HoldRule"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "transport",
|
||||
"Name": "QueueHoldRuleAdd",
|
||||
"Docs": "QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages\nmatching the hold rule will be marked \"on hold\".",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "hr",
|
||||
"Typewords": [
|
||||
"string"
|
||||
"HoldRule"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "r0",
|
||||
"Typewords": [
|
||||
"HoldRule"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueHoldRuleRemove",
|
||||
"Docs": "QueueHoldRuleRemove removes a hold rule. The Hold field of messages in\nthe queue are not changed.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "holdRuleID",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
},
|
||||
{
|
||||
"Name": "QueueList",
|
||||
"Docs": "QueueList returns the messages currently in the outgoing queue.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "filter",
|
||||
"Typewords": [
|
||||
"Filter"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "r0",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"Msg"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueNextAttemptSet",
|
||||
"Docs": "QueueNextAttemptSet sets a new time for next delivery attempt of matching\nmessages from the queue.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "filter",
|
||||
"Typewords": [
|
||||
"Filter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "minutes",
|
||||
"Typewords": [
|
||||
"int32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "affected",
|
||||
"Typewords": [
|
||||
"int32"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueNextAttemptAdd",
|
||||
"Docs": "QueueNextAttemptAdd adds a duration to the time of next delivery attempt of\nmatching messages from the queue.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "filter",
|
||||
"Typewords": [
|
||||
"Filter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "minutes",
|
||||
"Typewords": [
|
||||
"int32"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "affected",
|
||||
"Typewords": [
|
||||
"int32"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueHoldSet",
|
||||
"Docs": "QueueHoldSet sets the Hold field of matching messages in the queue.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "filter",
|
||||
"Typewords": [
|
||||
"Filter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "onHold",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "affected",
|
||||
"Typewords": [
|
||||
"int32"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueFail",
|
||||
"Docs": "QueueFail fails delivery for matching messages, causing DSNs to be sent.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "filter",
|
||||
"Typewords": [
|
||||
"Filter"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "affected",
|
||||
"Typewords": [
|
||||
"int32"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueDrop",
|
||||
"Docs": "QueueDrop removes a message from the queue.",
|
||||
"Docs": "QueueDrop removes matching messages from the queue.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "id",
|
||||
"Name": "filter",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
"Filter"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "affected",
|
||||
"Typewords": [
|
||||
"int32"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueSaveRequireTLS",
|
||||
"Docs": "QueueSaveRequireTLS updates the requiretls field for a message in the queue,\nto be used for the next delivery.",
|
||||
"Name": "QueueRequireTLSSet",
|
||||
"Docs": "QueueRequireTLSSet updates the requiretls field for matching messages in the\nqueue, to be used for the next delivery.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "id",
|
||||
"Name": "filter",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
"Filter"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -750,7 +890,40 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"Returns": []
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "affected",
|
||||
"Typewords": [
|
||||
"int32"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "QueueTransportSet",
|
||||
"Docs": "QueueTransportSet initiates delivery of a message from the queue and sets the transport\nto use for delivery.",
|
||||
"Params": [
|
||||
{
|
||||
"Name": "filter",
|
||||
"Typewords": [
|
||||
"Filter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "transport",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Returns": [
|
||||
{
|
||||
"Name": "affected",
|
||||
"Typewords": [
|
||||
"int32"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "LogLevels",
|
||||
|
@ -3371,6 +3544,119 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "HoldRule",
|
||||
"Docs": "HoldRule is a set of conditions that cause a matching message to be marked as on\nhold when it is queued. All-empty conditions matches all messages, effectively\npausing the entire queue.",
|
||||
"Fields": [
|
||||
{
|
||||
"Name": "ID",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Account",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "SenderDomain",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"Domain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "RecipientDomain",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"Domain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "SenderDomainStr",
|
||||
"Docs": "Unicode.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "RecipientDomainStr",
|
||||
"Docs": "Unicode.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Filter",
|
||||
"Docs": "Filter filters messages to list or operate on. Used by admin web interface\nand cli.\n\nOnly non-empty/non-zero values are applied to the filter. Leaving all fields\nempty/zero matches all messages.",
|
||||
"Fields": [
|
||||
{
|
||||
"Name": "IDs",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"[]",
|
||||
"int64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Account",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "From",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "To",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Hold",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"nullable",
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Submitted",
|
||||
"Docs": "Whether submitted before/after a time relative to now. \"\u003e$duration\" or \"\u003c$duration\", also with \"now\" for duration.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "NextAttempt",
|
||||
"Docs": "\"\u003e$duration\" or \"\u003c$duration\", also with \"now\" for duration.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Transport",
|
||||
"Docs": "",
|
||||
"Typewords": [
|
||||
"nullable",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Msg",
|
||||
"Docs": "Msg is a message in the queue.\n\nUse MakeMsg to make a message with fields that Add needs. Add will further set\nqueueing related fields.",
|
||||
|
@ -3396,6 +3682,13 @@
|
|||
"timestamp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Hold",
|
||||
"Docs": "If set, delivery won't be attempted.",
|
||||
"Typewords": [
|
||||
"bool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "SenderAccount",
|
||||
"Docs": "Failures are delivered back to this local account. Also used for routing.",
|
||||
|
@ -3417,6 +3710,13 @@
|
|||
"IPDomain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "SenderDomainStr",
|
||||
"Docs": "For filtering, unicode.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "RecipientLocalpart",
|
||||
"Docs": "Typically a remote user and domain.",
|
||||
|
@ -3433,7 +3733,7 @@
|
|||
},
|
||||
{
|
||||
"Name": "RecipientDomainStr",
|
||||
"Docs": "For filtering.",
|
||||
"Docs": "For filtering, unicode.",
|
||||
"Typewords": [
|
||||
"string"
|
||||
]
|
||||
|
|
165
webadmin/api.ts
165
webadmin/api.ts
|
@ -465,6 +465,34 @@ export interface ClientConfigsEntry {
|
|||
Note: string
|
||||
}
|
||||
|
||||
// HoldRule is a set of conditions that cause a matching message to be marked as on
|
||||
// hold when it is queued. All-empty conditions matches all messages, effectively
|
||||
// pausing the entire queue.
|
||||
export interface HoldRule {
|
||||
ID: number
|
||||
Account: string
|
||||
SenderDomain: Domain
|
||||
RecipientDomain: Domain
|
||||
SenderDomainStr: string // Unicode.
|
||||
RecipientDomainStr: string // Unicode.
|
||||
}
|
||||
|
||||
// Filter filters messages to list or operate on. Used by admin web interface
|
||||
// and cli.
|
||||
//
|
||||
// Only non-empty/non-zero values are applied to the filter. Leaving all fields
|
||||
// empty/zero matches all messages.
|
||||
export interface Filter {
|
||||
IDs?: number[] | null
|
||||
Account: string
|
||||
From: string
|
||||
To: string
|
||||
Hold?: boolean | null
|
||||
Submitted: string // Whether submitted before/after a time relative to now. ">$duration" or "<$duration", also with "now" for duration.
|
||||
NextAttempt: string // ">$duration" or "<$duration", also with "now" for duration.
|
||||
Transport?: string | null
|
||||
}
|
||||
|
||||
// Msg is a message in the queue.
|
||||
//
|
||||
// Use MakeMsg to make a message with fields that Add needs. Add will further set
|
||||
|
@ -473,12 +501,14 @@ export interface Msg {
|
|||
ID: number
|
||||
BaseID: number // A message for multiple recipients will get a BaseID that is identical to the first Msg.ID queued. The message contents will be identical for each recipient, including MsgPrefix. If other properties are identical too, including recipient domain, multiple Msgs may be delivered in a single SMTP transaction. For messages with a single recipient, this field will be 0.
|
||||
Queued: Date
|
||||
Hold: boolean // If set, delivery won't be attempted.
|
||||
SenderAccount: string // Failures are delivered back to this local account. Also used for routing.
|
||||
SenderLocalpart: Localpart // Should be a local user and domain.
|
||||
SenderDomain: IPDomain
|
||||
SenderDomainStr: string // For filtering, unicode.
|
||||
RecipientLocalpart: Localpart // Typically a remote user and domain.
|
||||
RecipientDomain: IPDomain
|
||||
RecipientDomainStr: string // For filtering.
|
||||
RecipientDomainStr: string // For filtering, unicode.
|
||||
Attempts: number // Next attempt is based on last attempt and exponential back off based on attempts.
|
||||
MaxAttempts: number // Max number of attempts before giving up. If 0, then the default of 8 attempts is used instead.
|
||||
DialedIPs?: { [key: string]: IP[] | null } // For each host, the IPs that were dialed. Used for IP selection for later attempts.
|
||||
|
@ -780,7 +810,7 @@ export type Localpart = string
|
|||
// be an IPv4 address.
|
||||
export type IP = string
|
||||
|
||||
export const structTypes: {[typename: string]: boolean} = {"AuthResults":true,"AutoconfCheckResult":true,"AutodiscoverCheckResult":true,"AutodiscoverSRV":true,"CheckResult":true,"ClientConfigs":true,"ClientConfigsEntry":true,"DANECheckResult":true,"DKIMAuthResult":true,"DKIMCheckResult":true,"DKIMRecord":true,"DMARCCheckResult":true,"DMARCRecord":true,"DMARCSummary":true,"DNSSECResult":true,"DateRange":true,"Directive":true,"Domain":true,"DomainFeedback":true,"Evaluation":true,"EvaluationStat":true,"Extension":true,"FailureDetails":true,"IPDomain":true,"IPRevCheckResult":true,"Identifiers":true,"MTASTSCheckResult":true,"MTASTSRecord":true,"MX":true,"MXCheckResult":true,"Modifier":true,"Msg":true,"Pair":true,"Policy":true,"PolicyEvaluated":true,"PolicyOverrideReason":true,"PolicyPublished":true,"PolicyRecord":true,"Record":true,"Report":true,"ReportMetadata":true,"ReportRecord":true,"Result":true,"ResultPolicy":true,"Reverse":true,"Row":true,"SMTPAuth":true,"SPFAuthResult":true,"SPFCheckResult":true,"SPFRecord":true,"SRV":true,"SRVConfCheckResult":true,"STSMX":true,"Summary":true,"SuppressAddress":true,"TLSCheckResult":true,"TLSRPTCheckResult":true,"TLSRPTDateRange":true,"TLSRPTRecord":true,"TLSRPTSummary":true,"TLSRPTSuppressAddress":true,"TLSReportRecord":true,"TLSResult":true,"Transport":true,"TransportSMTP":true,"TransportSocks":true,"URI":true,"WebForward":true,"WebHandler":true,"WebRedirect":true,"WebStatic":true,"WebserverConfig":true}
|
||||
export const structTypes: {[typename: string]: boolean} = {"AuthResults":true,"AutoconfCheckResult":true,"AutodiscoverCheckResult":true,"AutodiscoverSRV":true,"CheckResult":true,"ClientConfigs":true,"ClientConfigsEntry":true,"DANECheckResult":true,"DKIMAuthResult":true,"DKIMCheckResult":true,"DKIMRecord":true,"DMARCCheckResult":true,"DMARCRecord":true,"DMARCSummary":true,"DNSSECResult":true,"DateRange":true,"Directive":true,"Domain":true,"DomainFeedback":true,"Evaluation":true,"EvaluationStat":true,"Extension":true,"FailureDetails":true,"Filter":true,"HoldRule":true,"IPDomain":true,"IPRevCheckResult":true,"Identifiers":true,"MTASTSCheckResult":true,"MTASTSRecord":true,"MX":true,"MXCheckResult":true,"Modifier":true,"Msg":true,"Pair":true,"Policy":true,"PolicyEvaluated":true,"PolicyOverrideReason":true,"PolicyPublished":true,"PolicyRecord":true,"Record":true,"Report":true,"ReportMetadata":true,"ReportRecord":true,"Result":true,"ResultPolicy":true,"Reverse":true,"Row":true,"SMTPAuth":true,"SPFAuthResult":true,"SPFCheckResult":true,"SPFRecord":true,"SRV":true,"SRVConfCheckResult":true,"STSMX":true,"Summary":true,"SuppressAddress":true,"TLSCheckResult":true,"TLSRPTCheckResult":true,"TLSRPTDateRange":true,"TLSRPTRecord":true,"TLSRPTSummary":true,"TLSRPTSuppressAddress":true,"TLSReportRecord":true,"TLSResult":true,"Transport":true,"TransportSMTP":true,"TransportSocks":true,"URI":true,"WebForward":true,"WebHandler":true,"WebRedirect":true,"WebStatic":true,"WebserverConfig":true}
|
||||
export const stringsTypes: {[typename: string]: boolean} = {"Align":true,"Alignment":true,"CSRFToken":true,"DKIMResult":true,"DMARCPolicy":true,"DMARCResult":true,"Disposition":true,"IP":true,"Localpart":true,"Mode":true,"PolicyOverride":true,"PolicyType":true,"RUA":true,"ResultType":true,"SPFDomainScope":true,"SPFResult":true}
|
||||
export const intsTypes: {[typename: string]: boolean} = {}
|
||||
export const types: TypenameMap = {
|
||||
|
@ -840,7 +870,9 @@ export const types: TypenameMap = {
|
|||
"Reverse": {"Name":"Reverse","Docs":"","Fields":[{"Name":"Hostnames","Docs":"","Typewords":["[]","string"]}]},
|
||||
"ClientConfigs": {"Name":"ClientConfigs","Docs":"","Fields":[{"Name":"Entries","Docs":"","Typewords":["[]","ClientConfigsEntry"]}]},
|
||||
"ClientConfigsEntry": {"Name":"ClientConfigsEntry","Docs":"","Fields":[{"Name":"Protocol","Docs":"","Typewords":["string"]},{"Name":"Host","Docs":"","Typewords":["Domain"]},{"Name":"Port","Docs":"","Typewords":["int32"]},{"Name":"Listener","Docs":"","Typewords":["string"]},{"Name":"Note","Docs":"","Typewords":["string"]}]},
|
||||
"Msg": {"Name":"Msg","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"BaseID","Docs":"","Typewords":["int64"]},{"Name":"Queued","Docs":"","Typewords":["timestamp"]},{"Name":"SenderAccount","Docs":"","Typewords":["string"]},{"Name":"SenderLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"SenderDomain","Docs":"","Typewords":["IPDomain"]},{"Name":"RecipientLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"RecipientDomain","Docs":"","Typewords":["IPDomain"]},{"Name":"RecipientDomainStr","Docs":"","Typewords":["string"]},{"Name":"Attempts","Docs":"","Typewords":["int32"]},{"Name":"MaxAttempts","Docs":"","Typewords":["int32"]},{"Name":"DialedIPs","Docs":"","Typewords":["{}","[]","IP"]},{"Name":"NextAttempt","Docs":"","Typewords":["timestamp"]},{"Name":"LastAttempt","Docs":"","Typewords":["nullable","timestamp"]},{"Name":"LastError","Docs":"","Typewords":["string"]},{"Name":"Has8bit","Docs":"","Typewords":["bool"]},{"Name":"SMTPUTF8","Docs":"","Typewords":["bool"]},{"Name":"IsDMARCReport","Docs":"","Typewords":["bool"]},{"Name":"IsTLSReport","Docs":"","Typewords":["bool"]},{"Name":"Size","Docs":"","Typewords":["int64"]},{"Name":"MessageID","Docs":"","Typewords":["string"]},{"Name":"MsgPrefix","Docs":"","Typewords":["nullable","string"]},{"Name":"DSNUTF8","Docs":"","Typewords":["nullable","string"]},{"Name":"Transport","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]},{"Name":"FutureReleaseRequest","Docs":"","Typewords":["string"]}]},
|
||||
"HoldRule": {"Name":"HoldRule","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"Account","Docs":"","Typewords":["string"]},{"Name":"SenderDomain","Docs":"","Typewords":["Domain"]},{"Name":"RecipientDomain","Docs":"","Typewords":["Domain"]},{"Name":"SenderDomainStr","Docs":"","Typewords":["string"]},{"Name":"RecipientDomainStr","Docs":"","Typewords":["string"]}]},
|
||||
"Filter": {"Name":"Filter","Docs":"","Fields":[{"Name":"IDs","Docs":"","Typewords":["[]","int64"]},{"Name":"Account","Docs":"","Typewords":["string"]},{"Name":"From","Docs":"","Typewords":["string"]},{"Name":"To","Docs":"","Typewords":["string"]},{"Name":"Hold","Docs":"","Typewords":["nullable","bool"]},{"Name":"Submitted","Docs":"","Typewords":["string"]},{"Name":"NextAttempt","Docs":"","Typewords":["string"]},{"Name":"Transport","Docs":"","Typewords":["nullable","string"]}]},
|
||||
"Msg": {"Name":"Msg","Docs":"","Fields":[{"Name":"ID","Docs":"","Typewords":["int64"]},{"Name":"BaseID","Docs":"","Typewords":["int64"]},{"Name":"Queued","Docs":"","Typewords":["timestamp"]},{"Name":"Hold","Docs":"","Typewords":["bool"]},{"Name":"SenderAccount","Docs":"","Typewords":["string"]},{"Name":"SenderLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"SenderDomain","Docs":"","Typewords":["IPDomain"]},{"Name":"SenderDomainStr","Docs":"","Typewords":["string"]},{"Name":"RecipientLocalpart","Docs":"","Typewords":["Localpart"]},{"Name":"RecipientDomain","Docs":"","Typewords":["IPDomain"]},{"Name":"RecipientDomainStr","Docs":"","Typewords":["string"]},{"Name":"Attempts","Docs":"","Typewords":["int32"]},{"Name":"MaxAttempts","Docs":"","Typewords":["int32"]},{"Name":"DialedIPs","Docs":"","Typewords":["{}","[]","IP"]},{"Name":"NextAttempt","Docs":"","Typewords":["timestamp"]},{"Name":"LastAttempt","Docs":"","Typewords":["nullable","timestamp"]},{"Name":"LastError","Docs":"","Typewords":["string"]},{"Name":"Has8bit","Docs":"","Typewords":["bool"]},{"Name":"SMTPUTF8","Docs":"","Typewords":["bool"]},{"Name":"IsDMARCReport","Docs":"","Typewords":["bool"]},{"Name":"IsTLSReport","Docs":"","Typewords":["bool"]},{"Name":"Size","Docs":"","Typewords":["int64"]},{"Name":"MessageID","Docs":"","Typewords":["string"]},{"Name":"MsgPrefix","Docs":"","Typewords":["nullable","string"]},{"Name":"DSNUTF8","Docs":"","Typewords":["nullable","string"]},{"Name":"Transport","Docs":"","Typewords":["string"]},{"Name":"RequireTLS","Docs":"","Typewords":["nullable","bool"]},{"Name":"FutureReleaseRequest","Docs":"","Typewords":["string"]}]},
|
||||
"IPDomain": {"Name":"IPDomain","Docs":"","Fields":[{"Name":"IP","Docs":"","Typewords":["IP"]},{"Name":"Domain","Docs":"","Typewords":["Domain"]}]},
|
||||
"WebserverConfig": {"Name":"WebserverConfig","Docs":"","Fields":[{"Name":"WebDNSDomainRedirects","Docs":"","Typewords":["[]","[]","Domain"]},{"Name":"WebDomainRedirects","Docs":"","Typewords":["[]","[]","string"]},{"Name":"WebHandlers","Docs":"","Typewords":["[]","WebHandler"]}]},
|
||||
"WebHandler": {"Name":"WebHandler","Docs":"","Fields":[{"Name":"LogName","Docs":"","Typewords":["string"]},{"Name":"Domain","Docs":"","Typewords":["string"]},{"Name":"PathRegexp","Docs":"","Typewords":["string"]},{"Name":"DontRedirectPlainHTTP","Docs":"","Typewords":["bool"]},{"Name":"Compress","Docs":"","Typewords":["bool"]},{"Name":"WebStatic","Docs":"","Typewords":["nullable","WebStatic"]},{"Name":"WebRedirect","Docs":"","Typewords":["nullable","WebRedirect"]},{"Name":"WebForward","Docs":"","Typewords":["nullable","WebForward"]},{"Name":"Name","Docs":"","Typewords":["string"]},{"Name":"DNSDomain","Docs":"","Typewords":["Domain"]}]},
|
||||
|
@ -931,6 +963,8 @@ export const parser = {
|
|||
Reverse: (v: any) => parse("Reverse", v) as Reverse,
|
||||
ClientConfigs: (v: any) => parse("ClientConfigs", v) as ClientConfigs,
|
||||
ClientConfigsEntry: (v: any) => parse("ClientConfigsEntry", v) as ClientConfigsEntry,
|
||||
HoldRule: (v: any) => parse("HoldRule", v) as HoldRule,
|
||||
Filter: (v: any) => parse("Filter", v) as Filter,
|
||||
Msg: (v: any) => parse("Msg", v) as Msg,
|
||||
IPDomain: (v: any) => parse("IPDomain", v) as IPDomain,
|
||||
WebserverConfig: (v: any) => parse("WebserverConfig", v) as WebserverConfig,
|
||||
|
@ -1295,15 +1329,6 @@ export class Client {
|
|||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as ClientConfigs
|
||||
}
|
||||
|
||||
// QueueList returns the messages currently in the outgoing queue.
|
||||
async QueueList(): Promise<Msg[] | null> {
|
||||
const fn: string = "QueueList"
|
||||
const paramTypes: string[][] = []
|
||||
const returnTypes: string[][] = [["[]","Msg"]]
|
||||
const params: any[] = []
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as Msg[] | null
|
||||
}
|
||||
|
||||
// QueueSize returns the number of messages currently in the outgoing queue.
|
||||
async QueueSize(): Promise<number> {
|
||||
const fn: string = "QueueSize"
|
||||
|
@ -1313,33 +1338,109 @@ export class Client {
|
|||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as number
|
||||
}
|
||||
|
||||
// QueueKick initiates delivery of a message from the queue and sets the transport
|
||||
// to use for delivery.
|
||||
async QueueKick(id: number, transport: string): Promise<void> {
|
||||
const fn: string = "QueueKick"
|
||||
const paramTypes: string[][] = [["int64"],["string"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [id, transport]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
// QueueHoldRuleList lists the hold rules.
|
||||
async QueueHoldRuleList(): Promise<HoldRule[] | null> {
|
||||
const fn: string = "QueueHoldRuleList"
|
||||
const paramTypes: string[][] = []
|
||||
const returnTypes: string[][] = [["[]","HoldRule"]]
|
||||
const params: any[] = []
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as HoldRule[] | null
|
||||
}
|
||||
|
||||
// QueueDrop removes a message from the queue.
|
||||
async QueueDrop(id: number): Promise<void> {
|
||||
const fn: string = "QueueDrop"
|
||||
// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
|
||||
// matching the hold rule will be marked "on hold".
|
||||
async QueueHoldRuleAdd(hr: HoldRule): Promise<HoldRule> {
|
||||
const fn: string = "QueueHoldRuleAdd"
|
||||
const paramTypes: string[][] = [["HoldRule"]]
|
||||
const returnTypes: string[][] = [["HoldRule"]]
|
||||
const params: any[] = [hr]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as HoldRule
|
||||
}
|
||||
|
||||
// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
|
||||
// the queue are not changed.
|
||||
async QueueHoldRuleRemove(holdRuleID: number): Promise<void> {
|
||||
const fn: string = "QueueHoldRuleRemove"
|
||||
const paramTypes: string[][] = [["int64"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [id]
|
||||
const params: any[] = [holdRuleID]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
}
|
||||
|
||||
// QueueSaveRequireTLS updates the requiretls field for a message in the queue,
|
||||
// to be used for the next delivery.
|
||||
async QueueSaveRequireTLS(id: number, requireTLS: boolean | null): Promise<void> {
|
||||
const fn: string = "QueueSaveRequireTLS"
|
||||
const paramTypes: string[][] = [["int64"],["nullable","bool"]]
|
||||
const returnTypes: string[][] = []
|
||||
const params: any[] = [id, requireTLS]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
|
||||
// QueueList returns the messages currently in the outgoing queue.
|
||||
async QueueList(filter: Filter): Promise<Msg[] | null> {
|
||||
const fn: string = "QueueList"
|
||||
const paramTypes: string[][] = [["Filter"]]
|
||||
const returnTypes: string[][] = [["[]","Msg"]]
|
||||
const params: any[] = [filter]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as Msg[] | null
|
||||
}
|
||||
|
||||
// QueueNextAttemptSet sets a new time for next delivery attempt of matching
|
||||
// messages from the queue.
|
||||
async QueueNextAttemptSet(filter: Filter, minutes: number): Promise<number> {
|
||||
const fn: string = "QueueNextAttemptSet"
|
||||
const paramTypes: string[][] = [["Filter"],["int32"]]
|
||||
const returnTypes: string[][] = [["int32"]]
|
||||
const params: any[] = [filter, minutes]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as number
|
||||
}
|
||||
|
||||
// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
|
||||
// matching messages from the queue.
|
||||
async QueueNextAttemptAdd(filter: Filter, minutes: number): Promise<number> {
|
||||
const fn: string = "QueueNextAttemptAdd"
|
||||
const paramTypes: string[][] = [["Filter"],["int32"]]
|
||||
const returnTypes: string[][] = [["int32"]]
|
||||
const params: any[] = [filter, minutes]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as number
|
||||
}
|
||||
|
||||
// QueueHoldSet sets the Hold field of matching messages in the queue.
|
||||
async QueueHoldSet(filter: Filter, onHold: boolean): Promise<number> {
|
||||
const fn: string = "QueueHoldSet"
|
||||
const paramTypes: string[][] = [["Filter"],["bool"]]
|
||||
const returnTypes: string[][] = [["int32"]]
|
||||
const params: any[] = [filter, onHold]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as number
|
||||
}
|
||||
|
||||
// QueueFail fails delivery for matching messages, causing DSNs to be sent.
|
||||
async QueueFail(filter: Filter): Promise<number> {
|
||||
const fn: string = "QueueFail"
|
||||
const paramTypes: string[][] = [["Filter"]]
|
||||
const returnTypes: string[][] = [["int32"]]
|
||||
const params: any[] = [filter]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as number
|
||||
}
|
||||
|
||||
// QueueDrop removes matching messages from the queue.
|
||||
async QueueDrop(filter: Filter): Promise<number> {
|
||||
const fn: string = "QueueDrop"
|
||||
const paramTypes: string[][] = [["Filter"]]
|
||||
const returnTypes: string[][] = [["int32"]]
|
||||
const params: any[] = [filter]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as number
|
||||
}
|
||||
|
||||
// QueueRequireTLSSet updates the requiretls field for matching messages in the
|
||||
// queue, to be used for the next delivery.
|
||||
async QueueRequireTLSSet(filter: Filter, requireTLS: boolean | null): Promise<number> {
|
||||
const fn: string = "QueueRequireTLSSet"
|
||||
const paramTypes: string[][] = [["Filter"],["nullable","bool"]]
|
||||
const returnTypes: string[][] = [["int32"]]
|
||||
const params: any[] = [filter, requireTLS]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as number
|
||||
}
|
||||
|
||||
// QueueTransportSet initiates delivery of a message from the queue and sets the transport
|
||||
// to use for delivery.
|
||||
async QueueTransportSet(filter: Filter, transport: string): Promise<number> {
|
||||
const fn: string = "QueueTransportSet"
|
||||
const paramTypes: string[][] = [["Filter"],["string"]]
|
||||
const returnTypes: string[][] = [["int32"]]
|
||||
const params: any[] = [filter, transport]
|
||||
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as number
|
||||
}
|
||||
|
||||
// LogLevels returns the current log levels.
|
||||
|
|
|
@ -219,6 +219,7 @@ const [dom, style, attr, prop] = (function () {
|
|||
method: (s) => _attr('method', s),
|
||||
autocomplete: (s) => _attr('autocomplete', s),
|
||||
list: (s) => _attr('list', s),
|
||||
form: (s) => _attr('form', s),
|
||||
};
|
||||
const style = (x) => { return { _styles: x }; };
|
||||
const prop = (x) => { return { _props: x }; };
|
||||
|
|
|
@ -219,6 +219,7 @@ const [dom, style, attr, prop] = (function () {
|
|||
method: (s) => _attr('method', s),
|
||||
autocomplete: (s) => _attr('autocomplete', s),
|
||||
list: (s) => _attr('list', s),
|
||||
form: (s) => _attr('form', s),
|
||||
};
|
||||
const style = (x) => { return { _styles: x }; };
|
||||
const prop = (x) => { return { _props: x }; };
|
||||
|
|
|
@ -219,6 +219,7 @@ const [dom, style, attr, prop] = (function () {
|
|||
method: (s) => _attr('method', s),
|
||||
autocomplete: (s) => _attr('autocomplete', s),
|
||||
list: (s) => _attr('list', s),
|
||||
form: (s) => _attr('form', s),
|
||||
};
|
||||
const style = (x) => { return { _styles: x }; };
|
||||
const prop = (x) => { return { _props: x }; };
|
||||
|
|
Loading…
Reference in a new issue