From 877e9a0813773f26cee2b3528cee1000f1f5d556 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Mon, 3 Mar 2025 09:16:17 +1100 Subject: [PATCH 1/3] feat(modify): new modifier add_header --- .mkdocs.yml | 1 + docs/reference/modifiers/header.md | 23 ++++++++ internal/modify/add_header.go | 94 ++++++++++++++++++++++++++++++ internal/modify/add_header_test.go | 90 ++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 docs/reference/modifiers/header.md create mode 100644 internal/modify/add_header.go create mode 100644 internal/modify/add_header_test.go diff --git a/.mkdocs.yml b/.mkdocs.yml index 3e90eec1..0e8ca1dd 100644 --- a/.mkdocs.yml +++ b/.mkdocs.yml @@ -53,6 +53,7 @@ nav: - SMTP modifiers: - reference/modifiers/dkim.md - reference/modifiers/envelope.md + - reference/modifiers/header.md - Lookup tables (string translation): - reference/table/static.md - reference/table/regexp.md diff --git a/docs/reference/modifiers/header.md b/docs/reference/modifiers/header.md new file mode 100644 index 00000000..7c599439 --- /dev/null +++ b/docs/reference/modifiers/header.md @@ -0,0 +1,23 @@ +# Header Modifiers + +## Adding a new header + +`add_header` module modifies the message by adding an header. + +Note: the header must be present in the message prior to the execution of the modifier. +If the header already exist it will result in an error. + + +Definition: + +``` +add_header +``` + +Use examples: + +``` +modify { + add_header X-My-Header "header value" +} +``` diff --git a/internal/modify/add_header.go b/internal/modify/add_header.go new file mode 100644 index 00000000..fb5d6e2f --- /dev/null +++ b/internal/modify/add_header.go @@ -0,0 +1,94 @@ +/* +Maddy Mail Server - Composable all-in-one email server. +Copyright © 2019-2020 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 modify + +import ( + "context" + "errors" + "fmt" + + "github.com/emersion/go-message/textproto" + "github.com/foxcpp/maddy/framework/buffer" + "github.com/foxcpp/maddy/framework/config" + "github.com/foxcpp/maddy/framework/module" +) + +// addHeader is a simple module that adds a header to the message +type addHeader struct { + modName string + instName string + + headerName string + headerValue string +} + +func NewAddHeader(modName, instName string) (module.Module, error) { + r := addHeader{ + modName: modName, + instName: instName, + } + + return &r, nil +} + +func (m *addHeader) Configure(inlineArgs []string, cfg *config.Map) error { + if len(inlineArgs) != 2 { + return errors.New("modify.add_header: at least two arguments required") + } + m.headerName = inlineArgs[0] + m.headerValue = inlineArgs[1] + return nil +} + +func (r addHeader) Name() string { + return r.modName +} + +func (r addHeader) InstanceName() string { + return r.instName +} + +func (r addHeader) ModStateForMsg(ctx context.Context, msgMeta *module.MsgMetadata) (module.ModifierState, error) { + return r, nil +} + +func (r addHeader) RewriteSender(ctx context.Context, mailFrom string) (string, error) { + return mailFrom, nil +} + +func (r addHeader) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, error) { + return []string{rcptTo}, nil +} + +func (r addHeader) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error { + if h.Has(r.headerName) { + return fmt.Errorf("cannot add header `%s` as it is already present", r.headerName) + } + + h.Set(r.headerName, r.headerValue) + return nil +} + +func (r addHeader) Close() error { + return nil +} + +func init() { + module.Register("modify.add_header", NewAddHeader) +} diff --git a/internal/modify/add_header_test.go b/internal/modify/add_header_test.go new file mode 100644 index 00000000..0e8bf632 --- /dev/null +++ b/internal/modify/add_header_test.go @@ -0,0 +1,90 @@ +/* +Maddy Mail Server - Composable all-in-one email server. +Copyright © 2019-2020 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 modify + +import ( + "bytes" + "context" + "fmt" + "strings" + "testing" + + "github.com/emersion/go-message/textproto" + "github.com/foxcpp/maddy/framework/buffer" + "github.com/foxcpp/maddy/framework/config" + "github.com/foxcpp/maddy/framework/module" +) + +func TestAddHeader(t *testing.T) { + test := func(headerName string, headerValue string, duplicateHeader bool) { + t.Helper() + + mod, err := NewAddHeader("modify.add_header", "") + if err != nil { + t.Fatal(err) + } + m := mod.(*addHeader) + if err := m.Configure([]string{headerName, headerValue}, config.NewMap(nil, config.Node{})); err != nil { + t.Fatal(err) + } + + state, err := m.ModStateForMsg(context.Background(), &module.MsgMetadata{}) + if err != nil { + t.Fatal(err) + } + + testHdr := textproto.Header{} + testHdr.Add("From", "") + testHdr.Add("Subject", "heya") + testHdr.Add("To", "") + body := []byte("hello there\r\n") + + // modify.dkim expects RewriteSender to be called to get envelope sender + // (see module.Modifier docs) + + err = state.RewriteBody(context.Background(), &testHdr, buffer.MemoryBuffer{Slice: body}) + if err != nil { + if duplicateHeader && strings.Contains(err.Error(), "already present") { + return + } + t.Fatal(err) + } + if duplicateHeader && err == nil { + t.Fatalf("expected error on duplicate header") + } + + var fullBody bytes.Buffer + if err := textproto.WriteHeader(&fullBody, testHdr); err != nil { + t.Fatal(err) + } + if _, err := fullBody.Write(body); err != nil { + t.Fatal(err) + } + + if !strings.Contains(fmt.Sprintf("%s", &fullBody), fmt.Sprintf("%s: %s", headerName, headerValue)) { + t.Fatalf("new header not found in message") + } + } + + test("Something", "Somevalue", false) + test("X-Testing", "Somevalue", false) + // Test setting a header that is already present + test("To", "Somevalue", true) + test("To", "Somevalue", true) +} From 495e26427f9b894e0b0764e4dda660e77548bb94 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Mon, 3 Mar 2025 23:39:58 +1100 Subject: [PATCH 2/3] fix(modify): allow adding multiple entries for existing headers --- docs/reference/modifiers/header.md | 4 +--- internal/modify/add_header.go | 7 +------ internal/modify/add_header_test.go | 25 ++++++++++--------------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/docs/reference/modifiers/header.md b/docs/reference/modifiers/header.md index 7c599439..58b54104 100644 --- a/docs/reference/modifiers/header.md +++ b/docs/reference/modifiers/header.md @@ -4,9 +4,7 @@ `add_header` module modifies the message by adding an header. -Note: the header must be present in the message prior to the execution of the modifier. -If the header already exist it will result in an error. - +Note: Adding a header with an existing key will create multiple entries for that key in the message. Definition: diff --git a/internal/modify/add_header.go b/internal/modify/add_header.go index fb5d6e2f..0492bcd0 100644 --- a/internal/modify/add_header.go +++ b/internal/modify/add_header.go @@ -21,7 +21,6 @@ package modify import ( "context" "errors" - "fmt" "github.com/emersion/go-message/textproto" "github.com/foxcpp/maddy/framework/buffer" @@ -77,11 +76,7 @@ func (r addHeader) RewriteRcpt(ctx context.Context, rcptTo string) ([]string, er } func (r addHeader) RewriteBody(ctx context.Context, h *textproto.Header, body buffer.Buffer) error { - if h.Has(r.headerName) { - return fmt.Errorf("cannot add header `%s` as it is already present", r.headerName) - } - - h.Set(r.headerName, r.headerValue) + h.Add(r.headerName, r.headerValue) return nil } diff --git a/internal/modify/add_header_test.go b/internal/modify/add_header_test.go index 0e8bf632..d1066eb0 100644 --- a/internal/modify/add_header_test.go +++ b/internal/modify/add_header_test.go @@ -32,7 +32,7 @@ import ( ) func TestAddHeader(t *testing.T) { - test := func(headerName string, headerValue string, duplicateHeader bool) { + test := func(headerName string, headerValue string, expectedValues []string) { t.Helper() mod, err := NewAddHeader("modify.add_header", "") @@ -60,15 +60,8 @@ func TestAddHeader(t *testing.T) { err = state.RewriteBody(context.Background(), &testHdr, buffer.MemoryBuffer{Slice: body}) if err != nil { - if duplicateHeader && strings.Contains(err.Error(), "already present") { - return - } t.Fatal(err) } - if duplicateHeader && err == nil { - t.Fatalf("expected error on duplicate header") - } - var fullBody bytes.Buffer if err := textproto.WriteHeader(&fullBody, testHdr); err != nil { t.Fatal(err) @@ -77,14 +70,16 @@ func TestAddHeader(t *testing.T) { t.Fatal(err) } - if !strings.Contains(fmt.Sprintf("%s", &fullBody), fmt.Sprintf("%s: %s", headerName, headerValue)) { - t.Fatalf("new header not found in message") + for _, wantedValue := range expectedValues { + if !strings.Contains(fullBody.String(), fmt.Sprintf("%s: %s", headerName, wantedValue)) { + t.Fatalf("new header not found in message") + } } + } - test("Something", "Somevalue", false) - test("X-Testing", "Somevalue", false) - // Test setting a header that is already present - test("To", "Somevalue", true) - test("To", "Somevalue", true) + test("Something", "Somevalue", []string{"Somevalue"}) + test("X-Testing", "Somevalue", []string{"Somevalue"}) + // Test setting a header that is already present should result two entries + test("To", "Somevalue", []string{"", "Somevalue"}) } From 5b87a66f9065d98bc85c188a0a050909474f39e4 Mon Sep 17 00:00:00 2001 From: Matteo Sessa Date: Mon, 3 Mar 2025 23:43:12 +1100 Subject: [PATCH 3/3] chore: remote old comments --- internal/modify/add_header_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/modify/add_header_test.go b/internal/modify/add_header_test.go index d1066eb0..48ca7351 100644 --- a/internal/modify/add_header_test.go +++ b/internal/modify/add_header_test.go @@ -55,9 +55,6 @@ func TestAddHeader(t *testing.T) { testHdr.Add("To", "") body := []byte("hello there\r\n") - // modify.dkim expects RewriteSender to be called to get envelope sender - // (see module.Modifier docs) - err = state.RewriteBody(context.Background(), &testHdr, buffer.MemoryBuffer{Slice: body}) if err != nil { t.Fatal(err)