first commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
// Package sieve implements a Sieve (RFC 5228) interpreter covering the
|
||||
// common mail-filtering subset: header tests (:contains, :is), if/elsif/else,
|
||||
// and the fileinto/discard/keep/stop actions. Not full RFC 5228 — no
|
||||
// extensions (vacation, reject, notify), no envelope/size/address tests,
|
||||
// no allof/anyof boolean combinators. This covers what real users actually
|
||||
// write for "move mail matching X to folder Y" / "discard mail from Z",
|
||||
// which is the overwhelming majority of real-world Sieve scripts; broader
|
||||
// grammar support is a natural follow-up once client compatibility testing
|
||||
// calls for it.
|
||||
package sieve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type tokenKind int
|
||||
|
||||
const (
|
||||
tokIdent tokenKind = iota
|
||||
tokString
|
||||
tokTag // :contains, :is, etc.
|
||||
tokSemicolon
|
||||
tokLBrace
|
||||
tokRBrace
|
||||
tokEOF
|
||||
)
|
||||
|
||||
type token struct {
|
||||
kind tokenKind
|
||||
value string
|
||||
}
|
||||
|
||||
type lexer struct {
|
||||
input []rune
|
||||
pos int
|
||||
}
|
||||
|
||||
func newLexer(script string) *lexer {
|
||||
return &lexer{input: []rune(script)}
|
||||
}
|
||||
|
||||
func (l *lexer) next() (token, error) {
|
||||
l.skipWhitespaceAndComments()
|
||||
if l.pos >= len(l.input) {
|
||||
return token{kind: tokEOF}, nil
|
||||
}
|
||||
|
||||
c := l.input[l.pos]
|
||||
switch {
|
||||
case c == ';':
|
||||
l.pos++
|
||||
return token{kind: tokSemicolon, value: ";"}, nil
|
||||
case c == '{':
|
||||
l.pos++
|
||||
return token{kind: tokLBrace, value: "{"}, nil
|
||||
case c == '}':
|
||||
l.pos++
|
||||
return token{kind: tokRBrace, value: "}"}, nil
|
||||
case c == '"':
|
||||
return l.readString()
|
||||
case c == ':':
|
||||
return l.readTag()
|
||||
case unicode.IsLetter(c):
|
||||
return l.readIdent()
|
||||
default:
|
||||
return token{}, fmt.Errorf("unexpected character %q at position %d", c, l.pos)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lexer) skipWhitespaceAndComments() {
|
||||
for l.pos < len(l.input) {
|
||||
c := l.input[l.pos]
|
||||
if unicode.IsSpace(c) {
|
||||
l.pos++
|
||||
continue
|
||||
}
|
||||
// Single-line comment: # ... end of line
|
||||
if c == '#' {
|
||||
for l.pos < len(l.input) && l.input[l.pos] != '\n' {
|
||||
l.pos++
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Bracketed comment: /* ... */
|
||||
if c == '/' && l.pos+1 < len(l.input) && l.input[l.pos+1] == '*' {
|
||||
l.pos += 2
|
||||
for l.pos+1 < len(l.input) && !(l.input[l.pos] == '*' && l.input[l.pos+1] == '/') {
|
||||
l.pos++
|
||||
}
|
||||
l.pos += 2
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lexer) readString() (token, error) {
|
||||
l.pos++ // skip opening quote
|
||||
var sb strings.Builder
|
||||
for l.pos < len(l.input) && l.input[l.pos] != '"' {
|
||||
if l.input[l.pos] == '\\' && l.pos+1 < len(l.input) {
|
||||
l.pos++
|
||||
}
|
||||
sb.WriteRune(l.input[l.pos])
|
||||
l.pos++
|
||||
}
|
||||
if l.pos >= len(l.input) {
|
||||
return token{}, fmt.Errorf("unterminated string literal")
|
||||
}
|
||||
l.pos++ // skip closing quote
|
||||
return token{kind: tokString, value: sb.String()}, nil
|
||||
}
|
||||
|
||||
func (l *lexer) readTag() (token, error) {
|
||||
start := l.pos
|
||||
l.pos++ // skip ':'
|
||||
for l.pos < len(l.input) && (unicode.IsLetter(l.input[l.pos]) || l.input[l.pos] == '-') {
|
||||
l.pos++
|
||||
}
|
||||
return token{kind: tokTag, value: string(l.input[start:l.pos])}, nil
|
||||
}
|
||||
|
||||
func (l *lexer) readIdent() (token, error) {
|
||||
start := l.pos
|
||||
for l.pos < len(l.input) && (unicode.IsLetter(l.input[l.pos]) || unicode.IsDigit(l.input[l.pos]) || l.input[l.pos] == '_') {
|
||||
l.pos++
|
||||
}
|
||||
return token{kind: tokIdent, value: string(l.input[start:l.pos])}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user