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 }