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.
Quick scorecard
| Requirement | Status | Where KodMail enforces it |
|---|---|---|
| CAN-SPAM — physical postal address in every send | Yes Blocked at pre-flight | CampaignIssueDetector.no_postal_address |
| CAN-SPAM — working unsubscribe in every send | Yes Blocked at pre-flight | CampaignIssueDetector.no_unsubscribe |
| CAN-SPAM — honour opt-outs within 10 business days | Yes Immediate | UnsubscribeController.destroy + ListSuppression |
| CAN-SPAM — truthful headers + From line | Yes | List + Campaign from_name / from_email validated against verified sending domain |
| GDPR Art. 6 — lawful basis (consent) | Yes Captured per subscriber | list_subscribers.consent_method + consent_text + consent_source_url |
| GDPR Art. 7 — proof of consent | Yes Timestamp + IP + UA + agreement snapshot | Same row, immutable on signup |
| GDPR Art. 17 — right to be forgotten | Yes One-click erase | POST /lists/{id}/subscribers/{id}/gdpr-erase |
| GDPR Art. 20 — data portability | Yes JSON export | GET /lists/{id}/subscribers/{id}/gdpr-export |
| GDPR Art. 30 — records of processing | Yes Activity log per event | ActivityLog rows for create / confirm / unsub / erase |
| CASL — express consent + identification | Yes | Same consent capture + footer enforcement |
Gmail / Yahoo — List-Unsubscribe | Yes Every send | SendSubscriberAction header block |
Gmail / Yahoo — List-Unsubscribe-Post: One-Click | Yes Every send | Same — paired with the POST endpoint at /p/u/{campaign}/{subscriber} |
| Gmail / Yahoo — SPF + DKIM + DMARC alignment | Yes Per-domain | Mail setup → Domain authentication |
| Bounce + complaint feedback loop | Yes Automatic | BouncesProcessCommand + 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:
- Truthful header information — From, To, Reply-To, Routing must identify the sender. KodMail blocks campaigns whose
from_emaildomain isn't on the customer's verified sending domain list. - Honest subject line — no deceptive copy. The pre-flight detector flags subjects >60 chars + flags excessive caps / punctuation as warnings.
- 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.
- 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. - 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:
ip_address+user_agent— who and whereconsent_source_url— the exact URL the user opted in fromconsent_method— how (form / API / import / admin)consent_text— snapshot of the text they acceptedconfirmation_sent_at+confirmed_at— proves they double-opted-in if the list requires it
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:
- Wraps in a DB transaction
- Deletes custom field values, open + click tracking rows
- Nulls
ip_address,user_agent, every consent column, the confirmation token - Replaces the email with
erased+<sha256>@gdpr.local— row stays queryable, PII is gone - Stamps
erased_at+ flips status toblacklisted - Plants a
ListSuppressionwith the original email so future imports / re-signups for that person hit the wall - 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:
- The full subscriber profile
- Consent record (IP, UA, method, text, timing)
- Every custom field value
- Every delivery, open, and click tied to this subscriber
- Every automation run
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:
- Encrypted storage for delivery server credentials + AI API keys (
encryptedSetting type) - Bcrypt-hashed customer + admin passwords
- 2FA (TOTP) on both auth guards
- CSRF protection on every form (Laravel default)
- Per-customer query scoping on every controller method (data isolation in multi-tenant mode)
- Upload-content scanner that refuses executable signatures even when extensions are whitelisted (see Configuration → Uploads & security)
CASL (Canada)
Canada's Anti-Spam Legislation is stricter than CAN-SPAM in two ways:
- Express consent required — implied consent is allowed but narrowly. Best practice: turn on
opt_in_mode = doubleper list so every signup proves the recipient owns the inbox. - Sender identification — same physical-address rule as CAN-SPAM. The pre-flight detector enforces it.
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:
| Rule | What it means | How 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:
- Anti-bot — fake addresses can't confirm themselves.
- Lower complaint rate — confirmed addresses are people who actually want your mail.
- 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:
unsubscribe— recipient clicked unsubscribebounce— ESP returned a hard bouncecomplaint— ESP feedback-loop complaintmanual— admin added via UIimport— bulk-imported address known badgdpr_erased— planted by the erase endpoint
Documents the admin still owns
The app does the technical heavy lifting. These four artefacts still live with the operator:
- Privacy Policy — publish via Admin → Pages. KodMail links the page from every signup form footer.
- Terms of Service — same publishing flow.
- Data Processing Agreement (DPA) — required by B2B customers under GDPR Art. 28. Template wording in the next section.
- Records of Processing Activities (ROPA) — internal document, GDPR Art. 30. Template wording below.
DPA boilerplate (B2B operator)
ROPA template
Internal doc. One row per processing activity. Minimum columns:
| Field | Example for KodMail |
|---|---|
| Purpose | Send subscribers commercial email they consented to receive |
| Categories of data subjects | Newsletter subscribers, customers, leads |
| Categories of personal data | Email address, name (optional), IP at signup, engagement metrics |
| Recipients | Sending ESP (SES / SendGrid / Postmark / Mailgun / etc.) — listed in Admin → Settings → Sending |
| International transfers | Depends on ESP location — document per relay |
| Retention | Indefinite while consent stands; erased on Art. 17 request |
| Security measures | TLS-only transit, encrypted credential storage, 2FA, audit logging — see GDPR Art. 32 section above |
Pre-flight checklist before submitting to CodeCanyon
- Default
opt_in_mode=doubleon the seed list templates - Privacy Policy + Terms pages published
- List-level company address filled in (CAN-SPAM)
- At least one sending domain authenticated with DKIM (Gmail/Yahoo rule)
- Test campaign sent to a Gmail + Yahoo address — confirm both providers show the "unsubscribe" link in their UI
- Run the pre-flight detector on the demo campaign — should pass with zero errors
- Fire the GDPR export + erase flow on a demo subscriber to confirm both endpoints return clean JSON / suppress correctly