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:
Lucas RayanandClaude Opus 5 committed 2026-08-22 13:48:10 -03:00
1 parent 42420a7de2
commit d403c47a71
12 files changed
+828 -80

No files matched your search

@@ -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)
}