Skip to content

Commit 598c5ea

Browse files
committed
smtpserver: when logging recipients, actually show something about the recipient
before this change, we were logging an empty string, which turned into "[]", looking like an empty array. misleading and unhelpful. this is fixed by making struct fields on type recipient "exported" so they can get logged, and by changing the logging code to log nested struct/pointer/interface fields if we would otherwise wouldn't log anything (when only logging more basic data types). we'll now get log lines like: l=info m="deliver attempt to unknown user(s)" pkg=smtpserver recipients="[[email protected]]" for issue #232 by snabb, thanks for reporting!
1 parent 879477a commit 598c5ea

File tree

2 files changed

+66
-56
lines changed

2 files changed

+66
-56
lines changed

mlog/log.go

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -426,29 +426,39 @@ func stringValue(iscid, nested bool, v any) string {
426426
}
427427
n := rv.NumField()
428428
t := rv.Type()
429-
b := &strings.Builder{}
430-
first := true
431-
for i := 0; i < n; i++ {
432-
fv := rv.Field(i)
433-
if !t.Field(i).IsExported() {
434-
continue
435-
}
436-
if fv.Kind() == reflect.Struct || fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Interface {
437-
// Don't recurse.
438-
continue
439-
}
440-
vs := stringValue(false, true, fv.Interface())
441-
if vs == "" {
442-
continue
429+
430+
// We first try making a string without recursing into structs/pointers/interfaces,
431+
// but will try again with those fields if we otherwise would otherwise log an
432+
// empty string.
433+
for j := 0; j < 2; j++ {
434+
first := true
435+
b := &strings.Builder{}
436+
for i := 0; i < n; i++ {
437+
fv := rv.Field(i)
438+
if !t.Field(i).IsExported() {
439+
continue
440+
}
441+
if j == 0 && (fv.Kind() == reflect.Struct || fv.Kind() == reflect.Ptr || fv.Kind() == reflect.Interface) {
442+
// Don't recurse.
443+
continue
444+
}
445+
vs := stringValue(false, true, fv.Interface())
446+
if vs == "" {
447+
continue
448+
}
449+
if !first {
450+
b.WriteByte(' ')
451+
}
452+
first = false
453+
k := strings.ToLower(t.Field(i).Name)
454+
b.WriteString(k + "=" + vs)
443455
}
444-
if !first {
445-
b.WriteByte(' ')
456+
rs := b.String()
457+
if rs != "" {
458+
return rs
446459
}
447-
first = false
448-
k := strings.ToLower(t.Field(i).Name)
449-
b.WriteString(k + "=" + vs)
450460
}
451-
return b.String()
461+
return ""
452462
}
453463

454464
func writeAttr(w io.Writer, separator, group string, a slog.Attr) {

smtpserver/server.go

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -344,24 +344,24 @@ type conn struct {
344344
}
345345

346346
type rcptAccount struct {
347-
accountName string
348-
destination config.Destination
349-
canonicalAddress string // Optional catchall part stripped and/or lowercased.
347+
AccountName string
348+
Destination config.Destination
349+
CanonicalAddress string // Optional catchall part stripped and/or lowercased.
350350
}
351351

352352
type rcptAlias struct {
353-
alias config.Alias
354-
canonicalAddress string // Optional catchall part stripped and/or lowercased.
353+
Alias config.Alias
354+
CanonicalAddress string // Optional catchall part stripped and/or lowercased.
355355
}
356356

357357
type recipient struct {
358-
addr smtp.Path
358+
Addr smtp.Path
359359

360360
// If account and alias are both not set, this is not for a local address. This is
361361
// normal for submission, where messages are added to the queue. For incoming
362362
// deliveries, this will result in an error.
363-
account *rcptAccount // If set, recipient address is for this local account.
364-
alias *rcptAlias // If set, for a local alias.
363+
Account *rcptAccount // If set, recipient address is for this local account.
364+
Alias *rcptAlias // If set, for a local alias.
365365
}
366366

367367
func isClosed(err error) bool {
@@ -1732,7 +1732,7 @@ func (c *conn) isSMTPUTF8Required(part *message.Part) bool {
17321732
}
17331733
// Check all "RCPT TO".
17341734
for _, rcpt := range c.recipients {
1735-
if hasNonASCII(strings.NewReader(string(rcpt.addr.Localpart))) {
1735+
if hasNonASCII(strings.NewReader(string(rcpt.Addr.Localpart))) {
17361736
return true
17371737
}
17381738
}
@@ -2037,7 +2037,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
20372037
err = c.account.DB.Read(ctx, func(tx *bstore.Tx) error {
20382038
rcpts := make([]smtp.Path, len(c.recipients))
20392039
for i, r := range c.recipients {
2040-
rcpts[i] = r.addr
2040+
rcpts[i] = r.Addr
20412041
}
20422042
msglimit, rcptlimit, err := c.account.SendLimitReached(tx, rcpts)
20432043
xcheckf(err, "checking sender limit")
@@ -2136,7 +2136,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
21362136
qml := make([]queue.Msg, len(c.recipients))
21372137
for i, rcpt := range c.recipients {
21382138
if Localserve {
2139-
code, timeout := mox.LocalserveNeedsError(rcpt.addr.Localpart)
2139+
code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
21402140
if timeout {
21412141
c.log.Info("timing out submission due to special localpart")
21422142
mox.Sleep(mox.Context, time.Hour)
@@ -2160,11 +2160,11 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
21602160
// messages in a single smtp transaction.
21612161
var rcptTo string
21622162
if len(c.recipients) == 1 {
2163-
rcptTo = rcpt.addr.String()
2163+
rcptTo = rcpt.Addr.String()
21642164
}
21652165
xmsgPrefix := append([]byte(recvHdrFor(rcptTo)), msgPrefix...)
21662166
msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
2167-
qm := queue.MakeMsg(fp, rcpt.addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
2167+
qm := queue.MakeMsg(fp, rcpt.Addr, msgWriter.Has8bit, c.msgsmtputf8, msgSize, messageID, xmsgPrefix, c.requireTLS, now, header.Get("Subject"))
21682168
if !c.futureRelease.IsZero() {
21692169
qm.NextAttempt = c.futureRelease
21702170
qm.FutureReleaseRequest = c.futureReleaseRequest
@@ -2190,15 +2190,15 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
21902190
for i, rcpt := range c.recipients {
21912191
c.log.Info("messages queued for delivery",
21922192
slog.Any("mailfrom", *c.mailFrom),
2193-
slog.Any("rcptto", rcpt.addr),
2193+
slog.Any("rcptto", rcpt.Addr),
21942194
slog.Bool("smtputf8", c.smtputf8),
21952195
slog.Bool("msgsmtputf8", c.msgsmtputf8),
21962196
slog.Int64("msgsize", qml[i].Size))
21972197
}
21982198

21992199
err = c.account.DB.Write(ctx, func(tx *bstore.Tx) error {
22002200
for _, rcpt := range c.recipients {
2201-
outgoing := store.Outgoing{Recipient: rcpt.addr.XString(true)}
2201+
outgoing := store.Outgoing{Recipient: rcpt.Addr.XString(true)}
22022202
if err := tx.Insert(&outgoing); err != nil {
22032203
return fmt.Errorf("adding outgoing message: %v", err)
22042204
}
@@ -2418,7 +2418,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
24182418
// Give immediate response if all recipients are unknown.
24192419
nunknown := 0
24202420
for _, r := range c.recipients {
2421-
if r.account == nil && r.alias == nil {
2421+
if r.Account == nil && r.Alias == nil {
24222422
nunknown++
24232423
}
24242424
}
@@ -2653,7 +2653,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
26532653
}
26542654
var deliverErrors []deliverError
26552655
addError := func(rcpt recipient, code int, secode string, userError bool, errmsg string) {
2656-
e := deliverError{rcpt.addr, code, secode, userError, errmsg}
2656+
e := deliverError{rcpt.Addr, code, secode, userError, errmsg}
26572657
c.log.Info("deliver error",
26582658
slog.Any("rcptto", e.rcptTo),
26592659
slog.Int("code", code),
@@ -2666,9 +2666,9 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
26662666
// Sort recipients: local accounts, aliases, unknown. For ensuring we don't deliver
26672667
// to an alias destination that was also explicitly sent to.
26682668
rcptScore := func(r recipient) int {
2669-
if r.account != nil {
2669+
if r.Account != nil {
26702670
return 0
2671-
} else if r.alias != nil {
2671+
} else if r.Alias != nil {
26722672
return 1
26732673
}
26742674
return 2
@@ -2682,9 +2682,9 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
26822682
// addressee. Relies on c.recipients being sorted as above.
26832683
regularRecipient := func(addr smtp.Path) bool {
26842684
for _, rcpt := range c.recipients {
2685-
if rcpt.account == nil {
2685+
if rcpt.Account == nil {
26862686
break
2687-
} else if rcpt.addr.Equal(addr) {
2687+
} else if rcpt.Addr.Equal(addr) {
26882688
return true
26892689
}
26902690
}
@@ -2761,7 +2761,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
27612761
// we will consider a message delivered if we delivered it to at least one account
27622762
// (others may be over quota).
27632763
processRecipient := func(rcpt recipient) {
2764-
log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.addr))
2764+
log := c.log.With(slog.Any("mailfrom", c.mailFrom), slog.Any("rcptto", rcpt.Addr))
27652765

27662766
// If this is not a valid local user, we send back a DSN. This can only happen when
27672767
// there are also valid recipients, and only when remote is SPF-verified, so the DSN
@@ -2771,7 +2771,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
27712771
// deliveries, and return an error at the end? Though the failure conditions will
27722772
// probably prevent any other successful deliveries too...
27732773
// We'll continue delivering to other recipients. ../rfc/5321:3275
2774-
if rcpt.account == nil && rcpt.alias == nil {
2774+
if rcpt.Account == nil && rcpt.Alias == nil {
27752775
metricDelivery.WithLabelValues("unknownuser", "").Inc()
27762776
addError(rcpt, smtp.C550MailboxUnavail, smtp.SeAddr1UnknownDestMailbox1, true, "no such user")
27772777
return
@@ -2792,17 +2792,17 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
27922792
// check. We check all alias destinations, even if we already explicitly delivered
27932793
// to them: they may be the only destination that would accept the message.
27942794
var a0 *analysis // Analysis we've used for accept/reject decision.
2795-
if rcpt.alias != nil {
2795+
if rcpt.Alias != nil {
27962796
// Check if msgFrom address is acceptable. This doesn't take validation into
27972797
// consideration. If the header was forged, the message may be rejected later on.
2798-
if !aliasAllowedMsgFrom(rcpt.alias.alias, msgFrom) {
2798+
if !aliasAllowedMsgFrom(rcpt.Alias.Alias, msgFrom) {
27992799
addError(rcpt, smtp.C550MailboxUnavail, smtp.SePol7ExpnProhibited2, true, "not allowed to send to destination")
28002800
return
28012801
}
28022802

2803-
la = make([]analysis, 0, len(rcpt.alias.alias.ParsedAddresses))
2804-
for _, aa := range rcpt.alias.alias.ParsedAddresses {
2805-
a, err := messageAnalyze(log, rcpt.addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.alias.canonicalAddress)
2803+
la = make([]analysis, 0, len(rcpt.Alias.Alias.ParsedAddresses))
2804+
for _, aa := range rcpt.Alias.Alias.ParsedAddresses {
2805+
a, err := messageAnalyze(log, rcpt.Addr, aa.Address.Path(), aa.AccountName, aa.Destination, rcpt.Alias.CanonicalAddress)
28062806
if err != nil {
28072807
addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
28082808
return
@@ -2818,7 +2818,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
28182818
a0 = &la[0]
28192819
}
28202820
} else {
2821-
a, err := messageAnalyze(log, rcpt.addr, rcpt.addr, rcpt.account.accountName, rcpt.account.destination, rcpt.account.canonicalAddress)
2821+
a, err := messageAnalyze(log, rcpt.Addr, rcpt.Addr, rcpt.Account.AccountName, rcpt.Account.Destination, rcpt.Account.CanonicalAddress)
28222822
if err != nil {
28232823
addError(rcpt, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
28242824
return
@@ -2893,7 +2893,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
28932893
"Return-Path: <" + c.mailFrom.String() + ">\r\n" + // ../rfc/5321:3300
28942894
rcptAuthResults.Header() +
28952895
receivedSPF.Header() +
2896-
recvHdrFor(rcpt.addr.String()),
2896+
recvHdrFor(rcpt.Addr.String()),
28972897
)
28982898
la[i].d.m.Size += int64(len(la[i].d.m.MsgPrefix))
28992899
}
@@ -2986,7 +2986,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
29862986
Disposition: disposition,
29872987
AlignedDKIMPass: dmarcResult.AlignedDKIMPass,
29882988
AlignedSPFPass: dmarcResult.AlignedSPFPass,
2989-
EnvelopeTo: rcpt.addr.IPDomain.String(),
2989+
EnvelopeTo: rcpt.Addr.IPDomain.String(),
29902990
EnvelopeFrom: c.mailFrom.IPDomain.String(),
29912991
HeaderFrom: msgFrom.Domain.Name(),
29922992
}
@@ -3034,7 +3034,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
30343034
if !a0.accept {
30353035
for _, a := range la {
30363036
// Don't add message if address was also explicitly present in a RCPT TO command.
3037-
if rcpt.alias != nil && regularRecipient(a.d.deliverTo) {
3037+
if rcpt.Alias != nil && regularRecipient(a.d.deliverTo) {
30383038
continue
30393039
}
30403040

@@ -3084,7 +3084,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
30843084
}
30853085

30863086
delayFirstTime := true
3087-
if rcpt.account != nil && a0.dmarcReport != nil {
3087+
if rcpt.Account != nil && a0.dmarcReport != nil {
30883088
// todo future: add rate limiting to prevent DoS attacks. ../rfc/7489:2570
30893089
if err := dmarcdb.AddReport(ctx, a0.dmarcReport, msgFrom.Domain); err != nil {
30903090
log.Errorx("saving dmarc aggregate report in database", err)
@@ -3094,7 +3094,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
30943094
delayFirstTime = false
30953095
}
30963096
}
3097-
if rcpt.account != nil && a0.tlsReport != nil {
3097+
if rcpt.Account != nil && a0.tlsReport != nil {
30983098
// todo future: add rate limiting to prevent DoS attacks.
30993099
if err := tlsrptdb.AddReport(ctx, c.log, msgFrom.Domain, c.mailFrom.String(), a0.d.destination.HostTLSReports, a0.tlsReport); err != nil {
31003100
log.Errorx("saving TLSRPT report in database", err)
@@ -3115,7 +3115,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
31153115
}
31163116

31173117
if Localserve {
3118-
code, timeout := mox.LocalserveNeedsError(rcpt.addr.Localpart)
3118+
code, timeout := mox.LocalserveNeedsError(rcpt.Addr.Localpart)
31193119
if timeout {
31203120
log.Info("timing out due to special localpart")
31213121
mox.Sleep(mox.Context, time.Hour)
@@ -3147,7 +3147,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
31473147
for _, a := range la {
31483148
// Don't deliver to recipient that was explicitly present in SMTP transaction, or
31493149
// is sending the message to an alias they are member of.
3150-
if rcpt.alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
3150+
if rcpt.Alias != nil && (regularRecipient(a.d.deliverTo) || a.d.deliverTo.Equal(msgFrom.Path())) {
31513151
continue
31523152
}
31533153

0 commit comments

Comments
 (0)