add account option to skip the first-time sender delay

useful for accounts that automatically process messages and want to process quickly
This commit is contained in:
Mechiel Lukkien 2024-03-16 20:24:07 +01:00
parent 281411c297
commit 8b2c97808d
No known key found for this signature in database
9 changed files with 54 additions and 32 deletions

View file

@ -368,6 +368,7 @@ type Account struct {
JunkFilter *JunkFilter `sconf:"optional" sconf-doc:"Content-based filtering, using the junk-status of individual messages to rank words in such messages as spam or ham. It is recommended you always set the applicable (non)-junk status on messages, and that you do not empty your Trash because those messages contain valuable ham/spam training information."` // todo: sane defaults for junkfilter
MaxOutgoingMessagesPerDay int `sconf:"optional" sconf-doc:"Maximum number of outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 1000."`
MaxFirstTimeRecipientsPerDay int `sconf:"optional" sconf-doc:"Maximum number of first-time recipients in outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 200."`
NoFirstTimeSenderDelay bool `sconf:"optional" sconf-doc:"Do not apply a delay to SMTP connections before accepting an incoming message from a first-time sender. Can be useful for accounts that sends automated responses and want instant replies."`
Routes []Route `sconf:"optional" sconf-doc:"Routes for delivering outgoing messages through the queue. Each delivery attempt evaluates these account routes, domain routes and finally global routes. The transport of the first matching route is used in the delivery attempt. If no routes match, which is the default with no configured routes, messages are delivered directly from the queue."`
DNSDomain dns.Domain `sconf:"-"` // Parsed form of Domain.

View file

@ -1032,6 +1032,11 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
# this mail server in case of account compromise. Default 200. (optional)
MaxFirstTimeRecipientsPerDay: 0
# Do not apply a delay to SMTP connections before accepting an incoming message
# from a first-time sender. Can be useful for accounts that sends automated
# responses and want instant replies. (optional)
NoFirstTimeSenderDelay: false
# Routes for delivering outgoing messages through the queue. Each delivery attempt
# evaluates these account routes, domain routes and finally global routes. The
# transport of the first matching route is used in the delivery attempt. If no

View file

@ -1032,12 +1032,12 @@ func DestinationSave(ctx context.Context, account, destName string, newDest conf
return nil
}
// AccountLimitsSave saves new message sending limits for an account.
func AccountLimitsSave(ctx context.Context, account string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, quotaMessageSize int64) (rerr error) {
// AccountAdminSettingsSave saves new account settings for an account only an admin can change.
func AccountAdminSettingsSave(ctx context.Context, account string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, quotaMessageSize int64, firstTimeSenderDelay bool) (rerr error) {
log := pkglog.WithContext(ctx)
defer func() {
if rerr != nil {
log.Errorx("saving account limits", rerr, slog.String("account", account))
log.Errorx("saving admin account settings", rerr, slog.String("account", account))
}
}()
@ -1060,12 +1060,13 @@ func AccountLimitsSave(ctx context.Context, account string, maxOutgoingMessagesP
acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
acc.QuotaMessageSize = quotaMessageSize
acc.NoFirstTimeSenderDelay = !firstTimeSenderDelay
nc.Accounts[account] = acc
if err := writeDynamic(ctx, log, nc); err != nil {
return fmt.Errorf("writing domains.conf: %v", err)
}
log.Info("account limits saved", slog.String("account", account))
log.Info("admin account settings saved", slog.String("account", account))
return nil
}

View file

@ -2805,8 +2805,8 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
log.Check(err, "adding dmarc evaluation to database for aggregate report")
}
if !a.accept {
conf, _ := acc.Conf()
if !a.accept {
if conf.RejectsMailbox != "" {
present, _, messagehash, err := rejectPresent(log, acc, conf.RejectsMailbox, &m, dataFile)
if err != nil {
@ -2870,10 +2870,10 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
}
}
// If this is a first-time sender and not a forwarded message, wait before actually
// delivering. If this turns out to be a spammer, we've kept one of their
// connections busy.
if delayFirstTime && !m.IsForward && a.reason == reasonNoBadSignals && c.firstTimeSenderDelay > 0 {
// If this is a first-time sender and not a forwarded/mailing list message, wait
// before actually delivering. If this turns out to be a spammer, we've kept one of
// their connections busy.
if delayFirstTime && !m.IsForward && !m.IsMailingList && a.reason == reasonNoBadSignals && !conf.NoFirstTimeSenderDelay && c.firstTimeSenderDelay > 0 {
log.Debug("delaying before delivering from sender without reputation", slog.Duration("delay", c.firstTimeSenderDelay))
mox.Sleep(mox.Context, c.firstTimeSenderDelay)
}
@ -2915,7 +2915,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
metricDelivery.WithLabelValues("delivered", a.reason).Inc()
log.Info("incoming message delivered", slog.String("reason", a.reason), slog.Any("msgfrom", msgFrom))
conf, _ := acc.Conf()
conf, _ = acc.Conf()
if conf.RejectsMailbox != "" && m.MessageID != "" {
if err := acc.RejectsRemove(log, conf.RejectsMailbox, m.MessageID); err != nil {
log.Errorx("removing message from rejects mailbox", err, slog.String("messageid", messageID))

View file

@ -1957,10 +1957,10 @@ func (Admin) SetPassword(ctx context.Context, accountName, password string) {
xcheckf(ctx, err, "setting password")
}
// SetAccountLimits set new limits on outgoing messages for an account.
func (Admin) SetAccountLimits(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64) {
err := mox.AccountLimitsSave(ctx, accountName, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay, maxMsgSize)
xcheckf(ctx, err, "saving account limits")
// AccountSettingsSave set new settings for an account that only an admin can set.
func (Admin) AccountSettingsSave(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64, firstTimeSenderDelay bool) {
err := mox.AccountAdminSettingsSave(ctx, accountName, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay, maxMsgSize, firstTimeSenderDelay)
xcheckf(ctx, err, "saving account settings")
}
// ClientConfigsDomain returns configurations for email clients, IMAP and

View file

@ -795,12 +795,12 @@ var api;
const params = [accountName, password];
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
}
// SetAccountLimits set new limits on outgoing messages for an account.
async SetAccountLimits(accountName, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay, maxMsgSize) {
const fn = "SetAccountLimits";
const paramTypes = [["string"], ["int32"], ["int32"], ["int64"]];
// AccountSettingsSave set new settings for an account that only an admin can set.
async AccountSettingsSave(accountName, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay, maxMsgSize, firstTimeSenderDelay) {
const fn = "AccountSettingsSave";
const paramTypes = [["string"], ["int32"], ["int32"], ["int64"], ["bool"]];
const returnTypes = [];
const params = [accountName, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay, maxMsgSize];
const params = [accountName, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay, maxMsgSize, firstTimeSenderDelay];
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params);
}
// ClientConfigsDomain returns configurations for email clients, IMAP and
@ -1807,6 +1807,7 @@ const account = async (name) => {
let maxOutgoingMessagesPerDay;
let maxFirstTimeRecipientsPerDay;
let quotaMessageSize;
let firstTimeSenderDelay;
let formPassword;
let fieldsetPassword;
let password;
@ -1892,13 +1893,13 @@ const account = async (name) => {
}
form.reset();
window.location.reload(); // todo: only reload the destinations
}, fieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an email address. If empty, a catchall address is configured for the domain.')), dom.br(), localpart = dom.input()), '@', dom.label(style({ display: 'inline-block' }), dom.span('Domain'), dom.br(), domain = dom.select((domains || []).map(d => dom.option(domainName(d), domainName(d) === config.Domain ? attr.selected('') : [])))), ' ', dom.submitbutton('Add address'))), dom.br(), dom.h2('Limits'), dom.form(fieldsetLimits = dom.fieldset(dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum outgoing messages per day', attr.title('Maximum number of outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 1000. MaxOutgoingMessagesPerDay in configuration file.')), dom.br(), maxOutgoingMessagesPerDay = dom.input(attr.type('number'), attr.required(''), attr.value(config.MaxOutgoingMessagesPerDay || 1000))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum first-time recipients per day', attr.title('Maximum number of first-time recipients in outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 200. MaxFirstTimeRecipientsPerDay in configuration file.')), dom.br(), maxFirstTimeRecipientsPerDay = dom.input(attr.type('number'), attr.required(''), attr.value(config.MaxFirstTimeRecipientsPerDay || 200))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Disk usage quota: Maximum total message size ', attr.title('Default maximum total message size in bytes for the account, overriding any globally configured default maximum size if non-zero. A negative value can be used to have no limit in case there is a limit by default. Attempting to add new messages to an account beyond its maximum total size will result in an error. Useful to prevent a single account from filling storage.')), dom.br(), quotaMessageSize = dom.input(attr.value(formatQuotaSize(config.QuotaMessageSize))), ' Current usage is ', formatQuotaSize(Math.floor(diskUsage / (1024 * 1024)) * 1024 * 1024), '.'), dom.submitbutton('Save')), async function submit(e) {
}, fieldset = dom.fieldset(dom.label(style({ display: 'inline-block' }), dom.span('Localpart', attr.title('The localpart is the part before the "@"-sign of an email address. If empty, a catchall address is configured for the domain.')), dom.br(), localpart = dom.input()), '@', dom.label(style({ display: 'inline-block' }), dom.span('Domain'), dom.br(), domain = dom.select((domains || []).map(d => dom.option(domainName(d), domainName(d) === config.Domain ? attr.selected('') : [])))), ' ', dom.submitbutton('Add address'))), dom.br(), dom.h2('Settings'), dom.form(fieldsetLimits = dom.fieldset(dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum outgoing messages per day', attr.title('Maximum number of outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 1000. MaxOutgoingMessagesPerDay in configuration file.')), dom.br(), maxOutgoingMessagesPerDay = dom.input(attr.type('number'), attr.required(''), attr.value(config.MaxOutgoingMessagesPerDay || 1000))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Maximum first-time recipients per day', attr.title('Maximum number of first-time recipients in outgoing messages for this account in a 24 hour window. This limits the damage to recipients and the reputation of this mail server in case of account compromise. Default 200. MaxFirstTimeRecipientsPerDay in configuration file.')), dom.br(), maxFirstTimeRecipientsPerDay = dom.input(attr.type('number'), attr.required(''), attr.value(config.MaxFirstTimeRecipientsPerDay || 200))), dom.label(style({ display: 'block', marginBottom: '.5ex' }), dom.span('Disk usage quota: Maximum total message size ', attr.title('Default maximum total message size in bytes for the account, overriding any globally configured default maximum size if non-zero. A negative value can be used to have no limit in case there is a limit by default. Attempting to add new messages to an account beyond its maximum total size will result in an error. Useful to prevent a single account from filling storage.')), dom.br(), quotaMessageSize = dom.input(attr.value(formatQuotaSize(config.QuotaMessageSize))), ' Current usage is ', formatQuotaSize(Math.floor(diskUsage / (1024 * 1024)) * 1024 * 1024), '.'), dom.div(style({ display: 'block', marginBottom: '.5ex' }), dom.label(firstTimeSenderDelay = dom.input(attr.type('checkbox'), config.NoFirstTimeSenderDelay ? [] : attr.checked('')), ' ', dom.span('Delay deliveries from first-time senders.', attr.title('To slow down potential spammers, when the message is misclassified as non-junk. Turning off the delay can be useful when the account processes messages automatically and needs fast responses.')))), dom.submitbutton('Save')), async function submit(e) {
e.stopPropagation();
e.preventDefault();
fieldsetLimits.disabled = true;
try {
await client.SetAccountLimits(name, parseInt(maxOutgoingMessagesPerDay.value) || 0, parseInt(maxFirstTimeRecipientsPerDay.value) || 0, xparseSize(quotaMessageSize.value));
window.alert('Limits saved.');
await client.AccountSettingsSave(name, parseInt(maxOutgoingMessagesPerDay.value) || 0, parseInt(maxFirstTimeRecipientsPerDay.value) || 0, xparseSize(quotaMessageSize.value), firstTimeSenderDelay.checked);
window.alert('Settings saved.');
}
catch (err) {
console.log({ err });

View file

@ -637,6 +637,7 @@ const account = async (name: string) => {
let maxOutgoingMessagesPerDay: HTMLInputElement
let maxFirstTimeRecipientsPerDay: HTMLInputElement
let quotaMessageSize: HTMLInputElement
let firstTimeSenderDelay: HTMLInputElement
let formPassword: HTMLFormElement
let fieldsetPassword: HTMLFieldSetElement
@ -769,7 +770,7 @@ const account = async (name: string) => {
),
),
dom.br(),
dom.h2('Limits'),
dom.h2('Settings'),
dom.form(
fieldsetLimits=dom.fieldset(
dom.label(
@ -791,6 +792,13 @@ const account = async (name: string) => {
quotaMessageSize=dom.input(attr.value(formatQuotaSize(config.QuotaMessageSize))),
' Current usage is ', formatQuotaSize(Math.floor(diskUsage/(1024*1024))*1024*1024), '.',
),
dom.div(
style({display: 'block', marginBottom: '.5ex'}),
dom.label(
firstTimeSenderDelay=dom.input(attr.type('checkbox'), config.NoFirstTimeSenderDelay ? [] : attr.checked('')), ' ',
dom.span('Delay deliveries from first-time senders.', attr.title('To slow down potential spammers, when the message is misclassified as non-junk. Turning off the delay can be useful when the account processes messages automatically and needs fast responses.')),
),
),
dom.submitbutton('Save'),
),
async function submit(e: SubmitEvent) {
@ -798,8 +806,8 @@ const account = async (name: string) => {
e.preventDefault()
fieldsetLimits.disabled = true
try {
await client.SetAccountLimits(name, parseInt(maxOutgoingMessagesPerDay.value) || 0, parseInt(maxFirstTimeRecipientsPerDay.value) || 0, xparseSize(quotaMessageSize.value))
window.alert('Limits saved.')
await client.AccountSettingsSave(name, parseInt(maxOutgoingMessagesPerDay.value) || 0, parseInt(maxFirstTimeRecipientsPerDay.value) || 0, xparseSize(quotaMessageSize.value), firstTimeSenderDelay.checked)
window.alert('Settings saved.')
} catch (err) {
console.log({err})
window.alert('Error: ' + errmsg(err))

View file

@ -617,8 +617,8 @@
"Returns": []
},
{
"Name": "SetAccountLimits",
"Docs": "SetAccountLimits set new limits on outgoing messages for an account.",
"Name": "AccountSettingsSave",
"Docs": "AccountSettingsSave set new settings for an account that only an admin can set.",
"Params": [
{
"Name": "accountName",
@ -643,6 +643,12 @@
"Typewords": [
"int64"
]
},
{
"Name": "firstTimeSenderDelay",
"Typewords": [
"bool"
]
}
],
"Returns": []

View file

@ -1276,12 +1276,12 @@ export class Client {
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
}
// SetAccountLimits set new limits on outgoing messages for an account.
async SetAccountLimits(accountName: string, maxOutgoingMessagesPerDay: number, maxFirstTimeRecipientsPerDay: number, maxMsgSize: number): Promise<void> {
const fn: string = "SetAccountLimits"
const paramTypes: string[][] = [["string"],["int32"],["int32"],["int64"]]
// AccountSettingsSave set new settings for an account that only an admin can set.
async AccountSettingsSave(accountName: string, maxOutgoingMessagesPerDay: number, maxFirstTimeRecipientsPerDay: number, maxMsgSize: number, firstTimeSenderDelay: boolean): Promise<void> {
const fn: string = "AccountSettingsSave"
const paramTypes: string[][] = [["string"],["int32"],["int32"],["int64"],["bool"]]
const returnTypes: string[][] = []
const params: any[] = [accountName, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay, maxMsgSize]
const params: any[] = [accountName, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay, maxMsgSize, firstTimeSenderDelay]
return await _sherpaCall(this.baseURL, this.authState, { ...this.options }, paramTypes, returnTypes, fn, params) as void
}