Skip to content

Commit f3c19fe

Browse files
committed
reject/ereject extension
1 parent 44b13e7 commit f3c19fe

12 files changed

Lines changed: 136 additions & 10 deletions

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ go run ./cmd/sieve-run -scriptPath ./cmd/sieve-run/test.sieve -eml ./cmd/sieve-r
4444
- Some upstream tests are intentionally disabled with documented reasons (`tests/base_test.go`); preserve these expectations unless fixing underlying parser/address behavior.
4545
- `interp/dovecot_testsuite.go` intentionally treats `test_error` checks as no-op pass (go-sieve stops on first error; Pigeonhole collects multiple). `test_imap_metadata_set` is unimplemented and will error if used in `.svtest` files.
4646
- `CmdDovecotTest.Execute` copies `RuntimeData` via `d.Copy()` for isolation between test blocks — commands inside `test { ... }` do not affect outer state.
47+
- New upstream tests should be added to `tests/` as `.svtest` files, not as Go unit tests, to preserve the Dovecot test suite as the canonical source of accepted-invalid-script cases.
48+
- New features should be added with both Go unit tests (for API-level validation) and `.svtest` cases (to preserve Dovecot test suite coverage).
49+
- Tests that expect certain MTA behavior should be added to `tests/execute.go`. `ExecuteTestEnvironment` is an interface to mock MTA environment that applies actions and provides access to results (e.g. sent messages); use it for tests that care about action semantics. For tests that only care about script acceptance/rejection, `RunDovecotTest` without extra parameters is sufficient.
50+
- Tests added to `tests/execute.go` should pass with the simple `simpleExecuteRuntime` implemented in `tests/execute_test.go`. This ensures that go-sieve is able to provide enough data to the MTA to execute correct decisions.
4751

4852
## Known caveats to preserve
4953
- `README.md` lists accepted-invalid-script gaps and address parsing caveats; avoid changes that silently alter these behaviors without targeted tests.

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
go-sieve
22
====================
33

4-
Sieve email filtering language ([RFC 5228])
5-
implementation in Go.
4+
Sieve email filtering language ([RFC 5228]) interpreter implementation in Go.
5+
6+
## Features
7+
8+
* Binary representation for faster load/execute cycles (`Script.Save`, `sieve.RestoreSaved`).
9+
* Integration tests harness for MTA integration testing (see `tests/execute.go`).
610

711
## Supported extensions
812

@@ -12,6 +16,8 @@ implementation in Go.
1216
- imap4flags ([RFC 5232])
1317
- variables ([RFC 5229])
1418
- relational ([RFC 5231])
19+
- copy ([RFC 3894])
20+
- reject/ereject ([RFC 5429])
1521

1622
## Example
1723

@@ -27,4 +33,5 @@ See ./cmd/sieve-run.
2733
[RFC 5229]: https://datatracker.ietf.org/doc/html/rfc5229
2834
[RFC 5232]: https://datatracker.ietf.org/doc/html/rfc5232
2935
[RFC 5231]: https://datatracker.ietf.org/doc/html/rfc5231
30-
36+
[RFC 3894]: https://datatracker.ietf.org/doc/html/rfc3894
37+
[RFC 5429]: https://datatracker.ietf.org/doc/html/rfc5429

interp/action.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,30 @@ func (c CmdRemoveFlag) Execute(_ context.Context, d *RuntimeData) error {
224224
return nil
225225
}
226226

227+
type CmdReject struct {
228+
Reason string
229+
}
230+
231+
func (c CmdReject) Execute(ctx context.Context, d *RuntimeData) error {
232+
if err := d.OnAction(ctx, ActionReject{Reason: expandVars(d, c.Reason)}, d); err != nil {
233+
return err
234+
}
235+
d.ImplicitKeep = false
236+
return nil
237+
}
238+
239+
type CmdEReject struct {
240+
Reason string
241+
}
242+
243+
func (c CmdEReject) Execute(ctx context.Context, d *RuntimeData) error {
244+
if err := d.OnAction(ctx, ActionEReject{Reason: expandVars(d, c.Reason)}, d); err != nil {
245+
return err
246+
}
247+
d.ImplicitKeep = false
248+
return nil
249+
}
250+
227251
func init() {
228252
gob.Register(CmdStop{})
229253
gob.Register(CmdFileInto{})
@@ -233,4 +257,6 @@ func init() {
233257
gob.Register(CmdSetFlag{})
234258
gob.Register(CmdAddFlag{})
235259
gob.Register(CmdRemoveFlag{})
260+
gob.Register(CmdReject{})
261+
gob.Register(CmdEReject{})
236262
}

interp/applied_action.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,19 @@ type ActionRedirect struct {
3232
Copy bool
3333
}
3434

35-
func (ActionRedirect) testActionName() string { return "redirect" }
35+
func (ActionRedirect) testActionName() string { return "redirect" }
3636
func (a ActionRedirect) cancelsImplicitKeep() bool { return !a.Copy }
37+
38+
type ActionReject struct {
39+
Reason string
40+
}
41+
42+
func (ActionReject) testActionName() string { return "reject" }
43+
func (ActionReject) cancelsImplicitKeep() bool { return true }
44+
45+
type ActionEReject struct {
46+
Reason string
47+
}
48+
49+
func (ActionEReject) testActionName() string { return "ereject" }
50+
func (ActionEReject) cancelsImplicitKeep() bool { return true }

interp/dovecot_testsuite.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,9 @@ func (t TestDovecotResultAction) Check(_ context.Context, d *RuntimeData) (bool,
399399
if t.isCount() {
400400
entryCount := uint64(0)
401401
if t.Index != nil {
402-
if *t.Index < len(d.AppliedActions) {
402+
// Pigeonhole uses 1-based indexing for :index in test_result_action.
403+
idx := *t.Index - 1
404+
if idx >= 0 && idx < len(d.AppliedActions) {
403405
entryCount++
404406
}
405407
} else {
@@ -410,10 +412,12 @@ func (t TestDovecotResultAction) Check(_ context.Context, d *RuntimeData) (bool,
410412
}
411413

412414
if t.Index != nil {
413-
if *t.Index >= len(d.AppliedActions) {
415+
// Pigeonhole uses 1-based indexing for :index in test_result_action.
416+
idx := *t.Index - 1
417+
if idx < 0 || idx >= len(d.AppliedActions) {
414418
return false, nil
415419
}
416-
action := d.AppliedActions[*t.Index]
420+
action := d.AppliedActions[idx]
417421

418422
ok, err := t.matcherTest.tryMatch(d, action.testActionName())
419423
if err != nil {

interp/load.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ var supportedRequires = map[string]struct{}{
2323
"variables": {},
2424
"relational": {},
2525
"copy": {},
26+
"reject": {},
27+
"ereject": {},
2628
}
2729

2830
var (
@@ -50,6 +52,9 @@ func init() {
5052
"removeflag": loadRemoveFlag,
5153
// RFC 5229 (variables extension)
5254
"set": loadSet,
55+
// RFC 5429 (reject/ereject extensions)
56+
"reject": loadReject,
57+
"ereject": loadEReject,
5358
// vnd.dovecot.testsuite
5459
"test": loadDovecotTest,
5560
"test_set": loadDovecotTestSet,

interp/load_action.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,50 @@ func loadDiscard(s *Script, pcmd parser.Cmd) (Cmd, error) {
163163
return cmd, err
164164
}
165165

166+
func loadReject(s *Script, pcmd parser.Cmd) (Cmd, error) {
167+
if !s.RequiresExtension("reject") {
168+
return nil, parser.ErrorAt(pcmd.Position, "missing require 'reject'")
169+
}
170+
cmd := CmdReject{}
171+
err := LoadSpec(s, &Spec{
172+
Pos: []SpecPosArg{
173+
{
174+
MinStrCount: 1,
175+
MaxStrCount: 1,
176+
MatchStr: func(val []string) {
177+
cmd.Reason = val[0]
178+
},
179+
},
180+
},
181+
}, pcmd.Position, pcmd.Args, pcmd.Tests, pcmd.Block)
182+
if err != nil {
183+
return nil, err
184+
}
185+
return cmd, nil
186+
}
187+
188+
func loadEReject(s *Script, pcmd parser.Cmd) (Cmd, error) {
189+
if !s.RequiresExtension("ereject") {
190+
return nil, parser.ErrorAt(pcmd.Position, "missing require 'ereject'")
191+
}
192+
cmd := CmdEReject{}
193+
err := LoadSpec(s, &Spec{
194+
Pos: []SpecPosArg{
195+
{
196+
MinStrCount: 1,
197+
MaxStrCount: 1,
198+
MatchStr: func(val []string) {
199+
cmd.Reason = val[0]
200+
},
201+
},
202+
},
203+
}, pcmd.Position, pcmd.Args, pcmd.Tests, pcmd.Block)
204+
if err != nil {
205+
return nil, err
206+
}
207+
return cmd, nil
208+
}
209+
166210
func loadFlagCmd(s *Script, pcmd parser.Cmd) (variable string, flags []string, err error) {
167211
var arg1, arg2 []string
168212
err = LoadSpec(s, &Spec{

interp/load_dovecot.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,15 @@ func loadDovecotResultAction(s *Script, test parser.Test) (Test, error) {
266266
},
267267
},
268268
}), test.Position, test.Args, test.Tests, nil)
269-
return loaded, err
269+
if err != nil {
270+
return nil, err
271+
}
272+
273+
if err := loaded.setKey(s, loaded.Key); err != nil {
274+
return nil, err
275+
}
276+
277+
return loaded, nil
270278
}
271279

272280
func loadDovecotResultReset(s *Script, cmd parser.Cmd) (Cmd, error) {

interp/load_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ if envelope :is "from" "test@example.org" {
6767
Comparator: ComparatorASCIICaseMap,
6868
Match: MatchIs,
6969
Key: []string{"test@example.org"},
70+
matchCnt: 1,
7071
},
7172
AddressPart: All,
7273
Field: []string{"from"},
@@ -92,10 +93,10 @@ removeflag "flag2";
9293
Flags: Flags{"flag1", "flag2"},
9394
},
9495
CmdSetFlag{
95-
Flags: Flags{"flag1", "flag2"},
96+
Flags: Flags{"flag2", "flag1"},
9697
},
9798
CmdAddFlag{
98-
Flags: Flags{"flag1", "flag2"},
99+
Flags: Flags{"flag2", "flag1"},
99100
},
100101
CmdRemoveFlag{
101102
Flags: Flags{"flag2"},

interp/runtime.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ type ExecuteTestMessage struct {
4646
Flags []string
4747
}
4848

49+
// ExecuteTestEnvironment is a mock MTA interface used for integration tests. It is possible to test
50+
// the actual execution of actions (fileinto, redirect, keep, etc.) using this interface in a particular
51+
// MTA implementation.
4952
type ExecuteTestEnvironment interface {
5053
CreateMailbox(name string) error
5154
// GetDefaultMailbox returns the mailbox name used by keep action.

0 commit comments

Comments
 (0)