Matthew Meszaros
a268e3c54a
feat: limit the Warmup plan to the premium pool and uncapped mailboxes by gating sending, the inbox, AI, integrations, placement tests and paid storage on a plan that sends (HasProductPlan, with HasPremiumWarmup for pool placement), report 0 daily sends and refuse non-mailbox limit requests for workspaces that do not send, lock Warmup workspaces in the dashboard like free ones, show only the mailbox meter for them, and show the yearly price on yearly subscriptions
2026-09-28 09:16:42 -07:00
Matthew Meszaros
caf1852217
feat: keep placement seeds out of warmup pools as recipients too, store a probe's Message-ID before the send, hold queued probes against the sender's day and size a test to what is left, claim due monitors so one replica runs each, write comparison halves in one transaction, scan each seed's mail once per tick, keep test history when a mailbox is deleted, default to 20 seeds a minute apart, and render cloud-panel tests without a real lead by default
2026-09-25 21:46:09 -07:00
Matthew Meszaros
7f324a38ac
feat: open inbox placement tests to workspaces with probes rendered like the campaign send and paced as placement tasks, Message-ID matching instead of a subject token and warmup header, instance, workspace and Warmbly Cloud seed panels, a tracking comparison, scheduled campaign monitors with alerts and optional auto-pause, monthly allowances, realtime updates and org transfer registration
2026-09-25 20:48:15 -07:00
Matthew Meszaros
acecd62c88
feat: scope mailbox disconnect and warmup lifecycle to the workspace rather than the member who connected the mailbox so an admin can act on every mailbox the list already shows them, evict a mailbox whose row is gone from every live worker when its provider errors arrive so a deleted mailbox stops calling the provider once a sync interval forever, subscribe before publishing the credential-validation job and classify a socket deadline as the retryable timeout it is, give the worker's validation reply its own budget so a slow mail host no longer loses a finished verdict, guard every global key handler against a keydown carrying no key, drop exceptions whose whole message is an object's default toString, make the Postgres pool size configurable, and record the CASA and security invariants in AGENTS.md
2026-09-19 12:49:46 +02:00
Matthew Meszaros
e668a2a36b
feat: complete the ADA CASA v2.1.1 AL1 control set across authentication, sessions, access control, cryptography, input validation and configuration, adding a breached-password denylist and per-account login throttling, enforced multi-factor authentication on the admin panel, step-up confirmation before an action that mints a lasting credential, purpose-scoped session tokens, single-use TOTP steps, tenant verification on every cross-referenced identifier, security headers on every surface, encrypted webhook signing secrets, per-organization idempotency, PKCE and a minimal two-scope Gmail consent on the mailbox OAuth flow, bounded spreadsheet and archive decoding, a patched Go toolchain with govulncheck in CI, and the evidence pack under compliance/casa
2026-09-19 08:18:35 +02:00
Matthew Meszaros
f99ee57484
feat: scope mailbox disconnect to the workspace instead of the connecting member, so a teammate with manage_emails no longer gets 404 on a mailbox the list shows them, delete by id in the repository on the strength of that check while the worker removal still names the owner the consumer's unibox cleanup is keyed on, and read the API's own reason off the normalised AppError in the accounts page so a refused disconnect says why instead of "The mailbox couldn't be disconnected" on every failure
2026-09-19 06:01:03 +02:00
tunglambk
cf7e530e15
feat: release a cloud-managed mirror's cloud link when it is deleted through the mailbox delete, using the delete-only revocation that never calls back into the mailbox delete, so the mailbox returns to the cloud workspace instead of staying claimed by an instance that no longer holds it (issue #582 )
2026-09-18 07:17:40 +00:00
Matthew Meszaros
8240f14e8b
feat: fail closed on incomplete cloud mailbox revocation and document retry-safe deletion and TLS diagnostics
2026-09-17 21:21:34 -07:00
Matthew Meszaros
e37c5053c2
feat: make warmup refunds atomic, count confirmed partner diversity in local and cloud mailbox views, and document cloud-safe mailbox deletion
2026-09-17 21:15:28 -07:00
Matthew Meszaros
68babc30f3
feat: give the mailbox delete its own cloud revocation that calls the pool before dropping the local row and refuses an unreadable link, so a nil answer is proof the credential is gone rather than proof the local row went, and drive the greylisting evidence guard through the real handler with stub repositories so removing it fails CI where the live test skips
2026-09-17 20:52:54 -07:00
Matthew Meszaros
d2095a8a70
feat: stop a greylisted RCPT reply from being filed as bounce evidence now that the send carries the server's own words, revoke a cloud enrollment after the worker removal and put the mailbox back when the revocation is refused so a failed delete really changes nothing, drop the unenroll-under-Settings advice that makes the same failing call, and only log a within-workspace pairing when there is a sibling to draw
2026-09-17 20:19:46 -07:00
Matthew Meszaros
8ee1c50a1b
feat: make a failed SMTP send name the step and the cause behind it instead of one bare SERVER_UNREACHABLE sentinel, give a refused warmup send its day back so sent_today can no longer climb past the target while the cap frees the slot, revoke a mailbox's Warmbly Cloud enrollment when it is deleted so the pool stops holding its password, and prefer warmup partners outside the sender's own workspace while showing the partner diversity a mailbox is actually getting ( #574 , #575 )
2026-09-17 20:02:37 -07:00
SUMAN JANA
2134c7a143
feat(analytics): report on mail written by hand, with opt-in open and click tracking per mailbox
2026-09-17 10:26:25 +00:00
Matthew Meszaros
8d790ede6c
feat: send from any address Google has verified a Gmail mailbox to send as and import the signature its owner already wrote in Gmail, reading both through gmail.settings.basic at connect and on demand via GET/POST /emails/:id/identity, validating the choice against the provider's own list in the service and again inside the UPDATE, clearing it when the provider stops verifying it, and never applying it to warmup ( #514 )
2026-09-14 10:13:36 -07:00
Matthew Meszaros
d456bc48c6
feat: fix the Warmbly Cloud pool link across both roles ( #262 ): take an enrolled mailbox out of this instance's own warmup pool so local partners stop writing to it and their unverifiable warmup stops landing in the owner's unibox, recognise the cloud's warmup mail whose verify header did not survive delivery through a new warmup-deliveries lookup that ignores consumed_at because instance and cloud read the same mailbox, move the managed-mailbox access token route behind NODE_BROKER_TOKEN so the internet-facing tracking and forms services can no longer mint a live provider token, scope pause and resume to the caller's workspace, keep an enrolled mailbox listed once it goes inactive, release the cloud copy when the local mirror row cannot be written, refuse the one-time handshake when CREDENTIALS_ENCRYPTION_KEY is missing, blank an expired code's plaintext instance token, and stop errx answering 200 for a status outside its table
2026-09-12 06:58:25 -07:00
Matthew Meszaros
c4aece241b
feat: scope the tag, category and folder registries and unibox conversation labels to the organization instead of the creating user, so a teammate sees and can edit the labels the owner made, splitting a label two workspaces shared into one copy each and guarding every label write against ids from another workspace ( #457 )
2026-09-12 03:37:31 -07:00
Matthew Meszaros
7d58b874b8
feat: keep every recipient-facing and self-host-facing address on the deployment's own domain: mint unsubscribe links on a workspace's verified tracking domain (served by the tracking service, proxied to the backend that owns the pages), attach RFC 8058 one-click only over https, resolve all branding through config.Brand() gated on SelfHosted() so a self-host's email footer, sign-in links, stats card, API example and public form badge name nobody else, drop the app.warmbly.com fallback from AppBaseURL, blank TRACKING_DOMAIN and FORMS_DOMAIN on core-only installs, and have install.sh offer to configure a fresh interactive install instead of silently defaulting to localhost
2026-09-09 06:34:43 -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
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
35e8f95f2a
feat: make a release from the mailbox hold land where the rebalancer would put it by running lifecycle.Decide against the mailbox's current warmup health through a new GetLifecycleCandidate repository read, so an unhealthy mailbox goes straight to resting instead of sending cold for up to an hour before the hourly pass rests it again, report that outcome in the drawer toast and the API reference, and condense the new hold comments in the email service, backend wiring, repository, hook, client and drawer to the one-line form the repo convention asks for
2026-08-28 23:30:15 -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
bb5809a035
Merge remote-tracking branch 'origin/main' into fix/issue-247-report
2026-08-28 22:12:18 -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
230d80db64
feat: condense the new warmup pool comments in email_task.go, handler.go, service.go, pg_worker.go and warmup_scheduler.go to the one-line form the repo convention asks for, keeping the non-obvious constraints (the stored tier is never empty, the tier column records what is paid for rather than where the mailbox warms) and dropping the narration
2026-08-28 21:31:06 -07:00
Matthew Meszaros
c28ef3e82a
feat: keep a restricted or suspended workspace out of the premium warmup pool by checking the organization's risk posture before the stored warmup_pool_type in resolveWarmupPoolType (tasks and email services), which is never empty so the restricted branch below it was dead code, make the scheduler's recipient-capacity count risk-aware the same way, wire the org risk repository into the email service, and refuse the premium membership move in UpdateEmailAccountWarmupPoolType while the owning organization is restricted so a worker rebalance cannot readmit a risky tenant
2026-08-28 21:26:57 -07:00
Matthew Meszaros
36224c8caf
feat: give the deleted mailbox's worker its capacity back inside the delete's own transaction, because a refund made beside it can be lost for good: once the row is gone nothing records which worker was charged for that mailbox, so a partially applied unassign left account_count or load_score charged with nothing able to repair it; emailRepository.Delete now deletes the row RETURNING worker_id and credits the worker in the same transaction, taking the placement weight from the caller so it matches what assignment charged, and the long comments this PR added are condensed to the constraint they exist to state
2026-08-28 00:49:11 -07:00
Matthew Meszaros
bd435a2687
feat: tell the worker to drop a mailbox the customer disabled or disconnected, the two paths #217 left out: emailService.Update now publishes REMOVE_EMAIL when a status leaves active (and re-ships the mailbox when it returns to active instead of waiting on the reconciler), Delete publishes it before the row goes and refuses to delete when it cannot, since after the row there is no assignment left to read; delete also stopped failing outright, because tasks and warmup_admin_actions referenced email_accounts with no delete action so disconnecting anything that had ever warmed up or sent a step raised a foreign key violation (migration 000098), read the mailbox through an org-scoped query while passing a user id so the warmup pool cleanup, realtime event and webhook never ran, and never refunded the worker's account count or load score
2026-08-28 00:40:39 -07:00
Matthew Meszaros
cbb0b8f662
feat: keep a mailbox in exactly one warmup pool so a plan change moves it instead of leaving it in both: EnsurePoolMembership joined whichever pool the org is entitled to right now while RemovePoolMembership only ever deleted that same pool's row, so a downgraded mailbox kept its premium membership and went on being handed to paying customers as a warmup partner while every spam-score increment was counted once per row by the health evaluator; membership is now a move (one upsert keyed on the account that carries health state, block and spam score across, so changing pool cannot launder a penalty), removal is never pool-scoped, a tier change moves the participant row in the same transaction as email_accounts.warmup_pool_type, a new reconcile pass in the backend's warmup sweep evicts participants whose workspace lost warmup access and repairs pool drift for the mailboxes no warmup task ever revisits, an unreadable subscription no longer counts as "not entitled" anywhere that evicts, GetSpamScore takes the max rather than the sum and IncrementSpamScore clamps to the column ceiling instead of silently failing its CHECK, and migration 000097 merges any existing dual membership worst-wins before making one-pool-per-mailbox a unique index
2026-08-27 03:02:56 -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
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
e1750d8988
feat: add PATCH /emails/tags for bulk mailbox tagging: add_tags/remove_tags set semantics across up to 1000 mailboxes in one transaction (ownership of both mailboxes and tags enforced in SQL so stale ids are skipped, composite-PK insert makes re-adds no-ops), naturally idempotent so no Idempotency-Key needed, one audit entry with account/added/removed counts riding the email_account spine, and the endpoint registered in the docs scope map under WRITE_EMAILS
2026-07-19 16:13:43 +02:00
Matthew Meszaros
57e8705a82
feat: make public API list cursors opaque base64 tokens (internal/utils/paging) instead of raw record UUIDs, decoding on input with a 400 on a bad cursor; admin endpoints keep transparent UUID cursors
2026-06-13 07:18:23 +02:00
Matthew Meszaros
6ae5e52c4e
fix: scope email-account list/detail queries by organization_id instead of the logged-in user_id, so every member of a workspace sees the org's mailboxes (not just the owner who connected them)
2026-06-11 17:43:56 +02:00
Matthew Meszaros
3837cd3898
feat: bound auth login network waits
...
Add request and mail-delivery timeouts around auth flows so login requests cannot hang indefinitely when notification delivery stalls.
Allow the local admin dev origin through default CORS and update context-aware lint fixes so the repository lint gate passes.
2026-06-01 16:56:24 +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
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
d08c984c42
feat: snapshot current dashboard changes
2026-05-30 13:56:27 +00:00
Matthew Meszaros
878e8d921e
feat: enforce api key email scopes
2026-05-30 04:28:25 +00:00
Matthew Meszaros
60773b3d8e
feat: make dashboard realtime
2026-05-30 04:17:10 +00:00
Matthew Meszaros
ab445e2207
feat: customer webhook subscriptions with hmac signing and retry
...
- webhook_endpoints / webhook_deliveries schema (migration 42)
- service: dispatch + endpoint crud + hmac-sha256 signing
- delivery worker drains queue using FOR UPDATE SKIP LOCKED so multiple
api replicas can run safely without duplicate dispatch
- exponential backoff (30s → 1h cap, 8 attempts then abandoned)
- REST API under /webhooks: list/create/update/delete/rotate-secret/
list-deliveries. secret only returned at create + rotate
- header convention matches stripe-style: X-Warmbly-Signature: t=<unix>,v1=<hex>
- legacy header X-Warmbly-Token already renamed; new outbound webhook
headers are X-Warmbly-Signature / X-Warmbly-Event / X-Warmbly-Event-Id
- wired into email account connect/remove and warmup health transitions;
campaign/tracking/deliverability call sites will reuse the same
webhookService.Dispatch interface
2026-05-25 16:03:13 +00:00
Matthew Meszaros
d8d88c7f69
feat: add warmup health tracking, migrate repos to postgres, and overhaul web UI
2026-04-03 06:08:52 +00:00
Matthew Meszaros
772c19820d
New Repository: Add Backend Code
2026-01-17 14:11:14 +00:00