implement limits on outgoing messages for an account

by default 1000 messages per day, and to max 200 first-time receivers.
i don't think a person would reach those limits. a compromised account abused
by spammers could easily reach that limit. this prevents further damage.

the error message you will get is quite clear, pointing to the configuration
parameter that should be changed.
This commit is contained in:
Mechiel Lukkien
2023-03-28 20:50:36 +02:00
parent 9bd497b836
commit 9b57c69c1c
14 changed files with 262 additions and 4 deletions

View File

@ -1669,6 +1669,71 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
msgPrefix = append(msgPrefix, "Date: "+time.Now().Format(message.RFC5322Z)+"\r\n"...)
}
// Limit damage to the internet and our reputation in case of account compromise by
// limiting the max number of messages sent in a 24 hour window, both total number
// of messages and number of first-time recipients.
err = c.account.DB.Read(func(tx *bstore.Tx) error {
conf, _ := c.account.Conf()
msgmax := conf.MaxOutgoingMessagesPerDay
if msgmax == 0 {
// For human senders, 1000 recipients in a day is quite a lot.
msgmax = 1000
}
rcptmax := conf.MaxFirstTimeRecipientsPerDay
if rcptmax == 0 {
// Human senders may address a new human-sized list of people once in a while. In
// case of a compromise, a spammer will probably try to send to many new addresses.
rcptmax = 200
}
rcpts := map[string]time.Time{}
n := 0
err := bstore.QueryTx[store.Outgoing](tx).FilterGreater("Submitted", time.Now().Add(-24*time.Hour)).ForEach(func(o store.Outgoing) error {
n++
if rcpts[o.Recipient].IsZero() || o.Submitted.Before(rcpts[o.Recipient]) {
rcpts[o.Recipient] = o.Submitted
}
return nil
})
xcheckf(err, "querying message recipients in past 24h")
if n+len(c.recipients) > msgmax {
metricSubmission.WithLabelValues("messagelimiterror").Inc()
xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of messages (%d) over past 24h reached, try increasing per-account setting MaxOutgoingMessagesPerDay", msgmax)
}
// Only check if max first-time recipients is reached if there are enough messages
// to trigger the limit.
if n+len(c.recipients) < rcptmax {
return nil
}
isFirstTime := func(rcpt string, before time.Time) bool {
exists, err := bstore.QueryTx[store.Outgoing](tx).FilterNonzero(store.Outgoing{Recipient: rcpt}).FilterLess("Submitted", before).Exists()
xcheckf(err, "checking in database whether recipient is first-time")
return !exists
}
firsttime := 0
now := time.Now()
for _, rcptAcc := range c.recipients {
r := rcptAcc.rcptTo
if isFirstTime(r.XString(true), now) {
firsttime++
}
}
for r, t := range rcpts {
if isFirstTime(r, t) {
firsttime++
}
}
if firsttime > rcptmax {
metricSubmission.WithLabelValues("recipientlimiterror").Inc()
xsmtpUserErrorf(smtp.C451LocalErr, smtp.SePol7DeliveryUnauth1, "max number of new/first-time recipients (%d) over past 24h reached, try increasing per-account setting MaxFirstTimeRecipientsPerDay", rcptmax)
}
return nil
})
xcheckf(err, "read-only transaction")
// todo future: in a pedantic mode, we can parse the headers, and return an error if rcpt is only in To or Cc header, and not in the non-empty Bcc header. indicates a client that doesn't blind those bcc's.
// Add DKIM signatures.
@ -1763,6 +1828,9 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
}
metricSubmission.WithLabelValues("ok").Inc()
c.log.Info("submitted message delivered", mlog.Field("mailfrom", *c.mailFrom), mlog.Field("rcptto", rcptAcc.rcptTo), mlog.Field("smtputf8", c.smtputf8), mlog.Field("msgsize", msgSize))
err := c.account.DB.Insert(&store.Outgoing{Recipient: rcptAcc.rcptTo.XString(true)})
xcheckf(err, "adding outgoing message")
}
})
@ -1794,6 +1862,9 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
}
metricSubmission.WithLabelValues("ok").Inc()
c.log.Info("message queued for delivery", mlog.Field("mailfrom", *c.mailFrom), mlog.Field("rcptto", rcptAcc.rcptTo), mlog.Field("smtputf8", c.smtputf8), mlog.Field("msgsize", msgSize))
err := c.account.DB.Insert(&store.Outgoing{Recipient: rcptAcc.rcptTo.XString(true)})
xcheckf(err, "adding outgoing message")
}
}
err = dataFile.Close()