Commit Graph
339 Commits
Author SHA1 Message Date
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 def0a5e5f1 feat: restore workspace campaign access and enforce shared analytics permissions (#543) 2026-09-16 03:31:01 -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 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 ffd27bc46d fix: stop every out-of-office notice and bounce opening a high-priority CRM follow-up by classifying machine replies from their headers and gating the task on a per-intent setting, and give the Tasks page multi-select with select-all-matching, bulk status, priority and delete over new PATCH and DELETE /crm/tasks endpoints (issue #471) 2026-09-14 12:20:47 -07: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 13e9ce8e10 feat: hold a lead whose mailbox answers out of office until they are back, resuming at the return date it names, plus a manual per-contact pause in one campaign that unsubscribing and the suppression list were the only stand-ins for 2026-09-14 09:26:06 -07:00
Matthew Meszaros adfe4c17aa Self-hosted pool plan: a price the server resolves, and a checkout that reaches it (#494)
* feat: make the self-hosted pool plan buyable by resolving its Stripe price server-side behind a new /pool-link/offer and /pool-link/checkout pair, adding the plans.price_yearly column the yearly price id never had, and landing the instance's Unlimited button on a dialog that names the workspace and the billing period instead of a plans grid the non-public plan never appears in

* feat: apply the pool dialog's yearly default once per opening rather than on every offer result, so a background refetch cannot move the billing period out from under someone who already chose monthly
2026-09-13 21:22:12 -07:00
Matthew Meszaros 6b6efca865 fix: campaign follow-ups opened a new conversation instead of replying in the contact's thread, so carry In-Reply-To/References and the Gmail threadId from the previous send, give every step a reply-in-thread switch, and let a threading step inherit the conversation's subject (issue #472) (#489) 2026-09-13 20:51:41 -07:00
Matthew Meszaros 1dc4aedc3c fix: retire the warmup invalid-token band with its table, metric query, service and repository methods and admin tab, since nothing has fed it since #481 and no attributable forged-token signal exists; key the live pool fixtures on the canonical pool ids so the warmup, repository routing and tasks routing suites run on a fresh database, and correct every doc, site and advisor line that still described the retired signal or a spam-score threshold nothing implements (#490) 2026-09-13 08:09:01 -07:00
Matthew Meszaros 43dcbde06c feat: tester accounts, creatable from the admin panel (#483)
* feat: excuse one named account from the emailed login code, so a vendor reviewer who cannot read this instance's mail can sign in without turning codes off for everyone, with the reason recorded beside it and every run of warmblyctl status naming the accounts that hold one

* feat: create and manage tester accounts from the admin panel, so letting a reviewer in is a form rather than a shell, with the password shown once and every live exemption listed on one page because forgetting one is the way this goes wrong

* fix: give tester management its own permission bit rather than borrowing ban_users, create the account and its exemption in one transaction so no invisible orphan survives a failure, require an accountable operator on the CLI grant, stop a halted row scan reading as the whole exempt list, and show a failed query as an error instead of as no testers

* chore: re-run CI after the aggregator tripped on a cancelled job from the branch update, with every underlying job green

* chore: retrigger CI, the previous run sat queued indefinitely while other branches ran

* feat: roll back a half-created tester when its workspace step fails and backfill the manage-testers bit onto admins already holding every other permission, so the address is not left taken by an unusable account and the new routes are not 403 for the existing admin

* feat: make the 000151 manage-testers backfill one-way, because clearing bit 22 on the way down would also revoke it from an admin granted it explicitly afterwards and the up migration would not restore that
2026-09-13 03:07:10 -07:00
Matthew Meszaros 017f4cf60f feat: plans an operator can grant, visible in the admin panel (#467)
* feat: add operator-granted plans so a workspace can be paid without Stripe, surfaced in the admin panel as a badge, a filter and a card carrying who granted it and why, because the only alternative was writing a fake stripe subscription id into the database

* fix: hold a granted plan beside the paid one rather than over it so a Stripe workspace returns to the plan it pays for when the grant ends, route entitlement lookups through EffectivePlanID, separate a repository failure from an unknown plan, end a grant at local end of day, and drop an index that served no query
2026-09-12 09:45:31 -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 b0050507e4 fix: tell a joining node a tag that exists, falling back to the published prod tag instead of a latest this project has never published, and drop the WORKER_IMAGE docs left behind by the removed push-based provisioning (#455) 2026-09-12 01:00:11 -07:00
Matthew Meszaros f41eac289d Merge branch 'main' into fix/issue-432 2026-09-11 22:35:43 -07:00
Matthew Meszaros 069ab15194 feat: address the second review pass on the Edit with AI fix by reading a markdown destination with balanced parentheses so a link the model normalises out of the angle-bracket form it was given comes back whole instead of truncated at the first paren with a stray one left in the copy, restoring only spaces and tabs around the model's answer so a selection that ran to the start of the next paragraph joins the two the way a paste would rather than gaining a blank paragraph nobody typed, and capping the completion at a flat 4096 tokens, above the 3072 it was and below the smallest completion limit in common use, because a request over a backend's own cap earns a 400 naming max_tokens that openAIProvider.adaptParams latches for the life of the process and degrades every later call 2026-09-11 21:21:40 -07:00
Matthew Meszaros 131e9ff093 feat: give the JetStream stream a size ceiling from NATS_MAX_BYTES, accepting a byte count or a size like 2GiB, because a managed account can require every stream to declare one and Synadia's Max Bytes Required rejects creation without it, turning that refusal into an error naming the variable to set, and fix the retry path which passed a zero max age and would have recreated the stream with no age limit after a failure 2026-09-12 05:40:43 +02:00
Matthew Meszaros f1f5249153 feat: authenticate to NATS with a user JWT and nkey seed so a managed bus like Synadia Cloud can replace a self-run one, taking the credential from a file path for containers and from a single-line base64 value for the fleet, because a node receives environment variables rather than files and the env file docker reads cannot express the multi-line credentials format, in both the Go event bus and the Rust tracking publisher 2026-09-12 05:29:09 +02:00
Matthew Meszaros 1bdd1da800 feat: address the CodeRabbit review on the Edit with AI fix by leaving the caret after text written at a collapsed position instead of in front of it (an insertion maps to itself unless the position associates rightwards, so continuing to type went before the insert), splitting the model's blocks one separator at a time so a blank paragraph the author used as spacing survives a rewrite instead of being swallowed by a greedy newline run, carrying a link destination that holds a paren or a space through in markdown's angle-bracket form rather than dropping the link, putting the author's boundary whitespace back on the model's trimmed answer in both hosts so a selection ending on a space does not glue the rewrite to the next word and "did anything change?" compares exactly what was written, and sizing the completion cap from the passage's own rune count so an 8000-rune CJK body is not truncated by a cap chosen for English 2026-09-11 20:23:53 -07:00
Matthew Meszaros 6501cb599c feat: fix the "Edit with AI" rewrite in the campaign body and the unibox composer for issue #432 by running /generation/edit on a new generation.BuildEditRules system prompt through AIProvider.Complete instead of the cold-outreach writer prompt that redefined the model's role, capped it at 80 words and imposed a five-part email skeleton on every instruction, raising the completion cap so a full-body rewrite is no longer truncated at 1024 tokens and counting the request limits in runes rather than bytes, carrying merge variables, AI blocks, conditionals, form links and link destinations through the round trip in web/src/components/app/ai/richTextPassage.ts instead of deleting every atom node via doc.textBetween, replacing the passage with paste semantics so a phrase rewritten inside a sentence stops splitting its paragraph into three, saying "No change" when the model hands the passage back untouched, and clamping the floating AI card to the surface it is editing so it no longer draws outside the step drawer over the flow canvas 2026-09-11 20:04:32 -07:00
Matthew Meszaros 0e8a05b6df Merge remote-tracking branch 'origin/main' into fix/scanner-timing-window 2026-09-11 04:02:06 -07:00
Matthew Meszaros da4b89da0b feat: split a unibox message's provider placement into its own provider_folder column (migration 000146) so Archive and Delete in the thread header survive the next sync without the sync losing the ability to follow a real provider move, and narrow PATCH /unibox/folder to inbox/archive/trash behind the unibox feature gate with an audit entry so the move reaches every teammate's list live 2026-09-11 03:23:19 -07:00
SUMAN JANA 727ddb1482 feat: wire the unibox thread header's Mark as unread, Archive and Delete to a new PATCH /unibox/folder, add an Add as contact action for senders outside the CRM, and replace six private From-header parsers with one shared lib/helper/emailAddress that also understands the parenthesised form the IMAP sync stores, which left the reply composer's seeded To failing its own validator 2026-09-11 03:10:18 -07:00
Matthew Meszaros 184a3dc08e feat: make the automated-open and automated-click windows operator-editable under Instance settings and raise their defaults to 60s and 30s, because the ten-second window was anchored on dispatch to the worker rather than on delivery and routinely expired before the recipient-side gateway it was meant to catch had even seen the message, and add Barracuda's published Email Gateway Defense blocks to the scanner catalogue with Proofpoint, Mimecast and Cisco shipped commented out because browser isolation renders a clicked page from the vendor's own network 2026-09-11 02:53:34 -07:00
Matthew Meszaros 1866c45c67 feat: serve a PostHog reverse proxy at /ingest on the backend so the dashboard, admin panel and marketing site can report analytics and errors through this instance instead of posthog.com, which content blockers drop for a large share of visitors, splitting asset traffic to the bundle host because sending it to the ingestion host 404s, withholding the caller's cookies and Authorization from a third party, and preserving the trailing slash that path cleaning removes and PostHog's capture endpoint needs 2026-09-11 06:02:21 +02:00
Matthew Meszaros a84ab48729 Merge branch 'main' into feature/posthog-error-tracking 2026-09-10 10:25:21 -07:00
Matthew Meszaros ced741e352 feat: make PostHog the default error tracker across every runtime while keeping Sentry fully supported alongside or instead of it, by turning internal/observability/errs into a two-sink fan-out with a local-log fallback, adding $exception capture to the Go services, the Rust tracking service, the Elixir realtime service and the dashboard, admin and form apps, reporting gin panics with their route, request id, workspace and user, attaching that identity plus a route and failed-request trail to browser exceptions, and wiring POSTHOG_ERROR_TRACKING, the node join env, compose, source-map upload and the docs to match 2026-09-10 19:11:32 +02:00
Matthew Meszaros d51de7db3a feat: address the CodeRabbit review by quoting a fragment in the copy's own casing rather than the model's retyping of it, extracting the case-fold offset map into internal/pkg/casefold so the AI half gets the same Unicode safety the rules half has, giving a trigger term a span in each half it appears in instead of losing the second one to deduplication, scanning subject links before body anchors so the display cap cannot drop the subject's own, requiring WRITE_TEMPLATES on the credit-spending analyze route so a read-only key cannot spend the workspace balance, refusing to tell a customer their credits came back when the refund is what failed, and no longer letting a stale analysis retire the newer rules request that was about to replace it 2026-09-10 09:50:28 -07:00
Matthew Meszaros 331db196d8 feat: locate every content-check issue in the subject or the body with the exact fragments that caused it and a one-line fix, add POST /templates/analyze running the configured LLM over a campaign template for located spam findings quoted verbatim from the copy plus a rewritten subject and an overall score, verify every model quote against the draft so an invented sentence is dropped rather than shown, pin the analysis temperature so re-checking unchanged copy returns the same number, and give the editor panel a Re-check button that re-runs both passes and reports the movement since the last check 2026-09-10 09:28:00 -07:00
Matthew Meszaros 396bab0e06 Merge branch 'main' into feat/issue-414-delete-api-key 2026-09-10 05:36:06 -07:00
Matthew Meszaros 510ee692ba feat: address the review on the split-deployment branch by moving the two broker routes onto their own NODE_BROKER_TOKEN so the internet-facing tracking and forms services no longer hold a credential that can open any organization's data key, refusing to presign any key outside the prefixes a node reaches, fixing IAM policies that named an alias ARN KMS never resolves in a Resource element, bounding both brokered HTTP clients because the sync loop's context never expires, no longer reporting a 403 from the object store as a missing body, and redacting the DSN and URL credentials the dry-run listing printed in clear 2026-09-10 14:19:53 +02:00
Matthew Meszaros 9e37ea73a8 feat: add a permanent delete for API keys, DELETE /api-keys/:id/permanent plus a Delete key button under a revoked key in the dashboard drawer and warmbly key purge, taking the key's usage logs with it and refusing any key that could still authenticate so revoking stays the step that records why a credential ended (issue #414) 2026-09-10 05:02:05 -07:00
Matthew Meszaros 47ba13083e feat: make a split deployment work end to end by fixing the three defects that made an off-host node impossible to configure (nodeEnvKeys shipped S3_BUCKET and KMS_KEY_ID, which nothing reads, so an AWS-backed node silently used the default bucket and key alias; a joined consumer never received PRIMARY_DB and died at boot; and node.env was rewritten on every join with no file an operator could add to), then removing the need for cloud credentials on a node at all with brokered KMS and blob providers that renderNodeEnv hands out automatically, plus deploy/split-cloud, scripts/aws-bootstrap.sh, two fleet instance checks and the docs 2026-09-10 13:58:59 +02:00
Matthew Meszaros 9356c748b9 Merge remote-tracking branch 'origin/main' into fix/main-ci-failure-and-issue-400
# Conflicts:
#	docs/content/docs/guides/mailboxes.mdx
#	site/public/install.sh.sha256
2026-09-09 08:59:37 -07:00
Matthew Meszaros 18a8c7b009 feat: give a self-hosted instance on-demand TLS for customer tracking and forms domains, gating Caddy's ask on a /tls/authorize endpoint that answers only for domains this instance has verified, so a workspace CNAME stops serving every tracked link and opt-out link with no certificate 2026-09-09 08:56:08 -07:00
Matthew Meszaros 6ebf9cfdcf Merge remote-tracking branch 'origin/main' into fix/self-hosted-unsubscribe-domain 2026-09-09 08:24:04 -07:00
Matthew Meszaros a5993a1ed0 feat: put the standalone-statement note where the docs said it was, shellcheck the join-script checker as well since its own disable directives are load-bearing, and stop claiming the EnvironmentFile assertion covers two render variants when only the mount list varies with the environment 2026-09-09 07:04:58 -07:00
Matthew Meszaros 9203695a41 feat: harden the join-script guard against being fooled rather than against being reformatted, matching the ensure_blob_root call on its first field after a looser regex proved satisfiable by the name appearing inside a warn string, capturing the function body without a pipeline so a renamed function reports that instead of the assertion it happened to fail, and rendering both unit variants so the no-blob one is covered too 2026-09-09 06:59:14 -07:00
Matthew Meszaros eb4e1a8bb8 feat: make the join-script checker survive its own mutation tests, matching the ensure_blob_root call on the command field so a commented-out call no longer passes, tolerating a space before the parentheses in a function definition, and capturing the function body before asserting so a renamed function reports that rather than the assertion it happened to fail 2026-09-09 06:47:56 -07:00
Matthew Meszaros 6947aca9b3 feat: make the join-script check assert on what the script renders via a new --print-unit mode, after the previous version compared a heredoc copied into the checker and stayed green when the systemd command-substitution bug was put back, and make docker_mounts pure so the unit can be rendered without creating directories 2026-09-09 06:37:56 -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 29bf9b4320 feat: cover the join script with make join-check, wired into make lint, since nothing tested the highest-consequence non-Go file in the repo and three defects reached the branch through it; and fix the four the review found in the last round, matching the fs provider alias, creating blob parents under a 0022 umask rather than the 0700 one write_config leaves set, warning instead of silently mounting a blob root the node cannot write, and validating the path right after enrolment rather than halfway through the install 2026-09-09 06:28:30 -07:00
Matthew Meszaros 03255754ff feat: read the blob settings from the env in memory so the dry run reports what a real join would do instead of a previous join's file, create the blob root 0755 rather than inheriting the 0700 umask a co-located backend cannot read, refuse a relative BLOB_FS_ROOT that docker cannot mount, and stop discarding the errors that made a failed mount look like a successful join 2026-09-09 06:12:56 -07:00
Matthew Meszaros 75a068cd07 feat: always create and mount BLOB_FS_ROOT for a node because its storage layer exits when the directory is not writable and an unmounted path is root-owned by docker, and say plainly in the script and the docs that filesystem blobs cannot serve a fleet at all since the worker has to read the body the backend wrote 2026-09-09 06:06:04 -07:00
Matthew Meszaros 4fc6d1db77 feat: put every docker mount on one line in the generated unit, because command substitution strips the trailing newline and my previous multi-line blob mount collapsed the continuation so the node restart-looped on any filesystem-blob install, and mount BLOB_FS_ROOT only when it already exists rather than creating and recursively chowning a directory a co-located bare-metal backend owns 2026-09-09 05:59:44 -07:00
Matthew Meszaros 0a08983158 feat: keep the node's write path reachable and the image reference root-owned, by granting traverse on the state directory without making it writable, re-owning image-ref on every join so a machine joined by the earlier chown-everything script converges, mounting BLOB_FS_ROOT separately when blobs live under the state directory the mount no longer covers, and renaming the reserve dialog's remaining convert-to-dedicated button 2026-09-09 05:53:28 -07:00
Matthew Meszaros 65fe82c472 feat: stop the node container being able to choose the image root runs, by giving it a dedicated uid-1000 subdirectory to write its update target into instead of making the whole state directory writable, validating the tag it asks for before the updater acts on it, and no longer recursively chowning a directory a bare-metal install also keeps its blobs in 2026-09-09 05:41:16 -07:00