MCPcopy Create free account
hub / github.com/Mnexa-AI/e2a / DeliverOutbound

Method DeliverOutbound

internal/agent/api.go:1126–1224  ·  view source on GitHub ↗

DeliverOutbound is the shared send/reply/forward delivery tail, HTTP-free: HITL hold (HoldForApprovalCore), else self-send loopback, else SES send + record outbound + publish sent event. The caller has already authed, resolved + ownership-checked the agent, rate-limited, domain-verified, run the mes

(ctx context.Context, user *identity.User, agent *identity.AgentIdentity, req outbound.SendRequest, msgType, replyToEmailMessageID string, referenced *identity.Message)

Source from the content-addressed store, hash-verified

1124// extraction). On a nil-error return the side effect has committed, so the
1125// idempotency key must be Completed (cached), never Released.
1126func (a *API) DeliverOutbound(ctx context.Context, user *identity.User, agent *identity.AgentIdentity, req outbound.SendRequest, msgType, replyToEmailMessageID string, referenced *identity.Message) (*OutboundResult, *OutboundError) {
1127 // Suppression enforcement (decision 9 / Slice 4b): fail fast if any
1128 // recipient is on this tenant's suppression list. Enforced fresh on every
1129 // attempt and NOT cached under the idempotency key (it's a clearable state,
1130 // released like every other error).
1131 if supErr := a.checkSuppression(ctx, user.ID, req); supErr != nil {
1132 return nil, supErr
1133 }
1134
1135 // Outbound screening (Slice 5): the recipient gate (outbound_policy) + content
1136 // scan (outbound_scan) combine into one applied action. block ⇒ refuse;
1137 // review ⇒ hold; flag ⇒ send + annotate; allow ⇒ send.
1138 verdict := a.screenOutbound(ctx, agent, req)
1139 if verdict.Block() {
1140 // Egress block: refuse to the caller. No message row is persisted; the
1141 // audit lives in protection_events keyed to a STABLE soft-ref id so a
1142 // retried block doesn't write duplicate audit rows / events.
1143 a.auditRowless(ctx, agent, blockAuditID(agent.ID, req), req, verdict)
1144 a.emitBlockedOutbound(agent, blockAuditID(agent.ID, req), req, verdict)
1145 return nil, &OutboundError{http.StatusForbidden, "blocked_by_policy", "message blocked by outbound policy"}
1146 }
1147
1148 // Hold when outbound screening says review. The outbound recipient gate
1149 // (outbound_policy: allowlist+review is the trust-ramp) + content scan now
1150 // fully own the hold decision — hitl_enabled/hitl_mode were retired in
1151 // Slice 5b (their behavior is mapped forward by migration 042).
1152 if verdict.Review() {
1153 msg, err := a.HoldForApprovalCore(ctx, agent, req, msgType, replyToEmailMessageID)
1154 if err != nil {
1155 if errors.Is(err, errHoldAttachments) {
1156 return nil, &OutboundError{http.StatusInternalServerError, "internal_error", "failed to serialize attachments"}
1157 }
1158 return nil, &OutboundError{http.StatusInternalServerError, "internal_error", "failed to hold message for approval"}
1159 }
1160 // Tag the held row + audit only when screening drove the hold (a pure
1161 // legacy-HITL hold carries no screening verdict).
1162 if verdict.Annotate() {
1163 a.annotateAndAudit(ctx, agent, msg.ID, req, verdict)
1164 }
1165 return &OutboundResult{Held: true, PendingMessageID: msg.ID, ApprovalExpiresAt: msg.ApprovalExpiresAt}, nil
1166 }
1167
1168 // Record usage (side-effect only — never block on quota; the cap
1169 // pre-check is the gate).
1170 if _, err := a.usage.RecordAndCheck(ctx, user.ID, agent.ID, agent.Domain, "outbound"); err != nil {
1171 log.Printf("[api] usage recording error: %v", err)
1172 }
1173
1174 if isSelfSend(req, agent.EmailAddress()) {
1175 providerID, err := a.performSelfSend(ctx, agent, req, msgType)
1176 if err != nil {
1177 log.Printf("[api] self-send failed: agent=%s error=%v", agent.EmailAddress(), err)
1178 return nil, &OutboundError{http.StatusInternalServerError, "internal_error", "self-send failed"}
1179 }
1180 slug, _, _ := strings.Cut(agent.EmailAddress(), "@")
1181 log.Printf("[mail] dir=outbound type=%s method=loopback from=%s to=%s slug=%s conv_id=%s subject=%q provider_id=%s", msgType, agent.EmailAddress(), agent.EmailAddress(), slug, req.ConversationID, req.Subject, providerID)
1182 return &OutboundResult{MessageID: providerID, Method: "loopback"}, nil
1183 }

Calls 15

checkSuppressionMethod · 0.95
screenOutboundMethod · 0.95
auditRowlessMethod · 0.95
emitBlockedOutboundMethod · 0.95
HoldForApprovalCoreMethod · 0.95
annotateAndAuditMethod · 0.95
performSelfSendMethod · 0.95
publishSentMethod · 0.95
buildSentEventMethod · 0.95
IsValidationErrorFunction · 0.92
blockAuditIDFunction · 0.85
isSelfSendFunction · 0.85