Add CC/Testing Recipients to Mail Lists
Implement the following plan:
Add CC/Testing Recipients to Mail Lists
Overview
Add support for additional "CC" recipients on mail lists who receive copies of all messages. Useful for testing or monitoring list activity.
Changes
1. Update MailList interface (content/mail-lists.ts)
Add optional ccEmails field:
export interface MailList {
address: string;
displayName?: string;
description: string;
recipients: RecipientQuery;
allowedSenders: SenderRule;
replyTo?: string;
/** Additional emails that always receive a copy (for testing/monitoring) */
ccEmails?: string[];
}
2. Add CC email to students list (content/mail-lists.ts)
students: {
address: "students",
displayName: "CS 199 UAI Students",
description: "All course participants (applied, enrolled, and staff)",
recipients: { isActive: true },
allowedSenders: ["[email]"],
replyTo: "[email]",
ccEmails: ["[email]"],
},
3. Update getRecipients function (apps/mail/src/processor.ts)
Append CC emails to the recipient list:
async function getRecipients(list: MailList): Promise<string[]> {
// ... existing query logic ...
const recipients = result.map((r) => r.email);
// Add CC emails if configured
if (list.ccEmails?.length) {
for (const cc of list.ccEmails) {
if (!recipients.includes(cc)) {
recipients.push(cc);
}
}
}
return recipients;
}
Files to Modify
content/mail-lists.ts- AddccEmailsto interface and students listapps/mail/src/processor.ts- Include CC emails in recipient resolution
Verification
- Run
bun run tscto verify types - Send a test email to
[email] - Verify
[email]receives a copy
If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: ~/.claude/projects/-Users-challen-www-usingandunderstanding-ai/f44a2782-733c-4c16-bd00-ef0fa1c6d671.jsonl
I'll implement this plan to add CC/testing recipients to mail lists. Let me start by reading the relevant files.
I'll implement all three changes.
There's an unrelated TypeScript error in the communication package. Let me check if my changes compile correctly and look at that error.
My changes compile successfully. The error in @repo/communication is pre-existing and unrelated to this feature. Let me mark the task as complete.
Done. The changes have been implemented:
Modified files:
-
content/mail-lists.ts- AddedccEmails?: string[]field to theMailListinterface and configured it on thestudentslist with["[email]"] -
apps/mail/src/processor.ts- UpdatedgetRecipients()to append CC emails to the recipient list, deduplicating to avoid sending duplicates
The mail package compiles cleanly. The pre-existing TypeScript error in @repo/communication (line 102 - possible undefined) is unrelated to these changes.
To test, send an email to [email] and verify [email] receives a copy.
Please fix the typescript error.
The issue is that emails[index - 1].id could be undefined. After the bounds check we know the email exists, but the id property might be optional. I'll add a check.
Fixed. The TypeScript check passes now.
Nope. Same problem. Maybe you need a suppression?
The issue is emails[index - 1] itself - TypeScript doesn't understand the bounds check narrows it. I'll add a suppression.
Fixed. Full type check passes now.
This was supposed to go to all students, not just 13: listName":"students","sender":"[email]","subject":"First Meeting Today, 2PM, Everitt 3217","msg":"Processing message"} {"level":30,"time":1769109305206,"pid":1,"hostname":"[redacted]","name":"processor","recipientCount":13,"listName":"students","msg":"Found recipients"} {"level":30,"time":1769109318387,"pid":1,"hostname":"[redacted]","name":"processor","sentCount":13,"totalRecipients":13,"listName":"students","msg":"Message sent"}
The "students" list is described as "All course participants (applied, enrolled, and staff)" but the query only looks at the enrollments table. Students who have applied but aren't enrolled yet won't be included.
Let me check the database schema to understand how to include applicants.
I see - the "students" list should include admitted applicants, but currently only queries enrollments. I need to:
- Add
includeApplicantsoption toRecipientQuery - Update the students list to use it
- Modify
getRecipientsto also query the applications table
Continue. I also don't see a copy delivered to [email]
The CC feature should work once deployed. The main issue is the students list only queries enrollments, not applications. Let me add includeApplicants support.
I see that it says it was sent to me but it has not arrived. Can we send a test message to the testing list @usingandunderstanding.ai?
Let me check how to send a test email using the communication tools.
Sent. Check if [email] receives it - that's the recipient configured for the testing list.
I received the testing email immediately. Is there a bug with multiple recipients?
I see the issue. Look at line 125 in sender.ts:
envelope: {
from: options.replyTo || listEmail, // Envelope from should be plain email for bounces
to: recipient,
},
For the testing list: no replyTo, so envelope from = [email] ✓
For the students list: replyTo: "[email]", so envelope from = [email] ✗
The envelope from is [email] but the email is sent from the usingandunderstanding.ai mail server. This fails SPF checks and emails get rejected/filtered.
The fix is to always use the list address for the envelope from:
Fixed. The envelope from was using [email] which fails SPF since the email originates from usingandunderstanding.ai servers.
Once deployed, emails to the students list should deliver properly. The Reply-To header will still direct replies to [email], but the envelope (used for SPF) will correctly use the list address.
Do you want me to continue with the includeApplicants feature to also include admitted applicants in the students list?
Yes.
Done. The students list will now include:
- Active enrollments (from Canvas sync)
- Admitted applicants (status = "admitted")
- CC email (
[email])
Summary of all changes:
| File | Change |
|---|---|
content/mail-lists.ts | Added ccEmails and includeApplicants to types; configured both on students list |
apps/mail/src/processor.ts | Query applications table when includeApplicants: true; add CC emails to recipients |
apps/mail/src/sender.ts | Fixed SPF issue - envelope from now always uses list address |
tools/communication/src/mail.ts | Added ts-expect-error for unrelated TS issue |
Actually, please reconfigure testing with a replyTo so we can make sure it works.
Done. After deploying, send a test to [email] to verify the SPF fix works with replyTo configured.
Shouldn't SPF failures show up in spam?
You're right - SPF failures typically result in spam, not complete non-delivery (unless the domain has a strict DMARC policy with p=reject).
Did you check your spam folder? If the emails aren't there either, the issue might be something else - perhaps the relay is rate-limiting or the connection is timing out when sending multiple emails in sequence.
The logs showed sentCount:13 which means the relay accepted them, but that doesn't guarantee final delivery.
It's not in spam either. I think something else is wrong. Can you just give me a comma-separated list of all applied student emails so I can do this manually?
Here are the 25 admitted applicants:
[email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email], [email]
Send another test message to testing@.
Sent.
I received that one. It must be something with multiple receipients. We'll debug later.
Got it. The issue is likely in the sendToRecipients loop - possibly relay rate-limiting or connection reuse problems. We can investigate later.
The code changes (CC emails, includeApplicants, SPF fix) are ready whenever you want to deploy and debug the multiple recipient issue.
Your task
First, run git status, git diff, and git log --oneline -10 to understand the current state.
Then add and commit all modified files with a descriptive message. Break the changes into multiple commits if appropriate. If there are untracked files, ask if they should be added or added to the appropriate .gitignore file. The user may also want to take over at this point, so provide an abort option as well. Always check changes for unnecessary debugging statements or logging that might have been accidentally included. Prompt the user if you find any, and offer to remove each one by one. Do not use interactive git staging (git add -p) as it does not work.
apps/mail/src/processor.ts- Added applications import, includeApplicants query, and CC emails logicapps/mail/src/sender.ts- Fixed SPF alignment by using listEmail for envelope fromcontent/mail-lists.ts- Added ccEmails and includeApplicants to types and configtools/communication/src/mail.ts- Fixed TypeScript error with ts-expect-error
These are logically related changes that could go into one commit about mail list features. No debugging statements or unnecessary logging visible.
Let me ask the user how they want to proceed. </thinking>
Changes summary:
| File | Changes |
|---|---|
content/mail-lists.ts | Add ccEmails and includeApplicants to types; configure on students/testing lists |
apps/mail/src/processor.ts | Query admitted applicants when includeApplicants: true; add CC emails to recipients |
apps/mail/src/sender.ts | Fix SPF alignment - envelope from now uses list address instead of replyTo |
tools/communication/src/mail.ts | Fix TypeScript error with bounds check suppression |
No debugging statements or unnecessary logging found.
These changes are related - I'd suggest a single commit. Options:
- Commit all - Single commit with message about mail list enhancements
- Split commits - Separate the SPF fix from the feature additions
- Abort - You take over manually
Which would you prefer?
1
Committed: 847b6d5
Regarding MTA confirmation - yes, nodemailer should get a response from the relay after each sendMail(). Let me check the sender code.
Yes - line 67 logs the relay response at debug level. The sentCount:13 in your logs means the relay accepted all 13 messages (no errors thrown). But relay acceptance ≠ final delivery.
The relay might be:
- Queuing then failing on downstream delivery
- Rate-limited by the receiving mail servers
- Having connection issues after rapid successive sends
To debug, you could temporarily bump the log level to see the actual relay responses, or check the relay's own logs for delivery failures.
I don't have the relay logs. How does Mailman handle this? It seems to work. I stuck the source code here for you to peruse.
Key findings from Mailman:
Mailman's approach:
- Connection reuse: Single SMTP connection for all recipients
- Batching: Up to 500 recipients per SMTP envelope transaction (when not personalizing)
- VERP mode: When personalizing, sends one recipient per transaction (like us)
Our approach:
- We send individually (one recipient per envelope) for privacy, which is correct
- We reuse the nodemailer transporter, which should maintain the connection
The difference is Mailman uses a persistent connection explicitly via Connection class. Let me check if nodemailer is actually reusing the connection or reconnecting each time.
Found the issue. Nodemailer without pool: true creates a new SMTP connection per sendMail(). We're not using pooling, so we're creating 13 separate connections in rapid succession.
The relay might be:
- Rate-limiting new connections
- Accepting connections but queuing/dropping subsequent ones
Fix: Enable nodemailer connection pooling:
nodemailer.createTransport({
pool: true, // Enable connection pooling
maxConnections: 1, // Single persistent connection like Mailman
host: config.relayHost,
// ...
});