The send pipeline (publisher subject/body encryption, the S3 emsg blob,
and the worker-side decrypt) now keys off models.SendEmail.OrgID
instead of UserID. EmailMessage loses its UserID field entirely:
emailSender.Send derives the cipher identity from the email account's
OrganizationID and refuses to send for an account without one.
This also fixes two latent bugs. Emails sent through user_email_task
never set UserID, so they were silently encrypted under the zero-UUID
platform key. campaign_task's discarded Encrypt() pair is replaced with
an explicit DEK warm that fails fast when KMS is unavailable.
Support chained automation execution from native actions, guard automation recursion depth, carry idempotency context through campaign-launched automations, allow team task assignment in backend action execution, and reject deletes while campaign steps still reference an automation.
Restructure the docs navigation into product-focused pages, add automation expression reference content, and refresh related template and personalization surfaces.
Adds the shared template helper function map, send-path spintax expansion, server-side template preview validation, and the campaign template preview endpoint.
Resolve campaign senders as the union of explicit accounts and tag-selected mailboxes, with all active mailboxes as the empty-selection fallback. Update readiness checks, active-campaign counts, and rotation cursor handling for the unified pool.
Carry attachment references inside the stored email body blob, resolve bytes on workers, and encode attachments for Gmail API and SMTP sends without changing the Kafka send-email contract.
Scope campaign A/B variants to individual sequence steps and select step variants deterministically per contact and step. Keep campaign-level variants on the existing assignment flow.
Add OpenAI Batch API support for warmup content generation, including job metadata, polling, cancellation, and completed-batch ingestion.
Share the generation prompt between sync and batch modes and humanize/lint generated threads before storing them in the warmup content library.
Persist outbound warmup Message-IDs so later warmup replies can find thread parents.
Normalize reply headers and re-check partner health during recipient selection before using a mailbox as a warmup recipient.
Record warmup spam placements by recipient provider and surface provider breakdowns in the admin health summary.
Track warmup replies and use stable static conversation ids so warmup analytics can correlate content reliably.
Add warmup content generation and admin review surfaces, plus mailbox warmup appeal/status APIs.
Track warmup engagement and tampering signals so unsafe mailboxes can be handled by the warmup flow.
Make the warmup task reconciler re-check account state and org warmup access before scheduling replacement tasks. Remove pool membership for accounts whose org can no longer use warmup to avoid repeated no-access task churn.
Add explicit warmup participant roles so connected accounts can provide recipient-only pool capacity without becoming senders. Cap warmup scheduling by eligible recipients, avoid same-day recipient reuse, reschedule exhausted pools, and reconcile missing warmup task chains.
Limit organizations to 50 active campaigns before starting another campaign, make campaign wakeups idempotent per campaign, keep warmup wakeups idempotent per mailbox, and no-op stale non-pending campaign/warmup dispatches.
new warmup_routing_rules table + repo + REST endpoints under /warmup/
routing. each rule matches a (sender, recipient) pair by domain, TLD,
provider bucket, or any wildcard, with a weight multiplier on the
selector. weight > 1 prefers the pairing, < 1 discourages, 0 excludes.
rules are evaluated in priority order (ascending) and combined with the
existing domain-diversity weighting. example use case: a customer can
say 'send Gmail-recipient warmup only from Google-classified senders'
with one rule; or 'never send to acme.com from this org's mailboxes'
with weight=0. premium pool only — free pool ignores rules.
the two cipher.Encrypt calls on subject and body in HandleEmailTask
discarded their ciphertext and the plaintext was what got sent. warmup
mail is not stored at rest, so the only effect was warming the user's
DEK in cache via the preceding Cipher() call — also removed since
nothing else in the warmup path needs it.
rename outbound warmup verification header from X-Warmbly-Token to a
generic X-Mailtrace-Verify and accept both on receive during rollout.
add slot-based subject synthesis that fires ~40% of the time, yielding
thousands of unique strings on top of the static corpus so vendors
cannot trivially cluster on a fixed subject list.
extend recent-partner exclusion from 24h to 72h, weight selection by
inverse-frequency of recipient domain over the last 7 days so a single
provider does not absorb the bulk of warmup traffic, and log a warning
when a pool drops below the diversity threshold.
persist conversation_theme on the warmup_token at send time, and look
it back up on reply so the body comes from the same topical bucket.
previously the reply picked a random conversation, so 'Re: time-
blocking question' could come back as a paragraph about travel.
Increase warmup subjects from 5 to 20 and conversations from 3 to 12
to reduce repetition patterns that spam filters can detect. New themes
cover industry, tools, networking, feedback, planning, reading, travel,
wellness, and events.
- Replace all fmt.Printf calls in Kafka consumer/producer, tracking consumer,
and user email task with structured zerolog (log.Warn/Error/Info)
- Fix hashURL using SHA-256 instead of naive first-8-chars+length approach
which was collision-prone for deduplication