-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
304 additions
and
150 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,156 +1,11 @@ | ||
package interp | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/mail" | ||
"strconv" | ||
"strings" | ||
"unicode" | ||
) | ||
import "github.com/foxcpp/go-sieve/interp/match" | ||
|
||
type Match string | ||
|
||
const ( | ||
MatchContains Match = "contains" | ||
MatchIs Match = "is" | ||
MatchMatches Match = "matches" | ||
) | ||
|
||
type Comparator string | ||
|
||
const ( | ||
ComparatorOctet Comparator = "i;octet" | ||
ComparatorASCIICaseMap Comparator = "i;ascii-casemap" | ||
ComparatorASCIINumeric Comparator = "i;ascii-numeric" | ||
ComparatorUnicodeCaseMap Comparator = "i;unicode-casemap" | ||
) | ||
|
||
type AddressPart string | ||
|
||
const ( | ||
LocalPart AddressPart = "localpart" | ||
Domain AddressPart = "domain" | ||
All AddressPart = "all" | ||
) | ||
|
||
func split(addr string) (mailbox, domain string, err error) { | ||
if strings.EqualFold(addr, "postmaster") { | ||
return addr, "", nil | ||
} | ||
|
||
indx := strings.LastIndexByte(addr, '@') | ||
if indx == -1 { | ||
return "", "", errors.New("address: missing at-sign") | ||
} | ||
mailbox = addr[:indx] | ||
domain = addr[indx+1:] | ||
if mailbox == "" { | ||
return "", "", errors.New("address: empty local-part") | ||
} | ||
if domain == "" { | ||
return "", "", errors.New("address: empty domain") | ||
} | ||
return | ||
func matchOctet(pattern, value string) (bool, error) { | ||
return match.Match([]byte(pattern), []byte(value)) | ||
} | ||
|
||
var ErrComparatorMatchUnsupported = fmt.Errorf("match-comparator combination not supported") | ||
|
||
func matchString(pattern, key string) (bool, error) { | ||
// FIXME: This does not account for comparator differences. | ||
panic("match is not implemented") | ||
} | ||
|
||
func numericValue(s string) *uint64 { | ||
if len(s) == 0 { | ||
return nil | ||
} | ||
runes := []rune(s) | ||
if !unicode.IsDigit(runes[0]) { | ||
return nil | ||
} | ||
var sl string | ||
for i, r := range runes { | ||
if !unicode.IsDigit(r) { | ||
sl = string(runes[:i]) | ||
} | ||
} | ||
digit, _ := strconv.ParseUint(sl, 10, 64) | ||
return &digit | ||
} | ||
|
||
func testString(comparator Comparator, match Match, value, key string) (bool, error) { | ||
switch comparator { | ||
case ComparatorOctet: | ||
switch match { | ||
case MatchContains: | ||
return strings.Contains(value, key), nil | ||
case MatchIs: | ||
return value == key, nil | ||
case MatchMatches: | ||
return matchString(key, value) | ||
} | ||
case ComparatorASCIINumeric: | ||
switch match { | ||
case MatchContains: | ||
return false, ErrComparatorMatchUnsupported | ||
case MatchIs: | ||
lhsNum := numericValue(value) | ||
rhsNum := numericValue(key) | ||
if lhsNum == nil || rhsNum == nil { | ||
return false, nil | ||
} | ||
return *lhsNum == *rhsNum, nil | ||
case MatchMatches: | ||
return false, ErrComparatorMatchUnsupported | ||
} | ||
case ComparatorASCIICaseMap: | ||
// FIXME: Case-fold ASCII only. | ||
fallthrough | ||
case ComparatorUnicodeCaseMap: | ||
value = strings.ToLower(value) | ||
key = strings.ToLower(key) | ||
switch match { | ||
case MatchContains: | ||
return strings.Contains(value, key), nil | ||
case MatchIs: | ||
return value == key, nil | ||
case MatchMatches: | ||
return matchString(value, key) | ||
} | ||
} | ||
return false, nil | ||
} | ||
|
||
func testAddress(part AddressPart, comparator Comparator, match Match, headerVal []*mail.Address, addrValue string) (bool, error) { | ||
for _, addr := range headerVal { | ||
var valueToCompare string | ||
if addr.Address != "" { | ||
switch part { | ||
case LocalPart: | ||
localPart, _, err := split(addr.Address) | ||
if err != nil { | ||
continue | ||
} | ||
valueToCompare = localPart | ||
case Domain: | ||
_, domain, err := split(addr.Address) | ||
if err != nil { | ||
continue | ||
} | ||
valueToCompare = domain | ||
case All: | ||
valueToCompare = addr.Address | ||
} | ||
} | ||
|
||
ok, err := testString(comparator, match, valueToCompare, addrValue) | ||
if err != nil { | ||
return false, err | ||
} | ||
if ok { | ||
return true, nil | ||
} | ||
} | ||
return false, nil | ||
func matchUnicode(pattern, value string) (bool, error) { | ||
return match.Match([]rune(pattern), []rune(value)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Copyright 2010 The Go Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package match | ||
|
||
import ( | ||
"errors" | ||
) | ||
|
||
// ErrBadPattern indicates a pattern was malformed. | ||
var ErrBadPattern = errors.New("syntax error in pattern") | ||
|
||
// Match is a simplified version of path.Match that removes support | ||
// for character classes and operates on already decoded input ([]byte or []rune). | ||
func Match[T rune | byte](pattern, name []T) (matched bool, err error) { | ||
Pattern: | ||
for len(pattern) > 0 { | ||
var star bool | ||
var chunk []T | ||
star, chunk, pattern = scanChunk(pattern) | ||
if star && len(chunk) == 0 { | ||
return true, nil | ||
} | ||
// Look for match at current position. | ||
t, ok, err := matchChunk(chunk, name) | ||
// if we're the last chunk, make sure we've exhausted the name | ||
// otherwise we'll give a false result even if we could still match | ||
// using the star | ||
if ok && (len(t) == 0 || len(pattern) > 0) { | ||
name = t | ||
continue | ||
} | ||
if err != nil { | ||
return false, err | ||
} | ||
if star { | ||
// Look for match skipping i+1 bytes. | ||
for i := 0; i < len(name); i++ { | ||
t, ok, err := matchChunk(chunk, name[i+1:]) | ||
if ok { | ||
// if we're the last chunk, make sure we exhausted the name | ||
if len(pattern) == 0 && len(t) > 0 { | ||
continue | ||
} | ||
name = t | ||
continue Pattern | ||
} | ||
if err != nil { | ||
return false, err | ||
} | ||
} | ||
} | ||
// Before returning false with no error, | ||
// check that the remainder of the pattern is syntactically valid. | ||
for len(pattern) > 0 { | ||
_, chunk, pattern = scanChunk(pattern) | ||
if _, _, err := matchChunk(chunk, []T{}); err != nil { | ||
return false, err | ||
} | ||
} | ||
return false, nil | ||
} | ||
return len(name) == 0, nil | ||
} | ||
|
||
// scanChunk gets the next segment of pattern, which is a non-star string | ||
// possibly preceded by a star. | ||
func scanChunk[T rune | byte](pattern []T) (star bool, chunk, rest []T) { | ||
for len(pattern) > 0 && pattern[0] == '*' { | ||
pattern = pattern[1:] | ||
star = true | ||
} | ||
inrange := false | ||
var i int | ||
Scan: | ||
for i = 0; i < len(pattern); i++ { | ||
switch pattern[i] { | ||
case '\\': | ||
// error check handled in matchChunk: bad pattern. | ||
if i+1 < len(pattern) { | ||
i++ | ||
} | ||
case '[': | ||
inrange = true | ||
case ']': | ||
inrange = false | ||
case '*': | ||
if !inrange { | ||
break Scan | ||
} | ||
} | ||
} | ||
return star, pattern[0:i], pattern[i:] | ||
} | ||
|
||
// matchChunk checks whether chunk matches the beginning of s. | ||
// If so, it returns the remainder of s (after the match). | ||
// Chunk is all single-character operators: literals, char classes, and ?. | ||
func matchChunk[T rune | byte](chunk, s []T) (rest []T, ok bool, err error) { | ||
// failed records whether the match has failed. | ||
// After the match fails, the loop continues on processing chunk, | ||
// checking that the pattern is well-formed but no longer reading s. | ||
failed := false | ||
for len(chunk) > 0 { | ||
if !failed && len(s) == 0 { | ||
failed = true | ||
} | ||
switch chunk[0] { | ||
case '?': | ||
if !failed { | ||
if s[0] == '/' { | ||
failed = true | ||
} | ||
s = s[1:] | ||
} | ||
chunk = chunk[1:] | ||
|
||
case '\\': | ||
chunk = chunk[1:] | ||
if len(chunk) == 0 { | ||
return []T{}, false, ErrBadPattern | ||
} | ||
fallthrough | ||
|
||
default: | ||
if !failed { | ||
if chunk[0] != s[0] { | ||
failed = true | ||
} | ||
s = s[1:] | ||
} | ||
chunk = chunk[1:] | ||
} | ||
} | ||
if failed { | ||
return []T{}, false, nil | ||
} | ||
return s, true, nil | ||
} |
Oops, something went wrong.