Emailing¶
[!WARNING] This is still an experimental component. It's not ready to be used in production.
This component is responsible for sending emails to users. It has basic sending capabilities and more advanced features like define audience and schedule email shoot.
Overview¶
classDiagram
class EmailContent {
+str subject
+str body
+str pre_header
+dict attachments
+dict user_context
+dict template_settings
+TemplateData template_data
+EmailTemplate template
}
class TemplateData {
...args
}
class MessageFrequency {
+str idempotency_key
+dict idempotency_params
+int max_message_per_key
}
class EmailTemplate {
+str title
+str name
+str uri
+get_template_data(dict, dict) TemplateData
+get_body(TemplateData) str
+get_subject(TemplateData) str
+get_pre_header(TemplateData) str|None
+get_attachments(TemplateData) dict|None
+generate_email_content(dict, dict) EmailContent
}
class EmailSendingOptions {
+bool send_to_unsubscribed
+bool tracked
+bool disable_message_retention
+bool queue_draft
+str message_id
}
class EmailRequest {
+str user_id
+str recipient_address
+str campaign_name
+dict message_metadata
+EmailContent content
+EmailSendingOptions sending_options
+MessageFrequency message_frequency
}
class EmailLog {
+str email_log_ref
+str user_id
+str email_address
+str template_name
+dict user_context
+dict message_metadata
+str delivery_id
+datetime sent_at
+datetime delivered_at
+bool clicked
+bool opened
+datetime opened_at
+str campaign_name
+dict template_settings
}
TemplateData <|.. EmailContent
EmailTemplate <|.. EmailContent
EmailTemplate "1" *-- "1" TemplateData : has
EmailRequest "1" *-- "1" EmailSendingOptions : has
EmailRequest "1" *-- "1" EmailContent : has
EmailRequest "1" *-- "1" MessageFrequency : has
- An email content is a message that contains a subject, a body, a pre-header, and attachments. It also contains metadata used to render the content like template data, user context, and template settings.
- An email template is a template that defines how to render an email content. It defines a method to generate the template data from a user context and settings. It offers a method to generate the email content from the template data.
- An email request is a request to send an email. It contains the email content, the email sending options, and the message frequency limit
- An email log is a log of an email sent. It contains metadata about the email and its status.
How to send an email¶
๐ง Basic usage¶
If you want to handle template initialization yourself, you can use send_email_raw method
This method will simply send an email based on the request you defined using our current email provider (CustomerIO).
request = EmailRequest(
user_id="1234",
recipient_address="foo@bar.com",
email_content=EmailContent(
subject="Welcome to Foo",
body="Hello {{ first_name }}",
pre_header="Welcome to Foo",
attachments=None,
user_context={"first_name": "Foo"},
template_settings=None,
template_data=None,
template=None,
),
sending_options=EmailSendingOptions(
send_to_unsubscribed=False,
tracked=True,
disable_message_retention=False,
queue_draft=False,
message_id=None,
message_frequency=None,
),
)
send_email_raw(request)
๐จ Register a template¶
We offer the ability to declare and register EmailTemplate that will define how body and subject should be rendered based
on a user context.
Registering a template is not mandatory to send an email, but it's required to preview the email content for a specific user,
and for email scheduling.
A template:
- defines an HTML file that will be used to generate the email content using Jinja rendering
- defines a method to generate all requires template arguments
- template data should be defined in TemplateData dataclass, where each field is a variable used in the template
- template data should be computed from user_context: dict, and a custom set of settings
- by default email subject is a block defined in the template
- can define a list of attachments
Example¶
@dataclass
class WelcomeTemplateData(TemplateData):
first_name: str
company_name: str
@register_template
class WelcomeTemplate(Template[WelcomeTemplateData]):
name = "welcome_email"
uri = "welcome_email.html"
def get_template_data(self, user_ref: str, user_context: dict, settings: dict) -> WelcomeTemplateData:
user = get_user(user_ref)
return WelcomeTemplateData(
first_name=user.first_name,
company_name=user.admined_company,
)
Templates should be registered using register_template decorator. This will register the template in emailing
component. Then, this template will be used & identified through its name attribute.
๐จ Email sending¶
To send an email, you can use send_email method that will send the email to the recipient.
Using a registered template:
send_email(
user_ref='1234',
recipient_address='foo@bar.com',
user_context={'company_id': '123'}, # additional contextual data to generate template data
template_name='welcome_email', # template name registered in the emailing component using `@register_template` decorator
template_settings={'with_marmot_gif': False}, # additional settings to customise template
)
Without using any registered template:
send_email(
user_ref='1234',
recipient_address='foo@bar.com',
template_uri='welcome_email.html', # template uri
template_data={'first_name': 'Foo', 'company_name': 'Bar'}, # template data to generate email content
attachments={'foo.pdf': b'foo', 'bar.pdf': b'bar'}, # attachments
)
See method docstring for more options.
If you want to send an email without thinking about all this stuff, you can use
send_email_raw method (see below).
โ Asynchronous & side effects¶
We offer different ways to send emails:
- Synchronous:
async_send=False. RQ won't be used and the email will be sent through the same thread - Asynchronous:
async_send=True. RQ will be used to send the email in a separate process. - Deferred:
defer_send=True. Sending (sync or async) will be accumulated in the side_effects stack byside_effectcontext manager. This will ensure you explicitly handle properly the side effect - Not deferred:
defer_send=False. Sending (sync or async) will be requested immediately.
Example:
def my_super_high_level_method(commit: bool = True):
# do smart stuff
with side_effect(trigger_on_exit=commit): # YOU WANT TO SEND THE EMAIL ONLY IF THE COMMIT IS TRUE
do_stuff_that_trigger_a_deferred_email()
another_method() # YOU DON'T WANT TO SEND THE EMAIL IF THIS METHOD FAILS
๐ก๏ธ Email previewing¶
- The simplest way to generate an email content is to request a preview using
preview_email() -> EmailContent.EmailContentdataclass contains everything required to display an email content. - Email content also contains the metadata used to render the content
email_content.template_data
๐ Message frequency¶
To avoid sending too many emails to a user, we use a MessageFrequency object that defines a key and a max number of
emails that can be sent to a user with this key.
For example, if you want to send a welcome email to a user, you can define a MessageFrequency with a key welcome_email
and a max number of emails per key to 1. This will ensure that only one welcome email will be sent to a user.
Note that if you change the key it will be considered as a new email and will be sent to the user.
๐พ How to retrieve email logs¶
We keep track of all emails sent though EmailLog. It contains metadata about the email and its status. See
paginated_email_logs method to retrieve email logs.
How to schedule an email shoot¶
An email shoot is a set of emails that will be sent to a specific audience at a specific time. An audience defines
a set of users that will receive the email. It relies on a Segment object that basically defines a query to retrieve
users.
๐จโ๐ฉโ๐งโ๐ง Register a segment¶
A segment is a class that defines a query to retrieve users. It also defines a method to convert a user to an
EmailRecipient that will be used to send the email.
@register_segment
class AccountAdminSegment(Segment):
name = "account_admin"
def get_query(self, settings: dict, limit: int | None = None) -> AlanQuery:
query = current_session.query(Admin)
if account_ref := settings.get("account_ref"):
query = query.filter(Admin.account_ref == account_ref)
return query.limit(limit)
def to_audience_member(self, model: Admin) -> EmailRecipient:
return EmailRecipient(
user_ref=model.id,
email_address=model.email,
user_context={"account_ref": model.account_ref, "role": model.role},
)
[!NOTE] The vision is that each component publish its own set of segments. This will allow to reuse segments across our apps.
๐ Schedule an email shoot¶
To schedule an email shoot, you can use schedule_shoot method that will create an EmailShoot object. This object will be
used to send the email at the scheduled time.
shoot = schedule_shoot(
scheduled_at=datetime(2021, 1, 1, 12, 0, 0),
segment_name="account_admin", # segment name registered in the emailing component using `@register_segment` decorator
audience_settings={"account_ref": "1234"}, # only admins of account 1234 will receive the email
audience_limit=None, # no limit
template_name="welcome_email", # template name registered in the emailing component using `@register_template` decorator
template_settings=None,
tags=["contracting"],
message_frequency=None,
transactional_message_id='1234', # optional transactional message id
)
If scheduled time is in the past, the shoot will be sent immediately.
๐ Email shoot lifecycle¶
flowchart LR
scheduled --> sending
sending --> completed
scheduled --> cancelled
A shoot can have different status defined in shoot.state attribute. The status can be:
- scheduled: the shoot is scheduled and will be sent at the scheduled time
- sending: the shoot is being sent
- completed: all send request have been processed. It does not mean that all emails have been sent or delivered
- cancelled: the shoot has been cancelled and will not be sent
๐ฎ๐ปโโ๏ธ Force send an email shoot¶
To force the sending of a scheduled email shoot, you can use send_shoot method that will send the email to the
audience defined in the shoot.
๐ Shoot metrics¶
Email metrics are gather in the EmailShoot object through automatic webhook callbacks. You can also define a custom
webhook callback to perform specific actions based on the email status (see below).
How to register a webhook callback¶
After sending an email you can register a webhook callback that will be triggered when the email is sent, delivered, opened, click etc. This is useful to perform specific action depending on the email status.
Webhook callbacks are called based on the campaign_name you defined when sending the email. In other words, you can
define a webhook callback for a specific campaign.
Example
def send_a_specific_email():
# do stuff
send_email(
...
campaign_name="company_welcome_test_1",
)
@register_webhook("company_welcome_test_1")
def my_callback(email_log: EmailLog, event_type: EventType, occurred_at: datetime):
match event_type:
EventType.delivered:
# do something
Advanced usage¶
Renewal campaign¶
TBD