CreatePendingOutboundMessage stores a fully composed outbound email in pending_review status, including body_text, body_html, and attachments so that approval can reconstruct the original SendRequest (or accept edits) without the caller needing to retain it. ttlSeconds sets how long the message rema
(ctx context.Context, agentID string, toRecipients, cc, bcc []string, subject, bodyText, bodyHTML string, attachmentsJSON []byte, msgType, conversationID, replyToEmailMessageID string, ttlSeconds int)
| 1405 | // ([{filename, content_type, data}, ...]) or nil. Callers that already have |
| 1406 | // an []outbound.Attachment slice should json.Marshal it before passing in. |
| 1407 | func (s *Store) CreatePendingOutboundMessage(ctx context.Context, agentID string, toRecipients, cc, bcc []string, subject, bodyText, bodyHTML string, attachmentsJSON []byte, msgType, conversationID, replyToEmailMessageID string, ttlSeconds int) (*Message, error) { |
| 1408 | if ttlSeconds <= 0 || ttlSeconds > HITLMaxTTLSeconds { |
| 1409 | return nil, fmt.Errorf("ttl_seconds must be between 1 and %d", HITLMaxTTLSeconds) |
| 1410 | } |
| 1411 | |
| 1412 | id := "msg_" + generateID() |
| 1413 | now := time.Now() |
| 1414 | approvalExpiresAt := now.Add(time.Duration(ttlSeconds) * time.Second) |
| 1415 | |
| 1416 | var recipient string |
| 1417 | if len(toRecipients) > 0 { |
| 1418 | recipient = toRecipients[0] |
| 1419 | } |
| 1420 | |
| 1421 | var attachmentsArg interface{} |
| 1422 | if len(attachmentsJSON) > 0 { |
| 1423 | attachmentsArg = attachmentsJSON |
| 1424 | } |
| 1425 | |
| 1426 | m := &Message{ |
| 1427 | ID: id, |
| 1428 | AgentID: agentID, |
| 1429 | Direction: "outbound", |
| 1430 | Recipient: recipient, |
| 1431 | Subject: subject, |
| 1432 | EmailMessageID: replyToEmailMessageID, |
| 1433 | Type: msgType, |
| 1434 | ConversationID: conversationID, |
| 1435 | CreatedAt: now, |
| 1436 | ExpiresAt: now.Add(MessageTTL), |
| 1437 | ToRecipients: toRecipients, |
| 1438 | CC: cc, |
| 1439 | BCC: bcc, |
| 1440 | Status: MessageStatusPendingReview, |
| 1441 | ApprovalExpiresAt: &approvalExpiresAt, |
| 1442 | BodyText: bodyText, |
| 1443 | BodyHTML: bodyHTML, |
| 1444 | AttachmentsJSON: json.RawMessage(attachmentsJSON), |
| 1445 | // Sender is the agent itself (agent ID == email) so `from` isn't empty |
| 1446 | // on a held draft's detail/list view (B1). |
| 1447 | Sender: agentID, |
| 1448 | } |
| 1449 | _, err := s.pool.Exec(ctx, |
| 1450 | `INSERT INTO messages ( |
| 1451 | id, agent_id, direction, recipient, subject, email_message_id, message_type, |
| 1452 | conversation_id, created_at, expires_at, |
| 1453 | to_recipients, cc, bcc, |
| 1454 | status, approval_expires_at, |
| 1455 | body_text, body_html, attachments_json, sender) |
| 1456 | VALUES ($1, $2, $3, $4, $5, $6, $7, |
| 1457 | $8, $9, $10, |
| 1458 | $11, $12, $13, |
| 1459 | $14, $15, |
| 1460 | $16, $17, $18, $19)`, |
| 1461 | m.ID, m.AgentID, m.Direction, m.Recipient, m.Subject, m.EmailMessageID, m.Type, |
| 1462 | m.ConversationID, m.CreatedAt, m.ExpiresAt, |
| 1463 | m.ToRecipients, m.CC, m.BCC, |
| 1464 | m.Status, m.ApprovalExpiresAt, |