Skip to content

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

Hold "Alt" / "Option" to enable pan & zoom
  • 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 by side_effect context 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. EmailContent dataclass 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
Hold "Alt" / "Option" to enable pan & zoom

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