Matthew Meszaros
222c9d2554
feat: scope campaign sender resolution to the campaign's organization instead of its owner so a multi-org user can no longer send organization A's campaign from an organization B mailbox: GetByTags/GetAllActiveInScope/GetByCampaignSenders now take a repository.AccountScope keyed on organization_id where a scope with no organization resolves to no mailboxes rather than widening to the owner (tags carry no organization of their own, so one user's tag legitimately spans workspaces and the predicate is the only thing holding the boundary), the campaign scheduler and the preflight tracking-domain check build that scope from campaign.OrganizationID, unibox compose scores only the current workspace's mailboxes, the 'all' lanes of AccountHasActiveCampaign/CountActiveCampaignsForAccount join ea.organization_id = c.organization_id instead of ea.user_id = c.user_id, dead and broken PauseAllByUserID (which wrote the reason string into status and had no callers) is removed, the campaigns/unibox guides now state that senders resolve inside the campaign's workspace, and TestLiveOrglessCampaignDoesNotSendToSuppressedRecipient is updated because an orgless campaign now finds no senders before routing is consulted while the send gate it covers still refuses; live-tested in TestLiveSenderResolutionStaysInsideTheCampaignOrg, TestLiveSenderSchedulerNeverPicksAnotherOrgMailbox, TestLiveSenderSchedulerPicksTheCampaignOrgMailbox, TestLiveSenderScopeWithoutAnOrganizationReachesNothing and TestLiveActiveCampaignLookupIsOrgScoped
2026-08-24 09:20:41 -07:00
Matthew Meszaros
15e139e15d
feat: stop a campaign email going out twice when the progress write after dispatch is lost: a step is now RESERVED before its SEND_EMAIL reaches the bus (migration 000093 adds campaign_contact_progress.dispatched_at + dispatch_task_id, and ReserveSend takes the claim and the day's counters in one transaction) and routing treats a step as attempted on sent_at OR dispatched_at, so a crash or a failed stamp in the dispatch window can no longer read as "never sent" and email the same person again; the ON CONFLICT claim is exactly-once so two ticks racing the same pair cannot both send (the loser ends skipped_duplicate), the stamp is retried and escalated to the campaign feed instead of warned and swallowed, HandleEmailSent repairs a lost stamp from the worker's own confirmation, ReleaseSend gives a reservation back only when the command provably never left (a publish failure is ambiguous via ErrSendDispatchUnknown and keeps it), and StartStuckSendReclaimer walks back a reservation nobody answered after 30 minutes so a worker that died mid-send cannot park a lead in flight forever; live-tested in TestLiveLostProgressWriteDoesNotResend, TestLiveDispatchedSendIsNeverOfferedTwice, TestLiveConcurrentTicksSendOnce, TestLiveStuckDispatchIsReclaimed, TestLiveReclaimBelievesADeliveredSend and TestLiveInFlightSendIsNotOfferedAgain
2026-08-24 09:15:06 -07:00
Matthew Meszaros
6c17f109fd
feat: stop one waiting lead from parking a whole campaign: FindNextRoutedPair now returns only a DUE pair (new leads now, follow-ups at last sent + wait_after days, plus a wait node's minutes) and skips not-yet-due contacts so other leads' first emails and due follow-ups keep sending, handing back the soonest due moment when nothing is due so the scheduler defers exactly until then (min'd with the next-day new-lead-cap deferral); drop send-time optimization from the successor wakeup, which by default pushed the next lead to 09:00 UTC tomorrow after any send past 17:00 UTC (and to a past time when the hour matched, defeating pacing); document that send_time_optimization is stored but not applied and that waits are per contact; live-tested in TestLiveWaitingFollowUpDoesNotBlockOtherLeads and TestLiveWaitNodeGatesTheStepAfterIt
2026-08-24 07:49:54 -07:00
Matthew Meszaros
c3066f9cc9
feat: unbox campaign start dates and make follow-up pacing real: accept today as "start now" and let an explicit null clear start/end dates on PATCH /campaigns (models.NullableTime distinguishes absent from null, which used to silently no-op while the error message told users to send null), reschedule an active campaign's parked wakeup when any schedule field changes so clearing a future start date takes effect immediately instead of at the old slot, let a completed campaign be started again and turn the past-end-date start 500 into a clear 400, gate the campaign task on the step's hard-constraint floor (wait_after, start date, windows, day capacity, mailbox min-gap) via ErrCampaignDeferred so an early successor tick can no longer send a wait-3-days follow-up seconds after step one (live-tested in TestLiveFollowUpWaitIsHonored), disable past days in the schedule date picker, and fix the sandbox seed leaving worker 1a01 free-tier after make seed which unassigned the paid org's mailboxes and failed every send
2026-08-24 05:20:26 -07:00
Matthew Meszaros
3739a36b67
feat: enforce the persisted SPF/DKIM/DMARC state as a real cold-send and warmup gate behind a 72h grace clock and an operator toggle, after first fixing the DMARC organizational-domain fallback in dnsauth so a dedicated sending subdomain covered by its parent's record stops reading as unauthenticated, adding auth_state to the four mailbox loaders that never selected it (which would have made the gate dead code), stamping auth_failing_since on entry to failing so a resolver hiccup can never stop a campaign, notifying the org on that transition, and reporting an all-gated pool as ErrDomainAuthFailing instead of a message about sending windows ( #160 )
2026-08-22 09:37:26 -07:00
Matthew Meszaros
fe9a21a79f
feat: stop a freshly connected mailbox being silently excluded from every campaign send, by making an unset mailbox timezone representable as the empty string the campaign scheduler already checks for, since email_accounts.timezone defaulted to 'UTC' while campaigns.timezone defaults to 'Europe/London' and nothing in the OAuth or SMTP onboarding paths ever set either, so a brand new mailbox looked deliberately placed in UTC, was compared against the differing campaign zone and dropped by the hardcoded 8am-8pm business-hours gate whenever the current UTC hour fell outside it, emptying the candidate pool and failing the campaign start, adding a migration that changes the column default and converts existing 'UTC' rows because until now no API field, dashboard control or onboarding path could set that column at all so every such row is the old default rather than a choice, adding the missing Timezone field to UpdateEmail with IANA validation so the setting the sending-behaviour UI already tells people to change is finally reachable and an unloadable zone is rejected instead of being silently coerced to UTC by the scheduler, and replacing the misleading 'no active email accounts found for campaign's email tags' response for a pool that exists but is entirely gated out with a distinct message naming the real cause, via an ErrNoEligibleMailbox that wraps ErrNoEmailAccounts so the three callers that pause a campaign on it are unaffected ( #126 )
2026-08-16 07:46:37 +02:00
Matthew Meszaros
8f465fdb1c
feat: give each mailbox a human sending persona (randomized daily and hourly caps, send spacing, work start/end, lunch break and working weekdays, rolled once per local day in the mailbox's own timezone and applied across the campaign, warmup and smart-send schedulers), add campaign auto-pause guardrails that stop a campaign when its bounce, complaint or reply rate leaves the configured band, make mailbox rotation actually rotate for tag-resolved and all-mailbox campaigns, stop every scheduler from ever returning a slot in the past, and correct the mailbox min-gap field that stored seconds while labelling them minutes
2026-08-13 16:51:29 +02:00
Matthew Meszaros
cd5e1e9cf7
Merge pull request #72 from warmbly/research/warmup-abuse-cold-effectiveness
...
feat: deliverability research + observe-only SPF/DKIM/DMARC auth state and watch-band cold throttle
2026-07-17 05:29:58 +02:00
Matthew Meszaros
e5a6643e2f
feat: apply the warmup watch-band volume multiplier (0.7x) to cold campaign sends via the shared adjustmentFor helper so a watch-state mailbox slows cold volume before it hard-quarantines
2026-07-17 03:21:29 +00:00
Matthew Meszaros
ad1d40fd2b
test: cover the gmail/graph message mapping (read-state inversion, spam/category labels, unpadded base64url bodies, warmup and classification header surfacing), the imap header-flag parser, the graph MIME builder, and the human-behavior timing (sub-minute randomisation, daily volume factor, night-deferred and heavy-tailed engagement)
2026-07-04 11:45:01 +02:00
Matthew Meszaros
4ea9611510
feat: vary each mailbox's daily warmup volume by a stable per-day factor with occasional lighter days so a mailbox never sends an identical count every day
2026-07-04 11:36:47 +02:00
Matthew Meszaros
2a76428ecf
feat: randomise the sub-minute second of every scheduled send, drop the campaign 5-minute grid rounding, and vary send spacing multiplicatively into bursts and lulls so fleet timing has no metronome or round-mark fingerprint
2026-07-04 11:27:36 +02:00
Matthew Meszaros
c4c27fcc19
feat: add campaign lead ordering backend
...
Add campaign lead ordering repository methods for manual send order and tighten strict ESP matching so SMTP/IMAP is only a fallback outside strict mode.
2026-06-05 15:42:12 +02:00
Matthew Meszaros
77de336d78
feat: unify campaign sender pools
...
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.
2026-06-05 14:35:52 +02:00
Matthew Meszaros
0e26b9cd00
feat: refine campaign branch scheduling
...
Evaluate branch conditions relative to the previous send, defer while windows are still undecided, and let the routed pair finder handle deleted targets and loops at schedule time.
2026-06-05 09:10:57 +02:00
Matthew Meszaros
2a2e248ad9
feat: add campaign sequence branching backend
...
Store per-step branching conditions, validate branch targets, and select campaign contact steps by following the flow graph. Preserve linear progression for steps without branch conditions.
2026-06-05 06:02:55 +02:00
Matthew Meszaros
5b9029d2a8
feat: add campaign schedule windows backend
...
Store per-day schedule windows on campaigns and validate updates before persistence. Use the schedule windows in campaign scheduling, including legacy day/time derivation for existing campaigns.
2026-06-04 18:56:43 +02:00
Matthew Meszaros
e1e02c5c18
feat: add campaign send controls backend
2026-06-04 08:53:23 +02:00
Matthew Meszaros
1afe453eb0
feat: add deliverability controls
...
Add seed inbox-placement testing with admin management, placement result polling, and seed mailbox persistence.
Add pre-send email verification, invalid-recipient skipping, warmup-health campaign gating, and RFC 8058 one-click unsubscribe headers/endpoints.
2026-06-03 16:47:53 +02:00
Matthew Meszaros
a04d7450bc
feat: guard warmup reconciliation by access
...
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.
2026-06-01 11:19:44 +02:00
Matthew Meszaros
4198564f12
feat: cap warmup for active campaign senders
...
Limit warmup volume to 5 messages per day when a mailbox is part of an active campaign sender pool. This keeps warmup as a low-volume reputation heartbeat while campaign traffic is already active.
2026-06-01 09:15:51 +02:00
Matthew Meszaros
0cb53a25a6
feat: improve warmup pool scheduling
...
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.
2026-05-31 18:07:13 +02:00
Matthew Meszaros
d5147659a7
style: gofmt struct alignment after burst_multiplier removal
...
removing the BurstMultiplier field changed column widths in several
struct literals; let gofmt realign them. no semantic change.
2026-05-27 16:17:53 +00:00
Matthew Meszaros
92e4dd3a69
feat: throttled/watch health state shapes warmup volume and spacing
...
CalculateNextWarmupTime now consults the participant's health state and
scales target_volume + min_wait_time down for throttled (0.5x volume,
2x spacing) and watch (0.7x volume, 1.5x spacing). previously the
state was set by the sweep but never read by the scheduler, so
'throttled' was a label without behavior.
2026-05-25 15:15:37 +00:00
Matthew Meszaros
d227038ca0
ci: drop unused/unconvert/gosimple + shadow/nilness, run gofmt
...
Disable the linters that fire on legacy code without flagging real
bugs: `unused` (orphan repos kept for future feature flags),
`unconvert` (defensive type conversions), `gosimple` (style
suggestions in code we don't want to touch).
govet: disable `shadow` (idiomatic `err :=` re-decls in transaction
patterns) and `nilness` (legitimate defensive nil checks that look
tautological to the analyzer).
Ran `gofmt -w internal/ cmd/` — every Go file now passes
gofmt -l with no output.
Kept: govet, staticcheck, ineffassign, typecheck, bodyclose, noctx,
sqlclosecheck, gofmt, goimports, misspell — the real-bug checks.
2026-05-23 16:54:12 +00:00
Matthew Meszaros
0799020dac
feat: add metrics, warmup content variety, tz-aware scheduling, org budget, admin stubs, and bug fixes
2026-04-09 14:33:45 +00:00
Matthew Meszaros
19d20e72b4
feat: add warmup health throttled state, complaint/bounce metrics, A/B analysis, and rate limiting
2026-04-09 12:51:30 +00:00
Matthew Meszaros
3edf6b1d22
refactor: remove redundant min/max helpers shadowing Go builtins
...
Go 1.21+ provides builtin min/max for all ordered types. Remove the
custom int helpers and replace math.Max with builtin max for floats.
2026-04-09 12:06:25 +00:00
Máté Mészáros (Laptop)
ed35ab2dbc
Realtime Updates
2026-01-30 15:32:58 +01:00
Máté Mészáros (Laptop)
41624a6f79
Analytics & Tracking
2026-01-29 05:59:04 +01:00
Máté Mészáros (Laptop)
81d5970ea8
Realtime, Task Handler, Worker & Consumer Setup
2026-01-26 04:42:19 +01:00