Commit Graph
1248 Commits
Author SHA1 Message Date
Matthew Meszaros fb453921db feat: keep a failed message-map rollback pending on the mailbox and retry it at the start of every IMAP, Gmail and Graph sync pass, skipping the pass until it succeeds so an unpublished arrival is never read as known 2026-09-22 04:59:27 -07:00
Matthew Meszaros 9009c6ec24 feat: build inbound bounce and complaint reports before NEW_EMAIL but publish them only after it succeeds, so a message re-offered after a failed arrival publish does not apply its deliverability report twice 2026-09-22 04:49:26 -07:00
Matthew Meszaros 4a6f0c17c7 feat: advance each IMAP folder's sync cursor to the SELECT view its search ran against instead of the earlier LIST-STATUS, so Sent copies appended between the two are no longer skipped, and drop the message-map entry when NEW_EMAIL fails to publish so an unpublished message is re-offered instead of read as known (#645) 2026-09-22 04:39:18 -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 ff2204f8ef feat: route the sso_link completion through the one login path (completeLogin) so the ban check, the 2FA gate, login recording and device memory apply before a parked identity is attached, carry the session provider into every 2FA challenge so a Google or Apple sign-in on a 2FA account is recorded as such, make IdentityRepository.Link report a pair another account holds as ErrIdentityTaken instead of silently updating nothing, refuse a spent address budget before charging a link try, drop the dead expiry check and the duplicate link fields on the web Session model 2026-09-21 03:57:29 -07:00
Matthew Meszaros e0db7d3c72 Merge pull request #642 from warmbly/fix/reset-token-invalidation-after-password-change
feat: refuse a password reset link issued before the password was last changed
2026-09-21 10:50:49 +00:00
Matthew Meszaros 4578980b34 feat: fall back to My Organization when the first name is blank in displayname.DefaultWorkspace, and only treat a scheme as a link when a non-space follows its colon so names like Big Data: EU pass in both the Go and web validators 2026-09-21 03:42:17 -07:00
Matthew Meszaros a1b7a8ad9b feat: attach a parked federated identity only after the ban check and, on a 2FA account, only once the second factor passes by carrying it through the 2FA pending record into twofa.VerifyLogin, charge each sso_link password attempt atomically before the check, treat an identity a parallel challenge already linked as a re-login instead of a refusal, ask for no password when the identity cannot be linked, end an exhausted or expired challenge with sso_link_expired so the dashboard returns to the email step, and document the code in error-codes, the API reference and OpenAPI 2026-09-21 03:35:17 -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 36eb5e72e9 feat: stamp users.password_changed_at on every password write and refuse a password reset link issued at or before it, so a link requested earlier dies when the password is changed from settings, by another reset link or by warmblyctl, with the rule documented on the reset endpoint and the security guide 2026-09-21 03:23:14 -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 f11df9268f Merge pull request #640 from warmbly/fix/admin-list-pagination
feat: make every admin panel list page past the first and filter by id, and report failed admin requests
2026-09-21 09:29:48 +00:00
Matthew Meszaros 4dde9708c9 feat: honour an ascending created_at sort on the admin users list with a matching id tiebreak, and record an admin API failure whose call omitted the method as GET, the method axios sent 2026-09-21 02:24:36 -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 3ea6118a27 feat: redeliver a warmup retention delete when the mailbox is not loaded on the worker, drop the stored body when the IMAP message is already gone on a redelivery while returning a search failure rather than treating it as absence, and encode the accepted warmup_retention_days range (0, or 3 to 3650) in both OpenAPI schemas 2026-09-21 01:21:28 -07:00
Matthew Meszaros 0a5e7947b4 feat: return a failed warmup retention delete from the worker so the bus redelivers it up to five times, re-key a Graph message in the map on every move so the sender copy's body can be dropped, never expunge a whole IMAP folder for one message (UID EXPUNGE, else MOVE to Trash, else refuse), build the two retention indexes concurrently in their own migrations 000193 and 000194, keep the dashboard stepper off 1 and 2 days, and describe retention as applying wherever the placement files warmup mail 2026-09-21 01:11:22 -07:00
Matthew Meszaros 1513419a2a feat: delete warmup mail from each mailbox once past a per-mailbox retention window (email_accounts.warmup_retention_days, else retention.warmup_mail_days, default 30) via a consumer sweep that retires the receipt and sender copy and a worker delete action that trashes on Gmail, deletes on Graph, expunges on IMAP and drops the stored body, prune per-message warmup records after retention.warmup_event_days, and count a warmup deletion as tampering only within 24 hours of arrival and never for a retired message, judging Gmail's Trash label on the same rule 2026-09-21 00:52:02 -07:00
Matthew Meszaros bb96cfb030 feat: assert the quarantine term in the tampering-versus-rates band tests and require the expiry to be nil or set on both sides before comparing 2026-09-20 10:53:51 -07:00
Matthew Meszaros 0ea00926c9 feat: apply the warmup inbound cap inside the candidate query before the tier is sized or sampled and count mail dispatched today alongside verified arrivals, judge the tampering band apart from the rate bands and keep the more severe finding, and bound received analytics by UTC instants instead of a session-timezone date cast 2026-09-20 10:15:33 -07:00
Matthew Meszaros 26594391c8 feat: judge tampering with received warmup mail on a ladder inside the health bands, one deletion warns, two pause for seven days and four or two spam flags block for thirty, instead of a review-required block on the first deletion (#635) 2026-09-20 09:50:42 -07:00
Matthew Meszaros d6384d3c0e feat: make cross-tier warmup an exchange so a proven free mailbox writes back to the paying mailboxes that wrote to it, favour the inbox owed the most on every draw, cap what any inbox receives per day inside WarmupPartnerCandidates so a thin tier is neither starved nor flooded, and surface received counts in the mailbox drawer, warmup analytics and the API (#633) 2026-09-20 09:42:53 -07:00
Matthew Meszaros 6026168334 feat: stop blocking boot on the GeoIP download and swap the database in when it lands, retry a refused mirror with backoff honouring Retry-After, revalidate a database older than a week with If-Modified-Since instead of keeping the first copy forever, and add a twice-weekly job mirroring both MaxMind editions to a private bucket so the fleet stops spending a 30-a-day allowance per boot 2026-09-20 17:55:38 +02:00
Matthew Meszaros a5136f5bf6 Merge pull request #631 from warmbly/fix/inbox-awaiting-reply-actions
feat: make Archive leave every working unibox view, fix Awaiting reply's address match, and add row and multi-select triage actions
2026-09-20 14:24:42 +00:00
Matthew Meszaros 78fe43b8d2 feat: widen unibox_emails.mailbox to bigint so a folder whose UIDVALIDITY is at or above 2^31 can have its mail filed and listed instead of failing to bind against int4, finishing the widening 000179 started 2026-09-20 16:18:14 +02: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 1e11d90cae Merge pull request #628 from warmbly/fix/unibox-seen-follows-provider
Follow the provider's read state in the unibox
2026-09-20 14:01:05 +00:00
Matthew Meszaros 48ecca2400 feat: follow the provider's read state in the unibox by storing seen from the \Seen flag on every IMAP, Gmail and Graph arrival, carrying read-state changes onto unibox_emails.seen in the FLAGS_ADD/FLAGS_REMOVE and UPDATE_EMAIL handlers, and backfilling existing rows from their flags in 000190 2026-09-20 06:54:29 -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 4e37b968a2 feat: say in the mailboxes guide and the submission dialer comment that a refusal or an unresolvable name arriving before 587 is dialled is returned as is while a later one lets a connecting 587 be used, instead of claiming 587 would fail the same way 2026-09-20 13:25:46 +02:00
Matthew Meszaros c20d1c99f2 feat: race a 587 STARTTLS dial against a mailbox's silent port 465 on every send and connect check so the fleet keeps sending where outbound 465 is blocked, store a connect that passed that way with 587, name the port actually used in a refusal, make the Google app-password hint say Google itself refused the pair and name the alias and wrong-account causes, and render long error toasts wide, dismissable and longer-lived instead of a narrow four-second column 2026-09-20 13:16:52 +02:00
Matthew Meszaros 392bcc0478 feat: run the mailbox credential check off the worker's bus loop so it no longer waits behind queued sends and mailbox loads until the backend's fourteen-second wait expires, have the worker always answer with an error verdict when it cannot unseal the credentials so an untested mailbox is a server error rather than a mail-server timeout, name the leg that stayed silent and say whether the other one signed in with a 587 hint when 465 hangs, word the no-reply case as the worker not reporting back, dial both probes from WORKER_BIND_IP like the send and sync clients, and connect Gmail app-password mailboxes over 587 with STARTTLS because many hosts block outbound 465 2026-09-20 01:54:12 -07:00
Matthew Meszaros b728fff132 Merge pull request #619 from warmbly/feat/typesafe-judgments
feat: TypeSafe judgments across the product, with inbox tagging that acts and works out of the box
2026-09-20 07:41:39 +00:00
Matthew Meszaros abe6157d37 feat: tighten the mailbox connect probes after a self-review, turning every socket error and not only a failed dial into closed words so a net.OpError naming the worker's bound address never reaches a customer, keeping a server reply's reason when the deadline passes while it is read instead of reporting a refused password as a timeout, connecting TCP before the TLS handshake on implicit-TLS SMTP so a port that answers and then fails the handshake reads as a TLS problem on both legs, sending EHLO explicitly because net/smtp's Extension swallows a failed greeting and reported a server that hung up as a mailbox with nothing to verify, and widening the backend's wait to fourteen seconds so the worker's seven-second budget plus the bus fits inside it 2026-09-20 00:21:31 -07:00
Matthew Meszaros 8d3fa5fe01 feat: address the review on the mailbox connect verdict by describing a failed dial in closed words so a Go dial error naming the worker's own bound address never reaches a customer, counting only SMTP 534 and 535 and a tagged IMAP NO as a refused sign-in while a BAD, a 504 or a 530 is reported as the conversation failing, classifying an IMAP LOGIN refusal before the LOGOUT goes out and not waiting for its answer, bounding the worker's unseal plus probes to seven seconds so the verdict always lands inside the backend's nine-second wait, and saying in the docs that the message quotes the server only when it answered and that a different password adds no auth mechanism 2026-09-20 00:08:16 -07:00
Matthew Meszaros d1a4add567 feat: add the new CHECK constraints NOT VALID and validate them in migration 000189 so campaign_leads is not scanned under a write lock, keep a step's copy-judgment findings open when the Advisor's judge cap or an outage left it unjudged, validate every TypeSafe answer's type and bounds and version the copy-judgment cache key, mirror the reads-as-bulk rule in the editor's all-clear, and refuse inbox classification without a configured client 2026-09-19 23:59:19 -07:00
Matthew Meszaros c8162966a3 feat: tell a person connecting a mailbox what the mail server actually said instead of "invalid credentials" for everything, by having the worker's SMTP and IMAP probes classify a refused sign-in, an unreachable host, a failed TLS handshake, a retry-later reply and a timeout and publish that verdict as JSON ahead of the legacy digit, mapping it on the backend to mailbox_auth_refused, mailbox_unreachable, mailbox_tls_failed and mailbox_server_declined with the server's reply and a Gmail app-password hint in the message, starting the probe budget after the credentials are unsealed and bounding the SMTP conversation so a silent server no longer parks the worker, and normalizing passwords on every connect path so a Google app password pasted with its spaces works from the form, the CSV import, the API and the re-authorize dialog alike 2026-09-19 23:53:40 -07: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 3147804c24 Merge pull request #618 from warmbly/fix/pool-default-never-applied
fix: make the documented 25-connection pool default real instead of the host's core count
2026-09-19 18:39:49 +00:00
Matthew Meszaros 53f6df655a feat: make the documented 25-connection pool default real, because pgxpool fills MaxConns with the host's core count before we see it so the guard meant to apply our default never once fired and production quietly ran 48 connections per process against a 76-connection server 2026-09-19 20:34:35 +02:00
Matthew Meszaros f9db4f435f feat: actually deactivate a mailbox whose credentials have died, by writing its status against the mailbox id instead of through the organization-scoped update that was being handed the owner's user id and so matched no row, and stop labelling a mail error's own uuid as an email account in the message 2026-09-19 20:23:17 +02:00
Matthew Meszaros 4847b5cc57 feat: never let the connection clamp hand out more than the server's own share, since a comfort floor of ten on a twenty-connection database would have promised forty, count the phase offset toward a non-boot job's recorded next run so the panel does not show it overdue, and say in the configuration docs that the quarter assumes four clients and applies only when the probe answers 2026-09-19 20:08:11 +02:00
Matthew Meszaros af8b551d28 feat: stop exhausting the database's connection slots by clamping each process's pool to the share of max_connections the server actually leaves free, returning pooled connections after a minute instead of holding the high-water mark for thirty, and giving every scheduled job a fixed place in a 45-second window so the nine hourly loops stop opening a connection in the same instant 2026-09-19 19:47:56 +02: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 d667c1dacd feat: keep the send plan honest at the edges: a behaviour mailbox reopening after the window closes is closed for the day, only a mailbox coming back on its own (spent, closed, spaced out) holds its bound leads as waiting, the min gap is charged from when the mailbox is next open, an end date later today ends the day there, explainCap is stagedCap's last stage so there is one clamp chain, last-send times and warmup health are read for the pool in one query each, the plan cache is keyed on the campaign's status and updated_at so a start or an edit is answered fresh, and the workspace posture row links to /app/deliverability 2026-09-19 09:58:52 -07:00
Matthew Meszaros 89daae3f29 feat: make the send plan count a lead bound to a spent mailbox as waiting for it (leads.waiting_on_sender), charge a behaviour profile's spent budget and hourly ceiling to the plan rather than to hours or spacing, fold a foreign-timezone mailbox's 8pm close into its pacing, report the UTC budget day and keep the waterfall adding up when a cap was lowered after sends, read the pool's sends today in one query and cache the plan and workspace capacity for a few seconds, share one cold-ramp notice builder between the drawer and the plan, let the wizard estimate survive a counter miss, and stop a malformed plan payload from taking the campaign overview down 2026-09-19 09:45:03 -07: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