From 956f7a664e3dd504b9458118f5ebf5d35027b211 Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:58:37 -0400 Subject: [PATCH] target/ignore_error: add wrapper target to ignore delivery errors --- .mkdocs.yml | 1 + docs/reference/targets/ignore_error.md | 26 ++++ internal/target/ignore_error/ignore_error.go | 141 ++++++++++++++++++ .../target/ignore_error/ignore_error_test.go | 73 +++++++++ maddy.go | 1 + 5 files changed, 242 insertions(+) create mode 100644 docs/reference/targets/ignore_error.md create mode 100644 internal/target/ignore_error/ignore_error.go create mode 100644 internal/target/ignore_error/ignore_error_test.go diff --git a/.mkdocs.yml b/.mkdocs.yml index 60f15352d..4ae49b692 100644 --- a/.mkdocs.yml +++ b/.mkdocs.yml @@ -37,6 +37,7 @@ nav: - reference/blob/s3.md - reference/smtp-pipeline.md - SMTP targets: + - reference/targets/ignore_error.md - reference/targets/queue.md - reference/targets/remote.md - reference/targets/smtp.md diff --git a/docs/reference/targets/ignore_error.md b/docs/reference/targets/ignore_error.md new file mode 100644 index 000000000..423c5258e --- /dev/null +++ b/docs/reference/targets/ignore_error.md @@ -0,0 +1,26 @@ +# Ignore delivery errors + +Module that wraps another delivery target and turns any delivery error into a +logged warning instead of propagating it. + +This is useful for non-critical `deliver_to` copies (e.g. push notifications) +where a failure should not abort the rest of the pipeline. + +``` +deliver_to ignore_error smtp tcp://127.0.0.1:2525 +``` + +The wrapped target is given inline, exactly as it would be written after +`deliver_to`. Any configuration block attached to the directive belongs to the +wrapped target and is forwarded as-is: + +``` +deliver_to ignore_error smtp tcp://127.0.0.1:2525 { + connect_timeout 5s +} +``` + +Errors from every delivery stage of the wrapped target (connection, RCPT, body +and commit) are logged and then discarded, so the message is always reported as +delivered to the pipeline. If the wrapped target cannot be started at all, the +whole delivery becomes a no-op. diff --git a/internal/target/ignore_error/ignore_error.go b/internal/target/ignore_error/ignore_error.go new file mode 100644 index 000000000..82efc65e4 --- /dev/null +++ b/internal/target/ignore_error/ignore_error.go @@ -0,0 +1,141 @@ +/* +Maddy Mail Server - Composable all-in-one email server. +Copyright © 2019-2025 Max Mazurov , Maddy Mail Server contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +/* +Package ignore_error implements a delivery target that wraps another target and +turns any delivery error into a logged warning instead of propagating it. + +It is meant for non-critical deliver_to copies (e.g. push notifications) where a +failure should not affect the rest of the pipeline. The wrapped target is given +inline: + + deliver_to ignore_error smtp tcp://127.0.0.1:2525 + +Any configuration block attached to the directive belongs to the wrapped target +and is forwarded as-is. +*/ +package ignore_error + +import ( + "context" + + "github.com/emersion/go-message/textproto" + "github.com/emersion/go-smtp" + "github.com/foxcpp/maddy/framework/buffer" + "github.com/foxcpp/maddy/framework/config" + modconfig "github.com/foxcpp/maddy/framework/config/module" + "github.com/foxcpp/maddy/framework/container" + "github.com/foxcpp/maddy/framework/log" + "github.com/foxcpp/maddy/framework/module" + "github.com/foxcpp/maddy/framework/module/modules" + "github.com/foxcpp/maddy/internal/target" +) + +const modName = "target.ignore_error" + +type Target struct { + instName string + log *log.Logger + wrapped module.DeliveryTarget +} + +func New(c *container.C, _, instName string) (module.Module, error) { + return &Target{ + instName: instName, + log: c.DefaultLogger.Sublogger(modName), + }, nil +} + +func (t *Target) Configure(inlineArgs []string, cfg *config.Map) error { + // The wrapped target is defined inline, so its name and arguments arrive as + // inlineArgs and any attached block belongs to it. Forward both to the + // module loader instead of consuming them here. + return modconfig.ModuleFromNode("target", inlineArgs, cfg.Block, cfg.Globals, &t.wrapped) +} + +func (t *Target) Name() string { + return modName +} + +func (t *Target) InstanceName() string { + return t.instName +} + +func (t *Target) StartDelivery(ctx context.Context, msgMeta *module.MsgMetadata, mailFrom string) (module.Delivery, error) { + d := &delivery{ + log: target.DeliveryLogger(t.log, msgMeta), + } + + wrapped, err := t.wrapped.StartDelivery(ctx, msgMeta, mailFrom) + if err != nil { + // Leave d.wrapped nil so the rest of the delivery is a no-op. + d.log.Error("ignored error from wrapped target", err) + return d, nil + } + d.wrapped = wrapped + return d, nil +} + +type delivery struct { + wrapped module.Delivery + log *log.Logger +} + +func (d *delivery) AddRcpt(ctx context.Context, rcptTo string, opts smtp.RcptOptions) error { + if d.wrapped == nil { + return nil + } + if err := d.wrapped.AddRcpt(ctx, rcptTo, opts); err != nil { + d.log.Error("ignored error from wrapped target", err, "rcpt", rcptTo) + } + return nil +} + +func (d *delivery) Body(ctx context.Context, header textproto.Header, body buffer.Buffer) error { + if d.wrapped == nil { + return nil + } + if err := d.wrapped.Body(ctx, header, body); err != nil { + d.log.Error("ignored error from wrapped target", err) + } + return nil +} + +func (d *delivery) Abort(ctx context.Context) error { + if d.wrapped == nil { + return nil + } + if err := d.wrapped.Abort(ctx); err != nil { + d.log.Error("ignored error from wrapped target", err) + } + return nil +} + +func (d *delivery) Commit(ctx context.Context) error { + if d.wrapped == nil { + return nil + } + if err := d.wrapped.Commit(ctx); err != nil { + d.log.Error("ignored error from wrapped target", err) + } + return nil +} + +func init() { + modules.Register(modName, New) +} diff --git a/internal/target/ignore_error/ignore_error_test.go b/internal/target/ignore_error/ignore_error_test.go new file mode 100644 index 000000000..4f2b18976 --- /dev/null +++ b/internal/target/ignore_error/ignore_error_test.go @@ -0,0 +1,73 @@ +/* +Maddy Mail Server - Composable all-in-one email server. +Copyright © 2019-2025 Max Mazurov , Maddy Mail Server contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package ignore_error + +import ( + "errors" + "testing" + + "github.com/foxcpp/maddy/framework/config" + "github.com/foxcpp/maddy/internal/testutils" +) + +func wrap(t *testing.T, inner *testutils.Target) *Target { + return &Target{ + instName: "test", + log: testutils.Logger(t, modName), + wrapped: inner, + } +} + +func TestPassthroughOnSuccess(t *testing.T) { + inner := &testutils.Target{} + tgt := wrap(t, inner) + + testutils.DoTestDelivery(t, tgt, "from@example.org", []string{"to@example.org"}) + + testutils.CheckTestMessage(t, inner, 0, "from@example.org", []string{"to@example.org"}) +} + +func TestIgnoredErrors(t *testing.T) { + boom := errors.New("boom") + + cases := map[string]*testutils.Target{ + "StartDelivery": {StartErr: boom}, + "AddRcpt": {RcptErr: map[string]error{"to@example.org": boom}}, + "Body": {BodyErr: boom}, + "Commit": {CommitErr: boom}, + } + + for name, inner := range cases { + t.Run(name, func(t *testing.T) { + tgt := wrap(t, inner) + + if _, err := testutils.DoTestDeliveryErr(t, tgt, "from@example.org", []string{"to@example.org"}); err != nil { + t.Errorf("wrapped %s error was not ignored: %v", name, err) + } + }) + } +} + +func TestConfigureRequiresTarget(t *testing.T) { + tgt := &Target{instName: "test", log: testutils.Logger(t, modName)} + + if err := tgt.Configure(nil, config.NewMap(nil, config.Node{})); err == nil { + t.Error("expected an error when no wrapped target is given") + } +} diff --git a/maddy.go b/maddy.go index 1ae32ddcc..ea1919b23 100644 --- a/maddy.go +++ b/maddy.go @@ -77,6 +77,7 @@ import ( _ "github.com/foxcpp/maddy/internal/table" _ "github.com/foxcpp/maddy/internal/table/file" _ "github.com/foxcpp/maddy/internal/table/sql" + _ "github.com/foxcpp/maddy/internal/target/ignore_error" _ "github.com/foxcpp/maddy/internal/target/queue" _ "github.com/foxcpp/maddy/internal/target/remote" _ "github.com/foxcpp/maddy/internal/target/smtp"