Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/DataTalksClub/datamailer/llms.txt

Use this file to discover all available pages before exploring further.

Datamailer separates global contact identity from per-audience subscription state. A Contact row holds the canonical email address and any hard suppression signals that apply everywhere — hard bounces, complaints, and global unsubscribes. A Subscription row holds the opt-in status for a specific audience and client combination. This design means a single database lookup tells you whether you can send to someone globally, while subscription queries are scoped only to the audience you care about.

Contact Fields

Every email address in the system corresponds to exactly one Contact row, uniquely identified by its normalized_email.
FieldTypeDescription
emailstringOriginal email address as provided
normalized_emailstringCasefolded version; the uniqueness key
verified_atdatetime | nullWhen the contact completed email verification
email_validation_statusenumResult of external or manual validation (see below)
email_validation_reasonstringFree-text reason from the validation provider
email_validated_atdatetime | nullWhen the validation status was last set
global_unsubscribed_atdatetime | nullSet when the contact opts out of all marketing email
hard_bounced_atdatetime | nullSet on the first hard bounce from any send
complained_atdatetime | nullSet when SES reports a spam complaint

Email Normalization

Normalization is applied automatically in the model’s save() method:
self.normalized_email = self.email.casefold()
This ensures that Alice@Example.com, alice@example.com, and ALICE@EXAMPLE.COM all resolve to the same contact. The normalized_email field carries the unique database constraint; the original email field is preserved for display.

Email Validation Status

The email_validation_status field records the result of validation, whether performed by an external service or set manually by an operator.
StatusValueMeaning
unknowndefaultNo validation has been run
validdeliverableAddress passed all validation checks
invalid_syntaxnon-deliverableAddress fails RFC syntax checks
no_mxnon-deliverableDomain has no MX record
disposablenon-deliverableDisposable/temporary email domain
riskynon-deliverableFlagged as risky by validation provider
manually_invalidnon-deliverableOperator manually marked as invalid
externally_validateddeliverableValidated by an external system
Statuses are grouped into two sets in contacts.py:
NON_DELIVERABLE_EMAIL_VALIDATION_STATUSES = {
    EmailValidationStatus.INVALID_SYNTAX,
    EmailValidationStatus.NO_MX,
    EmailValidationStatus.DISPOSABLE,
    EmailValidationStatus.RISKY,
    EmailValidationStatus.MANUALLY_INVALID,
}

DELIVERABLE_EMAIL_VALIDATION_STATUSES = {
    EmailValidationStatus.VALID,
    EmailValidationStatus.EXTERNALLY_VALIDATED,
}
Contacts with a non-deliverable status are skipped during campaign snapshotting with the invalid_email skip reason.

Subscriptions

A Subscription row represents opt-in state for a specific (contact, audience, client) scope. The same contact can be subscribed to one client and unsubscribed from another within the same audience.
FieldDescription
contactThe global contact identity
audienceThe brand-level mailing list
clientThe application (nullable for audience-level subscriptions)
statuspending, subscribed, or unsubscribed
verified_atWhen the subscription was confirmed
unsubscribed_atWhen the unsubscribe was recorded
unsubscribe_reasonFree-text reason, e.g. public_unsubscribe

Subscription Statuses

StatusMeaning
pendingContact has been added but not yet confirmed
subscribedContact has opted in and can receive marketing email
unsubscribedContact has opted out of this audience/client scope
The uniqueness constraint (contact, audience, client) ensures at most one subscription record per scope. When client is null, a separate constraint (unique_audience_only_subscription_scope) enforces uniqueness for audience-level subscriptions.

Unsubscribe Scopes

Datamailer supports three unsubscribe scopes, controlled by the scope parameter on the public unsubscribe page:
ScopeWhat it does
clientUnsubscribes from the sending client within the audience
audienceUnsubscribes from the entire audience across all clients
globalSets global_unsubscribed_at on the contact — blocks all marketing email from all clients

Suppression Rules

The contacts.py service exposes two functions that encode the send-eligibility rules:
def is_transactional_email_allowed(contact):
    """Blocks sends to hard-bounced or complained contacts."""
    return not get_contact_suppression_state(contact).has_hard_suppression

def is_marketing_email_allowed(contact, audience, client=None):
    """Blocks sends when any marketing suppression is active,
    when email validation is non-deliverable, or when the contact
    is not subscribed for the given audience/client scope."""
    suppression = get_contact_suppression_state(contact)
    if suppression.has_marketing_suppression:
        return False
    if has_invalid_email_validation(contact):
        return False
    return Subscription.objects.filter(
        contact=contact, audience=audience, client=client,
        status=SubscriptionStatus.SUBSCRIBED,
    ).exists()
In plain terms:
  • Hard suppression (hard_bounced_at or complained_at is set) blocks both transactional and marketing email.
  • Marketing suppression additionally includes global_unsubscribed_at and non-deliverable email validation statuses.
  • A valid, active subscription is also required for marketing email.

Tags

Tags are audience-scoped labels that can be applied to contacts. They are used in campaign targeting via include_tags and exclude_tags filters on a campaign.
FieldDescription
audienceThe audience this tag belongs to
nameDisplay name (e.g., Python Developers)
slugURL-safe identifier, auto-generated from name via slugify
Tags are attached to contacts through the ContactTag join table. The (contact, tag) pair is unique, and (audience, slug) is unique within the tag table. Tags from one audience are never visible to other audiences.

Build docs developers (and LLMs) love