(ctx context.Context, in *replyInput)
| 238 | } |
| 239 | |
| 240 | func (s *Server) handleReply(ctx context.Context, in *replyInput) (*sendOutput, error) { |
| 241 | ag, inbound, user, err := s.loadInbound(ctx, in.Address, in.ID) |
| 242 | if err != nil { |
| 243 | return nil, err |
| 244 | } |
| 245 | b := in.Body |
| 246 | if b.Body == "" { |
| 247 | return nil, NewError(http.StatusBadRequest, "invalid_request", "body is required") |
| 248 | } |
| 249 | // Validate only the user-supplied CC/BCC; the implicit To comes from the |
| 250 | // (already-validated) inbound message — mirrors the legacy handler. |
| 251 | if env := recipientCountError(b.CC, b.BCC); env != nil { |
| 252 | return nil, env |
| 253 | } |
| 254 | if e := agent.ValidateRecipients(b.CC, b.BCC); e != nil { |
| 255 | return nil, NewError(http.StatusBadRequest, "invalid_recipient", e.Error()) |
| 256 | } |
| 257 | if e := validateConversationID(b.ConversationID); e != nil { |
| 258 | return nil, NewError(http.StatusBadRequest, "invalid_request", e.Error()) |
| 259 | } |
| 260 | // Build the reply request via the same outbound helpers the legacy |
| 261 | // handler uses (subject normalization, recipient parsing, References). |
| 262 | subject := inbound.Subject |
| 263 | if subject != "" && !strings.HasPrefix(strings.ToLower(subject), "re: ") { |
| 264 | subject = "Re: " + subject |
| 265 | } else if subject == "" { |
| 266 | subject = "Re: your message" |
| 267 | } |
| 268 | rr, e := outbound.ParseReplyRecipients(inbound.RawMessage, b.ReplyAll, b.CC) |
| 269 | if e != nil { |
| 270 | return nil, NewError(http.StatusBadRequest, "invalid_recipient", e.Error()) |
| 271 | } |
| 272 | replyTo := rr.To |
| 273 | if len(replyTo) == 0 { |
| 274 | replyTo = []string{inbound.Sender} |
| 275 | } |
| 276 | req := outbound.SendRequest{ |
| 277 | To: replyTo, CC: rr.CC, BCC: b.BCC, Subject: subject, Body: b.Body, HTMLBody: b.HTMLBody, |
| 278 | ReplyToMessageID: inbound.EmailMessageID, |
| 279 | References: outbound.BuildReferencesChain(inbound.RawMessage, inbound.EmailMessageID), |
| 280 | ConversationID: b.ConversationID, Attachments: b.Attachments, |
| 281 | } |
| 282 | req.CC = agent.StripAgentSelfAliases(req.CC, ag.EmailAddress()) |
| 283 | req.BCC = agent.StripAgentSelfAliases(req.BCC, ag.EmailAddress()) |
| 284 | // Re-count the FINAL, post-expansion recipient set. reply_all fans the |
| 285 | // thread's To+Cc into req.To/req.CC above, so the earlier b.CC/b.BCC check is |
| 286 | // not the real fan-out — without this, a reply_all to a large thread bypasses |
| 287 | // the cap that /send and /forward enforce (the downstream send path has no |
| 288 | // cap of its own). |
| 289 | if env := recipientCountError(req.To, req.CC, req.BCC); env != nil { |
| 290 | return nil, env |
| 291 | } |
| 292 | return s.deliver(ctx, user, ag, req, "reply", inbound.EmailMessageID, "/v1/reply/"+in.ID, in.IdempotencyKey, in.RawBody, inbound) |
| 293 | } |
| 294 | |
| 295 | // ForwardRequest mirrors the legacy forward body. |
| 296 | type ForwardRequest struct { |
nothing calls this directly
no test coverage detected