CC and BCC fields look trivial until your agent triggers a reply-all storm on a 20-person thread, or silently drops a stakeholder because it only read the To header. Getting these fields right means understanding how they work at the MIME layer, how they affect threading semantics, and what decisions your agent code should make at each step.
What CC and BCC actually mean at the protocol level
CC (Carbon Copy) addresses appear in the message headers and are delivered to those recipients. Every downstream recipient — To, CC, and BCC — can see the CC list. BCC (Blind Carbon Copy) recipients receive the message, but their addresses are stripped from the headers before delivery; no other recipient knows a BCC address received it.
At the SMTP level, BCC is handled entirely in the envelope (RCPT TO commands) rather than the message headers. When you send to a BCC address, your sending server issues a RCPT TO for that address but omits it from the Bcc: header in the message body — or strips the header entirely before forwarding to other recipients. Some implementations send a separate copy to BCC recipients with a Bcc: header containing only their own address.
The practical consequence for agents: if your agent receives a message, it was either in To, CC, or was BCC'd — but it cannot distinguish BCC from To/CC by inspecting headers alone. The only reliable signal is whether the agent's address appears in the To or CC header of the received message.
Received message headers:
To: alice@example.com
CC: agent@acme.mails.ai, bob@example.com
Bcc: (empty or absent)
If your agent's address is in CC but not To, the message was explicitly CC'd to it — treat it as informational unless your workflow logic says otherwise. If the agent's address appears in neither header, it was BCC'd and has no implied obligation to reply.
Parsing CC and BCC in inbound webhook payloads
When email arrives via an inbound email parsing webhook, the payload typically includes parsed address arrays for each field. The structure varies by provider, but you'll generally get something like:
// Typical inbound webhook event shape
const event = {
data: {
from: { address: "alice@example.com", name: "Alice" },
to: [{ address: "agent@acme.mails.ai" }],
cc: [
{ address: "bob@example.com", name: "Bob" },
{ address: "carol@example.com", name: "Carol" }
],
subject: "Project update",
extracted_text: "...",
message_id: "<unique-id@mail.example.com>",
// ...
}
};
Your agent needs to decide three things when it sees a CC'd message:
- Was the agent addressed directly (
To) or included for visibility (CC)? - Should the agent reply-all, preserving the CC list, or reply only to the sender?
- Are any addresses on the CC list ones the agent should explicitly exclude (e.g., other agent addresses, internal monitoring addresses)?
function shouldReply(event: TypedEvent): boolean {
const agentAddress = "agent@acme.mails.ai";
const toAddresses = event.data.to?.map(a => a.address.toLowerCase()) ?? [];
const ccAddresses = event.data.cc?.map(a => a.address.toLowerCase()) ?? [];
const inTo = toAddresses.includes(agentAddress);
const inCc = ccAddresses.includes(agentAddress);
if (inTo) return true; // direct — reply expected
if (inCc) return false; // informational by default
return false; // BCC — no reply implied
}
This is a default policy, not a universal rule. Some workflows deliberately CC an agent to have it take action (e.g., CC summarize@agent.example.com to trigger a summary). In those cases the routing logic lives in the agent's configuration, not in simple To/CC position.
Sending with CC and BCC from agent code
When your agent needs to send a message that includes additional recipients, the fields map directly to the send payload. Using the @mailsai/sdk:
import { mails } from "@mailsai/sdk";
const hello = mails.agent("hello");
await hello.send({
to: "alice@example.com",
cc: ["bob@example.com", "carol@example.com"],
bcc: "audit-log@internal.example.com",
subject: "Meeting notes",
body_text: "Here are the notes from today's sync..."
});
A few things worth being precise about:
- BCC is a one-way door. Once you BCC an address, your agent cannot infer from downstream replies that the BCC recipient received the original. If you need a full audit trail, write BCC recipients to your own datastore at send time.
- Each CC and BCC address counts against your sending allowance — one email per recipient. Sending to 1
To, 2CC, and 1BCCconsumes 4 emails from your plan allowance. - BCC addresses should never appear in reply chains. If your agent is building a threaded reply, strip BCC from the outbound headers explicitly.
Reply-all logic: the hard part
Reply-all is where most agent implementations go wrong. The naive approach — reply to everyone on the original CC list — creates several failure modes.
Loop creation. If another agent or automation is on the CC list, reply-all sends them the reply, which may trigger their own reply, which CC's back your agent. Break this by checking every outbound CC address against a known list of agent/automation addresses before sending.
Leaking BCC recipients. The original sender BCC'd someone for a reason. If you reply-all by copying the To and CC headers verbatim, that's correct — BCC recipients were never in those headers, so they won't be included. But if you're reconstructing headers from a database record that stored BCC addresses, you must exclude them from reply-all.
Including the agent's own address. When replying, strip the agent's own address from CC before sending. Many email clients do this automatically; your code must do it explicitly.
import { createClient } from "@mailsai/sdk";
const client = createClient();
function buildReplyAllCc(
originalCc: Array<{ address: string }>,
agentAddress: string,
blocklist: string[]
): string[] {
return originalCc
.map(r => r.address.toLowerCase())
.filter(addr => addr !== agentAddress.toLowerCase())
.filter(addr => !blocklist.includes(addr));
}
// Use client.messages.reply() for thread-correct replies —
// it sets In-Reply-To and References headers automatically.
await client.messages.reply(incomingMessageId, {
body_text: responseText,
cc: buildReplyAllCc(event.data.cc ?? [], "hello.acme@send.mails.ai", knownAgents)
});
Note: client.messages.reply() posts to POST /v1/messages/{id}/reply, which handles threading headers. You still control cc on the reply.
Threading and CC changes mid-thread
CC lists change over the course of a thread. Someone gets added in reply #3, someone else drops off in reply #5. Your agent needs a policy for handling this drift.
Option A: Follow the latest headers. Your agent always uses the CC list from the most recent message in the thread it's replying to. This mirrors how human email clients behave.
Option B: Use a fixed list defined at thread creation. For formal workflows (e.g., contract review, approval chains), you might define the stakeholder list at workflow creation and ignore subsequent CC additions or removals.
Option C: Merge and de-duplicate. Build a union of all addresses that have ever appeared in the thread, excluding addresses that explicitly unsubscribed or were removed. This is rarely correct for general-purpose agents.
Option A is the right default. Implement it by reading cc from the event's data field on each incoming message rather than from stored thread state.
hello.onReply(async (event) => {
const currentCc = event.data.cc ?? [];
const agentAddr = "hello.acme@send.mails.ai";
const replyCc = currentCc
.map(r => r.address)
.filter(addr => addr !== agentAddr);
await client.messages.reply(event.source_message_id, {
body_text: await generateResponse(event.data.extracted_text),
cc: replyCc
});
});
Loop prevention specific to CC
CC fields make loop detection harder than pure To replies because the signals are weaker. A few concrete checks:
- Auto-reply detection. Check for
Auto-Submitted: auto-repliedorAuto-Submitted: auto-generatedheaders. Messages with these headers should not trigger CC-based actions. - Precedence header.
Precedence: bulkorPrecedence: listindicates a mass-sent message. An agent CC'd on such a message should not reply-all — that's how you flood a mailing list. - X-Loop header. Some systems set
X-Loop: <identifier>explicitly. If your agent sets this on outbound messages and sees it on inbound, skip processing. - Cycle detection via thread_id. If your agent already sent in a thread and receives another message in the same thread CC'ing it again, check whether it's being looped back its own reply before responding.
For a systematic approach to loop prevention, see the email classification layer — which can help distinguish first-contact messages from automated replies.
A decision flow for incoming CC'd messages
flowchart LR
A[Message arrives] --> B{Agent in To?}
B -->|Yes| C[Treat as direct request]
B -->|No| D{Agent in CC?}
D -->|No| E[BCC or error - skip reply]
D -->|Yes| F{Workflow config says act on CC?}
F -->|No| G[Log and archive]
F -->|Yes| H[Execute CC action]
C --> I{Reply-all needed?}
I -->|Yes| J[Strip agent addr and blocklist from CC]
J --> K[Send reply with filtered CC]
I -->|No| L[Reply to sender only]
Practical patterns for common agent workflows
Approval workflows. Agent is CC'd on a request email. It logs the request, waits for a To-addressed follow-up with a decision, then notifies all original CC recipients of the outcome. The agent never replies-all to the original CC; it sends a new message to the full stakeholder list when the workflow completes.
Audit/observer agents. An agent address is BCC'd on outbound messages from humans to create a record. The agent never replies. Its only job is to parse the inbound webhook and write to a datastore. This is a valid use case — just ensure your inbound parsing setup routes BCC'd messages correctly.
Coordination agents. A human emails an agent (To) and CC's three colleagues. The agent replies-all with a draft for review. Each reviewer's reply is addressed to the agent (To) but CC's the others. The agent accumulates all replies and sends a final merged response to the full group. Here the CC list is the working group, and the agent maintains it across turns using the thread_id from the agent email platform.
Frequently Asked Questions
Can an agent detect that it was BCC'd on a message?
Not reliably from headers alone. If the agent's address is absent from both To and CC headers, it was likely BCC'd — but the message could also be a list delivery where addresses are suppressed. The safest approach: if the agent's address isn't in To or CC, treat the message as informational and do not auto-reply.
Does BCC create a separate email for each BCC recipient?
At the SMTP level, yes — the sending server issues a separate RCPT TO for each BCC address. The message body delivered to BCC recipients is usually identical to other recipients, but with the Bcc: header removed. For billing/allowance purposes on most email platforms, each BCC address counts as one sent email.
How should an agent handle a message where the CC list has 50+ addresses?
Don't reply-all unless the workflow explicitly requires it. With large CC lists, consider whether a reply to the sender (or a subset) is more appropriate. If reply-all is necessary, rate-limit the send across multiple requests to avoid triggering spam filters, and check for mailing list addresses (List-Id header) that should be excluded.
What happens to BCC when using client.messages.reply()?
The reply method sets threading headers (In-Reply-To, References) based on the original message. You can still pass bcc in the reply payload, and those addresses will receive the reply without being visible to To or CC recipients. Original BCC recipients of the first message do not automatically receive replies — you must explicitly re-BCC them if needed.
Should the agent's own address ever appear in outbound CC fields?
No. It wastes an email allowance slot, adds noise to the thread, and can confuse threading logic. Always filter the sending agent's address from CC before dispatching a reply. The agent will receive a copy via its normal inbound routing without being in its own CC list.
How do I prevent agents from replying to each other via CC?
Maintain a blocklist of agent addresses (e.g., all addresses on your *.mails.ai domain or a configured set) and filter them from outbound CC fields. Additionally, check inbound messages for Auto-Submitted headers and skip automated replies to those. For more systematic loop prevention, see inbound email parsing and routing patterns.
Related guides: Email Circuit Breaker Pattern for AI Agent Pipelines, Detecting Auto-Replies to Prevent Agent Email Loops, AI for Email: Integrating Agents Into Transactional Flows and AI for Email Automation: Triggers, Parsing & Actions.
Explore the product: Inbound email API, Email classification, All features and Pricing.
