Skip to content

Commit 03625af

Browse files
committed
Send per-recipient individual emails instead of Gmail-placeholder + BCC
Each tracking user is now the sole To: on their own email (SendGrid personalizations / per-recipient SES sends) rather than being BCC'd on a shared bulk message addressed to coursegrabappstore@gmail.com. This is a transactional 1:1 pattern that scores far better with strict inbox providers (Microsoft/Outlook) and keeps recipients' addresses private from one another.
1 parent cade43a commit 03625af

2 files changed

Lines changed: 41 additions & 31 deletions

File tree

src/app/coursegrab/notifications/push_notifications.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import time
44
import base64
55
import os
6-
from app.coursegrab.utils.constants import ALGORITHM, ANDROID, EMAIL, IOS, COURSEGRAB_FROM_EMAIL, COURSEGRAB_TO_EMAIL, MAX_BCC_SIZE
6+
from app.coursegrab.utils.constants import ALGORITHM, ANDROID, EMAIL, IOS, COURSEGRAB_FROM_EMAIL, COURSEGRAB_TO_EMAIL, MAX_RECIPIENTS_PER_SEND
77
from datetime import datetime
88
from hyper import HTTP20Connection
99
from firebase_admin import initialize_app, messaging
@@ -177,53 +177,61 @@ def send_emails(section, emails):
177177
end_section_index = serialized_section["section"].find("/")
178178
trimmed_section_name = serialized_section["section"][:end_section_index].strip()
179179

180-
# SES has a limit of 50 total recipients (to + cc + bcc) per request.
181-
# We use up 1 out of 50 to send the email to ourselves. Thus we have 49 emails left to fill up with user's emails
182-
# in the bcc section. So we partition the emails into chunks of size 49 max. (e.g. email_chunks = [ [49 emails], [21 emails] ])
183-
email_chunks = [emails[ind:ind + MAX_BCC_SIZE] for ind in range(0, len(emails), MAX_BCC_SIZE)]
180+
# Each recipient gets their own individual email (see send_single_email). We
181+
# partition into batches of MAX_RECIPIENTS_PER_SEND so SendGrid can deliver a
182+
# batch in a single API request (via one personalization per recipient).
183+
email_chunks = [emails[ind:ind + MAX_RECIPIENTS_PER_SEND] for ind in range(0, len(emails), MAX_RECIPIENTS_PER_SEND)]
184184

185185
try:
186186
# Send separate email for each chunk
187187
for chunk in email_chunks:
188188
send_single_email(subject_code, course_num, trimmed_section_name, chunk)
189+
# Send one monitoring copy of this notification to our own inbox so we keep
190+
# a running record of every notification that goes out.
191+
send_single_email(subject_code, course_num, trimmed_section_name, [COURSEGRAB_TO_EMAIL])
189192
except Exception as e:
190193
print("Error while sending email notifications:", e)
191194

192195

193-
def send_single_email (subject_code, course_num, trimmed_section_name, email_chunk):
194-
"""Send email notification to single chunk of user emails (max 49 bcc emails)"""
196+
def send_single_email(subject_code, course_num, trimmed_section_name, email_chunk):
197+
"""Send an individual notification email to each recipient in the chunk.
198+
199+
Each recipient is the sole To: address (a transactional 1:1 pattern) instead of
200+
being BCC'd on one shared bulk message. This scores far better with strict inbox
201+
providers (e.g. Microsoft/Outlook) and keeps recipients' addresses private from
202+
one another.
203+
"""
195204
course_name_full = f"{subject_code} {course_num} {trimmed_section_name}"
196205
subject = f"{course_name_full} is Now Open"
197206
html_content = email_body.replace("COURSE_NAME_NUM", course_name_full)
198207

199208
if EMAIL_PROVIDER == "ses":
200-
try:
201-
ses_client.send_email(
202-
Source=f"CourseGrab by AppDev <{COURSEGRAB_FROM_EMAIL}>",
203-
Destination={
204-
"ToAddresses": [COURSEGRAB_TO_EMAIL],
205-
"BccAddresses": list(email_chunk), # Add users' emails to bcc
206-
},
207-
Message={
208-
"Subject": {"Data": subject},
209-
"Body": {"Html": {"Data": html_content}},
210-
},
211-
)
212-
except Exception as e:
213-
print(f"Error sending email: {e}")
209+
# SES has no multi-recipient personalization; send one email per recipient.
210+
for recipient in email_chunk:
211+
try:
212+
ses_client.send_email(
213+
Source=f"CourseGrab by AppDev <{COURSEGRAB_FROM_EMAIL}>",
214+
Destination={"ToAddresses": [recipient]},
215+
Message={
216+
"Subject": {"Data": subject},
217+
"Body": {"Html": {"Data": html_content}},
218+
},
219+
)
220+
except Exception as e:
221+
print(f"Error sending email to {recipient}: {e}")
214222
else: # sendgrid
223+
# One request with a separate personalization per recipient: SendGrid
224+
# delivers an individual email to each, and recipients never see each other.
215225
message = Mail(
216226
from_email=Email(COURSEGRAB_FROM_EMAIL, "CourseGrab by AppDev"),
217227
subject=subject,
218228
html_content=html_content,
219229
)
220-
personalization = Personalization()
221-
personalization.add_to(To(COURSEGRAB_TO_EMAIL))
222-
for bcc_email in email_chunk: # Add users' emails to bcc
223-
personalization.add_bcc(Email(bcc_email))
224-
message.add_personalization(personalization)
230+
for recipient in email_chunk: # one To: per recipient
231+
personalization = Personalization()
232+
personalization.add_to(To(recipient))
233+
message.add_personalization(personalization)
225234
try:
226235
sendgrid_client.send(message)
227236
except Exception as e:
228237
print(f"Error sending email: {e}")
229-

src/app/coursegrab/utils/constants.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
COURSEGRAB_FROM_EMAIL = "noreply@coursegrab.me"
2727
COURSEGRAB_TO_EMAIL = "coursegrabappstore@gmail.com"
2828

29-
# Max number of bcc emails per SES email notification.
30-
# SES caps a single send at 50 total recipients (to + cc + bcc). One slot is
31-
# used by COURSEGRAB_TO_EMAIL, leaving 49 for bcc.
32-
MAX_BCC_SIZE = 49
29+
# Max recipients addressed per email send. Each recipient receives an individual
30+
# email (their own To:), not a shared BCC — a transactional pattern that scores far
31+
# better with strict inbox providers (e.g. Microsoft/Outlook). For SendGrid this is
32+
# the number of personalizations batched into one API request (provider limit 1000);
33+
# for SES we loop one send per recipient, so it just bounds the batch we iterate.
34+
MAX_RECIPIENTS_PER_SEND = 1000

0 commit comments

Comments
 (0)