Commit Graph
602 Commits
Author SHA1 Message Date
Matthew Meszaros 62e346c8e0 feat: restore cross-mailbox campaign reply tracking while preserving sent-folder safeguards 2026-09-17 06:03:56 -07:00
Matthew Meszaros b733d09f89 feat: make inbox follow-up labels safe for manual labels and automated mail 2026-09-17 04:57:52 -07:00
SUMAN JANA 3b8761d361 feat(inbox): explain every tag on hover, label live mail, and keep follow-ups working with no external service 2026-09-17 04:57:52 -07:00
SUMAN JANA dff6b42558 feat(inbox): follow-up labels for who owes whom a reply, computed in code and swept hourly 2026-09-17 04:57:52 -07:00
Matthew Meszaros dcf26a2361 feat: make automatic inbox tagging atomic live and reviewable in production 2026-09-17 04:57:24 -07:00
SUMAN JANA 056d639e2b feat(inbox): classify mid-conversation replies, which the first taxonomy had no bucket for 2026-09-17 04:57:24 -07:00
SUMAN JANA 7e556f49e3 feat(inbox): backfill historical mail through the tagger, and keep a confident kind when the intent cannot be read 2026-09-17 04:57:24 -07:00
SUMAN JANA 260898bf20 feat(inbox): automatic tagging and relevance scoring for inbound mail, optional and off by default 2026-09-17 04:57:24 -07:00
Matthew Meszaros ae1a324801 Merge pull request #562 from rocker1166/feat/direct-mail-analytics
feat: add accurate direct-mail analytics and opt-in engagement tracking
2026-09-17 11:47:50 +00:00
Matthew Meszaros 17206f1c29 Merge pull request #569 from warmbly/fix/validate-reported-defect
Fix Outlook opens misclassified as Apple privacy prefetches
2026-09-17 11:37:21 +00:00
Matthew Meszaros 59dbceaef3 feat: clarify direct-mail counts and HTML tracking limits in guides 2026-09-17 04:37:09 -07:00
Matthew Meszaros d1aa3d4361 feat: classify ambiguous Outlook and Apple image-proxy opens by delivery timing and stop inventing recipient device metadata 2026-09-17 04:27:34 -07:00
Matthew Meszaros 8745dd988d feat: correct direct-mail analytics attribution and publish live engagement updates 2026-09-17 04:27:00 -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
SUMAN JANA ebbe432d2c feat(unibox): search people and partial words, and offer to widen a search that found nothing in the current scope 2026-09-17 10:20:17 +00:00
Matthew Meszaros 2104770185 Merge pull request #559 from warmbly/fix/reopened-issue-feedback
fix: verify persisted reply direction and repair false replies
2026-09-17 05:14:21 +00:00
Matthew Meszaros 5855d7e32d feat: fully fence inbound reply processing and repair issue 549 state across campaigns and verification 2026-09-16 22:03:46 -07:00
Matthew Meszaros 0f5ca71155 feat: correct mailbox sending metrics and workspace analytics across dashboard surfaces 2026-09-16 21:44:42 -07:00
Matthew Meszaros b01984bec2 feat: verify reply direction from persisted mailbox state and repair issue 549 false Sent-folder replies 2026-09-16 21:11:54 -07:00
Matthew Meszaros baf38a4b03 Merge pull request #557 from warmbly/feature/self-host-instance-linking-upsell
Offer subscribing alongside self-hosting, and hide the self-host nudge from subscribers
2026-09-16 17:56:40 +00:00
Matthew Meszaros be78d46e28 feat: hide the self-host nudge on the hosted Accounts page for subscribed workspaces and offer subscribing alongside self-hosting on free ones in CloudPathsPanel 2026-09-16 19:53:35 +02:00
Matthew Meszaros 03f813dd43 feat: let an admin create a tester account that joins an existing workspace with an explicitly chosen role instead of always minting an empty one, so an OAuth reviewer signing in lands in a workspace that shows the app doing real work 2026-09-16 19:37:32 +02:00
Matthew Meszaros 27e9e0d7cb Merge pull request #551 from warmbly/fix/contact-activity-timeline
Fix contact activity timelines for workspace teammates
2026-09-16 15:39:48 +00:00
Matthew Meszaros 2255e2145d feat: reject outbound mailbox copies and mismatched campaign threads before they can mark contacts replied for stop-on-reply (issue #549) 2026-09-16 08:24:19 -07:00
Matthew Meszaros 6762b4e491 feat: scope contact activity timelines to the selected organization so teammates can load contacts and campaigns created by other members 2026-09-16 08:20:31 -07:00
Matthew Meszaros 4597156b6f Merge pull request #548 from warmbly/fix/dev-cloud-dashboard-server-errors
feat: fix dashboard Stripe checkout, portal eligibility, webhook recovery and audit rendering
2026-09-16 13:31:04 +00:00
Matthew Meszaros 6872ebbb23 feat: fix dashboard Stripe checkout, portal eligibility, webhook recovery and audit rendering 2026-09-16 15:23:17 +02:00
Matthew Meszaros 1fae8b18c2 feat: reject password login for external accounts without a stored hash and cover credential verification with PostgreSQL regressions 2026-09-16 15:03:21 +02:00
Suman Jana 94c7e414e2 feat: preserve private Unibox reply drafts and safely collapse quoted conversation history 2026-09-16 04:35:53 -07:00
Matthew Meszaros 31c1f101c2 feat: give EmailSentEvent and WarmupEmailSentEvent their own derived Avro schemas so the email-events and warmup-events analytics streams stop failing at serialize on every send and finally register a subject (#546) 2026-09-16 04:20:15 -07:00
Matthew Meszaros 140c7de436 feat: add the admin panel's Promo codes page and route the six /admin/discounts endpoints that existed as handlers but were never wired, so a launch offer is built in the operator UI instead of an INSERT against production, with caps that an explicit null can actually clear on PATCH 2026-09-16 04:04:09 -07:00
Matthew Meszaros 561f8ec671 feat: keep cloud and local warmup mail out of Unibox with durable verification and automatic cleanup 2026-09-16 03:55:36 -07:00
Matthew Meszaros f72d1f8d5c feat: prevent sender views from tracking opens and keep sent messages out of the default Inbox (#542) 2026-09-16 03:42:58 -07:00
Matthew Meszaros def0a5e5f1 feat: restore workspace campaign access and enforce shared analytics permissions (#543) 2026-09-16 03:31:01 -07:00
Matthew Meszaros f106c8541d feat: make the bus envelopes Avro-encodable (#535)
* feat: make both bus envelopes Avro-encodable by deriving each one's schema from a declared registry of body types, with a union branch per body and our own struct walk that skips unexported fields and honours avro:"-" before descending, so the schema describes exactly what encoding/json already puts on the wire, and narrow the two sync cursors on the wire DTOs to int64 because Avro has no unsigned 64-bit type

* feat: stop the instance health check, the config registry and the docs all claiming Avro cannot serialize a worker envelope, which stopped being true once the envelopes carried a declared union, and check the one thing that is still a real misconfiguration instead: avro selected with no SCHEMA_REGISTRY_URL to resolve against

* feat: emit a reference the second time a record appears in an envelope schema instead of defining it again, because Avro names a record once and a document that defines warmbly.events.Token three times is rejected outright, and keep the Schema Registry round-trip as a skip-by-default test since only a registry judges the document rather than the objects it was built from

* feat: carry uint64 as Avro fixed(8) rather than long, which lets the sync cursors keep their unsigned type instead of being narrowed, name every event field after its json tag so the schema and the JSON wire agree, and populate every field in the round-trip test because zero values are why a uint64 mapped to long passed in the first place

* feat: frame Avro in Confluent's wire format and encode through hamba's default API instead of going through avrov2, whose private avro.API holds a type resolver avro.Register cannot reach, so a union body failed there with unable to resolve type while encoding cleanly against the same schema, and keep the registry round-trip as a skip-by-default test
2026-09-15 10:50:30 -07:00
Matthew Meszaros ae012dd13f Clear the live error-tracking issues, and the workspace rename that renamed the wrong workspace (#533)
* feat: stop a managed Kafka cluster refusing topic creation from failing the publish, by treating a topic- or cluster-authorization failure from CreateTopics as a topic the cluster owns rather than one that is missing, which on Confluent Cloud dropped every warmup event and filed one issue per message because the topic never became known

* feat: drop a report whose error is a cancelled context in errs rather than at ninety call sites, so a browser navigating away or a container draining on deploy stops filing one issue per query that happened to be in flight, while a deadline this process set and blew through still reports

* feat: stop renaming one workspace from renaming another, by keying the workspace settings editor on the workspace id so a switch re-seeds the name field instead of leaving the previous workspace's name against the new workspace's autosave baseline, and pinning every save on the workspace, sending and tracking pages to the workspace its draft was hydrated from

* feat: drop Script error. and the ResizeObserver notice on the marketing site and the hosted form page the way the dashboard and admin panel already do, since those two carry no stack and no bug and between them were the largest issues in error tracking, all of it from warmbly.com

* feat: rename the forms Turnstile script module to turnstileScript.ts so it no longer differs only in case from the Turnstile.tsx component, which resolved both imports to one file on a case-insensitive filesystem and failed forms' typecheck with TS1149

* feat: upload source maps from the static build:pages build as well as the image build, so the dashboards served from a static host stop reporting every stack frame as a minified name beside 'Invalid source map: bad json', which is PostHog falling back to fetching the .map from a host that answers with its SPA fallback

* feat: build every admin list in pg_admin.go with make rather than declaring it nil, so an empty page serializes as [] instead of null, and guard the audit table's own empty check, which is what crashed admin.warmbly.com/audit with 'null is not an object (evaluating d.data.length)' whenever a filter matched nothing

* feat: match the whole broker description rather than a substring when deciding a topic create was refused for permissions, since that answer remembers the topic as present, and clear the cached promise and dead tag when the forms Turnstile script fails to load so a blocked first attempt no longer leaves every later mount with the same rejection and the captcha permanently missing
2026-09-15 09:05:53 -07:00
Matthew Meszaros 853dd29474 feat: answer the forms host's bare root with a short styled page instead of gin's plain-text 404, take over every other unknown path with the same page shape while /api keeps answering JSON, cover both in internal/formserver tests, and document what a visitor who types the forms domain sees 2026-09-15 13:39:05 +02:00
Matthew Meszaros 2f15fecaad feat: document the MaxMind download endpoint that a current licence key actually authenticates against, account id as the username and key as the password in the URL userinfo, because the app/geoip_download permalink every guide still shows refuses a key issued today with Invalid license key (#530) 2026-09-15 03:37:14 -07:00
Matthew Meszaros 4784ee7d39 feat: fetch the MaxMind databases instead of requiring a mounted file (#529)
* feat: let the backend, consumer and tracking service fetch their own MaxMind databases from GEODB_URL and TRACKING_SCANNER_ASN_DB_URL, reading the archive shape from the content so a permalink tar.gz, a gzipped mmdb and a bare mmdb all work, never replacing a file already at the path, opening the bytes before installing them so a licence-key error page cannot become the database forever, skipping the AppleDouble sidecars a macOS tar writes ahead of the real file, and treating both URLs as secrets because the permalink carries the licence key

* feat: drop the trailing blank line cargo fmt --check rejects at the end of tracking/src/asndb.rs

* feat: stream the downloaded ASN archive instead of decompressing it whole, sizing each buffer from the gzip footer and the tar header so the member is allocated exactly once, which drops the peak of unwrapping a permalink tar.gz from 38 MB to 11.9 MB, essentially the database itself

* feat: stop the MaxMind licence key reaching the logs through net/http's and reqwest's own error text, which both print the URL they were given and so defeated the redaction beside them, drop userinfo as well as the query when redacting, refuse plain http for a URL carrying a credential and refuse an https-to-http redirect, and apply the size cap to the decoded database rather than the compressed transfer so a gzip bomb cannot fill the disk

* feat: strip basic-auth userinfo as well as the query when the tracking service redacts its database URL, parsing it rather than cutting at the first question mark so where a credential sits is the URL library's problem and not a guess
2026-09-15 03:06:34 -07:00
Matthew Meszaros e67b13e57e feat: stop reporting a mailbox's DKIM as missing when its selector was simply never probed, by deriving candidate selectors from the sending domain's own SPF and MX records on top of a wider default set, reporting a miss as the tri-state dkim_status undetermined rather than a red Missing row in the drawer, dropping DKIM from the Advisor's missing-records finding entirely, refusing a revoked p= key, holding the summary back from accusing anything when DNS never answered, and fixing the CLI auth-check table whose columns read mailbox fields the endpoint does not return (#528) 2026-09-15 02:27:38 -07:00
Matthew Meszaros 0a1ed6f04e feat: resolve scanner ASNs from a GeoLite2-ASN database so the catalogue's asn: entries match without a Cloudflare transform rule, ship the Proofpoint, Mimecast and Cisco ASNs enabled behind a new probable certainty that widens the consumer's machine window instead of deciding the verdict, make the tracking event dedupe claim one coalesced operation, and report an ASN database that opened cleanly but resolves nothing (#440) 2026-09-15 01:48:45 -07:00
Matthew Meszaros dd98187231 fix: shorten recipient unsubscribe links to 22-character, 128-bit stored tickets (#498) (#525)
* feat: shorten every recipient unsubscribe link from a 96-character signed token to a 22-character stored ticket carrying 128 bits from crypto/rand, minted once per recipient per campaign and reused by every step, so the address the text/plain half of a cold email prints in full fits on one line and cannot be guessed, keeping the signed form working for links already in inboxes and as the fallback when the store cannot be written, and answering a failed lookup with a retryable 'try again shortly' instead of telling the recipient their opt-out is invalid (issue #498)

* fix: restore the disabled-signer guard in URLOn, which factoring the URL builder moved behind a token mint that dereferences the signing key, so a nil or origin-less signer returns the empty string every caller reads as 'no link can be minted' instead of panicking (PR #525 review)
2026-09-15 00:42:45 -07:00
Tung Lam dd4234980b fix: reconcile expunged IMAP drafts so a Gmail autosave replacement stops leaving duplicate copies in a thread, by diffing each drafts folder's live UID set against the rows the backend holds for that UIDVALIDITY generation and removing the ones the server no longer reports, only in drafts and only when the selected generation still matches the listing (issue #516) 2026-09-15 00:00:20 -07:00
Matthew Meszaros 95885fa714 fix: save a contact's edited email address (#511) (#521)
* fix: save a contact's edited email address by giving models.UpdateContact the Email field the dashboard was already sending, normalizing it to a bare lowercased address, refusing one another contact in the workspace holds with a 409 contact_email_taken instead of letting the unique index 500, and dropping the verification verdict plus the delivery evidence that belonged to the old mailbox (issue #511)

* fix: reset a contact's verification evidence behind a watermark the delivery-credit job honours, clear the cached esp_provider the old domain produced, write a case-only edit in place instead of answering 200 and changing nothing, and normalize the address on create and import too so the two paths cannot disagree about what an address is

* fix: refuse a verification observation whose campaign step provably left before the contact's address was edited, so a hard bounce or an open for the old mailbox arriving afterwards cannot mark the corrected address invalid or valid, read the credit watermark against dispatched_at rather than the sent_at a worker result stamps later, and answer a racing address collision with the documented 409 instead of the unique index's 500

* fix: build the deliverability evidence step from the resolved campaign task so both halves name one real row, drop the email format hint from the OpenAPI schema now that the endpoint also accepts a display-name address, and drain the shared delivery-credit backlog in the live test instead of assuming one pass reaches this contact
2026-09-14 23:05:16 -07:00
Matthew Meszaros 50711a8e66 fix: a campaign's linked segments are its audience, so detaching one withdraws its leads (#510) (#523)
* fix: make a campaign's linked segments the audience rather than an accumulator, so detaching one withdraws the leads it enrolled instead of leaving the old list mixed in with the new, tracked by a new campaign_leads.source that keeps a hand-picked lead, an overlapping segment's member and anyone the campaign has already emailed out of the withdrawal, and reported back as withdrawn/contacted counts the dialog confirms and explains (issue #510)

* fix: serialize the linked-segment sweep against a link replacement by taking the same campaign lock, so a sweep that read the old set cannot re-enrol the audience the replacement just withdrew, and word the dialog's confirm and toast so the campaign, not the segment, is what has already emailed a lead

* fix: stop the one-shot segment enrol from re-stamping leads that are already in the campaign, since the Leads tab's Add back runs through it and pinning a whole linked audience as hand-picked because one held-out member was restored is the accumulation this change exists to end

* fix: lock the leads a detachment is about to withdraw before deciding, because a send is reserved by stamping campaign_contact_progress and only then locking the lead row, so a reservation committing mid-pass was invisible to the delete's snapshot and could withdraw a lead whose first email had already gone; and report the already-emailed count on every toast branch, since a detach where the whole audience had been emailed changed no count and said nothing after confirming a removal

* fix: add the campaign_leads.source check constraint NOT VALID, which still enforces every insert and update while skipping a full scan of the largest table in the product under ACCESS EXCLUSIVE to learn that a one-statement-old column holds its own default everywhere
2026-09-14 22:12:15 -07:00
Matthew Meszaros e18f2efc05 feat: hold an out-of-office contact in every campaign they are a lead of instead of only the one the auto-reply was attributed to, through a new HoldLeadEverywhere applying the same per-row guard so a member's own pause and a longer running hold both survive, and leaving completed campaigns alone (issue #518) 2026-09-14 21:54:05 -07:00
Matthew Meszaros 619eb2729a fix: read a mailbox's Gmail send-as addresses from the worker holding it rather than from the backend, which was decrypting a mailbox credential in the control plane and showing Google a second client address for a mailbox whose mail moves through a worker, by round-tripping a new MAILBOX_IDENTITY command answered on the process channel like a credential validation, leaving the OAuth handshake as the one place the control plane still calls the provider (#522) 2026-09-14 21:28:07 -07:00
Matthew Meszaros 1ca3bd19b3 feat: carry a unibox read or unread change out to the mailbox itself through a new MESSAGE_SEEN worker command, so a conversation read in Warmbly stops showing bold in Gmail, Outlook and IMAP, relaying the state the row holds rather than the one the request asked for, only for messages that actually changed, dispatched detached from the request and never retried (#515) 2026-09-14 21:20:42 -07:00
Suman Jana 20a56dc41b feat: add a full-screen toggle to the unibox compose window that lifts the same composer to the centre of the screen over a dimmed backdrop, collapses back to the corner on a backdrop click or Escape before a second Escape closes the window, and document it on the unibox composing page (#503) 2026-09-14 21:14:08 -07:00
Matthew Meszaros 25e128c409 fix: stop the form builder canvas tearing apart when fields are reordered by dropping dnd-kit's rectSortingStrategy, whose scaleX/scaleY are the ratio of two mismatched field rects and stretched every sibling out of the card, for a canvas that holds still behind an insertion caret, plus a real end-of-form drop target so dragging the first field to the bottom lands it last instead of resolving to the pane-sized canvas droppable and doing nothing (issue #497) 2026-09-14 20:59:10 -07:00