Matthew Meszaros
d8e99877b2
feat: let an update finish work in flight instead of cutting it off, by having the import runner stop claiming on shutdown, hand back rows that never started without counting the claim and finish the rows it is connecting on an uncancelled context, the backend wait up to 45 seconds for those rows and for in-flight connects, the event bus finish the message being handled before it stops reading, and node units, the repository compose file and the installer's stack allow 60 seconds before a forced stop, with the grace period documented for Railway, Docker and Kubernetes
2026-09-24 18:31:06 +02:00
Matthew Meszaros
06f4a270cc
feat: keep the mailbox imports menu live and self-explaining (deferred rather than dropped progress events, a five-second refresh while an import runs, each import's state and what it waits on in words, a badge for imports that need you, and an × that hides an import for the workspace through POST /emails/imports/:id/dismiss with migration 000211, stopping a running one first), show the vendor's own status and the Microsoft and Google time expectations on rows being authorized, record a vendor row's mail host so it gets its provider icon and a working Sign in, drop the Fix button from rows the vendor is authorizing, and count warming mailboxes rather than connected ones in the pool banner, refreshed when mailboxes change
2026-09-24 17:46:46 +02:00
Matthew Meszaros
b196b3c8be
feat: count warmup placements exactly once through a consumer sweep that seeds history in batches instead of a locking migration backfill, add a concurrent unplaced-receipt index, publish WARMUP_PLACEMENT only when a receipt is counted, share one spam-flag list across warmup, placement tests and unibox folders, name ranked mailboxes from their own rows, default from to 30 days before to, keep ScrollStrip scrolling to itself, refresh account statuses at most every 5 minutes, and document rescued as requested
2026-09-24 05:28:51 -07:00
Matthew Meszaros
b2d54fc873
feat: record where every warmup email lands (inbox, Gmail tab, spam) per sender, day and recipient host in warmup_placement_daily, serve it from GET /analytics/warmup/placement, cap mailbox health at the measured 7-day inbox rate, and show it as an Inbox column, a mailbox Deliverability tab and a workspace placement section; replace every visible native checkbox in the dashboard with a themed Checkbox and make the mailbox drawer's tab bar scroll
2026-09-24 05:28:51 -07:00
Matthew Meszaros
ee70b7697a
Merge remote-tracking branch 'origin/main' into fix/unibox-forward-original-message
...
# Conflicts:
# docs/content/docs/api/reference/unibox.mdx
# docs/content/docs/guides/unibox.mdx
# docs/public/openapi.json
# web/src/components/app/unibox/ReplyComposer.tsx
# web/src/components/app/unibox/replyComposerDraft.test.tsx
2026-09-23 21:48:54 -07:00
Matthew Meszaros
bbd7942313
feat: unibox forward requires READ_UNIBOX alongside WRITE_UNIBOX, restores click tickets in a forwarded Warmbly send to their destinations, fills an empty-placeholder HTML note from its text, previews an empty-note forward in the Scheduled view, keeps a reply's leading blank lines, shares the stored-body read with GET /unibox/:id, and shows the Reply/Forward bar when a forward's message is no longer in the thread
2026-09-23 21:38:59 -07:00
Matthew Meszaros
a544847fcb
feat: resolve a Unibox reply's Gmail thread per sending mailbox (its own copy of the conversation, or the thread its earlier reply started, and none for other providers or on a failed lookup keeps the handle), honour an API key's mailbox allowlist on scheduled list and cancel, fall back from a saved mailbox that is gone and block Send until the sender can send, fetch From candidates only when the picker opens, and fix the Unibox threading and drafts docs
2026-09-23 21:29:24 -07:00
Matthew Meszaros
6c4931c28a
feat: unibox forward carries the original message, attached server-side from a new forward_message_id on POST /unibox/reply and stored on the queued task (migration 000208) so it goes out under the note and signature with its From, Date, Subject, To and Cc lines, sanitized HTML and text part; the composer allows an empty note and previews the forwarded message ( #668 )
2026-09-23 21:22:17 -07:00
Matthew Meszaros
ce5eb4fd25
feat: let a Unibox reply or forward choose its sending mailbox in From (the shared MailboxPicker without Auto, kept in the draft and the undo-send), send the provider thread handle only from a mailbox that holds the thread so a switched reply threads on In-Reply-To alone, and scope scheduled sends, their cancel, count and cap to the organization whose mailboxes send them ( #670 )
2026-09-23 21:18:20 -07:00
Matthew Meszaros
c40eeb1978
Merge pull request #665 from warmbly/fix/campaign-sender-resolution
...
feat: count the all-active-mailboxes fallback in the advisor's campaign sender count, re-run the advisor when a campaign's senders, tags or status change, start a new lead from a mailbox whose min gap (warmup included) has elapsed instead of deferring the campaign, and scope the pre-start mailbox check to the organization
2026-09-23 16:10:17 +00:00
Matthew Meszaros
145e9c45d7
feat: refresh the advisor on a status-only campaign PATCH, fall back to the whole pool under ESP prefer when no matching mailbox is past its min gap, and draw each candidate's behaviour gap once so the selection filter and the send enforce the same gap
2026-09-23 09:06:28 -07:00
Matthew Meszaros
d474fdc4b2
feat: count the all-active-mailboxes fallback in the advisor's campaign sender count, re-run the advisor when a campaign's senders, tags or status change, start a new lead from a mailbox whose min gap (warmup included) has elapsed instead of deferring the campaign, and scope the pre-start mailbox check to the organization
2026-09-23 08:43:06 -07:00
Matthew Meszaros
e58921484d
feat: rebuild mailbox import around column mapping and automatic host and sign-in detection (CSV, XLSX, pasted lists, saved mappings, retryable rows with fixes, migrations 000205-000206), connect whole Google Workspace domains and Microsoft 365 organizations through a proved administrator grant, import from inbox vendors (InboxKit, Zapmail, Mailforge, Infraforge, Maildoso, Cheap Inboxes, ScaledMail) with vendor-managed forwarding and DNS, add a sending domains page with per-domain tracking and verified root redirects, unify Add account into one Google and one Microsoft entry with per-method choices, mark per-mailbox Google sign-in as retiring with in-place moves to the admin grant or an app password, allow the loopback security mode in the credential columns (migration 000207), read semicolon-separated CSVs, and add a mock vendor API to the sandbox
2026-09-23 08:41:01 -07:00
Matthew Meszaros
fd96a8c47c
Merge pull request #654 from warmbly/fix/stale-state-after-mailbox-removal
...
feat: close Advisor findings about a mailbox, campaign or step inside its delete transaction, keep re-evaluating workspaces that still hold open findings after their last mailbox is gone, re-evaluate the Advisor right after a mailbox change or a campaign/step delete with coalesced reruns, and refresh the unread badge, inbox, campaigns, analytics and Advisor caches on every mailbox removal including a teammate's
2026-09-23 09:06:00 +00:00
Matthew Meszaros
c75b3b3b0d
Merge pull request #651 from warmbly/feature/import-existing-custom-fields
...
feat: map import columns onto the workspace's existing custom fields
2026-09-23 04:45:33 +00:00
Matthew Meszaros
c35e05c9eb
Merge pull request #652 from warmbly/feature/sending-window-timezone
...
feat: give each workspace a timezone that mailbox warmup and campaign sending windows follow by default, with a Timezones control centre on Settings > Profile and the first-email delay moved into campaign Settings
2026-09-23 03:42:59 +00:00
Matthew Meszaros
7a86186f5b
feat: close Advisor findings about a mailbox, campaign or step inside its delete transaction, keep re-evaluating workspaces that still hold open findings after their last mailbox is gone, re-evaluate the Advisor right after a mailbox change or a campaign/step delete with coalesced reruns, and refresh the unread badge, inbox, campaigns, analytics and Advisor caches on every mailbox removal including a teammate's
2026-09-22 20:29:13 -07:00
Matthew Meszaros
0e97ccc88d
feat: make every warmbly CLI command send the body its endpoint binds (contact create, mailbox send and set-tracking, form set-domain, campaign test, task due dates, advisor snooze, warmup appeal, integration push, oauth-app scopes, corrected inbox and suppression examples, automations and webhook edit documented as full writes), pinned by a test that decodes each body strictly into the server struct, and let POST /v1/campaigns/:id/steps take the PATCH fields as an optional body so add-step no longer creates a blank step
2026-09-22 20:26:34 -07:00
Matthew Meszaros
6cadcc725d
feat: answer every public request-body bind failure with a 400 that names the problem (empty body, JSON syntax error with its byte offset, wrong JSON type for the body or a named field, missing or out-of-range fields by json key) instead of a blanket malformed-JSON message or a 500, accept a single contact object on POST /v1/contacts as the CLIs send it, and drop the API-key lookup cache whose 300ns TTL made it a Redis round trip that saved nothing
2026-09-22 20:21:41 -07:00
Matthew Meszaros
38f8382cf8
Merge remote-tracking branch 'origin/main' into feature/sending-window-timezone
2026-09-22 20:13:55 -07:00
Matthew Meszaros
45c045a5bb
feat: give each workspace a timezone that mailbox warmup and campaign sending windows follow by default (organizations.timezone, migration 000198), let campaigns and mailboxes follow it or pin their own, add a Timezones control centre on Settings > Profile with inline per-campaign and per-mailbox zones, default new workspaces and the campaign wizard to the browser zone, expose effective_timezone on campaigns, and move the first-email delay from the Schedule tab and wizard into campaign Settings > First email
2026-09-22 20:13:55 -07:00
Matthew Meszaros
2bdbfe5f68
feat: list the workspace's existing custom fields in the contact import and Google Sheets column mapper (searchable, with inline create), auto-map headers to existing fields ignoring case and separators in one shared server-side suggester, flag new fields, near-duplicates and columns sharing a field, and document the matching
2026-09-22 20:07:57 -07:00
Matthew Meszaros
ee9f5bf84e
feat: make every skipped-folder purge drop the message map entries and parked arrivals with the rows so a message can be imported again if it moves back, persist each folder's delimiter (migration 000197) so the backend purge matches the same subfolders and case the worker skips, fail the mailbox load closed when the skip list cannot be read, mark a folder renamed into the skipped subtree as skipped, never remove a row re-fetched this pass, cap the reconciliation at 50 searches per pass and continue next pass, publish one EMAIL_DELETED after a folder purge, and resolve the account once for GET /emails/:id/sync
2026-09-22 05:33:56 -07:00
Matthew Meszaros
5a967cc3ce
feat: let an IMAP mailbox exclude folders from sync (email_accounts.sync_skip_folders, migration 000196) so a folder another tool fills never reaches the unified inbox: the worker drops skipped folders and their subfolders before the walk, retires an already-synced one with its stored mail, and removes mail that later moves into one only when its Message-ID is found there; PUT /emails/:id/sync and the drawer's Sync card set the list, GET reports it with the server's folder list, warmbly mailbox skip-folders mirrors it, with docs, OpenAPI and error-code invalid_sync_folder
2026-09-22 05:15:49 -07:00
Matthew Meszaros
e1989f9116
Merge remote-tracking branch 'origin/main' into fix/password-change-session-revocation
...
# Conflicts:
# internal/app/auth/reset_password.go
2026-09-21 04:56:50 -07:00
Matthew Meszaros
eac3f6d615
Merge remote-tracking branch 'origin/main' into fix/sso-link-existing-password-account
...
# Conflicts:
# docs/content/docs/guides/security.mdx
2026-09-21 04:28:20 -07:00
Matthew Meszaros
db0f0c6132
feat: carry the caller's session into ReissueSession from the request instead of looking it up, evict every revoked session from the cache and write a revoked tombstone where the delete is refused, and answer a password change whose reissue failed with the distinct 409 password_changed_sign_in_again that the dashboard turns into a sign-out, documented in error-codes, the endpoint reference and OpenAPI
2026-09-21 04:23:46 -07:00
Matthew Meszaros
9426c0da51
feat: validate every person, workspace and company name through internal/pkg/displayname on each write path (profile, onboarding, setup, IdP sign-in, org create and rename, org import, enterprise inquiry, admin testers, warmblyctl) with a 400 invalid_name code, render stored names in platform email through the same rules, mirror them in the web forms, check the org slug format, and document the rules in error-codes, security and AGENTS.md
2026-09-21 03:34:03 -07:00
Matthew Meszaros
0f60fd9b84
feat: attach a Google, Apple or OIDC identity to an existing password account only after that account's password is presented: resolveFederatedUser parks the sign-in as link_required with a single-use sso_link pending token, POST /auth/sso/link checks the password against the provider-asserted address on the sign-in failure budget and links then issues the session through finishLoginAs, the dashboard collects it on a new login step, and the API reference, endpoints list, security guide and OpenAPI spec describe the third login result
2026-09-21 03:25:29 -07:00
Matthew Meszaros
7626aaefe4
feat: end every session on a password change, the calling one included, and answer POST /auth/me/password with the token pair of a fresh session for that device; the dashboard stores the new pair, and the endpoint reference, security guide and OpenAPI document the response
2026-09-21 03:18:27 -07:00
Matthew Meszaros
cc244e5159
feat: make every admin panel list page past the first and filter by id: bind query-string ids through models.ParamUUID since gin cannot set a uuid.UUID, page the explorers and secondary lists by an opaque offset cursor with an id tiebreak instead of an id keyset that disagreed with the sort, page the audit log on (created_at, id) with an inclusive YYYY-MM-DD end day and read next_cursor on its page, cast every before-date bound to timestamptz, coalesce nullable audit ip and user agent, and report failed admin queries and 5xx mutations to PostHog or Sentry with method, path, status, code and request id
2026-09-21 02:11:38 -07:00
Matthew Meszaros
7b93e48bd4
feat: make Archive mean something everywhere by keeping filed conversations out of every working unibox view except All mail and the Archive folder, read a mailbox address out of the raw From header so Awaiting reply stops missing every thread sent as "Name <addr>", file and mark read by thread id rather than by message id, and add per-row triage actions plus a multi-select selection bar to the conversation list
2026-09-20 07:16:35 -07:00
Matthew Meszaros
5b16a09b02
feat: write public objects without a canned ACL so a bucket whose ownership is owner-enforced still stores avatars, form assets, OAuth logos and email-body images, give the passkey login challenge its own per-IP budget separate from the one password sign-in draws on, and surface the API's own message at upload and passkey call sites instead of a generic sentence
2026-09-20 15:40:52 +02:00
Matthew Meszaros
addb956ad6
feat: shared TypeSafe client under internal/pkg/typesafe with inbox tagging phases 2 and 3 (hold, stop, task, suppress behind workspace switches, reversible ones on by default), labels seeded at workspace creation and by the follow-up sweep, premade inbox views (hot leads, needs a reply, follow up, declined, automated), typed reply classification and a reply_intent branch condition, an inbox agent draft gate, Advisor copy judgment with a cached editor re-check, warmup content lint, bounce cause classification that keeps a blocked address sendable, and per-form submission triage
2026-09-19 23:33:37 -07:00
Matthew Meszaros
dee54417a3
Merge pull request #612 from warmbly/feature/campaign-daily-send-view
...
feat: add a Today's sending plan to the campaign overview and feed the sidebar meter and wizard estimate from the scheduler's own clamps (issue #606 )
2026-09-19 17:04:19 +00:00
Matthew Meszaros
ed50c278fa
feat: make view-preference writes partial so a sort click before the layout loads keeps the saved columns (PUT /me/views/:view coalesces on the bound parameter and returns the row in one query, with a live test), validate column ids against each view's known columns and sorts against the contacts search's, reject a malformed custom sort on the bulk select-all and export paths too, key the browser's layout cache by user as well as workspace, commit a drag reorder once on drop instead of once per crossed row, save a Name-only layout as ["name"] so it cannot read as the default, start text sorts ascending from the Sort menu as a header click does, and share the pickers' checkbox square as a ui primitive
2026-09-19 09:38:59 -07:00
Matthew Meszaros
150dc7df6e
feat: add a Today's sending plan to the campaign overview (GET /campaigns/:id/send-plan, derived through the scheduler's own gates: per-mailbox cap clamps, warmup graduation, health bands, other campaigns on the same mailbox, hours, spacing, window, plan allowance, new-lead cap and leads due, as a waterfall that adds up), feed the sidebar meter and the wizard estimate from the same clamps instead of summing configured caps, floor the campaign chain's next tick at the pool's spacing rather than one mailbox's whole gap, fold the compact Advisor strip to one line, add warmbly campaign plan and warmblyctl campaign plan, and document it (issue #606 )
2026-09-19 09:31:46 -07:00
Matthew Meszaros
62201a2dd3
feat: let each member choose, reorder and persist the contact list's columns (custom fields included) and sort on any of them: a user_view_preferences table (migration 000187) behind GET/PUT/DELETE /v1/me/views/:view, a column registry that renders the contacts and campaign Leads tables from a saved layout, a Columns chooser with drag reorder and a Sort menu on both toolbars, click-to-sort headers, sort_by custom:<key> plus company and phone sorts in POST /contacts/search resolved once for Search and SearchIDs with a nullable keyset cursor, and the contacts guide, API reference, openapi, error codes and export-import docs updated
2026-09-19 09:24:17 -07:00
Matthew Meszaros
e9d1a7e734
feat: reserve the per-account reauth attempt atomically before checking a password or 2FA code, keep the 2FA dialogs open while a request is in flight, move and trap focus in them, show a retry row when 2FA status fails to load, and drop Idempotency-Key from the recovery-code route with the reason documented
2026-09-19 08:36:59 -07:00
Matthew Meszaros
de10ca5216
feat: put 2FA disable and recovery-code regeneration behind the per-account reauth attempt budget and document it in the account API reference
2026-09-19 08:16:53 -07:00
Matthew Meszaros
274ff888a5
feat: rebuild two-factor setup in Settings > Security as a three-step wizard with a QR code, manual setup key and TOTP parameters, inline code errors, and recovery codes with download, copy and print; show enable date and remaining recovery codes, add POST /auth/2fa/recovery-codes to regenerate them, return two_fa_invalid_code on a mismatched code, and update the security guide, API reference, error codes and OpenAPI
2026-09-19 08:15:31 -07:00
Matthew Meszaros
464ec521ca
feat: present the $15 pool plan as the Warmup plan everywhere: rename the plan row (migration 000185), add it to the dashboard catalog so the header and billing overview name it, show the cloud tier in a self-hosted instance's header pill and a Plan section under Settings > Warmbly Cloud with upgrade and manage links to the cloud billing page, nudge on the mailboxes page only when the free pool is full, drop the self-host framing from the cloud's checkout dialog, paths panel and locked screen, rebuild the checkout dialog in the plan chooser's style, pitch Premium on deliverability from one shared benefit list, and update the billing and Warmbly Cloud guides and the pricing FAQ
2026-09-19 05:57:41 -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
b09ec39907
Merge pull request #599 from warmbly/chore/casa-al1-security-assessment
...
feat: complete the ADA CASA AL1 control set and ship the assessment evidence pack
2026-09-19 06:39:12 +00:00
Matthew Meszaros
03f59d4d6f
docs: state the tenant and key-shape invariants in these comments as the constraints they are, rather than as an account of what each check replaced, since this repository is public and every self-hosted instance that has not updated yet reads the same text
2026-09-19 08:28:41 +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
6428e6b3e9
Merge pull request #594 from warmbly/feature/disable-google-oauth-new-mailboxes
...
feat: route new Gmail mailboxes through a guided app-password connect instead of Google sign-in, behind BOX_GOOGLE_OAUTH_CONNECT, leaving existing OAuth mailboxes sending and re-authorizable
2026-09-19 06:11:15 +00:00
Matthew Meszaros
49acd51b64
feat: stop one recurring fault burying error tracking by reporting it once per five minutes with the count it stands for, keep a cache outage from answering every signed-in request with a 500 and from taking realtime down by treating an unreachable Redis as a miss and the websocket handshake nonce nothing reads as best-effort, answer a 5xx with a sentence the reader can act on while the call site's own words go to the log against the same request id, prefer the API's own message over the HTTP class in the admin and dashboard clients, and name the fix on a schema registry refusal, an SES sandbox rejection and a mailbox check that could not be run
2026-09-19 07:39:40 +02:00
Matthew Meszaros
ee46cb49e8
feat: route new Gmail and Google Workspace mailboxes through a guided three-step app-password connect over smtp.gmail.com and imap.gmail.com instead of Google sign-in, behind BOX_GOOGLE_OAUTH_CONNECT (off by default) and announced to clients as gmail_oauth_connect on /auth/config, refusing a new gmail OAuth start with 403 mailbox_gmail_oauth_disabled in both the direct and Warmbly Cloud broker paths while leaving mailboxes already connected that way sending, syncing and re-authorizable
2026-09-18 22:06:09 -07: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