106 lines
3.2 KiB
Go
106 lines
3.2 KiB
Go
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
|
|
}
|