author | Alberto Bertogli
<albertito@blitiri.com.ar> 2025-05-02 11:06:49 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2025-05-06 08:31:53 UTC |
parent | 9999a690862eb57301a4e2a3aa5811b6a36800aa |
chasquid.go | +1 | -0 |
docs/man/chasquid.conf.5 | +31 | -95 |
docs/man/chasquid.conf.5.md | +16 | -0 |
docs/man/chasquid.conf.5.pod | +16 | -0 |
etc/chasquid/chasquid.conf | +13 | -0 |
internal/config/config.go | +26 | -0 |
internal/config/config.pb.go | +40 | -9 |
internal/config/config.proto | +14 | -0 |
internal/config/config_test.go | +16 | -0 |
internal/queue/queue.go | +25 | -15 |
internal/queue/queue_test.go | +2 | -2 |
internal/smtpsrv/server.go | +9 | -0 |
diff --git a/chasquid.go b/chasquid.go index 05a6b11..a9bef08 100644 --- a/chasquid.go +++ b/chasquid.go @@ -142,6 +142,7 @@ func main() { STSCache: stsCache, } s.InitQueue(conf.DataDir+"/queue", localC, remoteC) + s.SetQueueLimits(conf.MaxQueueItems, conf.GiveUpSendAfterDuration()) // Load the addresses and listeners. systemdLs, err := systemd.Listeners() diff --git a/docs/man/chasquid.conf.5 b/docs/man/chasquid.conf.5 index 019863d..7dbfcc8 100644 --- a/docs/man/chasquid.conf.5 +++ b/docs/man/chasquid.conf.5 @@ -1,4 +1,5 @@ -.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.43) +.\" -*- mode: troff; coding: utf-8 -*- +.\" Automatically generated by Pod::Man 5.0102 (Pod::Simple 3.45) .\" .\" Standard preamble: .\" ======================================================================== @@ -15,29 +16,12 @@ .ft R .fi .. -.\" Set up some character translations and predefined strings. \*(-- will -.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left -.\" double quote, and \*(R" will give a right double quote. \*(C+ will -.\" give a nicer C++. Capital omega is used to do unbreakable dashes and -.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, -.\" nothing in troff, for use with C<>. -.tr \(*W- -.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' +.\" \*(C` and \*(C' are quotes in nroff, nothing in troff, for use with C<>. .ie n \{\ -. ds -- \(*W- -. ds PI pi -. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch -. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch -. ds L" "" -. ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ -. ds -- \|\(em\| -. ds PI \(*p -. ds L" `` -. ds R" '' . ds C` . ds C' 'br\} @@ -68,82 +52,20 @@ . \} .\} .rr rF -.\" -.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). -.\" Fear. Run. Save yourself. No user-serviceable parts. -. \" fudge factors for nroff and troff -.if n \{\ -. ds #H 0 -. ds #V .8m -. ds #F .3m -. ds #[ \f1 -. ds #] \fP -.\} -.if t \{\ -. ds #H ((1u-(\\\\n(.fu%2u))*.13m) -. ds #V .6m -. ds #F 0 -. ds #[ \& -. ds #] \& -.\} -. \" simple accents for nroff and troff -.if n \{\ -. ds ' \& -. ds ` \& -. ds ^ \& -. ds , \& -. ds ~ ~ -. ds / -.\} -.if t \{\ -. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" -. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' -. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' -. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' -. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' -. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' -.\} -. \" troff and (daisy-wheel) nroff accents -.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' -.ds 8 \h'\*(#H'\(*b\h'-\*(#H' -.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] -.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' -.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' -.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] -.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] -.ds ae a\h'-(\w'a'u*4/10)'e -.ds Ae A\h'-(\w'A'u*4/10)'E -. \" corrections for vroff -.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' -.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' -. \" for low resolution devices (crt and lpr) -.if \n(.H>23 .if \n(.V>19 \ -\{\ -. ds : e -. ds 8 ss -. ds o a -. ds d- d\h'-1'\(ga -. ds D- D\h'-1'\(hy -. ds th \o'bp' -. ds Th \o'LP' -. ds ae ae -. ds Ae AE -.\} -.rm #[ #] #H #V #F C .\" ======================================================================== .\" .IX Title "chasquid.conf 5" -.TH chasquid.conf 5 "2020-11-12" "" "" +.TH chasquid.conf 5 2025-05-02 "" "" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh -.SH "NAME" +.SH NAME chasquid.conf(5) \-\- chasquid configuration file -.SH "SYNOPSIS" +.SH SYNOPSIS .IX Header "SYNOPSIS" \&\fBchasquid.conf\fR\|(5) is \fBchasquid\fR\|(1)'s main configuration file. -.SH "DESCRIPTION" +.SH DESCRIPTION .IX Header "DESCRIPTION" The file is in protocol buffers' text format. .PP @@ -152,12 +74,12 @@ Comments start with \f(CW\*(C`#\*(C'\fR. Empty lines are allowed. Values are of \&\f(CW\*(C`false\*(C'\fR). .PP Some values might be repeated, for example the listening addresses. -.SH "OPTIONS" +.SH OPTIONS .IX Header "OPTIONS" .IP "\fBhostname\fR (string):" 8 .IX Item "hostname (string):" Default hostname to use when saying hello. This is used to say hello to -clients (for aesthetic purposes), and as the \s-1HELO/EHLO\s0 domain on outgoing \s-1SMTP\s0 +clients (for aesthetic purposes), and as the HELO/EHLO domain on outgoing SMTP connections (so ideally it would resolve back to the server, but it isn't a big deal if it doesn't). Default: the system's hostname. .IP "\fBmax_data_size_mb\fR (int):" 8 @@ -165,26 +87,26 @@ big deal if it doesn't). Default: the system's hostname. Maximum email size, in megabytes. Default: 50. .IP "\fBsmtp_address\fR (repeated string):" 8 .IX Item "smtp_address (repeated string):" -Addresses to listen on for \s-1SMTP\s0 (usually port 25). Default: \*(L"systemd\*(R", which +Addresses to listen on for SMTP (usually port 25). Default: "systemd", which means systemd passes sockets to us. systemd sockets must be named with \&\fBFileDescriptorName=smtp\fR. .IP "\fBsubmission_address\fR (repeated string):" 8 .IX Item "submission_address (repeated string):" -Addresses to listen on for submission (usually port 587). Default: \*(L"systemd\*(R", +Addresses to listen on for submission (usually port 587). Default: "systemd", which means systemd passes sockets to us. systemd sockets must be named with \&\fBFileDescriptorName=submission\fR. .IP "\fBsubmission_over_tls_address\fR (repeated string):" 8 .IX Item "submission_over_tls_address (repeated string):" Addresses to listen on for submission-over-TLS (usually port 465). Default: -\&\*(L"systemd\*(R", which means systemd passes sockets to us. systemd sockets must be +"systemd", which means systemd passes sockets to us. systemd sockets must be named with \fBFileDescriptorName=submission_tls\fR. .IP "\fBmonitoring_address\fR (string):" 8 .IX Item "monitoring_address (string):" -Address for the monitoring \s-1HTTP\s0 server. Do \s-1NOT\s0 expose this to the public +Address for the monitoring HTTP server. Do NOT expose this to the public internet. Default: no monitoring server. .IP "\fBmail_delivery_agent_bin\fR (string):" 8 .IX Item "mail_delivery_agent_bin (string):" -Mail delivery agent (\s-1MDA,\s0 also known as \s-1LDA\s0) to use. This should point +Mail delivery agent (MDA, also known as LDA) to use. This should point to the binary to use to deliver email to local users. The content of the email will be passed via stdin. If it exits unsuccessfully, we assume the mail was not delivered. Default: \fImaildrop\fR. @@ -236,12 +158,26 @@ overridden using the \f(CW\*(C`dovecot_userdb_path\*(C'\fR and \f(CW\*(C`dovecot needed. .IP "\fBhaproxy_incoming\fR (bool):" 8 .IX Item "haproxy_incoming (bool):" -\&\fB\s-1EXPERIMENTAL\s0\fR, might change in backwards-incompatible ways. +\&\fBEXPERIMENTAL\fR, might change in backwards-incompatible ways. .Sp -If true, expect incoming \s-1SMTP\s0 connections to use the HAProxy protocol. +If true, expect incoming SMTP connections to use the HAProxy protocol. This allows deploying chasquid behind a HAProxy server, as the address -information is preserved, and \s-1SPF\s0 checks can be performed properly. +information is preserved, and SPF checks can be performed properly. Default: \f(CW\*(C`false\*(C'\fR. +.IP "\fBmax_queue_items\fR (int):" 8 +.IX Item "max_queue_items (int):" +Maximum number of items in the queue. +.Sp +If we have this many items in the queue, we reject new incoming email. Be +careful when increasing this, as we keep all items in memory. +Default: \f(CW200\fR (but may change in the future). +.IP "\fBgive_up_send_after\fR (string):" 8 +.IX Item "give_up_send_after (string):" +How long do we keep retrying sending an email before we give up. Once we give +up, a DSN will be sent back to the sender. +.Sp +The format is a Go duration string (e.g. "48h" or "360m"; note days are not a +supported unit). Default: \f(CW"20h"\fR (but may change in the future). .SH "SEE ALSO" .IX Header "SEE ALSO" \&\fBchasquid\fR\|(1) diff --git a/docs/man/chasquid.conf.5.md b/docs/man/chasquid.conf.5.md index 68b1a7e..9457328 100644 --- a/docs/man/chasquid.conf.5.md +++ b/docs/man/chasquid.conf.5.md @@ -118,6 +118,22 @@ Some values might be repeated, for example the listening addresses. information is preserved, and SPF checks can be performed properly. Default: `false`. +- **max\_queue\_items** (int): + + Maximum number of items in the queue. + + If we have this many items in the queue, we reject new incoming email. Be + careful when increasing this, as we keep all items in memory. + Default: `200` (but may change in the future). + +- **give\_up\_send\_after** (string): + + How long do we keep retrying sending an email before we give up. Once we give + up, a DSN will be sent back to the sender. + + The format is a Go duration string (e.g. "48h" or "360m"; note days are not a + supported unit). Default: `"20h"` (but may change in the future). + # SEE ALSO [chasquid(1)](chasquid.1.md) diff --git a/docs/man/chasquid.conf.5.pod b/docs/man/chasquid.conf.5.pod index 436d5e3..ff576a8 100644 --- a/docs/man/chasquid.conf.5.pod +++ b/docs/man/chasquid.conf.5.pod @@ -122,6 +122,22 @@ This allows deploying chasquid behind a HAProxy server, as the address information is preserved, and SPF checks can be performed properly. Default: C<false>. +=item B<max_queue_items> (int): + +Maximum number of items in the queue. + +If we have this many items in the queue, we reject new incoming email. Be +careful when increasing this, as we keep all items in memory. +Default: C<200> (but may change in the future). + +=item B<give_up_send_after> (string): + +How long do we keep retrying sending an email before we give up. Once we give +up, a DSN will be sent back to the sender. + +The format is a Go duration string (e.g. "48h" or "360m"; note days are not a +supported unit). Default: C<"20h"> (but may change in the future). + =back =head1 SEE ALSO diff --git a/etc/chasquid/chasquid.conf b/etc/chasquid/chasquid.conf index cd18579..298151a 100644 --- a/etc/chasquid/chasquid.conf +++ b/etc/chasquid/chasquid.conf @@ -101,3 +101,16 @@ submission_over_tls_address: ":465" # properly. # Default: false #haproxy_incoming: false + +# Maximum number of items in the queue. +# If we have this many items in the queue, we reject new incoming email. Be +# careful when increasing this, as we keep all items in memory. +# Default: 200 (but may change in the future). +#max_queue_items: 200 + +# How long do we keep retrying sending an email before we give up. +# Once we give up, a DSN will be sent back to the sender. +# The format is a Go duration string (e.g. "48h" or "360m"; note days are not +# a supported unit). +# Default: "20h" (but may change in the future). +#give_up_send_after: "20h" diff --git a/internal/config/config.go b/internal/config/config.go index 1de2353..ca2a850 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,6 +7,7 @@ package config import ( "fmt" "os" + "time" "blitiri.com.ar/go/log" @@ -30,6 +31,9 @@ var defaultConfig = &Config{ DropCharacters: proto.String("."), MailLogPath: "<syslog>", + + MaxQueueItems: 200, + GiveUpSendAfter: "20h", } // Load the config from the given file, with the given overrides. @@ -67,6 +71,12 @@ func Load(path, overrides string) (*Config, error) { } } + // Validate the GiveUpSendAfter value. + if _, err := time.ParseDuration(c.GiveUpSendAfter); err != nil { + return nil, fmt.Errorf( + "invalid give_up_send_after value %q: %v", c.GiveUpSendAfter, err) + } + return c, nil } @@ -126,6 +136,13 @@ func override(c, o *Config) { if o.HaproxyIncoming { c.HaproxyIncoming = true } + + if o.MaxQueueItems > 0 { + c.MaxQueueItems = o.MaxQueueItems + } + if o.GiveUpSendAfter != "" { + c.GiveUpSendAfter = o.GiveUpSendAfter + } } // LogConfig logs the given configuration, in a human-friendly way. @@ -153,4 +170,13 @@ func LogConfig(c *Config) { log.Infof(" Dovecot auth: %v (%q, %q)", c.DovecotAuth, c.DovecotUserdbPath, c.DovecotClientPath) log.Infof(" HAProxy incoming: %v", c.HaproxyIncoming) + log.Infof(" Max queue items: %d", c.MaxQueueItems) + log.Infof(" Give up send after: %s", c.GiveUpSendAfterDuration()) +} + +func (c *Config) GiveUpSendAfterDuration() time.Duration { + // We validate the string value at config load time, so we know it is well + // formed. + d, _ := time.ParseDuration(c.GiveUpSendAfter) + return d } diff --git a/internal/config/config.pb.go b/internal/config/config.pb.go index eddbfa4..08e07a1 100644 --- a/internal/config/config.pb.go +++ b/internal/config/config.pb.go @@ -109,6 +109,18 @@ type Config struct { // This allows deploying chasquid behind a HAProxy server, as the // address information is preserved. HaproxyIncoming bool `protobuf:"varint,16,opt,name=haproxy_incoming,json=haproxyIncoming,proto3" json:"haproxy_incoming,omitempty"` + // Maximum number of items in the queue. + // If we have this many items in the queue, we reject new incoming + // email. Be careful when increasing this, as we keep all items in + // memory. + // Default: 200 (but may change in the future). + MaxQueueItems uint32 `protobuf:"varint,17,opt,name=max_queue_items,json=maxQueueItems,proto3" json:"max_queue_items,omitempty"` + // How long do we keep retrying sending an email before we give up. + // Once we give up, a DSN will be sent back to the sender. + // The format is a Go duration string (e.g. "48h" or "360m"; note days + // are not a supported unit). + // Default: "20h" (but may change in the future). + GiveUpSendAfter string `protobuf:"bytes,18,opt,name=give_up_send_after,json=giveUpSendAfter,proto3" json:"give_up_send_after,omitempty"` } func (x *Config) Reset() { @@ -255,11 +267,25 @@ func (x *Config) GetHaproxyIncoming() bool { return false } +func (x *Config) GetMaxQueueItems() uint32 { + if x != nil { + return x.MaxQueueItems + } + return 0 +} + +func (x *Config) GetGiveUpSendAfter() string { + if x != nil { + return x.GiveUpSendAfter + } + return "" +} + var File_config_proto protoreflect.FileDescriptor var file_config_proto_rawDesc = []byte{ - 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf4, - 0x05, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, + 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc9, + 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x6d, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, @@ -303,13 +329,18 @@ var file_config_proto_rawDesc = []byte{ 0x6f, 0x76, 0x65, 0x63, 0x6f, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x12, 0x29, 0x0a, 0x10, 0x68, 0x61, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x68, 0x61, 0x70, 0x72, - 0x6f, 0x78, 0x79, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x42, 0x14, 0x0a, 0x12, 0x5f, - 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x5f, 0x73, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x6f, 0x72, - 0x73, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x63, 0x68, 0x61, 0x72, 0x61, - 0x63, 0x74, 0x65, 0x72, 0x73, 0x42, 0x2c, 0x5a, 0x2a, 0x62, 0x6c, 0x69, 0x74, 0x69, 0x72, 0x69, - 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x2f, 0x67, 0x6f, 0x2f, 0x63, 0x68, 0x61, 0x73, 0x71, - 0x75, 0x69, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x78, 0x79, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x26, 0x0a, 0x0f, 0x6d, + 0x61, 0x78, 0x5f, 0x71, 0x75, 0x65, 0x75, 0x65, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x11, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x6d, 0x61, 0x78, 0x51, 0x75, 0x65, 0x75, 0x65, 0x49, 0x74, + 0x65, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x12, 0x67, 0x69, 0x76, 0x65, 0x5f, 0x75, 0x70, 0x5f, 0x73, + 0x65, 0x6e, 0x64, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0f, 0x67, 0x69, 0x76, 0x65, 0x55, 0x70, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72, + 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x5f, 0x73, 0x65, 0x70, 0x61, + 0x72, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x64, 0x72, 0x6f, 0x70, 0x5f, + 0x63, 0x68, 0x61, 0x72, 0x61, 0x63, 0x74, 0x65, 0x72, 0x73, 0x42, 0x2c, 0x5a, 0x2a, 0x62, 0x6c, + 0x69, 0x74, 0x69, 0x72, 0x69, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x2f, 0x67, 0x6f, 0x2f, + 0x63, 0x68, 0x61, 0x73, 0x71, 0x75, 0x69, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/config/config.proto b/internal/config/config.proto index 4fd143c..4ffa9a1 100644 --- a/internal/config/config.proto +++ b/internal/config/config.proto @@ -101,4 +101,18 @@ message Config { // This allows deploying chasquid behind a HAProxy server, as the // address information is preserved. bool haproxy_incoming = 16; + + // Maximum number of items in the queue. + // If we have this many items in the queue, we reject new incoming + // email. Be careful when increasing this, as we keep all items in + // memory. + // Default: 200 (but may change in the future). + uint32 max_queue_items = 17; + + // How long do we keep retrying sending an email before we give up. + // Once we give up, a DSN will be sent back to the sender. + // The format is a Go duration string (e.g. "48h" or "360m"; note days + // are not a supported unit). + // Default: "20h" (but may change in the future). + string give_up_send_after = 18; } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 29f6100..8d8c0ec 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -58,6 +58,7 @@ func TestFullConfig(t *testing.T) { monitoring_address: ":1111" max_data_size_mb: 26 suffix_separators: "" + max_queue_items: 345 ` tmpDir, path := mustCreateConfig(t, confStr) @@ -68,6 +69,7 @@ func TestFullConfig(t *testing.T) { submission_address: ":999" dovecot_auth: true drop_characters: "" + give_up_send_after: "7h" ` expected := &Config{ @@ -90,6 +92,9 @@ func TestFullConfig(t *testing.T) { MailLogPath: "<syslog>", DovecotAuth: true, + + MaxQueueItems: 345, + GiveUpSendAfter: "7h", } c, err := Load(path, overrideStr) @@ -134,6 +139,17 @@ func TestBrokenOverride(t *testing.T) { } } +func TestInvalidGiveUpSendingAfter(t *testing.T) { + tmpDir, path := mustCreateConfig( + t, `give_up_send_after: "10"`) + defer testlib.RemoveIfOk(t, tmpDir) + + c, err := Load(path, "") + if err == nil { + t.Fatalf("loaded an invalid config: %v", c) + } +} + // Run LogConfig, overriding the default logger first. This exercises the // code, we don't yet validate the output, but it is an useful sanity check. func testLogConfig(c *Config) { diff --git a/internal/queue/queue.go b/internal/queue/queue.go index 1b8295a..0b177d4 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -33,12 +33,6 @@ import ( ) const ( - // Maximum size of the queue; we reject emails when we hit this. - maxQueueSize = 200 - - // Give up sending attempts after this duration. - giveUpAfter = 20 * time.Hour - // Prefix for item file names. // This is for convenience, versioning, and to be able to tell them apart // temporary files and other cruft. @@ -83,12 +77,6 @@ func init() { // Queue that keeps mail waiting for delivery. type Queue struct { - // Items in the queue. Map of id -> Item. - q map[string]*Item - - // Mutex protecting q. - mu sync.RWMutex - // Couriers to use to deliver mail. localC courier.Courier remoteC courier.Courier @@ -101,6 +89,18 @@ type Queue struct { // Aliases resolver. aliases *aliases.Resolver + + // The maximum number of items in the queue. + MaxItems int + + // Give up sending attempts after this long. + GiveUpAfter time.Duration + + // Mutex protecting q. + mu sync.RWMutex + + // Items in the queue. Map of id -> Item. + q map[string]*Item } // New creates a new Queue instance. @@ -115,6 +115,16 @@ func New(path string, localDomains *set.String, aliases *aliases.Resolver, localDomains: localDomains, path: path, aliases: aliases, + + // We reject emails when we hit this. + // Note the actual default used in the daemon is set in the config. We + // put a non-zero value here just to be safe. + MaxItems: 100, + + // We give up sending (and return a DSN) after this long. + // Note the actual default used in the daemon is set in the config. We + // put a non-zero value here just to be safe. + GiveUpAfter: 20 * time.Hour, } return q, err } @@ -155,8 +165,8 @@ func (q *Queue) Put(tr *trace.Trace, from string, to []string, data []byte) (str tr = tr.NewChild("Queue.Put", from) defer tr.Finish() - if q.Len() >= maxQueueSize { - tr.Errorf("queue full") + if nItems := q.Len(); nItems >= q.MaxItems { + tr.Errorf("queue full (%d items)", nItems) return "", errQueueFull } putCount.Add(1) @@ -305,7 +315,7 @@ func (item *Item) SendLoop(q *Queue) { defer tr.Finish() tr.Printf("from %s", item.From) - for time.Since(item.CreatedAt) < giveUpAfter { + for time.Since(item.CreatedAt) < q.GiveUpAfter { // Send to all recipients that are still pending. var wg sync.WaitGroup for _, rcpt := range item.Rcpt { diff --git a/internal/queue/queue_test.go b/internal/queue/queue_test.go index 9b86656..62892fc 100644 --- a/internal/queue/queue_test.go +++ b/internal/queue/queue_test.go @@ -197,9 +197,9 @@ func TestFullQueue(t *testing.T) { tr := trace.New("test", "TestFullQueue") defer tr.Finish() - // Force-insert maxQueueSize items in the queue. + // Force-insert as many items in the queue as it supports. oneID := "" - for i := 0; i < maxQueueSize; i++ { + for i := 0; i < q.MaxItems; i++ { item := &Item{ Message: Message{ ID: <-newID, diff --git a/internal/smtpsrv/server.go b/internal/smtpsrv/server.go index fc9fa7e..ea27185 100644 --- a/internal/smtpsrv/server.go +++ b/internal/smtpsrv/server.go @@ -243,6 +243,15 @@ func (s *Server) InitQueue(path string, localC, remoteC courier.Courier) { }) } +func (s *Server) SetQueueLimits(maxItems uint32, giveUpAfter time.Duration) { + if maxItems > 0 { + s.queue.MaxItems = int(maxItems) + } + if giveUpAfter > 0 { + s.queue.GiveUpAfter = giveUpAfter + } +} + func (s *Server) aliasResolveRPC(tr *trace.Trace, req url.Values) (url.Values, error) { rcpts, err := s.aliasesR.Resolve(tr, req.Get("Address")) if err != nil {