#8: implement ManageSieve protocol server (RFC 5804)
This commit is contained in:
1 parent
5a7dcc002a
commit
0e6fc27bc1
13 files changed
+1634
No files matched your search
@@ -0,0 +1,12 @@
|
||||
package entity
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// SieveScript represents a user's Sieve filtering script entity.
|
||||
type SieveScript struct {
|
||||
ID uuid.UUID
|
||||
MailboxID uuid.UUID
|
||||
Name string
|
||||
Script string
|
||||
IsActive bool
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyScript = errors.New("sieve script cannot be empty")
|
||||
ErrUnmatchedBrace = errors.New("unmatched curly brace in sieve script")
|
||||
ErrUnmatchedBracket = errors.New("unmatched square bracket in sieve script")
|
||||
ErrUnterminatedQuote = errors.New("unterminated string literal in sieve script")
|
||||
ErrUnterminatedComment = errors.New("unterminated comment block in sieve script")
|
||||
)
|
||||
|
||||
// ValidateSieveScript performs structural syntax validation of an RFC 5228 Sieve script.
|
||||
func ValidateSieveScript(script string) error {
|
||||
trimmed := strings.TrimSpace(script)
|
||||
if trimmed == "" {
|
||||
return ErrEmptyScript
|
||||
}
|
||||
|
||||
var braceCount int
|
||||
var bracketCount int
|
||||
inSingleQuote := false
|
||||
inMultiComment := false
|
||||
inString := false
|
||||
|
||||
runes := []rune(script)
|
||||
n := len(runes)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
r := runes[i]
|
||||
|
||||
// Handle multi-line comment /* ... */
|
||||
if inMultiComment {
|
||||
if r == '*' && i+1 < n && runes[i+1] == '/' {
|
||||
inMultiComment = false
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle string literal "..."
|
||||
if inString {
|
||||
if r == '\\' {
|
||||
i++ // skip escaped char
|
||||
continue
|
||||
}
|
||||
if r == '"' {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle single line comment # ...
|
||||
if inSingleQuote {
|
||||
if r == '\n' {
|
||||
inSingleQuote = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect start of comment /*
|
||||
if r == '/' && i+1 < n && runes[i+1] == '*' {
|
||||
inMultiComment = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect start of comment #
|
||||
if r == '#' {
|
||||
inSingleQuote = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect start of string "
|
||||
if r == '"' {
|
||||
inString = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Track braces & brackets
|
||||
switch r {
|
||||
case '{':
|
||||
braceCount++
|
||||
case '}':
|
||||
braceCount--
|
||||
if braceCount < 0 {
|
||||
return ErrUnmatchedBrace
|
||||
}
|
||||
case '[':
|
||||
bracketCount++
|
||||
case ']':
|
||||
bracketCount--
|
||||
if bracketCount < 0 {
|
||||
return ErrUnmatchedBracket
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if inMultiComment {
|
||||
return ErrUnterminatedComment
|
||||
}
|
||||
if inString {
|
||||
return ErrUnterminatedQuote
|
||||
}
|
||||
if braceCount != 0 {
|
||||
return fmt.Errorf("%w: balance is %d", ErrUnmatchedBrace, braceCount)
|
||||
}
|
||||
if bracketCount != 0 {
|
||||
return fmt.Errorf("%w: balance is %d", ErrUnmatchedBracket, bracketCount)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateSieveScript_ValidScripts(t *testing.T) {
|
||||
validScripts := []string{
|
||||
`require ["fileinto", "reject"];
|
||||
if header :is "Subject" "Spam" {
|
||||
fileinto "Junk";
|
||||
stop;
|
||||
}`,
|
||||
`# Simple keep script
|
||||
keep;`,
|
||||
`/* Multi-line comment
|
||||
testing Sieve script */
|
||||
if size :over 1M {
|
||||
discard;
|
||||
} else {
|
||||
keep;
|
||||
}`,
|
||||
}
|
||||
|
||||
for i, s := range validScripts {
|
||||
if err := ValidateSieveScript(s); err != nil {
|
||||
t.Errorf("script %d failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSieveScript_InvalidScripts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
script string
|
||||
wantErr error
|
||||
}{
|
||||
{"empty", "", ErrEmptyScript},
|
||||
{"whitespace", " \n\t ", ErrEmptyScript},
|
||||
{"unmatched opening brace", `if true { keep;`, ErrUnmatchedBrace},
|
||||
{"unmatched closing brace", `if true { keep; }}`, ErrUnmatchedBrace},
|
||||
{"unmatched bracket", `require ["fileinto";`, ErrUnmatchedBracket},
|
||||
{"unterminated string", `fileinto "Junk;`, ErrUnterminatedQuote},
|
||||
{"unterminated comment", `/* comment without end`, ErrUnterminatedComment},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateSieveScript(tt.script)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for %s, got nil", tt.name)
|
||||
}
|
||||
if !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("error = %v, want %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user