feat(webmail): pastas próprias na barra lateral e regras em linguagem comum
Regras. A tela pedia um tipo de correspondência do Sieve pelo nome - :contains, :is, :matches (regex) - e colava o valor escrito diretamente num script. Punha uma linguagem de padrões à frente de quem só queria dizer "o que vem do banco vai para Faturas", e bastava uma aspa no valor para o script deixar de fazer parse, desativando silenciosamente todas as regras de uma vez. O vocabulário passa a ser o de quem escreve: um campo, uma condição em português, um valor e uma ação. Os curingas são gerados a partir de "começa com" e "termina com" - ninguém os escreve, e um asterisco escrito por acaso continua a ser um asterisco. A pasta de destino é escolhida de uma lista das pastas que existem mesmo, em vez de ser escrita à mão: antes uma regra podia nomear uma pasta inexistente e nunca disparar, sem dizer porquê. A ordem passa a ser visível e alterável, porque a primeira regra que arquiva uma mensagem decide para onde ela vai. Um script que esta tela não consegue exprimir - escrito à mão, ou por um cliente de secretária sobre ManageSieve - é mostrado tal como está, em vez de ser reescrito na coisa mais parecida que este formulário sabe dizer, o que mudaria em silêncio o que o correio de alguém faz. Pastas próprias. Existiam na base de dados e as regras podiam arquivar nelas, mas a barra lateral listava só as seis fixas, portanto não havia como ver uma - nem como criar. Criar fica onde a vontade aparece, na própria barra; apagar avisa que leva as mensagens junto, porque leva. Encontrado a escrever isto: o motor tratava "\\*" como barra seguida de curinga em vez de um asterisco literal. Como é esta tela que gera os curingas, uma regra sobre um assunto com "50% *" teria correspondido a todos os assuntos começados por "50% ". Corrigido no motor, com testes dos dois lados. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42420a7de2
commit
d403c47a71
12 files changed
+828
-80
No files matched your search
@@ -355,3 +355,40 @@ func TestMatchesDoesNotBlowUpOnManyStars(t *testing.T) {
|
||||
t.Fatal("wildcard matching took more than three seconds; it is backtracking exponentially")
|
||||
}
|
||||
}
|
||||
|
||||
// A backslash escapes a wildcard, per RFC 5228 section 2.7.1: "\*" is a real
|
||||
// asterisk, not "any run of characters".
|
||||
//
|
||||
// This matters because the rules screen generates the wildcards itself. When
|
||||
// someone writes a rule about a subject containing "50% *", the screen escapes
|
||||
// their asterisk so it stays literal - and if this engine ignored the escape,
|
||||
// that rule would match every subject starting with "50% " instead.
|
||||
func TestEscapedWildcardIsALiteralCharacter(t *testing.T) {
|
||||
script := `if header :matches "Subject" "50% \\* desconto*" { fileinto "X"; }`
|
||||
|
||||
if out := evaluate(t, script, Message{Headers: map[string][]string{
|
||||
"subject": {"50% * desconto de agosto"},
|
||||
}}); out.Folder != "X" {
|
||||
t.Errorf("the literal asterisk did not match: %q", out.Folder)
|
||||
}
|
||||
// The escaped asterisk must not behave as a wildcard.
|
||||
if out := evaluate(t, script, Message{Headers: map[string][]string{
|
||||
"subject": {"50% enorme desconto de agosto"},
|
||||
}}); out.Folder != "" {
|
||||
t.Errorf("the escaped asterisk was treated as a wildcard: %q", out.Folder)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapedQuestionMarkIsALiteralCharacter(t *testing.T) {
|
||||
script := `if header :matches "Subject" "Onde\\?" { fileinto "X"; }`
|
||||
if out := evaluate(t, script, Message{Headers: map[string][]string{
|
||||
"subject": {"Onde?"},
|
||||
}}); out.Folder != "X" {
|
||||
t.Errorf("the literal question mark did not match: %q", out.Folder)
|
||||
}
|
||||
if out := evaluate(t, script, Message{Headers: map[string][]string{
|
||||
"subject": {"OndeX"},
|
||||
}}); out.Folder != "" {
|
||||
t.Errorf("the escaped question mark matched any character: %q", out.Folder)
|
||||
}
|
||||
}
|
||||
@@ -80,29 +80,56 @@ func compare(comparator, actual, want string) bool {
|
||||
// that take exponential time on a crafted subject line.
|
||||
func wildcardMatch(value, pattern string) bool {
|
||||
v, p := []rune(value), []rune(pattern)
|
||||
|
||||
// literalAt reports whether the pattern rune at i is escaped, and how wide
|
||||
// the escape is. RFC 5228 section 2.7.1: a backslash makes the next
|
||||
// character literal, which is how a rule can be about a real asterisk.
|
||||
// The rules screen relies on this - it escapes any wildcard the user typed
|
||||
// so it stays the character they meant.
|
||||
escaped := func(i int) (rune, int, bool) {
|
||||
if p[i] == '\\' && i+1 < len(p) {
|
||||
return p[i+1], 2, true
|
||||
}
|
||||
return p[i], 1, false
|
||||
}
|
||||
|
||||
// Iterative backtracking: linear in the common case, and it cannot blow
|
||||
// the stack on a long subject the way a recursive version can.
|
||||
vi, pi := 0, 0
|
||||
star, mark := -1, 0
|
||||
for vi < len(v) {
|
||||
switch {
|
||||
case pi < len(p) && (p[pi] == '?' || p[pi] == v[vi]):
|
||||
vi++
|
||||
pi++
|
||||
case pi < len(p) && p[pi] == '*':
|
||||
star = pi
|
||||
mark = vi
|
||||
pi++
|
||||
case star >= 0:
|
||||
if pi < len(p) {
|
||||
ch, width, isEscaped := escaped(pi)
|
||||
switch {
|
||||
case isEscaped && ch == v[vi]:
|
||||
vi++
|
||||
pi += width
|
||||
continue
|
||||
case !isEscaped && (ch == '?' || ch == v[vi]):
|
||||
vi++
|
||||
pi += width
|
||||
continue
|
||||
case !isEscaped && ch == '*':
|
||||
star = pi
|
||||
mark = vi
|
||||
pi += width
|
||||
continue
|
||||
}
|
||||
}
|
||||
if star >= 0 {
|
||||
pi = star + 1
|
||||
mark++
|
||||
vi = mark
|
||||
default:
|
||||
return false
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
for pi < len(p) && p[pi] == '*' {
|
||||
pi++
|
||||
for pi < len(p) {
|
||||
ch, width, isEscaped := escaped(pi)
|
||||
if isEscaped || ch != '*' {
|
||||
break
|
||||
}
|
||||
pi += width
|
||||
}
|
||||
return pi == len(p)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user