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:
Mechiel Lukkien
2024-03-18 08:50:42 +01:00
parent 79f1054b64
commit 40ade995a5
19 changed files with 2554 additions and 565 deletions

View File

@ -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.

View File

@ -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 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
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) {
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) {
e.preventDefault();
e.stopPropagation();
await check(e.target, client.QueueKick(m.ID, transport.value));
window.location.reload(); // todo: only refresh the list
})), dom.td(dom.clickbutton('Remove', async function click(e) {
e.preventDefault();
if (!window.confirm('Are you sure you want to remove this message? It will be removed completely.')) {
return;
}
await check(e.target, client.QueueDrop(m.ID));
window.location.reload(); // todo: only refresh the list
})));
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;
// 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(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();
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();
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.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();
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;
}
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 () => {

View File

@ -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.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()
}),
),
),
),
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.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: SubmitEvent) {
e.preventDefault()
e.stopPropagation()
await check(requiretlsFieldset, client.QueueSaveRequireTLS(m.ID, requiretls.value === '' ? null : requiretls.value === 'yes'))
}
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.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: SubmitEvent) {
e.preventDefault()
e.stopPropagation()
await check(e.target! as HTMLButtonElement, client.QueueKick(m.ID, transport.value))
window.location.reload() // todo: only refresh the list
}
),
),
dom.td(
dom.clickbutton('Remove', async function click(e: MouseEvent) {
e.preventDefault()
if (!window.confirm('Are you sure you want to remove this message? It will be removed completely.')) {
return
}
await check(e.target! as HTMLButtonElement, client.QueueDrop(m.ID))
window.location.reload() // todo: only refresh the list
}),
),
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: SubmitEvent) {
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.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: SubmitEvent) {
e.preventDefault()
e.stopPropagation()
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.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? Notifications of delivery failure will be sent (DSNs).')) {
return
}
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
}),
),
),
)

View File

@ -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.",
"Name": "QueueHoldRuleList",
"Docs": "QueueHoldRuleList lists the hold rules.",
"Params": [],
"Returns": [
{
"Name": "r0",
"Typewords": [
"[]",
"HoldRule"
]
}
]
},
{
"Name": "QueueHoldRuleAdd",
"Docs": "QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages\nmatching the hold rule will be marked \"on hold\".",
"Params": [
{
"Name": "id",
"Name": "hr",
"Typewords": [
"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": "transport",
"Name": "minutes",
"Typewords": [
"string"
"int32"
]
}
],
"Returns": []
"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"
]

View File

@ -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.