Phase 6: rules editor + unsubscribe detection (AUD-72)
- FR-11 rules editor: plain-text area at /rules, saved and versioned (SaveRules appends a version; restore re-saves an old version), parse errors surfaced, one-click re-classify. The reply model now reads the rules document too (FR-11). - FR-7 unsubscribe: parse and store List-Unsubscribe / List-Unsubscribe-Post (migration v6), detect email and web links (internal/unsubscribe), a domain-level ignore list, and an explicit record of what was marked. No code path contacts an unsubscribe link; acting on one needs Sam's approval. - Migration v6 is additive; unsubscribe gains url/updated_at. - Tests: detector, parser, store migration/records/candidates, rules editor, unsubscribe handlers, message-detail rendering. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -20,15 +20,19 @@ type Message struct {
|
||||
// MessageIDHeader is the RFC822 Message-ID header (Phase 5, FR-8). A reply
|
||||
// uses it for In-Reply-To and References so Gmail keeps the thread.
|
||||
MessageIDHeader string
|
||||
FromAddr string
|
||||
ToAddr string
|
||||
Subject string
|
||||
BodyText string
|
||||
SourceAddress string
|
||||
DateSent time.Time
|
||||
ReceivedAt time.Time
|
||||
IMAPUID int64
|
||||
IMAPUIDValidity int64
|
||||
// ListUnsubscribe and ListUnsubscribePost are the raw RFC 2369/8058 headers
|
||||
// (Phase 6, FR-7). Detection runs over them on demand.
|
||||
ListUnsubscribe string
|
||||
ListUnsubscribePost string
|
||||
FromAddr string
|
||||
ToAddr string
|
||||
Subject string
|
||||
BodyText string
|
||||
SourceAddress string
|
||||
DateSent time.Time
|
||||
ReceivedAt time.Time
|
||||
IMAPUID int64
|
||||
IMAPUIDValidity int64
|
||||
// Folder is the app folder the message is filed under (Phase 4, FR-6/FR-9):
|
||||
// inbox, delete, saved, or replied.
|
||||
Folder string
|
||||
@@ -71,19 +75,22 @@ func (s *Store) UpsertMessage(ctx context.Context, m *Message) (int64, error) {
|
||||
const q = `
|
||||
INSERT INTO messages (
|
||||
gmail_id, thread_id, message_id_header, from_addr, to_addr, subject, body_text,
|
||||
source_address, date_sent, received_at, imap_uid, imap_uidvalidity
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
source_address, list_unsubscribe, list_unsubscribe_post, date_sent, received_at,
|
||||
imap_uid, imap_uidvalidity
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (gmail_id) DO UPDATE SET
|
||||
thread_id = excluded.thread_id,
|
||||
message_id_header = excluded.message_id_header,
|
||||
from_addr = excluded.from_addr,
|
||||
to_addr = excluded.to_addr,
|
||||
subject = excluded.subject,
|
||||
body_text = excluded.body_text,
|
||||
source_address = excluded.source_address,
|
||||
date_sent = excluded.date_sent,
|
||||
imap_uid = excluded.imap_uid,
|
||||
imap_uidvalidity = excluded.imap_uidvalidity`
|
||||
thread_id = excluded.thread_id,
|
||||
message_id_header = excluded.message_id_header,
|
||||
from_addr = excluded.from_addr,
|
||||
to_addr = excluded.to_addr,
|
||||
subject = excluded.subject,
|
||||
body_text = excluded.body_text,
|
||||
source_address = excluded.source_address,
|
||||
list_unsubscribe = excluded.list_unsubscribe,
|
||||
list_unsubscribe_post = excluded.list_unsubscribe_post,
|
||||
date_sent = excluded.date_sent,
|
||||
imap_uid = excluded.imap_uid,
|
||||
imap_uidvalidity = excluded.imap_uidvalidity`
|
||||
|
||||
receivedAt := m.ReceivedAt
|
||||
if receivedAt.IsZero() {
|
||||
@@ -98,6 +105,8 @@ ON CONFLICT (gmail_id) DO UPDATE SET
|
||||
nullString(m.Subject),
|
||||
nullString(m.BodyText),
|
||||
nullString(m.SourceAddress),
|
||||
nullString(m.ListUnsubscribe),
|
||||
nullString(m.ListUnsubscribePost),
|
||||
nullTime(m.DateSent),
|
||||
receivedAt.UTC().Format(sqlTimeLayout),
|
||||
m.IMAPUID,
|
||||
@@ -124,7 +133,8 @@ SELECT m.id, m.gmail_id, COALESCE(m.thread_id, ''), COALESCE(m.from_addr, ''),
|
||||
COALESCE(m.source_address, ''), COALESCE(m.date_sent, ''), m.received_at,
|
||||
COALESCE(m.imap_uid, 0), COALESCE(m.imap_uidvalidity, 0),
|
||||
COALESCE(m.folder, 'inbox'), COALESCE(m.mailbox, 'INBOX'),
|
||||
COALESCE(m.message_id_header, '')
|
||||
COALESCE(m.message_id_header, ''),
|
||||
COALESCE(m.list_unsubscribe, ''), COALESCE(m.list_unsubscribe_post, '')
|
||||
FROM messages m WHERE m.id = ?`
|
||||
var (
|
||||
m Message
|
||||
@@ -135,6 +145,7 @@ FROM messages m WHERE m.id = ?`
|
||||
&m.ID, &m.GmailID, &m.ThreadID, &m.FromAddr, &m.ToAddr, &m.Subject,
|
||||
&m.BodyText, &m.SourceAddress, &dateSent, &received,
|
||||
&m.IMAPUID, &m.IMAPUIDValidity, &m.Folder, &m.Mailbox, &m.MessageIDHeader,
|
||||
&m.ListUnsubscribe, &m.ListUnsubscribePost,
|
||||
)
|
||||
if err != nil {
|
||||
return Message{}, fmt.Errorf("load message %d: %w", id, err)
|
||||
|
||||
Reference in New Issue
Block a user