Phase 5: reply drafts — template/LLM draft, edit, SMTP send (AUD-71)
Implements Phase 5 of AGENTS.md §11 (FR-8, FR-15): - Message detail pane (GET /partials/message) with body, recommended action, reply identity, template picker, editable draft, and history. - internal/reply: OmniRoute (OpenAI-compatible) chat client and a template-based draft generator. All non-JEV models go through OmniRoute (§10). - internal/mailops: SMTP send over smtp.gmail.com:587 with STARTTLS and the app password, forcing IPv4 (§9); FR-15 reply identity (send as the arrived-at X-Pm-Original-To address, else "Sam Rolfe"); threading via In-Reply-To/References; -check-smtp safe verification; and a [Gmail]/Sent Mail thread check. - Migration v5 stores messages.message_id_header and drafts.sent_at; mailfetch parses Message-ID. - Send is user-triggered only: CSRF + UI confirm + confirm=yes guard, no auto-send, duplicate-send refused. A sent reply files the original under Replied-to/<address> and logs every step (FR-13). - Tests for reply, mailops SMTP/identity/thread check, store drafts and migration v5, and the server draft/edit/send guards. - docs/phase5-replies.md, README, .env.example, docker-compose updated. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/config"
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/mailfetch"
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/mailops"
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/reply"
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/session"
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/store"
|
||||
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/version"
|
||||
@@ -43,6 +44,8 @@ type Server struct {
|
||||
fetcher *mailfetch.Fetcher
|
||||
classifier *classify.Classifier
|
||||
actions MailActions
|
||||
replies ReplyGenerator
|
||||
sender MailSender
|
||||
}
|
||||
|
||||
// New builds a Server. It does not start listening.
|
||||
@@ -61,6 +64,7 @@ func New(cfg *config.Config, st *store.Store, logger *slog.Logger) (*Server, err
|
||||
return nil, fmt.Errorf("session manager: %w", err)
|
||||
}
|
||||
|
||||
mail := mailops.New(cfg, logger)
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
store: st,
|
||||
@@ -72,7 +76,12 @@ func New(cfg *config.Config, st *store.Store, logger *slog.Logger) (*Server, err
|
||||
classify.NewClient(cfg.JEVBaseURL, cfg.JEVAPIKey, cfg.JEVModel, logger),
|
||||
st, logger, cfg.ClassifyConcurrency, cfg.ClassifyMaxMessages,
|
||||
),
|
||||
actions: mailops.New(cfg, logger),
|
||||
actions: mail,
|
||||
sender: mail,
|
||||
replies: reply.NewGenerator(
|
||||
reply.NewClient(cfg.OmniRouteBaseURL, cfg.OmniRouteAPIKey, cfg.OmniRouteModel, logger),
|
||||
logger,
|
||||
),
|
||||
}
|
||||
s.http = &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
@@ -128,6 +137,12 @@ func (s *Server) Handler() http.Handler {
|
||||
// Phase 4 (FR-6/FR-9): bulk triage actions (delete, move, mark) and restore.
|
||||
mux.HandleFunc("POST /actions/bulk", s.requireAuth(s.handleBulkAction))
|
||||
|
||||
// Phase 5 (FR-8/FR-15): message detail, reply draft, edit, and send.
|
||||
mux.HandleFunc("GET /partials/message", s.requireAuth(s.handleMessageDetail))
|
||||
mux.HandleFunc("POST /actions/draft", s.requireAuth(s.handleDraft))
|
||||
mux.HandleFunc("POST /actions/draft/save", s.requireAuth(s.handleSaveDraft))
|
||||
mux.HandleFunc("POST /actions/send", s.requireAuth(s.handleSendReply))
|
||||
|
||||
// FR-14: editable category list.
|
||||
mux.HandleFunc("GET /categories", s.requireAuth(s.handleCategories))
|
||||
mux.HandleFunc("POST /categories/add", s.requireAuth(s.handleCategoryAdd))
|
||||
|
||||
Reference in New Issue
Block a user