first commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package sieve
|
||||
|
||||
import "strings"
|
||||
|
||||
// Result is the outcome of running a script against one message.
|
||||
type Result struct {
|
||||
Action string // "fileinto" | "discard" | "keep" (default if nothing else fired)
|
||||
Folder string // set only when Action == "fileinto"
|
||||
}
|
||||
|
||||
// Execute runs script against the given headers (case-insensitive header
|
||||
// names, matching real email header semantics) and returns the first
|
||||
// decisive action encountered. "stop" halts execution immediately with
|
||||
// whatever result has accumulated so far. If no action fires, the default
|
||||
// result is "keep" (deliver to INBOX), matching RFC 5228 §2.10's implicit
|
||||
// keep behavior.
|
||||
//
|
||||
// Simplification: real Sieve treats keep/fileinto/discard as an
|
||||
// accumulating SET of actions (a message can be filed into a folder AND
|
||||
// kept in INBOX, for instance) — this implementation tracks only the single
|
||||
// most recent action instead, last-one-wins. This still matches RFC 5228's
|
||||
// core rule that "discard cancels the implicit keep, but an explicit keep
|
||||
// after it still delivers" (§4.4) — a discard followed by an unconditional
|
||||
// keep with no stop in between DOES deliver the message, correctly. What's
|
||||
// NOT supported is a script that intends both fileinto AND keep to fire
|
||||
// simultaneously (message copied to a folder AND left in INBOX) — write
|
||||
// "stop;" after the decisive action if that's not the intended behavior,
|
||||
// same as real Sieve authors are advised to do to avoid ambiguity.
|
||||
func Execute(script *Script, headers map[string]string) Result {
|
||||
result := Result{Action: "keep"}
|
||||
execStatements(script.Statements, headers, &result)
|
||||
return result
|
||||
}
|
||||
|
||||
// execStatements returns true if execution should stop (a "stop" action fired).
|
||||
func execStatements(stmts []Statement, headers map[string]string, result *Result) bool {
|
||||
for _, stmt := range stmts {
|
||||
switch s := stmt.(type) {
|
||||
case Action:
|
||||
switch s.Name {
|
||||
case "fileinto":
|
||||
result.Action = "fileinto"
|
||||
result.Folder = s.Arg
|
||||
case "discard":
|
||||
result.Action = "discard"
|
||||
case "keep":
|
||||
result.Action = "keep"
|
||||
case "stop":
|
||||
return true
|
||||
}
|
||||
case IfStatement:
|
||||
if evalTest(s.Test, headers) {
|
||||
if execStatements(s.Then, headers, result) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for _, ei := range s.ElseIfs {
|
||||
if evalTest(ei.Test, headers) {
|
||||
matched = true
|
||||
if execStatements(ei.Then, headers, result) {
|
||||
return true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched && s.HasElse {
|
||||
if execStatements(s.Else, headers, result) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func evalTest(t Test, headers map[string]string) bool {
|
||||
switch t.Kind {
|
||||
case "true":
|
||||
return true
|
||||
case "header":
|
||||
actual, ok := lookupHeader(headers, t.Header)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch t.MatchType {
|
||||
case "contains":
|
||||
return strings.Contains(strings.ToLower(actual), strings.ToLower(t.Value))
|
||||
case "is":
|
||||
return strings.EqualFold(strings.TrimSpace(actual), strings.TrimSpace(t.Value))
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func lookupHeader(headers map[string]string, name string) (string, bool) {
|
||||
// Case-insensitive lookup — email headers are case-insensitive per RFC 5322.
|
||||
for k, v := range headers {
|
||||
if strings.EqualFold(k, name) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
Reference in New Issue
Block a user