Matthew Meszaros
b2ea1f1961
feat: add self-hosted update awareness and one-click updates: every binary is stamped with its version and commit, the backend polls GitHub Releases and a new host-side updater (cmd/updater, compose profile or systemd unit) reports the checkout's commit distance, the admin panel's top bar shows a version pill that turns into an update indicator and opens a dialog with confirmation, live step progress and log, restart tracking and result, the dashboard header shows the same pill to every member of a self-hosted instance with the full update flow for platform admins, Setup and health gains update_available and updater_unreachable checks, warmblyctl status prints the version, make upgrade and scripts/upgrade-bare-metal.sh cover the by-hand paths, and docs gain an Updates page plus configuration, health, deployment and API reference updates
2026-09-03 05:04:30 -07:00
Matthew Meszaros
64f2d1637b
feat: address the review on the unsubscribe PR: register the suppression family in warmblyctl, require the confirm field on the browser unsubscribe POST and cap its body, render a chosen A/B variant through the template engine so its merge fields and unsubscribe link resolve, fold curly apostrophes before opt-out phrase matching, write pasted suppression lists in one transaction, clamp copy by runes instead of bytes, add the constraints NOT VALID plus a lower(email) index in migration 000123, scope the unsubscribe link type-ahead to email bodies, and document DELETE /suppressions/:id
2026-09-03 02:20:47 -07:00
Matthew Meszaros
9485503e03
feat: merge main into the unsubscribe branch, keeping the new campaigns.kind column alongside unsubscribe_mode in every campaign scanner and renumbering the opt-out migration to 000123 because main took 000122 for campaign kind
2026-09-03 01:56:38 -07:00
Matthew Meszaros
5eb92c601c
feat: give every campaign email a working opt-out: a reply-to-opt-out line by default or an unsubscribe link (workspace setting under Settings > Sending with a per-campaign override and a {{.UnsubscribeLink}} variable), signed per-recipient unsubscribe links served on the API origin so the List-Unsubscribe header no longer points at a dead warmbly.com page, a confirm page on GET with RFC 8058 one-click on POST and a resubscribe button, reply opt-out detection through the whole-word compliance lexicon with quoted history stripped, a first-class suppression list (Contacts tab, GET/POST/DELETE /suppressions with address and domain entries, audited removal, contact drawer action), the contact Subscribed flag enforced in campaign routing, migration 000122 with a shared recipient_suppressed() predicate, and docs for all of it
2026-09-03 01:52:40 -07:00
Matthew Meszaros
6b1ddd8bb8
feat: address the CodeRabbit review on the one-time email preset: rate-limit POST /campaigns-estimate as a read, count a mailbox whose sent-today counter fails as having nothing left today instead of untouched, lock the campaign row FOR UPDATE before counting email steps so concurrent inserts cannot give a one-time campaign two messages, keep a plain-text test email free of the HTML signature, and make the wizard's estimate panel say an audience beyond the two-year horizon cannot be projected instead of reading the null as one sending day
2026-09-03 01:42:00 -07:00
Matthew Meszaros
24b7e26fbb
feat: add the one-time email campaign preset from #288 : campaigns.kind column with sequence and one_time values and a kind field on POST /campaigns, GET /campaigns?kind= and an one_time count on /campaigns-overview, a POST /campaigns-estimate endpoint projecting deduplicated segment recipients against the mailbox pool's per-day capacity and finish date, a wizard type picker whose one-time flow runs Basics, Email, Audience, Sending and Send with send-now or scheduled start and the estimate panel and then creates, links segments and starts the campaign, a guard refusing a second email step on a one-time campaign, draft/scheduled/sending/sent wording with a One-time badge and Type filter in the campaigns list, kind on the AI create_campaign_draft tool, honouring text_only in the send and test-email paths so plain-text campaigns ship no HTML part or tracking, and the campaigns, segments, API reference and endpoint docs
2026-09-03 01:42:00 -07:00
Matthew Meszaros
3601147c7c
feat: address review on the avatar persistence fix: avatar object keys carry a random nonce next to the millisecond epoch so two uploads in the same millisecond cannot share an immutably cached URL, the org avatar hooks capture the target workspace id when the mutation starts and patch the org pointer, list and current-org caches only for that id so a mid-flight workspace switch cannot stamp the avatar onto the wrong org, and the API docs describe the stored file deletion on remove as best effort
2026-09-03 01:15:10 -07:00
Matthew Meszaros
f579497c81
feat: make profile and workspace avatar changes survive a refresh: user avatar upload and remove go through the user service so the cached /auth/me copy is dropped instead of serving the old avatar for 30 minutes, the org list mapper carries avatar_url into the persisted workspace pointer it previously discarded, the avatar mutation hooks patch the cached user, org pointer and org queries with the server's answer before invalidating, all four handlers delete the replaced or removed object under our own avatars/ prefix with millisecond keys so quick replacements cannot collide on an immutable URL, the org avatar removal is audited, the uploader copy stops promising WebP and GIF, and the API docs note that remove deletes the stored file ( fixes #295 )
2026-09-03 01:06:36 -07:00
Matthew Meszaros
290f1f00aa
Merge remote-tracking branch 'origin/main' into feature/public-forms
...
# Conflicts:
# cmd/backend/main.go
# docs/content/docs/guides/workspace-export-import.mdx
2026-09-01 09:29:53 -07:00
Matthew Meszaros
05d74dbe9f
feat: forms v2, a full redesign of the hosted form and its builder: eight one-click themes, card/wide/split layouts with a brand cover panel, classic paged and Typeform-style focus modes split on a new page_break block, logo/cover/background uploads with size, fit and veil controls, an optional header bar that can span the page or sit with the form, a real HSV colour picker and font preview, per-contact personalized links that prefill and attribute without email verification, a render-token gate so the form JSON cannot be scraped without loading the page, funnel analytics with per-page drop-off and identified visitors, a leads-style forms list and responses table, and an organization custom forms domain verified by CNAME with hourly re-checks
2026-09-01 09:27:28 -07:00
Matthew Meszaros
128e314103
Merge remote-tracking branch 'origin/main' into fix/issue-277
2026-09-01 04:03:37 -07:00
Matthew Meszaros
d6ddf1f170
feat: fix implicit-TLS SMTP on 465 and IMAP STARTTLS on 143 behind a stored per-mailbox security mode that accepts any port, stop worker ID churn orphaning mailbox assignments via flock-claimed persistent worker ids, give the unibox a standard mail-folder sidebar (inbox/sent/drafts/archive/spam/trash) backed by a provider-derived folder column, and expose the AI tool registry over REST for non-MCP function-calling agents ( #283 )
2026-09-01 03:53:19 -07:00
Matthew Meszaros
5e87b1bbbf
Merge remote-tracking branch 'origin/main' into feature/public-forms
2026-09-01 01:17:54 -07:00
Matthew Meszaros
60c9e316d9
feat: hosted lead-capture forms end to end: drag-and-drop builder with field settings, design panel, embed/share and submissions tabs in the dashboard, a public TanStack form app (forms/) served by the new standalone forms service (cmd/forms + internal/formserver) on FORMS_DOMAIN with per-form frame-ancestors CSP, honeypot/fill-time/Turnstile/per-IP submit protection and a same-origin JSON API proxying the backend internal API, form submissions creating contacts with categories and campaign enrollment plus realtime, audit, webhook and org-transfer coverage, migration 000114, seed forms, CI jobs, Dockerfile, systemd/nginx/compose manifests and docs
2026-09-01 01:17:51 -07:00
Matthew Meszaros
8db032a656
feat: address review on segment-campaign linking: a campaign_lead_removals table (migration 000115, orgtransfer-registered) records hand-removed leads so the automatic segment sync never re-adds them while a manual add or the one-shot enrol clears the record, PUT /campaigns/:id/segments rejects an omitted segment_ids so {} cannot detach everything, rows.Err checks on the four new link queries so a truncated read cannot commit as success, write-path enrolment syncs detached from the request goroutine with a per-org in-flight dedupe, per-campaign 30s deadlines in the sweep instead of one shared budget, and an error-with-retry state in the linked-segments dialog
2026-09-01 00:33:42 -07:00
Matthew Meszaros
66a105fb9f
feat: make segments a live campaign audience and round out lead management (issue #277 ): a campaign_segments link table with GET/PUT /campaigns/:id/segments (max 20, replace semantics), immediate enrolment of linked-segment members as leads plus targeted re-syncs on contact writes, segment edits, pin-ins and imports and a 2-minute backend sweep for drift, waking active campaigns and restarting completed ones through the full launch checks; a Segments manager dialog and toolbar button on the campaign Leads tab; Remove from campaign as the row action and a selection-bar bulk action so leads can leave a campaign without deleting the contact; campaign and segment pickers in the CSV import wizard backed by a new segment_ids import option that pins imported rows as include overrides; a delete guard naming linked campaigns; orgtransfer registration, live repo tests and docs for all of it
2026-08-31 08:45:27 -07:00
Matthew Meszaros
896fc60f4d
feat: add the missing mailbox re-verification flow (issue #274 ): POST /emails/onboarding/oauth/reauth/:id re-runs the provider consent for an existing Gmail/Outlook mailbox (login_hint preselect, same-address check, token rewrite keeping the stored refresh token) and PUT /emails/onboarding/smtp-imap/:id replaces SMTP/IMAP credentials after live validation; both resolve the credential-class error rows and reactivate the mailbox onto its worker, the drawer's Needs attention banner gains Re-authorize and Update credentials buttons, and the mailboxes guide documents reconnecting
2026-08-31 05:29:35 -07:00
Matthew Meszaros
7a7c6051bc
Merge remote-tracking branch 'origin/main' into feature/contact-segments
2026-08-30 02:07:56 -07:00
Matthew Meszaros
14a21bf9bd
feat: expose the segments API (list, fields, preview, create, get, patch, delete, members, member lookup, overrides, add-to-campaign) and GET /contacts/:id/segments behind contact permissions, wire the service into the backend and audit each mutation on the realtime spine
2026-08-29 23:45:05 -07:00
Matthew Meszaros
a02ff7c936
feat: address verification overhaul for #264 : MillionVerifier as a pay-as-you-go integration plugin with org-sealed key and automatic fallback to the built-in check, built-in prober gains domain cache, Microsoft/Yahoo fingerprinting, MX fallback, disposable/role sub-statuses and a self-check breaker, imports and POST /contacts accept verification results in any known provider vocabulary with auto-detected columns, verdict provenance and expiry columns (migration 000110), campaigns park at paused_undeliverable with re-verify/send-anyway instead of finishing, POST/GET /contacts/verification bulk actions and overview, launch gate override via acknowledge_list_risk, animated verification marks, banner and settings card in the dashboard, and docs
2026-08-29 23:11:22 -07:00
Matthew Meszaros
44b2c18906
feat: address review on cloud-managed mailboxes: the consumer now asks the cloud to vouch for a warmup token in a mailbox it warms (GET /pool-link/instance/mailboxes/:id/warmup-tokens/:token) and files anything unverified as ordinary mail instead of dropping on a sender-controlled header, disconnect keeps local mirrors and the link until the cloud confirms the instance is released so managed mailboxes cannot be stranded, and long narrative comments are cut to one line
2026-08-29 10:07:36 -07:00
Matthew Meszaros
2a831e9783
feat: let a linked self-hosted instance sign Google and Microsoft mailboxes in through Warmbly Cloud's own OAuth apps and send with cloud-brokered access tokens: the cloud runs the consent (pool_link_mailboxes.managed, brokered state in Redis, the existing /addresses/*/callback completes it and redirects to the instance's /cloud-oauth/done), keeps the refresh grant, mints short-lived tokens at /pool-link/instance/mailboxes/:id/token and refuses them for revoked links, removed, inactive or blocked mailboxes; the instance mirrors such mailboxes without a credential (cloud_link_mailboxes.managed), ships them to the worker as brokered so goog/msgraph init on a token source that pulls from /api/v1/internal/cloud-link/token/:id, lets the consumer ignore cloud warmup tokens for enrolled mailboxes, and can adopt mailboxes connected directly on the workspace; Add account shows the cloud path and the adoptable list, and the Warmbly Cloud guide documents the model
2026-08-29 09:50:52 -07:00
Matthew Meszaros
d621d9b79b
feat: sign a new account in as part of registration: createAccount returns the user, both the verification-free and the code-confirm paths mint a session through finishLoginAs (2FA challenge included), /auth/register/confirm answers with that session instead of 204, and the dashboard drops into the workspace right after signing up instead of bouncing to the sign-in form
2026-08-29 08:41:34 -07:00
Matthew Meszaros
b053a1ca80
feat: surface the Warmbly Cloud link where self-hosters already are: a skippable last onboarding step with the shared link card, a connect banner and dialog on the Mailboxes page, a Cloud badge plus the cloud's daily count and warm/pause/resume/remove items in each row's warmup menu, a cloud block in the mailbox drawer that replaces the local controls for enrolled mailboxes, the coverage notice pointing at the dialog instead of the marketing site, reads on /cloud-link open to any member and per-mailbox enrollment gated on manage mailboxes, and the guide updated for the new entry points
2026-08-29 08:05:11 -07:00
Matthew Meszaros
3bdb0fb82d
feat: address the Greptile review on the pool link: require https for the cloud URL (loopback exempt for local development) since the instance token and mailbox passwords travel on it, remove the cloud copy when the local enrollment row cannot be written so a mailbox never warms in both places, delete the local enrollment row before the cloud one and restore it if the cloud call fails so a mailbox is never left with no warmup anywhere, and trim the new multi-line comments to the one-line style
2026-08-29 07:17:10 -07:00
Matthew Meszaros
b75fdcf86c
Merge remote-tracking branch 'origin/main' into feature/self-hosted-warmup-pool-access
...
# Conflicts:
# internal/scheduler/warmup_scheduler.go
2026-08-29 07:10:38 -07:00
Matthew Meszaros
37b60b59d3
feat: let a self-hosted instance warm its mailboxes in the hosted pool: device-code link approved at /connect, instance-token API that enrolls SMTP/IMAP mailboxes as warmup-only accounts (no history import, non-warmup mail dropped), free for 10 mailboxes and unlimited on the seeded $15 pool plan, tier fallback to proven healthy mailboxes when a pool runs thin, local warmup stands down for enrolled mailboxes, Settings > Warmbly Cloud step flow and linked-instances page, docs guide, marketing copy, and fix SetWarmupLifecycle re-reading the row with an org-scoped lookup so every warmup start/pause returned 404
2026-08-29 07:09:04 -07:00
Matthew Meszaros
5821fc2a1c
feat: merge origin/main into the website tracking branch after PR #260 landed 000106_contact_source, renumber the website tracking migration to 000107, and fold the page_hit timeline type, its query source and its Website filter, meta line and expandable details into the reworked contact Activity tab and models from the contact-activity PR
2026-08-29 04:38:35 -07:00
Matthew Meszaros
8daefbe8c4
feat: address the Greptile review on website tracking by believing forwarded-IP headers only from TRACKING_TRUSTED_PROXIES (socket peer otherwise, proxy-appended last X-Forwarded-For entry, applied to pixel, click and page-hit paths), making IdentifyVisitor report whether it claimed the row so a lost identification race re-reads the visitor and splits onto a fresh record instead of attributing the hit to the wrong contact, forgetting the edge dedupe entry when a forward to the backend fails so the retry is counted, and trimming the new Go and Rust comments to the one-line style
2026-08-29 04:16:28 -07:00
Matthew Meszaros
896dc4fe3c
feat: merge origin/main (campaign lead engagement filters, contact export org scoping) into the issue #255 branch, keeping both the engagement and the next-action sections of the campaigns guide
2026-08-29 03:40:13 -07:00
Matthew Meszaros
7d33544540
feat: pass the organization id, not the user id, from ExportContacts through contactService.Export into contactRepository.ExportAll, because contact Search became organization-scoped with the issue #187 fix and every contact export since then returned only the header row, which also left the new campaign lead_* export columns with nothing to fill (issue #250 )
2026-08-29 03:30:59 -07:00
Matthew Meszaros
a4739b63ec
feat: add first-touch contact source attribution (migration 000106: contacts.source with a CHECK, source_detail, first_seen_at, existing rows stamped unknown) stamped at every creation site (dashboard manual/campaign, file import, Google Sheets sync, API key, AI assistant), write contact_created / campaign_added / campaign_removed / category_added / category_removed lifecycle events into contact_activities inside the same transactions as the links and merge them into the contact timeline, refactor FindNextRoutedPair's routing into a campaignRouter shared with a per-contact RouteContact and split CalculateNextCampaignTime into campaignSenders + placeCampaignSend so a read-only PreviewContactSend derives a contact's next step through the scheduler's own constraints, expose it as GET /contacts/:id/campaigns behind the contact service, render a per-campaign state panel, Campaigns/Lifecycle filter chips and expandable event rows in the Activity tab plus a Source section on Overview, cover it with TestLive* for the timeline events and the next-action preview (step wait, sending window, paused), and document it in the contacts, campaigns and API reference docs (issue #255 )
2026-08-29 03:28:16 -07:00
Matthew Meszaros
27630eec0a
feat: add website visitor tracking for issue #255 with migration 000106 (website_tracking_settings, website_visitors, website_page_hits, all registered in the orgtransfer spec), a consent-gated dependency-free tracking.js served by the Rust tracking service with a rate-limited, size-capped, prefetch-filtered POST /p ingest that forwards to a new backend internal page-hits endpoint for server-side user-agent and GeoIP enrichment, contact identification only through the click ticket the redirect appends to registered hosts, a per-workspace retention job, page_hit events with an expandable detail view in the contact Activity timeline, a Settings > Website tracking page for the snippet and consent, location and retention configuration, realtime PAGE_HIT fanout, and a website tracking guide plus endpoint, export and configuration docs
2026-08-29 03:25:50 -07:00
Matthew Meszaros
3552fffae1
feat: stop stranding a resting mailbox whose warmup stopped by letting lifecycle.Decide run the rest clock on an unknown health signal and resume the mailbox after the 72h window instead of re-stamping probation on every tick, make POST /emails/:id/release the manual exit from resting with its own reason, add a Put back into campaigns action and warmup-aware copy to the resting notice in the mailbox drawer, and document the automatic and manual exits in the mailboxes guide, API reference and endpoint map (issue #243 )
2026-08-29 02:04:18 -07:00
Matthew Meszaros
4cee178c7a
Merge remote-tracking branch 'origin/main' into fix/issue-244-reserve-hold
2026-08-28 23:33:26 -07:00
Matthew Meszaros
5c832461b0
feat: wire the unreachable reserve send-lifecycle state to a real per-mailbox hold (POST /emails/:id/hold and /release behind manage_emails and WRITE_EMAILS, SetSendHold on the email service forcing the lifecycle past the rebalancer guard, a Hold from campaigns toggle on the mailbox drawer's Overview tab with the reserve notice rewritten to point at it, warmblyctl mailbox hold/release, and docs in the mailboxes guide, API reference and scope map), and drop the warming state nothing ever set from the model, the web type, the docs table and the DB check via migration 000104, which folds any legacy warming row back to active
2026-08-28 22:47:31 -07:00
Matthew Meszaros
0ab947dc5d
Merge remote-tracking branch 'origin/main' into fix/issue-241
2026-08-28 22:47:28 -07:00
Matthew Meszaros
a5ea55bba5
feat: give a suspended workspace a way back, because risk_state was a one-way door: the derived band is no longer pinned at suspended by the UPDATE in pg_org_risk, an operator's decision is now an explicit risk_override that outranks the score and survives every later detector write until it is lifted, the one-shot detectors (signup origin, import list quality, repeated sign-in anomalies) file findings with a 30-day expiry that a six-hourly consumer sweep retires so a score falls on its own, migration 000104 backfills that expiry onto findings already on file, and four admin endpoints plus an Abuse posture panel in admin/ let an operator finally read the evidence the customer endpoint withholds, retract a finding, pin a band and lift the pin; also stops the reviewing admin's identity reaching the tenant's own audit feed (which resolves an actor to a name and email) by recording the platform as the actor there and the operator in the admin trail, and stops risk_signals riding along in a customer-downloadable org export
2026-08-28 22:42:57 -07:00
Matthew Meszaros
3d72c7386e
feat: condense the explanatory comments on the signature editor DOM sync, the patchEmailLists cache helper and the org-scoped auth-check lookup to the one-line form the repo convention asks for
2026-08-28 22:09:18 -07:00
Matthew Meszaros
6da2c3dfaa
feat: fix the three mailbox Settings bugs from #247 : patch both cache shapes under ["emails", "list"] (paginated pages and the flat directory array) through one patchEmailLists helper so saving, removing, warmup lifecycle and tracking-domain writes no longer crash on undefined.map, stop rewriting the signature editor's contentEditable innerHTML on every keystroke so the caret stays where the user is typing, and scope the auth-check GET/POST handlers by organization instead of user id so the SPF/DKIM/DMARC check stops returning 404
2026-08-28 22:05:41 -07:00
Matthew Meszaros
1e37fb6aa3
feat: capture and score a signup's origin instead of discarding it ( #234 )
...
* feat: give an organization one fused abuse posture, because every existing control watches a single subject and an actor slightly wrong on several axes sits under all of them: organizations gains risk_state, risk_score, risk_reason and an append-only risk_signals evidence blob, modelled on the warmup participant health machine that already works rather than a second vocabulary for the same idea; restricted cuts per-mailbox cold volume to a quarter and forces the free warmup pool so a risky tenant cannot spend the paid pool's shared reputation, suspended stops sending at the send gate, and watch deliberately changes nothing a customer can feel so evidence accumulates before anything is taken away; an operator's suspension outranks the derived band so a detector clearing cannot release a workspace a human suspended, transitions ride the audit spine to every teammate's dashboard, a banner says which limit is active and why rather than letting volume drop silently, and the posture never travels in a workspace archive since it is one platform's verdict reached from evidence the destination never saw
* feat: make the suspension actually stop sending, and emit the audit transitions the PR claimed: emailsend.SendEmail is only the manual and API path, so campaign and warmup sends went nowhere near the gate and a suspended workspace kept sending on its schedule, while the restricted multiplier floored every mailbox at one a day which quietly turned suspension into a trickle rather than a stop; the campaign scheduler now defers the whole campaign with a reason and the warmup task skips as org-suspended, since warmup is outbound mail from the same domains; separately the band change emitted no audit entry at all despite the entity type and the frontend spine entry both existing, so no banner moved for a teammate and there was no trail of who was restricted when, and only a real transition is logged so a detector re-recording the same finding cannot fill the feed; one of my own live tests also asserted how far out a slot lands, which depends on the hour the suite runs, and now asserts the property it was about
* feat: capture and score a signup's origin instead of discarding it
* feat: document what a signup records, and prove the origin write is on the account path with a seam test rather than only testing the scorer
* chore: drop the em dashes and trim the comments the review flagged
2026-08-28 11:26:44 -07:00
Matthew Meszaros
b019174910
feat: give an organization one fused abuse posture ( #233 )
...
* feat: give an organization one fused abuse posture, because every existing control watches a single subject and an actor slightly wrong on several axes sits under all of them: organizations gains risk_state, risk_score, risk_reason and an append-only risk_signals evidence blob, modelled on the warmup participant health machine that already works rather than a second vocabulary for the same idea; restricted cuts per-mailbox cold volume to a quarter and forces the free warmup pool so a risky tenant cannot spend the paid pool's shared reputation, suspended stops sending at the send gate, and watch deliberately changes nothing a customer can feel so evidence accumulates before anything is taken away; an operator's suspension outranks the derived band so a detector clearing cannot release a workspace a human suspended, transitions ride the audit spine to every teammate's dashboard, a banner says which limit is active and why rather than letting volume drop silently, and the posture never travels in a workspace archive since it is one platform's verdict reached from evidence the destination never saw
* feat: make the suspension actually stop sending, and emit the audit transitions the PR claimed: emailsend.SendEmail is only the manual and API path, so campaign and warmup sends went nowhere near the gate and a suspended workspace kept sending on its schedule, while the restricted multiplier floored every mailbox at one a day which quietly turned suspension into a trickle rather than a stop; the campaign scheduler now defers the whole campaign with a reason and the warmup task skips as org-suspended, since warmup is outbound mail from the same domains; separately the band change emitted no audit entry at all despite the entity type and the frontend spine entry both existing, so no banner moved for a teammate and there was no trail of who was restricted when, and only a real transition is logged so a detector re-recording the same finding cannot fill the feed; one of my own live tests also asserted how far out a slot lands, which depends on the hour the suite runs, and now asserts the property it was about
* chore: drop a test that belongs to the signup-risk change and reached this branch by mistake, where the type it exercises does not exist
2026-08-28 11:20:19 -07:00
Matthew Meszaros
029bc27bfd
feat: bind a browser sign-in to the browser that started it, so a handoff link cannot be forwarded: one-time state proves the callback answers a request this server made, not one THIS browser made, so anyone could run the flow against their own Google or OIDC account and send the resulting URL to someone else, whose browser would then hold the sender's session (RFC 9700 4.7.1); begin now mints a binding secret that never reaches the provider and never appears in a URL, the callback carries it into the handoff, and the exchange refuses a collection that cannot present it with sso_wrong_browser, while the comments this PR added are condensed to the constraint they exist to state
2026-08-28 01:44:39 -07:00
Matthew Meszaros
9ab42cfd65
feat: build the browser half of social sign-in, which was never wired: GOOGLE_CLIENT_ID was read at boot and made the login screen render a Google button, but the button opened a popup at /auth/google/login which no route served, and authService.GoogleAuth/AppleAuth had no caller anywhere in the codebase; internal/app/socialauth now runs Google and Apple through the flow generic OIDC already used (one-time state, PKCE, nonce, id_token verified against the provider JWKS, identity keyed on issuer and subject, JIT provisioning, the ban and 2FA gates), the redirect URI defaults to API_PUBLIC_URL/v1/auth/<provider>/callback and is logged at boot because registering the dashboard origin instead is the mistake that produces a valid OAuth client and a dead button, /auth/config advertises only providers the backend can actually complete, the SSO landing page no longer swallows a two_fa_required response, and OIDC_PROVIDER_NAME finally reaches the button it documents
2026-08-28 01:33:08 -07:00
Matthew Meszaros
1c4fcff558
feat: make the admin panel's broken queries run: seven statements referenced schema that does not exist and failed 100% of the time, so a force-stop wrote status = 'stopped' and stopped_at to a campaign_status enum and a campaigns table that have neither, the plan writes named a duration column that became duration_id long ago, the user rate-limit read and write named a daily_email_limit column that user_rate_limits has never had, and the user preview compared email_accounts.user_id (uuid) against a text parameter and then swallowed the error so every operator saw an empty mailbox list; the same uuid = text defect in GetUserEmails, which the prepare sweep cannot see because that WHERE clause is assembled at runtime, was a live 500 on GET /admin/users/:id/emails; a stop now parks the campaign at 'paused' like the owner-facing stop and records the reason the UI has always sent and the backend has always dropped into both the audit log and the owner's campaign feed, the plan writes resolve durations.title to duration_id and answer 400 rather than a constraint violation on an unknown period, the rate-limit editor now covers the seven real limit columns instead of one that never existed and patches insert-then-update in a transaction because every column is NOT NULL, AdminWorkerEmail.LastSyncedAt is a pointer so a never-synced mailbox stops being silently dropped from every admin list, and TestLiveEveryQueryPrepares now fails on undefined columns, tables, operators and enum values instead of only reporting them
2026-08-27 03:09:40 -07:00
Matthew Meszaros
d396f53fc4
feat: add Delete and Duplicate campaign actions to the dashboard (issue #185 ): every campaign row and the detail header get a ⋯ menu (Edit, Duplicate, Start/Pause, Delete) plus a Delete card at the bottom of Settings, all permission-gated with a confirm that spells out what goes; DELETE /campaigns/:id is now organization-scoped instead of user_id-scoped so teammates can delete, runs in one transaction that also deletes the campaign's pending tasks and cancels a wakeup tick claimed at that moment (campaign_tasks only nulls its link, so those rows kept firing), removes attachment objects and publishes CAMPAIGN_DELETED so a teammate's open detail page is sent back to the list; new POST /campaigns/:id/duplicate copies the campaign row as a draft with steps and their branch graph rewired onto new step ids, tags, folders, senders with rotation reset, A/B variants, advanced settings and attachments (quota-checked, blobs undone if the copy fails) and none of the leads, progress, logs, counters, ramp level, guardrail trip or past dates, naming it (copy)/(copy N) inside the 50 byte cap without splitting runes; a claimed campaign tick whose campaign vanished now ends the chain instead of staying active forever; covered by TestLiveCampaignLifecycle* against real SQL, RemapBranchTargets and duplicateName unit tests and a react-query vitest for the list cache, with API reference, endpoint map and campaigns guide updated
2026-08-25 08:54:51 -07:00
Matthew Meszaros
cbf4190f42
feat: make a custom tracking domain verifiable instead of permanently "Pending DNS": the CNAME target is now this install's TRACKING_DOMAIN rather than a hardcoded t.warmbly.com that resolves nowhere, matching is exact on the label boundary (or on shared addresses, so a provider-flattened CNAME stops reading as no record at all) instead of strings.Contains, and every outcome carries the reason plus what DNS actually returned, including when the tracking host the customer is told to point at has no record of its own; a pasted URL is normalized to its host and a malformed one is rejected up front instead of saved and left pending forever; only a VERIFIED mailbox domain is used at send time with the shared host as the fallback and a campaign-feed entry saying why; pixels and click tickets are built from the configured host, and with none configured mail ships untracked rather than carrying links to another deployment's tracking service; adds GET /emails/:id/track and POST /emails/:id/track/verify plus an hourly re-resolution sweep so a record that propagates later starts being used and one that breaks stops routing links; and scopes the tracking-domain write by organization like the read, which also fixes GET /emails/:id passing a user id to an org-scoped query and 404ing for every caller
2026-08-24 09:02:29 -07:00
Matthew Meszaros
1018068942
feat: stop recipient suppression and the entitlement gate being skipped when a campaign has no organization: the send path now fails closed (an orgless campaign is paused with the reason in its activity feed instead of mailing an unsubscribed, bounced or complained address, since routing's own suppression filter joins on the campaign's organization_id and matches nothing when it is NULL), the same tenancy gate covers warmup and unibox sends and an orgless mailbox resolves to the free pool rather than defaulting into the paid one, the state is no longer creatable (sequenceRepository.Create inherits organization_id from its campaign instead of inserting NULL, campaign creation and mailbox onboarding refuse without a workspace via errx.ErrNoOrganization, guardInboxLimit / guardMailboxThrottle / the campaign creation throttle no longer treat a missing org as exempt, and a new session resolves the user's default workspace so the orgless session that produced these rows cannot exist), and migration 000092 backfills then sets organization_id NOT NULL on campaigns, contacts, email_accounts and sequences plus sessions.current_organization_id, provisioning a recovery workspace for any user with none so no row is deleted to satisfy the constraint (live-tested in TestLiveOrglessCampaignDoesNotSendToSuppressedRecipient, TestLiveSuppressedRecipientIsSkipped, TestLiveCampaignRequiresAnOrganization and TestLiveHealthyCampaignStillSends)
2026-08-24 08:47:08 -07:00
Matthew Meszaros
6b614ad4bb
feat: never hand a send to a worker that is not heartbeating (registry row plus the Redis heartbeat key, which the worker now sets at boot instead of 90 seconds later), because a command queued for a dead worker is neither executed nor answered; reload a worker's mailboxes the moment its boot heartbeat arrives instead of waiting for the reconciler's republish window, make the publisher fail instead of silently succeeding when no bus or object storage is configured, and log a send that could not reach a worker to the campaign feed as a red, retrying entry
2026-08-23 10:18:50 -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