ApproveInboundReviewCore releases a held INBOUND message to its agent's inbox (status pending_review → review_approved, now readable) and fires email.review_approved — the inbound analogue of ApprovePendingCore. There is no SES send and no draft edit: an inbound hold is a screening decision, not a d
(ctx context.Context, userID string, msg *identity.ReviewMessageMeta)
| 247 | // status='pending_review' AND agent_id, so a concurrent reviewer or the TTL sweep |
| 248 | // racing this call results in ErrNotPendingReview (409), never a double release. |
| 249 | func (a *API) ApproveInboundReviewCore(ctx context.Context, userID string, msg *identity.ReviewMessageMeta) *OutboundError { |
| 250 | if err := a.store.ApproveInboundReview(ctx, msg.ID, msg.AgentID, userID); err != nil { |
| 251 | if errors.Is(err, identity.ErrNotPendingReview) { |
| 252 | return &OutboundError{http.StatusConflict, "message_not_pending", "message is not pending review"} |
| 253 | } |
| 254 | log.Printf("[api] approve inbound review %s: %v", msg.ID, err) |
| 255 | return &OutboundError{http.StatusInternalServerError, "internal_error", "failed to approve message"} |
| 256 | } |
| 257 | log.Printf("[mail:%s] dir=inbound type=%s status=%s agent=%s approved_by=user:%s", |
| 258 | msg.ID, msg.Type, identity.MessageStatusReviewApproved, msg.AgentID, userID) |
| 259 | // Post-side-effect publish (the release row is already committed): reuse the |
| 260 | // approved-event plumbing (deterministic id off the message id → MTA/retry |
| 261 | // idempotent). A minimal *identity.Message carries the id publishApproved needs. |
| 262 | a.publishApproved(ctx, a.buildInboundReleasedEvent(msg, a.reviewOwnerID(ctx, msg.AgentID, userID), userID), &identity.Message{ID: msg.ID, AgentID: msg.AgentID}) |
| 263 | return nil |
| 264 | } |
| 265 | |
| 266 | // reviewOwnerID returns the agent's owner user id — the webhook routing key for |
| 267 | // an inbound review event. It equals the reviewer today (the endpoint is |