O assunto que chegou a uma caixa real: "Fora do escritorio: Re:
=?UTF-8?Q?Re=3A_Fora_do_escrit=C3=B3rio?=". Duas coisas erradas, e a
segunda maior do que parece.
Um assunto com acento viaja como palavra codificada (RFC 2047). Colei o
cabecalho em bruto, portanto aquilo foi lido tal e qual por uma pessoa.
E foi tambem o que derrotou a verificacao do "Re:" ja existente:
codificado, todo o assunto comeca por "=?", nunca por "Re:", e por isso
foi acrescentado outro por cima. Descodificar corrige os dois sintomas
de uma vez. Os prefixos acumulados ("Re: RE: Re:") passam a ser
reduzidos a um.
O que estava por tras: aquele assunto era "Re: Fora do escritorio" -
alguem tinha respondido a propria mensagem automatica, e ela respondeu
de novo. Faltava a supressao por remetente. O RFC 5230 seccao 4.1 pede
exactamente isto e propoe sete dias: sem ela, uma conversa de cinco
mensagens produzia cinco copias do mesmo aviso, e quem respondesse ao
aviso recebia outro - que e como uma pessoa e um respondedor automatico
acabam a falar um para o outro indefinidamente.
A supressao falha aberta: um registo que nao se consegue ler deixa a
resposta sair. Ser avisado duas vezes e uma falha menor do que nunca
saber que a caixa esta ausente, que e o proposito da funcionalidade. O
envio so e registado depois de a resposta estar mesmo na fila, para que
um envio falhado nao silencie a mensagem seguinte da mesma pessoa.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 lines
1.5 KiB
Go
57 lines
1.5 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// VacationLog records who has already been told a mailbox is unattended.
|
|
type VacationLog struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewVacationLog(pool *pgxpool.Pool) *VacationLog {
|
|
return &VacationLog{pool: pool}
|
|
}
|
|
|
|
// LastRepliedAt returns when this sender was last answered.
|
|
//
|
|
// A sender who has never been answered comes back as the zero time rather than
|
|
// an error: never having replied is the ordinary case, not a failure.
|
|
func (l *VacationLog) LastRepliedAt(
|
|
ctx context.Context, mailboxID uuid.UUID, to string,
|
|
) (time.Time, error) {
|
|
var last time.Time
|
|
err := l.pool.QueryRow(ctx, `
|
|
SELECT last_sent_at FROM vacation_replies
|
|
WHERE mailbox_id = $1 AND recipient = $2
|
|
`, mailboxID, to).Scan(&last)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return time.Time{}, nil
|
|
}
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("read the vacation log for %s: %w", to, err)
|
|
}
|
|
return last, nil
|
|
}
|
|
|
|
// RecordReply notes that this sender has been told, now.
|
|
func (l *VacationLog) RecordReply(ctx context.Context, mailboxID uuid.UUID, to string) error {
|
|
_, err := l.pool.Exec(ctx, `
|
|
INSERT INTO vacation_replies (mailbox_id, recipient, last_sent_at)
|
|
VALUES ($1, $2, NOW())
|
|
ON CONFLICT (mailbox_id, recipient)
|
|
DO UPDATE SET last_sent_at = NOW()
|
|
`, mailboxID, to)
|
|
if err != nil {
|
|
return fmt.Errorf("record the vacation reply to %s: %w", to, err)
|
|
}
|
|
return nil
|
|
}
|