HoldForApprovalCore is the HTTP-free core of the HITL hold: it persists the pending outbound message, fires the async reviewer notification, and publishes the pending-approval event, returning the held message. Both the legacy handler and the v1 httpapi layer call it so there is exactly one hold-and
(ctx context.Context, agent *identity.AgentIdentity, req outbound.SendRequest, msgType, replyToEmailMessageID string)
| 1027 | // legacy handler and the v1 httpapi layer call it so there is exactly one |
| 1028 | // hold-and-notify path (api-v1-redesign — outbound extraction). |
| 1029 | func (a *API) HoldForApprovalCore(ctx context.Context, agent *identity.AgentIdentity, req outbound.SendRequest, msgType, replyToEmailMessageID string) (*identity.Message, error) { |
| 1030 | var attachmentsJSON []byte |
| 1031 | if len(req.Attachments) > 0 { |
| 1032 | b, err := json.Marshal(req.Attachments) |
| 1033 | if err != nil { |
| 1034 | log.Printf("[api] hitl: marshal attachments: %v", err) |
| 1035 | return nil, errHoldAttachments |
| 1036 | } |
| 1037 | attachmentsJSON = b |
| 1038 | } |
| 1039 | |
| 1040 | msg, err := a.store.CreatePendingOutboundMessage( |
| 1041 | ctx, agent.ID, |
| 1042 | req.To, req.CC, req.BCC, |
| 1043 | req.Subject, req.Body, req.HTMLBody, |
| 1044 | attachmentsJSON, |
| 1045 | msgType, req.ConversationID, replyToEmailMessageID, |
| 1046 | agent.HITLTTLSeconds, |
| 1047 | ) |
| 1048 | if err != nil { |
| 1049 | log.Printf("[api] hitl: create pending message: agent=%s err=%v", agent.ID, err) |
| 1050 | return nil, err |
| 1051 | } |
| 1052 | |
| 1053 | slug, _, _ := strings.Cut(agent.EmailAddress(), "@") |
| 1054 | log.Printf("[mail:%s] dir=outbound type=%s status=%s from=%s to=%v slug=%s conv_id=%s subject=%q approval_expires_at=%s", |
| 1055 | msg.ID, msgType, msg.Status, agent.EmailAddress(), req.To, slug, req.ConversationID, req.Subject, msg.ApprovalExpiresAt.Format(time.RFC3339)) |
| 1056 | |
| 1057 | // Fire the reviewer notification asynchronously. Failures are logged |
| 1058 | // inside the notifier and must never block the response — the pending |
| 1059 | // row is already persisted and the expiration worker will finalize it |
| 1060 | // even if every notification email bounces. |
| 1061 | if a.notifier != nil { |
| 1062 | a.notifier.NotifyPendingApprovalAsync(msg, agent) |
| 1063 | } |
| 1064 | |
| 1065 | a.publishPendingApproval(ctx, a.buildPendingApprovalEvent(agent, msg, req, msgType), msg.ID) |
| 1066 | return msg, nil |
| 1067 | } |
| 1068 | |
| 1069 | // OutboundResult is the HTTP-free outcome of DeliverOutbound. |
| 1070 | type OutboundResult struct { |
no test coverage detected