Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions docs/reference/targets/ignore_error.md
Original file line number Diff line number Diff line change
@@ -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.
141 changes: 141 additions & 0 deletions internal/target/ignore_error/ignore_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2025 Max Mazurov <fox.cpp@disroot.org>, 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 <https://www.gnu.org/licenses/>.
*/

/*
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)
}
73 changes: 73 additions & 0 deletions internal/target/ignore_error/ignore_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2025 Max Mazurov <fox.cpp@disroot.org>, 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 <https://www.gnu.org/licenses/>.
*/

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")
}
}
1 change: 1 addition & 0 deletions maddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down