Phase 5 follow-up: discover the \Sent mailbox; record the approved real send (AUD-71)
The approved real test reply was sent and the thread updated (same X-GM-THRID as the original). The live test exposed one bug: the thread check hardcoded Gmail's sent mailbox as "[Gmail]/Sent Mail", but this account exposes it as "[Google Mail]/Sent Mail". - internal/mailfetch: LIST parser, ListMailboxes, and SentMailbox that finds the mailbox flagged \Sent (RFC 6154) with name fallbacks. - internal/mailops: VerifyThreadUpdated selects the discovered \Sent mailbox instead of a hardcoded name. - cmd/email-agent: -check-thread <id> exercises the check against real Gmail without sending mail. - Tests for LIST parsing, sent-mailbox discovery, and the discovery error path. - docs/phase5-replies.md: record send-as verification, the sent reply's thread id, the app confirmation (sent_reply_in_thread=true), and the fix. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -29,6 +30,7 @@ func main() {
|
||||
classifyNow = flag.Bool("classify", false, "classify pending messages with JEV and exit")
|
||||
classifyAll = flag.Bool("classify-all", false, "re-classify every stored message with JEV and exit")
|
||||
checkSMTP = flag.Bool("check-smtp", false, "verify the Gmail SMTP connection (IPv4 + STARTTLS + login) and exit; sends no mail")
|
||||
checkThread = flag.Int64("check-thread", 0, "verify that a stored message's Gmail thread has a sent reply (read-only), then exit")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
@@ -79,6 +81,14 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if *checkThread > 0 {
|
||||
if err := runCheckThread(cfg, logger, *checkThread); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "thread check failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := run(cfg, logger); err != nil {
|
||||
logger.Error("fatal", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -194,6 +204,37 @@ func runCheckSMTP(cfg *config.Config, logger *slog.Logger) error {
|
||||
return mailops.New(cfg, logger).CheckSMTP(ctx)
|
||||
}
|
||||
|
||||
// runCheckThread reports whether a stored message's Gmail thread already has a
|
||||
// message in the Sent mailbox. It is read-only and exists so the Phase 5 thread
|
||||
// check can be exercised against real Gmail without sending anything.
|
||||
func runCheckThread(cfg *config.Config, logger *slog.Logger, messageID int64) error {
|
||||
if !cfg.GmailConfigured() {
|
||||
return mailops.ErrNotConfigured
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
st, err := store.Open(ctx, cfg.DBPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open store: %w", err)
|
||||
}
|
||||
defer func() { _ = st.Close() }()
|
||||
|
||||
msg, err := st.MessageByID(ctx, messageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(msg.ThreadID) == "" {
|
||||
return fmt.Errorf("message %d has no Gmail thread id", messageID)
|
||||
}
|
||||
ok, err := mailops.New(cfg, logger).VerifyThreadUpdated(ctx, msg.ThreadID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("message=%d thread=%s sent_reply_in_thread=%v\n", messageID, msg.ThreadID, ok)
|
||||
return nil
|
||||
}
|
||||
|
||||
// probeHealth requests the local health endpoint. It exists so the container
|
||||
// healthcheck does not need an external HTTP client in the image.
|
||||
func probeHealth(port int) error {
|
||||
|
||||
Reference in New Issue
Block a user