Compliance — email-marketing rules KodMail enforces

Every commercial email sent through this app needs to clear four sets of rules: CAN-SPAM (US), GDPR (EU/UK), CASL (Canada), and the Gmail + Yahoo Feb 2024 sender requirements that gate inbox placement for high-volume senders. KodMail wires the technical pieces; this page is the walkthrough so a buyer can answer "is this app compliant?" with evidence.

Disclaimer This document explains the technical controls KodMail ships. It is not legal advice. Consult a qualified attorney for jurisdiction-specific obligations — especially DPAs, ROPA documentation, and the lawful basis you cite per subscriber.

Quick scorecard

RequirementStatusWhere KodMail enforces it
CAN-SPAM — physical postal address in every sendYes Blocked at pre-flightCampaignIssueDetector.no_postal_address
CAN-SPAM — working unsubscribe in every sendYes Blocked at pre-flightCampaignIssueDetector.no_unsubscribe
CAN-SPAM — honour opt-outs within 10 business daysYes ImmediateUnsubscribeController.destroy + ListSuppression
CAN-SPAM — truthful headers + From lineYesList + Campaign from_name / from_email validated against verified sending domain
GDPR Art. 6 — lawful basis (consent)Yes Captured per subscriberlist_subscribers.consent_method + consent_text + consent_source_url
GDPR Art. 7 — proof of consentYes Timestamp + IP + UA + agreement snapshotSame row, immutable on signup
GDPR Art. 17 — right to be forgottenYes One-click erasePOST /lists/{id}/subscribers/{id}/gdpr-erase
GDPR Art. 20 — data portabilityYes JSON exportGET /lists/{id}/subscribers/{id}/gdpr-export
GDPR Art. 30 — records of processingYes Activity log per eventActivityLog rows for create / confirm / unsub / erase
CASL — express consent + identificationYesSame consent capture + footer enforcement
Gmail / Yahoo — List-UnsubscribeYes Every sendSendSubscriberAction header block
Gmail / Yahoo — List-Unsubscribe-Post: One-ClickYes Every sendSame — paired with the POST endpoint at /p/u/{campaign}/{subscriber}
Gmail / Yahoo — SPF + DKIM + DMARC alignmentYes Per-domainMail setup → Domain authentication
Bounce + complaint feedback loopYes AutomaticBouncesProcessCommand + ESP webhooks

CAN-SPAM (US)

Federal Trade Commission rule covering commercial email sent to or from US addresses. The five rules that apply per send:

  1. Truthful header information — From, To, Reply-To, Routing must identify the sender. KodMail blocks campaigns whose from_email domain isn't on the customer's verified sending domain list.
  2. Honest subject line — no deceptive copy. The pre-flight detector flags subjects >60 chars + flags excessive caps / punctuation as warnings.
  3. Identify as ad when commercial — KodMail does not police this at send time; admins should add the "List-Id" merge tag or an explicit "advertisement" line in templates when the message is purely promotional.
  4. Working unsubscribe link — every send carries {{ unsubscribe_url }}. The pre-flight detector now blocks send (not just warns) when the body lacks an unsubscribe link.
  5. Physical postal address — every commercial email must include the sender's valid physical postal address. Set it per list under List → Compliance → Company address, then reference it in your template footer with {{ list.company_address }}. The pre-flight detector blocks send when neither the merge tag nor the literal address appears in the body.

Opt-out timing. KodMail flips a subscriber to unsubscribed + plants a ListSuppression row the instant they click the link. Subsequent campaigns scoped to that list automatically skip them. CAN-SPAM allows 10 business days — KodMail is immediate, well inside the window.

GDPR + UK GDPR (EU + UK)

Applies whenever the data subject is in the EU or UK regardless of where your server lives. Six clauses matter for an email-marketing tool:

Article 6 — Lawful basis

You need a documented lawful basis per subscriber. For marketing email, the practical choice is "consent". KodMail captures the basis as consent_method (form / api / import / admin / automation / webhook) and the literal text the subscriber agreed to as consent_text. Persist your privacy-policy URL on the list so it appears alongside the consent box on every form.

Article 7 — Conditions for consent

Consent must be: freely given, specific, informed, unambiguous. KodMail records the surrounding evidence:

Every signup writes this row immutably. A regulator asking "show me consent for this address" gets the answer from one row.

Article 17 — Right to be forgotten

Subscribers can demand erasure. KodMail's erase endpoint:

  1. Wraps in a DB transaction
  2. Deletes custom field values, open + click tracking rows
  3. Nulls ip_address, user_agent, every consent column, the confirmation token
  4. Replaces the email with erased+<sha256>@gdpr.local — row stays queryable, PII is gone
  5. Stamps erased_at + flips status to blacklisted
  6. Plants a ListSuppression with the original email so future imports / re-signups for that person hit the wall
  7. Writes an audit row keyed on sha256(email) — proof of erasure without re-introducing the PII

Where to find it: Customer → List → Subscriber detail → Erase data. Or call POST /lists/{list}/subscribers/{subscriber}/gdpr-erase directly.

Article 20 — Right to data portability

Subscribers can demand a machine-readable export of everything you hold. KodMail's export endpoint returns a JSON file with:

Where to find it: Customer → List → Subscriber detail → Export data (JSON). Or hit GET /lists/{list}/subscribers/{subscriber}/gdpr-export.

Article 30 — Records of processing

Every consent + lifecycle event writes to ActivityLog with a stable type slug. Filter the admin audit log for:

public.subscriber.created    — new signup
subscriber.confirmed         — double-opt-in confirmed
public.subscriber.unsubscribed
customer.subscriber.gdpr_export
customer.subscriber.gdpr_erase

Article 32 — Security

KodMail ships:

CASL (Canada)

Canada's Anti-Spam Legislation is stricter than CAN-SPAM in two ways:

If you serve Canadian audiences, leave double opt-in on by default and don't import lists without documented consent.

Gmail + Yahoo Feb 2024 sender requirements

For senders > 5,000 messages/day, both providers now enforce three technical rules. KodMail satisfies all three out of the box:

RuleWhat it meansHow KodMail handles it
List-Unsubscribe header Every commercial email must carry an RFC 8058 unsubscribe header that recipients' mail clients can wire to a one-click button. Added to every SendableEmail in SendSubscriberAction. URL points at POST /p/u/{campaign}/{subscriber}.
List-Unsubscribe-Post: List-Unsubscribe=One-Click The header signals "this endpoint accepts a one-click POST" — no confirmation page, no auth. Same header block in SendSubscriberAction. The matching POST route bypasses CSRF + auth and finalises the unsub immediately.
SPF + DKIM + DMARC alignment The Authenticated-Received-Chain has to align — domain in From must match what SPF + DKIM signed. Per-domain DKIM keys auto-generated under Customer → Domains. SPF + DMARC instructions printed inline. See Mail setup.

Spam-rate ceiling: Gmail wants a Postmaster Tools-measured complaint rate < 0.3%. KodMail tracks per-customer complaint volume in Admin → Abuse complaints; if the rate climbs, throttle that customer or pause their sending.

Double opt-in — recommended default

Per list, set List → Settings → Opt-in mode = Double. New signups land in unconfirmed status, a confirmation email goes out, and only after the recipient clicks the token-secured link do we flip them to confirmed. Three reasons to leave it on:

  1. Anti-bot — fake addresses can't confirm themselves.
  2. Lower complaint rate — confirmed addresses are people who actually want your mail.
  3. GDPR Art. 7 proof — the timestamp + token round-trip is the strongest possible consent record.

How the token works: at signup KodMail generates a 48-char random token, stamps confirmation_token + confirmation_sent_at, and emails a URL of the form /p/c/<uid>/<token>. The landing controller does a constant-time hash_equals compare so timing attacks can't enumerate valid tokens. After confirmation the token is nulled — single-use, can't be replayed.

Suppression list lifecycle

Every unsubscribe, hard bounce, complaint, and manual block writes a ListSuppression row. The campaign dispatcher checks suppression before each delivery. Once on the list, a subscriber stays suppressed forever unless an admin explicitly removes the entry (Customer → List → Suppressions). Sources tracked:

Documents the admin still owns

The app does the technical heavy lifting. These four artefacts still live with the operator:

  1. Privacy Policy — publish via Admin → Pages. KodMail links the page from every signup form footer.
  2. Terms of Service — same publishing flow.
  3. Data Processing Agreement (DPA) — required by B2B customers under GDPR Art. 28. Template wording in the next section.
  4. Records of Processing Activities (ROPA) — internal document, GDPR Art. 30. Template wording below.

DPA boilerplate (B2B operator)

Sample wording "Operator processes personal data (subscriber email addresses, IP addresses, engagement metrics) solely on behalf of the Customer, in accordance with Customer's documented instructions. Operator retains no rights to the data. Operator complies with the technical and organisational measures set out in GDPR Art. 32 and provides Customer with the audit log, data export, and erasure tooling necessary to fulfil Data Subject requests under GDPR Art. 15–20 within the statutory 30-day window."

ROPA template

Internal doc. One row per processing activity. Minimum columns:

FieldExample for KodMail
PurposeSend subscribers commercial email they consented to receive
Categories of data subjectsNewsletter subscribers, customers, leads
Categories of personal dataEmail address, name (optional), IP at signup, engagement metrics
RecipientsSending ESP (SES / SendGrid / Postmark / Mailgun / etc.) — listed in Admin → Settings → Sending
International transfersDepends on ESP location — document per relay
RetentionIndefinite while consent stands; erased on Art. 17 request
Security measuresTLS-only transit, encrypted credential storage, 2FA, audit logging — see GDPR Art. 32 section above

Pre-flight checklist before submitting to CodeCanyon

  1. Default opt_in_mode = double on the seed list templates
  2. Privacy Policy + Terms pages published
  3. List-level company address filled in (CAN-SPAM)
  4. At least one sending domain authenticated with DKIM (Gmail/Yahoo rule)
  5. Test campaign sent to a Gmail + Yahoo address — confirm both providers show the "unsubscribe" link in their UI
  6. Run the pre-flight detector on the demo campaign — should pass with zero errors
  7. Fire the GDPR export + erase flow on a demo subscriber to confirm both endpoints return clean JSON / suppress correctly