a75ea012a0
feat: import a mailbox's recent history on connect and govern sync by fair use: a backfill on every provider (newest first, inside an operator-editable window and cap, resumable through a durable per-provider cursor relayed as SYNC_STATE), a per-mailbox sync governor with priority, live and backfill lanes on shared Redis windows that defers over-budget mail with the cursor held instead of dropping it and only deactivates a mailbox for a flood or chronic daily overage, sync.* budgets on the admin instance settings shipped inside ADD_EMAIL, saved IMAP folder cursors and last_synced_at finally written, a Sync card in the mailbox drawer fed by GET /emails/:id/sync, and docs
main
Matthew Meszaros
2026-08-18 09:09:52 -07:00
5ecdf7cee8
feat: document mailbox sync fair use: a What gets synced section on the mailboxes guide covering the initial import (window, cap, newest first, folders read and skipped, the drawer's Sync card), live sync, the per-mailbox and per-organization budgets with deferral instead of dropping and the priority given to outreach replies, and the two patterns that deactivate a mailbox; the four sync.* instance settings on the configuration page; GET /emails/:id/sync in the endpoint scope map and the OpenAPI spec with MailboxSync, MailboxSyncState and MailboxSyncPolicy schemas; SYNC_STATE on the events page and ACCOUNT_SYNC_STATE on the realtime page; the architecture anti-abuse list pointing at the governor instead of the removed ratelimit.go; and the AGENTS.md worker-side abuse detection section rewritten around the governor's lanes, deferral, escalation rules and relayed state
feature/smart-mailbox-sync
Matthew Meszaros
2026-08-18 08:43:47 -07:00
9a42772a75
feat: show sync progress and fair-use status in the mailbox drawer through a SyncStatusCard fed by GET /emails/:id/sync (importing with a live progress bar and running count, up to date with what was imported from how many days, or waiting on the sync budget until a time with the exhausted budget named and a note that outreach replies keep syncing, plus the number of messages waiting on the server), refetching every 20 seconds while an import runs and on the ACCOUNT_SYNC_STATE realtime event which now invalidates the mailbox's sync query
Matthew Meszaros
2026-08-18 08:43:47 -07:00
7980266fe6
feat: add a Mailbox sync fair use card to the admin instance settings page with the four operator-editable budgets (import window in days, import cap per mailbox, daily new-mail budget per mailbox and per organization) validated against the same bands the backend clamps to, plus the sync section on the InstanceSettings client type
Matthew Meszaros
2026-08-18 08:43:47 -07:00
6f9bfe3994
feat: stop the consumer's dead-worker sweep publishing a credential-less ADD_EMAIL for every reassigned mailbox, which the worker rejected with an error log per mailbox because NewWMail requires the provider credentials the payload never carried, and leave the load to the backend's worker reconciler which republishes the full payload with decrypted credentials, cursors and sync policy
Matthew Meszaros
2026-08-18 08:43:47 -07:00
bea3f21da2
feat: replace the worker's flat mailbox-sync rate limiter, which fetched every message of every IMAP folder oldest first on first sight and then deactivated the mailbox after 100 new emails in five minutes with no resume state while Gmail and Outlook imported nothing historical at all, with a per-mailbox sync governor and a real backfill on every provider: three Redis fixed-window lanes shared across workers (priority for mail in a conversation the mailbox owns, resolved through the internal own-conversation endpoint and never held behind ordinary inbound mail; live for new mail after connect with burst, hourly, per-mailbox daily and per-organization daily budgets; backfill paced per minute and charged to the org budget) that defer over-budget mail with the provider cursor pinned before the first deferred message rather than dropping it, escalate to the existing EMAIL_RATE_LIMITED path only for a flood (SyncFloodPerHour new live messages seen in one hour, deduped through a lane cache so a held backlog is not re-counted every pass) or chronic overage (per-mailbox daily budget exhausted on three of the last seven days), cancel the mailbox context on termination so the sync goroutine no longer keeps ticking against a removed entry, and fail open on a Redis outage; an initial import newest first inside the policy window and cap with a resumable per-provider cursor relayed as SYNC_STATE, walking IMAP via UID SEARCH SINCE per eligible folder (skipping trash, drafts, junk and Gmail's All Mail) below a saved UID floor, Gmail via messages.list with a page token, and Graph via a filtered ordered folder listing over inbox, sent items and archive; IMAP live sync moved from CHANGEDSINCE window fetches to UID SEARCH MODSEQ plus envelope-only fetches with bodies read only for admitted messages, first sight of a folder now baselining its HIGHESTMODSEQ instead of walking it, and saved folder cursors seeded from the payload; Gmail history and Graph delta callbacks reshaped to offer ids first so dedupe and admission happen before hydration, with history checkpoints and delta links only advancing past fully stored records and pages, bounded pages per pass, Graph now tracking sent items live so a thread shows both sides, Graph priming walking uncapped and unpersisted to its deltaLink, and a Graph 404 on a just-deleted message treated as a skip; Gmail and Graph bodies capped at MaxEmailBodySize like IMAP already was; a provider 429 during sync backing the loop off instead of being relayed as a rate-limit event that deactivated the mailbox; the loop interval now adaptive with jitter and a five minute ceiling while held; a republished ADD_EMAIL applying a changed policy to an already loaded mailbox; and the dead StartImapWorker removed
Matthew Meszaros
2026-08-18 08:43:35 -07:00
44da2f464f
feat: add the control plane for mailbox sync fair use, so a mailbox syncs under an operator-editable policy and its progress survives worker replacement: a sync section on the instance settings document (backfill window in days, backfill cap per mailbox, daily new-mail budget per mailbox and per organization, each clamped on read and write with compiled defaults in constants.go), models.SyncPolicy and models.SyncState with a provider-shaped jsonb SyncCursor, a new email_sync_state table plus an index on tasks.message_id that the reply lookup was scanning sequentially without, an EmailSyncStateRepository whose Put also stamps email_accounts.last_synced_at which nothing had written since the baseline so every admin and dashboard Last synced surface read NULL, an OrganizationID and Sync block on the ADD_EMAIL payload resolved by the loader from instance settings and the saved state and, for IMAP, the saved unibox_mailboxes folder cursors that the loader had never populated so every worker restart re-walked every folder from scratch, a SYNC_STATE consumer handler that persists the relay and publishes ACCOUNT_SYNC_STATE plus a warning when the import completes or fair use flips, an internal own-conversation endpoint the worker's priority lane asks whether a new message replies to a campaign task, a mapped message or a stored thread, GET /emails/:id/sync for the dashboard, and SYNC_FLOOD and SYNC_FAIR_USE mail error codes with user copy for the two patterns that deactivate a mailbox
Matthew Meszaros
2026-08-18 08:43:20 -07:00
93e8451738
feat: organization data export and import for moving a workspace between instances (#132)
Matthew Meszaros
2026-08-18 07:53:39 -07:00
c39c29ab6b
feat: rebuild the new-campaign wizard with animated step transitions, a numbered stepper, the shared Toggle instead of a broken hand-rolled switch, per-step validation that explains itself and a discard guard, register PopoverMenu's click-outside in the capture phase so dropdowns inside dialogs close on click-away, add a Campaigns back link and clickable breadcrumb crumbs, add a From contacts leads picker with category filter and select-all-matching backed by the bulk add_campaigns path whose SQL now scopes campaigns by organization instead of the caller, and stop self-hosted no-billing deployments presenting as a free trial or plan-metered by exposing billing_enabled on GET /auth/config, showing a Self-hosted badge, hiding Billing and Refer & earn, and reporting AI credits as unlimited with the header gauge and cost copy hidden
Matthew Meszaros
2026-08-18 07:48:58 -07:00
94477efd5f
feat: add an 'interaction details are part of done' checklist to the dashboard UI conventions in AGENTS.md covering that every dropdown must close on click-away and Escape even inside a dialog (capture-phase click-outside listeners because dialog cards stop mousedown propagation, and Escape closes only the innermost layer by bailing while a data-floating popover or the alertdialog confirm is on screen), that switches are the shared Toggle with the double-toggle traps spelled out, that every detail page needs a route back on all viewports since the header breadcrumb is desktop-only, that multi-step flows animate, explain blocked steps, refuse to skip ahead and confirm before discarding, and that a control whose action cannot succeed must not ship, after the campaign wizard shipped with all of these wrong
feature/campaign-wizard-selfhost-leads
Matthew Meszaros
2026-08-18 07:42:07 -07:00
5f4b7ef408
feat: stop presenting plan-based AI credits on a deployment without billing, where internal/app/credits/service.go already bypasses the ledger for BILLING_PROVIDER=none but GET /subscription/credits still returned the empty ledger plus a plan-derived allowance so the header gauge read '0 of 0 plan credits left' in red and the agent panel printed the 1,000,000,000 sentinel literally, by adding CreditService.Unmetered() and returning unlimited: true with zeroed fields from that endpoint, adding a useAiMetered hook in web keyed on the same billing flag, hiding the CreditsMeter and the agent panel credit count when unmetered, and dropping the per-action cost copy that described a meter that is not running (workspace inbox-agent 'paid feature' line, contact research confirm and Research tab, the AI caret 'from N credits' hints), while correcting docs/content/docs/guides/ai-credits.mdx which claimed self-hosting is still metered against a paid provider, and noting the field in api/endpoints.mdx and the deployment guide's billing row
Matthew Meszaros
2026-08-18 07:42:07 -07:00
0185c1cfeb
feat: let a campaign's Leads tab pull in people who are already in the workspace by adding web/src/components/app/contacts/AddFromContactsDialog.tsx, a picker over the contact search (debounced name/email/company query plus the shared CategoryPicker filter) that shows category chips, marks contacts already in the campaign as leads and skips them, supports ticking rows, Select loaded, Load more and Select all matching which pages through the whole result up to the backend's 1000-per-batch bulk cap with a footer note to repeat, and attaches the selection through the existing PATCH /contacts add_campaigns path before refreshing the leads list, wiring it into ContactsTable as a From contacts toolbar action and as the primary empty-state call to action for a fresh campaign, and documenting the four entry points in a new Adding leads section of docs/content/docs/guides/campaigns.mdx
Matthew Meszaros
2026-08-18 07:42:07 -07:00
0c0fb5cefb
feat: scope the bulk contact update's add_campaigns and remove_campaigns SQL and its campaign readback in internal/repository/pg_contact.go by campaigns.organization_id instead of cam.user_id = caller, because campaigns are organization assets and are listed and opened org-wide, so a teammate attaching contacts to a campaign someone else created (through bulk edit or the import wizard's attach-to-campaign path) got a silent no-op with no rows inserted, matching the earlier fix that moved the contacts list itself to organization scoping while leaving categories user-scoped as that change noted
Matthew Meszaros
2026-08-18 07:42:07 -07:00
9dea6d1e22
feat: stop a self-hosted workspace being shown as on a free trial by adding a billing_enabled field to GET /auth/config in internal/api/handler/auth_config.go that mirrors the backend feature gate exactly (false when BILLING_PROVIDER=none, which self_hosted alone cannot express because a self-host may still run Stripe), and making the dashboard honour it: useFeatureAccess short-circuits to fully unlocked with a new billing flag on the confirmed answer only so an unreachable backend never reads as unlocked, PlanPill shows a Self-hosted badge instead of 'Free · Trial', the Billing and Refer & earn settings tabs are hidden and redirect to Workspace when hit directly, and the org picker drops the meaningless trial plan label, since the backend already unlocked everything but the client derived the plan purely from /subscription and would even lock Unibox and advanced surfaces client-side once the unenforced 14-day trial row lapsed, with docs updated in api/endpoints.mdx, development/configuration.mdx and development/first-run.mdx
Matthew Meszaros
2026-08-18 07:42:07 -07:00
59ef31b330
feat: give the campaign detail page a way back on every viewport by adding a 'Campaigns' back link above the title in web/src/app/app/campaigns/[id]/layout.tsx, and turn the desktop header breadcrumb crumbs in web/src/components/layout/AppHeader.tsx from plain spans into links to their own path prefix, keyed on the real current path so 'Campaigns' stays clickable on /campaigns/<id> where the hidden UUID is the last segment, because a member who opened a campaign had no route back other than the browser button
Matthew Meszaros
2026-08-18 07:41:32 -07:00
8d5f1ab99b
feat: rebuild the new-campaign wizard in web/src/components/app/campaigns/NewCampaignDialog.tsx with directional slide transitions between steps and a numbered stepper whose connectors fill as steps complete, replace the hand-rolled sending toggles whose 24px knob translated out of a 28px circular track with the shared sky Toggle used on the campaign settings tabs (row click toggles once, the switch stops propagation, no label htmlFor double-fire), use the shared WeekdayBitmask cells plus Weekdays and Every day shortcuts and a live window summary on the schedule step, reject an empty day set and an end time before the start time with a footer explanation instead of a silently disabled Continue, forbid jumping ahead past an incomplete step from the stepper, make the first email optional so it can be written in the Steps tab editor while still requiring a half-written one to be finished, add a summary line on the last step that hops back to edit, confirm before discarding a dirty draft on Escape, backdrop and close button while letting an open dropdown or the confirm own Escape, and drop the 'Open & launch after create' checkbox that only changed the toast text and could never launch because start requires contacts, updating the wizard paragraph in docs/content/docs/guides/campaigns.mdx to the new step names and behaviour
Matthew Meszaros
2026-08-18 07:41:32 -07:00
dca23ab904
feat: register PopoverMenu's click-outside listener in the capture phase so a dropdown inside a dialog still closes on click-away, because dialog cards stop mousedown propagation to keep the backdrop from closing them and React's stopPropagation also halts the native event before it reaches a bubble-phase document listener, which is why the sender pool tag picker, the timezone SelectMenu and every TimePicker stayed open inside the new-campaign wizard, NewMeetingDialog and ContactsEditBulk while useClickOutside-based pickers, which already used capture, worked
Matthew Meszaros
2026-08-18 07:41:32 -07:00
7521575cef
feat: correct the documented order of the four blocks warmblyctl status prints, in both the new warmblyctl reference and the first-run sample output, after running the command against a live instance showed printStatus emits Instance, Platform admins and How to get in before runStatus calls printChecks, so the checks come last and not third as both pages claimed, which also means the first-run sample had its Checks and How to get in blocks transposed, and an operator scrolling for the recovery commands was being told to look past a findings list that is actually printed after them (#129)
Matthew Meszaros
2026-08-16 08:47:46 +02:00
619f4fd9a1
feat: add a warmblyctl reference page at docs/content/docs/development/warmblyctl.mdx documenting all nine commands with every flag, because the README and the recovery sections only ever showed 'warmblyctl user create --email ... --admin' without saying where the password comes from, leaving self-hosters with an account they could not sign in to and no page that answered it, covering that user create prompts for the password twice on a terminal and refuses on a non-TTY unless --password-stdin is passed, that docker compose exec allocates the TTY those prompts need unless -T is given and piped input needs -T precisely because it removes it, that the password rule is the dashboard's own 8 to 128 characters, and that signing in afterwards needs nothing else on a stock self-host since AUTH_LOGIN_CODE defaults to off and REQUIRE_EMAIL_VERIFICATION to false when self-hosted, captcha stays off without TURNSTILE_SECRET, and --admin opens the panel on ADMIN_URL rather than APP_URL, plus the status JSON contract and exit codes, the four environment variables each command reads, the per-command behaviour when Redis is down, the admin role masks read from AdminRolePermissions, and the make wrappers, registering it in the Development meta.json between accounts-and-access and configuration, linking it from the four pages that already print these commands, correcting the super-admin mask in the first-run sample output from 4294967295 to the 4194303 that AllAdminPermissions actually is since it is (1 << 22) - 1 and not the full uint32 range, and shortening the README self-hosting section by folding the three-bullet gotcha list into a four-row table that also names make doctor and dropping the duplicated make dev claim warning already stated above it, keeping every fact (#128)
Matthew Meszaros
2026-08-16 08:39:57 +02:00
bd8545a1c5
feat: stop a freshly connected mailbox being silently excluded from every campaign send, by making an unset mailbox timezone representable as the empty string the campaign scheduler already checks for, since email_accounts.timezone defaulted to 'UTC' while campaigns.timezone defaults to 'Europe/London' and nothing in the OAuth or SMTP onboarding paths ever set either, so a brand new mailbox looked deliberately placed in UTC, was compared against the differing campaign zone and dropped by the hardcoded 8am-8pm business-hours gate whenever the current UTC hour fell outside it, emptying the candidate pool and failing the campaign start, adding a migration that changes the column default and converts existing 'UTC' rows because until now no API field, dashboard control or onboarding path could set that column at all so every such row is the old default rather than a choice, adding the missing Timezone field to UpdateEmail with IANA validation so the setting the sending-behaviour UI already tells people to change is finally reachable and an unloadable zone is rejected instead of being silently coerced to UTC by the scheduler, and replacing the misleading 'no active email accounts found for campaign's email tags' response for a pool that exists but is entirely gated out with a distinct message naming the real cause, via an ErrNoEligibleMailbox that wraps ErrNoEmailAccounts so the three callers that pause a campaign on it are unaffected (#126) (#125) (#127)
Matthew Meszaros
2026-08-16 07:58:52 +02:00
f0846eb034
feat: stop a freshly connected mailbox being silently excluded from every campaign send, by making an unset mailbox timezone representable as the empty string the campaign scheduler already checks for, since email_accounts.timezone defaulted to 'UTC' while campaigns.timezone defaults to 'Europe/London' and nothing in the OAuth or SMTP onboarding paths ever set either, so a brand new mailbox looked deliberately placed in UTC, was compared against the differing campaign zone and dropped by the hardcoded 8am-8pm business-hours gate whenever the current UTC hour fell outside it, emptying the candidate pool and failing the campaign start, adding a migration that changes the column default and converts existing 'UTC' rows because until now no API field, dashboard control or onboarding path could set that column at all so every such row is the old default rather than a choice, adding the missing Timezone field to UpdateEmail with IANA validation so the setting the sending-behaviour UI already tells people to change is finally reachable and an unloadable zone is rejected instead of being silently coerced to UTC by the scheduler, and replacing the misleading 'no active email accounts found for campaign's email tags' response for a pool that exists but is entirely gated out with a distinct message naming the real cause, via an ErrNoEligibleMailbox that wraps ErrNoEmailAccounts so the three callers that pause a campaign on it are unaffected (#126) (#125)
Matthew Meszaros
2026-08-16 07:54:45 +02:00
0f5acab493
Merge branch 'main' into fix/108-orphaned-worker-assignment
Matthew Meszaros
2026-08-16 07:47:10 +02:00
fe9a21a79f
feat: stop a freshly connected mailbox being silently excluded from every campaign send, by making an unset mailbox timezone representable as the empty string the campaign scheduler already checks for, since email_accounts.timezone defaulted to 'UTC' while campaigns.timezone defaults to 'Europe/London' and nothing in the OAuth or SMTP onboarding paths ever set either, so a brand new mailbox looked deliberately placed in UTC, was compared against the differing campaign zone and dropped by the hardcoded 8am-8pm business-hours gate whenever the current UTC hour fell outside it, emptying the candidate pool and failing the campaign start, adding a migration that changes the column default and converts existing 'UTC' rows because until now no API field, dashboard control or onboarding path could set that column at all so every such row is the old default rather than a choice, adding the missing Timezone field to UpdateEmail with IANA validation so the setting the sending-behaviour UI already tells people to change is finally reachable and an unloadable zone is rejected instead of being silently coerced to UTC by the scheduler, and replacing the misleading 'no active email accounts found for campaign's email tags' response for a pool that exists but is entirely gated out with a distinct message naming the real cause, via an ErrNoEligibleMailbox that wraps ErrNoEmailAccounts so the three callers that pause a campaign on it are unaffected (#126)
Matthew Meszaros
2026-08-16 07:46:37 +02:00
ee61c19faf
feat: create /data/blobs in the backend, consumer and worker images owned by the non-root user they run as, because Docker seeds a fresh named volume from the image and the path did not exist there, so it created the mount point root:root while every service runs as uid 1000 and the very first attempt to store an outbound body failed with 'mkdir /data/blobs/emails: permission denied' and the task went to the dead-letter queue, which is invisible until a real send is attempted since the stack is otherwise healthy, and documenting the one-time chown that existing installs still need because Docker only applies image ownership when it first creates the volume (#124)
Matthew Meszaros
2026-08-16 07:46:22 +02:00
248a32dbec
feat: fix the tracking dedupe insert that failed on every single open and click event, where the ON CONFLICT target named the expression COALESCE(url_hash, '') while the table's primary key is the three plain columns (task_id, event_type, url_hash) and no matching expression index exists, so Postgres rejected every insert with SQLSTATE 42P10 regardless of whether the row was actually a duplicate and tracking_events_processed stayed permanently empty, and NULLIF turned an open event's empty url_hash into NULL against a NOT NULL DEFAULT empty-string column which would have been refused even had the conflict target been right, leaving the consumer-level dedupe with nothing to check so a JetStream redelivery (MaxDeliver is 10, so not hypothetical) or an event slipping past the Rust service's in-memory hourly cache after a tracking restart could double-count an open or a click, and dropping the equally redundant COALESCE from the IsProcessed lookup so it can use the primary key (#123)
Matthew Meszaros
2026-08-16 07:46:19 +02:00
ec4cd3160b
feat: thread Unibox dashboard replies into the conversation they answer, by carrying the composer's thread_id all the way from email_tasks to the provider (EmailMessage had no ThreadID field at all, so the column was read from the database and silently dropped in user_email_task, and Gmail only appends to an existing thread when threadId is set on the outbound message since a matching Subject and In-Reply-To do not do it), populating the models.SendEmail.Parent field that already existed with an avro tag and that the worker already read but nothing ever set, replacing the worker's gate that required InReplyTo to be non-empty before it would look at Parent (a dashboard reply never sets that header, so a perfectly valid ThreadID was discarded and the provider opened a new conversation) with a parentReference helper shared by the Gmail and Graph send paths that resolves the two genuinely independent handles separately, and backfilling the RFC In-Reply-To header server-side in UniboxReply from the newest Message-ID in the thread via a new org-scoped LatestMessageIDInThread query, because a provider thread id is meaningless outside the mailbox that issued it and the recipient's mail client can only thread on References and In-Reply-To (#122)
Matthew Meszaros
2026-08-16 07:46:16 +02:00
7f1f46ea02
feat: resume a reloaded Gmail mailbox from its saved history checkpoint instead of the never-written email_accounts.last_id column, which no code in the repository ever assigns so it is NULL forever and every worker restart, deploy, crash or docker compose up handed the mailbox a zero cursor that re-bootstrapped straight to Gmail's current historyId and silently skipped everything that arrived since the last successful sync, unrecoverably because the history API only walks forward from the id it is given, by threading the EmailHistoryIDRepository the consumer already writes to into emailService as the Google counterpart of the existing WireGraphDelta cursor plumbing for Outlook, reading it in buildAddWorkerEmail through a lastHistoryFor helper that prefers the live checkpoint, still falls back to last_id for rows carrying a value from before the checkpoint table existed, and returns zero only when genuinely nothing is known so a first-time mailbox bootstraps a fresh baseline, plus tests covering the precedence and every fallback including an unwired repository (#121)
Matthew Meszaros
2026-08-16 07:46:13 +02:00
2c56dc9075
feat: make the Gmail history checkpoint persist and advance, by setting UserID and EmailID on the JobEventHistoryIDUpdate that NewHistoryID publishes (email_history_ids is keyed (user_id, email_id) with a foreign key to users, so the zero UUIDs it was sending made every checkpoint write fail on email_history_ids_user_id_fkey and no Gmail mailbox ever got a row, which is why replies, opens via label changes and every other inbound signal never reached Unibox), bootstrapping a mailbox with no baseline from Users.GetProfile instead of calling history.list with startHistoryId=0 which Gmail rejects with 'Requested entity was not found' so a freshly connected mailbox could never establish one, advancing the in-memory GoogleData.LastHistoryID after a successful walk since it was only ever written at construction and a stale cursor re-walks the window just processed while a zero one re-bootstraps past everything that arrived in between, and no longer discarding a MailError when a partial history was processed in the same tick, matching the field assignment the already-correct but uncalled ImapGoogleSync has had all along (#120)
Matthew Meszaros
2026-08-16 07:46:11 +02:00
4e14df16e2
feat: send every Gmail message as raw RFC 5322 instead of the structured gmail.MessagePart payload, which Gmail's users.messages.send rejects outright with "'raw' RFC822 payload message string or uploading message via /upload/* URL required" because the structured Payload tree is the read representation returned by messages.get and is not accepted on send, so every non-attachment Gmail send failed with a 400 that was retried and then dead-lettered under a misleading SERVER_UNREACHABLE label while the attachment path already built raw correctly, building the narrowest correct MIME structure per message rather than routing everything through the multipart/mixed attachment builder (bare text/plain for warmup and text-only campaigns, multipart/alternative once there is an HTML body, multipart/mixed only when files are attached, because a needlessly nested tree is a structural difference cold outreach does not need), RFC 2047-encoding the Subject and building the From header through net/mail.Address now that header encoding is ours rather than the API's, so a non-ASCII display name is no longer emitted as bare 8-bit bytes and a name containing a comma no longer splits the header into two recipients, and adding tests that parse the built message back with net/mail and mime/multipart to assert the structure instead of matching strings (#119)
Matthew Meszaros
2026-08-16 07:46:08 +02:00
16b672e6f6
feat: wire OnTokenRefresh on the Gmail worker client so every send and sync stops panicking, since goog.Client was constructed with all four message and label callbacks but no token callback while goog.Init unconditionally wrapped the token source in stoken, whose Token() calls that callback on every single request from inside the oauth2 transport's RoundTrip, making the nil func value a guaranteed nil-pointer dereference on the first Gmail API call any mailbox made (the Outlook path immediately below it set the same field correctly, so no Microsoft mailbox was affected), additionally guarding both goog.Init and msgraph.Init so the stoken wrapper is only installed when there is somewhere to persist a refreshed token to, hardening stoken.Token itself against a nil callback because it runs inside RoundTrip where a panic takes down the caller's request rather than surfacing as an error, and adding a regression test that panics without the guard and passes with it (#118)
Matthew Meszaros
2026-08-16 07:46:00 +02:00
cdf200191c
feat: accept the API's origin in the mailbox OAuth callback listener so connecting Gmail or Outlook completes on a split-domain deployment, where the bridge page is served by the backend (deliberately, so the registered redirect_uri survives front-end changes) and therefore arrives with event.origin equal to API_URL while the dashboard only ever compared it against APP_URL, silently discarding every callback and leaving the connect modal on 'Waiting for authorization' forever even though the provider exchange had already succeeded, normalising both configured bases through URL.origin so a trailing slash no longer breaks the comparison either, and separately deriving the bridge's postMessage target origin from APP_URL when APP_ORIGIN is unset instead of falling back to a wildcard that posts the authorization code to whatever origin the opener happens to have, since compose never set APP_ORIGIN despite the configuration table claiming it was derived, with the app_origin_wildcard health check and both docs pages updated to match the narrower condition that now triggers it (#117)
Matthew Meszaros
2026-08-16 07:45:57 +02:00
8d2968eaa8
feat: build the Gmail and Outlook mailbox-connect redirect_uri from API_PUBLIC_URL instead of API_HOST, which is the listener's bind address and stays 0.0.0.0:8080 in a container, so every self-hosted OAuth connect sent Google and Microsoft a redirect_uri that is not even an absolute URI and was rejected with invalid_request before the account picker appeared, adding an oauthPublicBaseURL helper next to the existing oidcRedirectURL that reads the same already-documented variable and falls back to a browsable http://localhost:PORT derived from the bind address (mapping the 0.0.0.0, :: and bare-port wildcard forms to localhost, and passing through a value that is already a URL) so a stock local install emits exactly the http://localhost:8080/addresses/google/callback the guide tells operators to register, plus a test pinning the resolution order and the root-registered callback path, and a deployment-guide note naming API_PUBLIC_URL as the variable that has to match the registered URI (#116)
Matthew Meszaros
2026-08-16 07:45:54 +02:00
1a7cdc955f
feat: seal Gmail and Outlook OAuth access and refresh tokens at rest in email_accounts_oauth instead of storing the provider's raw tokens, which the connect UI already promised were encrypted and which the read path could never open because it unconditionally hex-decodes, adding sealCredential/openCredential helpers that fail closed when CREDENTIALS_ENCRYPTION_KEY is unset, encrypting on both write paths (NewOauthAccount at connect time and RefreshBoxToken on every worker token refresh, which would otherwise revert a sealed row to plaintext on first refresh), migrating pre-existing plaintext rows lazily on first read because the key lives in the application and no SQL-only migration can reach it, restoring the missing return on the OAuth insert failure that let a failed token write commit an account row with no credentials, keeping token parameters out of Sentry error reports, and correcting the two docs tables that scoped the key to SMTP and IMAP only (#115)
Matthew Meszaros
2026-08-16 07:45:52 +02:00
95caa83126
feat: re-place a mailbox whose assigned worker has gone away, instead of leaving it stranded until someone notices, because worker assignment only ever ran when email_accounts.worker_id was NULL so a mailbox pointing at a worker that no longer heartbeats was never reconsidered and every send failed with 'email account not found in worker' with no recovery path, which is routine rather than exotic given a worker started without WORKER_ID mints a fresh UUID on every boot and each docker compose up -d worker therefore abandons the previous row with its mailboxes still attached, adding IsWorkerLive to the worker repository using the same active-and-heartbeating-inside-WorkerLivenessWindow predicate that placement already applies when selecting a worker, and a releaseDeadWorker step in the reconciler's load path that releases the mailbox through the existing UnassignWorkerFromEmail (so the dead worker's account count and load score are corrected) and lets the normal assignment path place it on a live worker, deliberately keeping the current assignment when the liveness lookup itself fails so a database blip cannot churn placements and change the IP a mailbox sends from, and correcting the deployment guide which described the abandoned rows as harmless clutter when they were actually holding live mailboxes
Matthew Meszaros
2026-08-16 07:26:36 +02:00
533a9ca0a2
feat: stop a freshly connected mailbox being silently excluded from every campaign send, by making an unset mailbox timezone representable as the empty string the campaign scheduler already checks for, since email_accounts.timezone defaulted to 'UTC' while campaigns.timezone defaults to 'Europe/London' and nothing in the OAuth or SMTP onboarding paths ever set either, so a brand new mailbox looked deliberately placed in UTC, was compared against the differing campaign zone and dropped by the hardcoded 8am-8pm business-hours gate whenever the current UTC hour fell outside it, emptying the candidate pool and failing the campaign start, adding a migration that changes the column default and converts existing 'UTC' rows because until now no API field, dashboard control or onboarding path could set that column at all so every such row is the old default rather than a choice, adding the missing Timezone field to UpdateEmail with IANA validation so the setting the sending-behaviour UI already tells people to change is finally reachable and an unloadable zone is rejected instead of being silently coerced to UTC by the scheduler, and replacing the misleading 'no active email accounts found for campaign's email tags' response for a pool that exists but is entirely gated out with a distinct message naming the real cause, via an ErrNoEligibleMailbox that wraps ErrNoEmailAccounts so the three callers that pause a campaign on it are unaffected
fix/103-mailbox-timezone
Matthew Meszaros
2026-08-16 07:22:30 +02:00
01f88b4e28
feat: give every long-running compose service restart: unless-stopped and stop the tracking service exiting on a transient event-bus failure at boot, because the compose file had no restart policy on any service so Docker's default of no applied everywhere and a container that died stayed dead, which for tracking meant a startup DNS race against nats (its producer had no retry and exited immediately) left the service Exited(1) indefinitely with zero visible symptoms anywhere else since open and click tracking is fire-and-forget from the sender's side, so campaigns kept sending and nothing recorded an open or a click until someone thought to run docker compose ps -a, adding bounded retry with exponential backoff capped at ten seconds over eight attempts so the race self-heals without needing the restart policy as a safety net, excluding the one-shot seed job from the policy, and making the tracking host port overridable with TRACKING_PORT since 3000 is a very common default for other self-hosted software and a collision silently stops the container from publishing
Matthew Meszaros
2026-08-16 07:17:56 +02:00
0a0d68f7b0
feat: create /data/blobs in the backend, consumer and worker images owned by the non-root user they run as, because Docker seeds a fresh named volume from the image and the path did not exist there, so it created the mount point root:root while every service runs as uid 1000 and the very first attempt to store an outbound body failed with 'mkdir /data/blobs/emails: permission denied' and the task went to the dead-letter queue, which is invisible until a real send is attempted since the stack is otherwise healthy, and documenting the one-time chown that existing installs still need because Docker only applies image ownership when it first creates the volume
fix/104-blobs-volume-ownership
Matthew Meszaros
2026-08-16 07:15:39 +02:00
15cd8e33aa
feat: fix the tracking dedupe insert that failed on every single open and click event, where the ON CONFLICT target named the expression COALESCE(url_hash, '') while the table's primary key is the three plain columns (task_id, event_type, url_hash) and no matching expression index exists, so Postgres rejected every insert with SQLSTATE 42P10 regardless of whether the row was actually a duplicate and tracking_events_processed stayed permanently empty, and NULLIF turned an open event's empty url_hash into NULL against a NOT NULL DEFAULT empty-string column which would have been refused even had the conflict target been right, leaving the consumer-level dedupe with nothing to check so a JetStream redelivery (MaxDeliver is 10, so not hypothetical) or an event slipping past the Rust service's in-memory hourly cache after a tracking restart could double-count an open or a click, and dropping the equally redundant COALESCE from the IsProcessed lookup so it can use the primary key
fix/113-tracking-dedupe
Matthew Meszaros
2026-08-16 07:13:42 +02:00
57ae54c139
feat: thread Unibox dashboard replies into the conversation they answer, by carrying the composer's thread_id all the way from email_tasks to the provider (EmailMessage had no ThreadID field at all, so the column was read from the database and silently dropped in user_email_task, and Gmail only appends to an existing thread when threadId is set on the outbound message since a matching Subject and In-Reply-To do not do it), populating the models.SendEmail.Parent field that already existed with an avro tag and that the worker already read but nothing ever set, replacing the worker's gate that required InReplyTo to be non-empty before it would look at Parent (a dashboard reply never sets that header, so a perfectly valid ThreadID was discarded and the provider opened a new conversation) with a parentReference helper shared by the Gmail and Graph send paths that resolves the two genuinely independent handles separately, and backfilling the RFC In-Reply-To header server-side in UniboxReply from the newest Message-ID in the thread via a new org-scoped LatestMessageIDInThread query, because a provider thread id is meaningless outside the mailbox that issued it and the recipient's mail client can only thread on References and In-Reply-To
fix/111-unibox-reply-threading
Matthew Meszaros
2026-08-16 07:11:51 +02:00
dc83d6de89
feat: resume a reloaded Gmail mailbox from its saved history checkpoint instead of the never-written email_accounts.last_id column, which no code in the repository ever assigns so it is NULL forever and every worker restart, deploy, crash or docker compose up handed the mailbox a zero cursor that re-bootstrapped straight to Gmail's current historyId and silently skipped everything that arrived since the last successful sync, unrecoverably because the history API only walks forward from the id it is given, by threading the EmailHistoryIDRepository the consumer already writes to into emailService as the Google counterpart of the existing WireGraphDelta cursor plumbing for Outlook, reading it in buildAddWorkerEmail through a lastHistoryFor helper that prefers the live checkpoint, still falls back to last_id for rows carrying a value from before the checkpoint table existed, and returns zero only when genuinely nothing is known so a first-time mailbox bootstraps a fresh baseline, plus tests covering the precedence and every fallback including an unwired repository
fix/110-history-checkpoint-reload
Matthew Meszaros
2026-08-16 07:07:19 +02:00
f878e83832
feat: make the Gmail history checkpoint persist and advance, by setting UserID and EmailID on the JobEventHistoryIDUpdate that NewHistoryID publishes (email_history_ids is keyed (user_id, email_id) with a foreign key to users, so the zero UUIDs it was sending made every checkpoint write fail on email_history_ids_user_id_fkey and no Gmail mailbox ever got a row, which is why replies, opens via label changes and every other inbound signal never reached Unibox), bootstrapping a mailbox with no baseline from Users.GetProfile instead of calling history.list with startHistoryId=0 which Gmail rejects with 'Requested entity was not found' so a freshly connected mailbox could never establish one, advancing the in-memory GoogleData.LastHistoryID after a successful walk since it was only ever written at construction and a stale cursor re-walks the window just processed while a zero one re-bootstraps past everything that arrived in between, and no longer discarding a MailError when a partial history was processed in the same tick, matching the field assignment the already-correct but uncalled ImapGoogleSync has had all along
fix/109-gmail-history-checkpoint
Matthew Meszaros
2026-08-16 07:05:29 +02:00
7ff10fa789
feat: send every Gmail message as raw RFC 5322 instead of the structured gmail.MessagePart payload, which Gmail's users.messages.send rejects outright with "'raw' RFC822 payload message string or uploading message via /upload/* URL required" because the structured Payload tree is the read representation returned by messages.get and is not accepted on send, so every non-attachment Gmail send failed with a 400 that was retried and then dead-lettered under a misleading SERVER_UNREACHABLE label while the attachment path already built raw correctly, building the narrowest correct MIME structure per message rather than routing everything through the multipart/mixed attachment builder (bare text/plain for warmup and text-only campaigns, multipart/alternative once there is an HTML body, multipart/mixed only when files are attached, because a needlessly nested tree is a structural difference cold outreach does not need), RFC 2047-encoding the Subject and building the From header through net/mail.Address now that header encoding is ours rather than the API's, so a non-ASCII display name is no longer emitted as bare 8-bit bytes and a name containing a comma no longer splits the header into two recipients, and adding tests that parse the built message back with net/mail and mime/multipart to assert the structure instead of matching strings
fix/107-gmail-raw-send
Matthew Meszaros
2026-08-16 07:03:20 +02:00
9d6e09072c
feat: wire OnTokenRefresh on the Gmail worker client so every send and sync stops panicking, since goog.Client was constructed with all four message and label callbacks but no token callback while goog.Init unconditionally wrapped the token source in stoken, whose Token() calls that callback on every single request from inside the oauth2 transport's RoundTrip, making the nil func value a guaranteed nil-pointer dereference on the first Gmail API call any mailbox made (the Outlook path immediately below it set the same field correctly, so no Microsoft mailbox was affected), additionally guarding both goog.Init and msgraph.Init so the stoken wrapper is only installed when there is somewhere to persist a refreshed token to, hardening stoken.Token itself against a nil callback because it runs inside RoundTrip where a panic takes down the caller's request rather than surfacing as an error, and adding a regression test that panics without the guard and passes with it
fix/106-gmail-token-refresh
Matthew Meszaros
2026-08-16 07:00:24 +02:00
eda99d5998
feat: accept the API's origin in the mailbox OAuth callback listener so connecting Gmail or Outlook completes on a split-domain deployment, where the bridge page is served by the backend (deliberately, so the registered redirect_uri survives front-end changes) and therefore arrives with event.origin equal to API_URL while the dashboard only ever compared it against APP_URL, silently discarding every callback and leaving the connect modal on 'Waiting for authorization' forever even though the provider exchange had already succeeded, normalising both configured bases through URL.origin so a trailing slash no longer breaks the comparison either, and separately deriving the bridge's postMessage target origin from APP_URL when APP_ORIGIN is unset instead of falling back to a wildcard that posts the authorization code to whatever origin the opener happens to have, since compose never set APP_ORIGIN despite the configuration table claiming it was derived, with the app_origin_wildcard health check and both docs pages updated to match the narrower condition that now triggers it
fix/102-oauth-postmessage-origin
Matthew Meszaros
2026-08-16 06:58:35 +02:00
2f542e890b
feat: build the Gmail and Outlook mailbox-connect redirect_uri from API_PUBLIC_URL instead of API_HOST, which is the listener's bind address and stays 0.0.0.0:8080 in a container, so every self-hosted OAuth connect sent Google and Microsoft a redirect_uri that is not even an absolute URI and was rejected with invalid_request before the account picker appeared, adding an oauthPublicBaseURL helper next to the existing oidcRedirectURL that reads the same already-documented variable and falls back to a browsable http://localhost:PORT derived from the bind address (mapping the 0.0.0.0, :: and bare-port wildcard forms to localhost, and passing through a value that is already a URL) so a stock local install emits exactly the http://localhost:8080/addresses/google/callback the guide tells operators to register, plus a test pinning the resolution order and the root-registered callback path, and a deployment-guide note naming API_PUBLIC_URL as the variable that has to match the registered URI
fix/101-oauth-redirect-public-url
Matthew Meszaros
2026-08-16 06:55:31 +02:00
ce930299ad
feat: seal Gmail and Outlook OAuth access and refresh tokens at rest in email_accounts_oauth instead of storing the provider's raw tokens, which the connect UI already promised were encrypted and which the read path could never open because it unconditionally hex-decodes, adding sealCredential/openCredential helpers that fail closed when CREDENTIALS_ENCRYPTION_KEY is unset, encrypting on both write paths (NewOauthAccount at connect time and RefreshBoxToken on every worker token refresh, which would otherwise revert a sealed row to plaintext on first refresh), migrating pre-existing plaintext rows lazily on first read because the key lives in the application and no SQL-only migration can reach it, restoring the missing return on the OAuth insert failure that let a failed token write commit an account row with no credentials, keeping token parameters out of Sentry error reports, and correcting the two docs tables that scoped the key to SMTP and IMAP only
fix/105-encrypt-oauth-tokens
Matthew Meszaros
2026-08-16 06:52:44 +02:00
734cb5fe08
feat: make self-hosted onboarding survivable by fixing invite_only, which could not onboard anyone (the accept route is JWT-only, so redeeming the invitation that would create your account required already having one, making the self-host default silently identical to fully closed), threading the invitation token through registration so an invited person lands in the inviting organization instead of a stray workspace, gating SSO just-in-time provisioning behind DISABLE_REGISTRATION (it bypassed the gate entirely, so an instance set to true was still open to anyone the IdP would assert) with SSO_AUTO_PROVISION as the opt-out, correcting the OIDC redirect URL that pointed at /api/v1 against a route at /v1 and 404'd every SSO login, scoping the first-launch exemption so it no longer overrides an explicit lockdown, preserving the remaining TTL when restoring a losing setup token so a public endpoint cannot hold the claim window open forever, replacing a generic 403 with typed registration_invite_only, registration_closed, invitation_invalid, setup_token_invalid and setup_already_complete codes that name the next step, logging why no claim link was issued on an already-claimed instance instead of staying silent, adding a warmblyctl operator CLI (status with health checks and a non-zero exit, reissuable setup-link, user create/list/reset-password/grant-admin/revoke-admin/disable-2fa, hash-password) so a locked-out operator no longer needs hand-written psql, adding read-only instance configuration over 104 environment variables with structural secret redaction and fingerprints, 35 health checks, a database-backed settings tier for the three keys no environment variable owns, hiding the signup form when the config already says invite_only rather than failing the whole form with a toast, and documenting first run, accounts and access, configuration, instance health and troubleshooting alongside the root .env.example the README told operators to write but never shipped (#114)
Matthew Meszaros
2026-08-16 05:58:11 +02:00
2c148cef2a
feat: make self-hosted onboarding survivable by fixing invite_only, which could not onboard anyone (the accept route is JWT-only, so redeeming the invitation that would create your account required already having one, making the self-host default silently identical to fully closed), threading the invitation token through registration so an invited person lands in the inviting organization instead of a stray workspace, gating SSO just-in-time provisioning behind DISABLE_REGISTRATION (it bypassed the gate entirely, so an instance set to true was still open to anyone the IdP would assert) with SSO_AUTO_PROVISION as the opt-out, correcting the OIDC redirect URL that pointed at /api/v1 against a route at /v1 and 404'd every SSO login, scoping the first-launch exemption so it no longer overrides an explicit lockdown, preserving the remaining TTL when restoring a losing setup token so a public endpoint cannot hold the claim window open forever, replacing a generic 403 with typed registration_invite_only, registration_closed, invitation_invalid, setup_token_invalid and setup_already_complete codes that name the next step, logging why no claim link was issued on an already-claimed instance instead of staying silent, adding a warmblyctl operator CLI (status with health checks and a non-zero exit, reissuable setup-link, user create/list/reset-password/grant-admin/revoke-admin/disable-2fa, hash-password) so a locked-out operator no longer needs hand-written psql, adding read-only instance configuration over 104 environment variables with structural secret redaction and fingerprints, 35 health checks, a database-backed settings tier for the three keys no environment variable owns, hiding the signup form when the config already says invite_only rather than failing the whole form with a toast, and documenting first run, accounts and access, configuration, instance health and troubleshooting alongside the root .env.example the README told operators to write but never shipped
feat/selfhost-first-run
Matthew Meszaros
2026-08-16 05:54:53 +02:00
0ae4db2c41
feat: make self-hosted auth work without a mail relay by rewriting the platform SMTP transport with real AUTH and TLS (it did neither, so SMTP_USERNAME/SMTP_PASSWORD were dead and every documented relay was unreachable), adding MAIL_TRANSPORT=smtp|log|ses with a log transport that prints codes so a fresh install can sign in with no relay, demoting the emailed login code to AUTH_LOGIN_CODE=always|new_device|off (off on self-host, per NIST SP 800-63B and OWASP ASVS), claiming the first owner through a single-use setup link or WARMBLY_BOOTSTRAP_* instead of register-then-psql, deriving every emailed URL from APP_URL rather than a hardcoded app.warmbly.com that leaked live reset tokens to the vendor, fixing the confirm hooks that read path params against paramless routes and broke login, register and reset confirmation in the dashboard everywhere, adding generic OIDC with PKCE, one-time state, verified nonce and (issuer,subject) identity binding, enforcing 2FA on the social paths that skipped it, adding a per-IP limiter and trusted-proxy handling to the unthrottled auth group, refusing boot on the published default secrets, and dropping mailpit from the default stack (#99)
Matthew Meszaros
2026-08-14 14:57:09 +02:00
1387541bf0
feat: make self-hosted auth work without a mail relay by rewriting the platform SMTP transport with real AUTH and TLS (it did neither, so SMTP_USERNAME/SMTP_PASSWORD were dead and every documented relay was unreachable), adding MAIL_TRANSPORT=smtp|log|ses with a log transport that prints codes so a fresh install can sign in with no relay, demoting the emailed login code to AUTH_LOGIN_CODE=always|new_device|off (off on self-host, per NIST SP 800-63B and OWASP ASVS), claiming the first owner through a single-use setup link or WARMBLY_BOOTSTRAP_* instead of register-then-psql, deriving every emailed URL from APP_URL rather than a hardcoded app.warmbly.com that leaked live reset tokens to the vendor, fixing the confirm hooks that read path params against paramless routes and broke login, register and reset confirmation in the dashboard everywhere, adding generic OIDC with PKCE, one-time state, verified nonce and (issuer,subject) identity binding, enforcing 2FA on the social paths that skipped it, adding a per-IP limiter and trusted-proxy handling to the unthrottled auth group, refusing boot on the published default secrets, and dropping mailpit from the default stack
selfhost-auth
Matthew Meszaros
2026-08-14 10:37:14 +02:00
8f465fdb1c
feat: give each mailbox a human sending persona (randomized daily and hourly caps, send spacing, work start/end, lunch break and working weekdays, rolled once per local day in the mailbox's own timezone and applied across the campaign, warmup and smart-send schedulers), add campaign auto-pause guardrails that stop a campaign when its bounce, complaint or reply rate leaves the configured band, make mailbox rotation actually rotate for tag-resolved and all-mailbox campaigns, stop every scheduler from ever returning a slot in the past, and correct the mailbox min-gap field that stored seconds while labelling them minutes
Matthew Meszaros
2026-08-13 16:51:29 +02:00
a75590d628
feat: stop every scheduler from ever handing back a slot in the past (symmetric jitter and the sub-minute humanizer could both move a near-term send backwards, where it fires with none of its spacing or is cancelled as overdue), return 404 instead of 500 for a mailbox the caller does not own, and add live Postgres integration tests that drive the real campaign scheduler and guardrail sweep end to end
feat/human-sending-behavior
Matthew Meszaros
2026-08-13 16:45:48 +02:00
12bf9480af
feat: make mailbox rotation actually rotate for tag-resolved and all-mailbox campaigns by deriving the round-robin cursor from today's send count and least-recently-used from real send history when a mailbox has no campaign_senders row, correct the mailbox min-gap field that stored seconds while labelling them minutes, surface today's rolled workday on the mailbox overview, extend the audit spine to refresh per-mailbox detail reads, and enable a sending persona on the sandbox showcase mailboxes
Matthew Meszaros
2026-08-13 14:21:14 +02:00
d282913928
feat: give each mailbox a human sending persona (randomized daily and hourly caps, send spacing, work start/end, lunch break, working weekdays, all rolled once per local day in the mailbox's own timezone and applied across the campaign, warmup and smart-send schedulers) and add campaign auto-pause guardrails that stop a campaign when its bounce, complaint or reply rate leaves the configured band
Matthew Meszaros
2026-08-13 13:39:47 +02:00
8bd2c2b57a
feat: make self-hosting work end to end and rewrite the guide around what was tested (#97)
Matthew Meszaros
2026-08-13 09:47:46 +02:00
446ecad33c
feat: tell the user what to do when they try to connect a Gmail or Microsoft mailbox on a deployment with no OAuth client, replacing the generic 500 with a 503 carrying a stable mailbox_provider_not_configured code, the exact BOX_* variables to set, and a link to the environment setup guide, rendered as an inline panel in the add-mailbox modal instead of a truncated toast
feat/self-hosting-fixes
Matthew Meszaros
2026-08-13 09:36:13 +02:00
6fc3836659
fix: drop the stale web/package-lock.json so trivy stops failing CI on a lockfile nothing builds from, since web installs with pnpm in the Dockerfile, in CI, and locally, leaving the npm lockfile as an unused copy that only pins vulnerable transitive versions like nanoid 3.3.16
Matthew Meszaros
2026-08-13 06:53:42 +02:00
90fd048055
fix: clear the two HIGH trivy findings blocking CI by overriding the transitive nanoid to 3.3.17+ (CVE-2026-67213 infinite-loop DoS) across web, admin, docs, and site, and raising the existing js-yaml pin to 4.3.1 (GHSA-5p4m-2wfm-xmqj quadratic CPU use resolving !!omap)
Matthew Meszaros
2026-08-13 06:49:54 +02:00
0c09a42089
feat: make self-hosting work end to end by shipping the free-trial plan in a migration so the first signup no longer violates the subscriptions FK, treating every org as paid and skipping stale-heartbeat workers so mailbox placement can find the bundled worker at all, sending a farewell heartbeat on shutdown, putting the decryption keys and blob config into enrolled worker envs plus a blob mount in the installer, fixing the duplicate org-channel realtime delivery, driving the whole stack from one root .env, and rewriting the self-hosting guide as a tested step-by-step walkthrough
Matthew Meszaros
2026-08-13 06:42:46 +02:00
a7518a8558
docs: refresh the documentation site, fix inaccurate claims and contact addresses, add SEO primitives (#90)
v0.2.0
Matthew Meszaros
2026-08-05 10:37:27 +02:00
50f50e680d
feat: make the inbound mail pipeline work end to end by never publishing the eventbus partition key as Nats-Msg-Id (JetStream deduped every event after the first per mailbox), fetching IMAP message bodies after the outer FETCH closes instead of nesting one inside it (which deadlocked sync on the first message), wrapping NEW_EMAIL in JobEventNewEmail across all three providers so the consumer stops nil-derefing, coalescing nil arrays before the NOT NULL unibox columns, sealing validation credentials on a copy so stored SMTP/IMAP passwords are not double encrypted, routing the email task type to the user email handler, and returning false instead of closing a nil conn in VerifySMTP (#88)
Matthew Meszaros
2026-07-31 09:41:36 +02:00
5e6287c920
feat: add the Advisor, continuous sending checks surfaced on the row they are about (#86)
Matthew Meszaros
2026-07-30 17:15:09 +02:00
b342629534
Merge pull request #85 from Turtlesfr/fix/duplicate-migration-000077
Matthew Meszaros
2026-07-26 19:44:20 +02:00
764500391c
fix: clear the trivy security scan by bumping react-router-dom to 7.18.1 in web and admin, postcss to 8.5.23 across web/admin/docs (docs via a pnpm-workspace override since next pins 8.4.31), next and eslint-config-next to 16.2.11, and ignoring the RSC-only react-router CSRF advisory
Matthew Meszaros
2026-07-26 18:16:12 +02:00
a8dcc8e49b
fix: renumber migration 000077_ai_variables_resolved to 000078 to resolve the duplicate version with 000077_warmup_generation_scale that makes golang-migrate fail on backend boot
Alexandre
2026-07-26 11:05:59 -04:00
7efd4c09df
Merge pull request #84 from warmbly/release-frontend-assets
v0.1.0
Matthew Meszaros
2026-07-22 18:43:25 +02:00
53a05fa7dd
docs: document the web and admin release images and their WARMBLY_* runtime env in the deployment guide
Matthew Meszaros
2026-07-22 18:40:06 +02:00
084dd0a3d9
ci: build the web and admin images on PRs that touch them and run the web typecheck so broken frontend Dockerfiles and type errors are caught before a release
Matthew Meszaros
2026-07-22 18:40:06 +02:00
79efbd4b5b
feat: publish the web and admin images to GHCR in the release workflow and serve those production builds from the prod docker-compose instead of the Vite dev servers
Matthew Meszaros
2026-07-22 18:40:06 +02:00
664774e1e3
feat: add production nginx Dockerfiles for the web dashboard and admin panel that build the SPA once and render /config.js from WARMBLY_* env at container start
Matthew Meszaros
2026-07-22 18:40:06 +02:00
611ecba1c2
feat: add the same runtime config shim to the admin panel so its built image reads api url, dashboard url, env label, and turnstile key from container env
Matthew Meszaros
2026-07-22 18:40:06 +02:00
9a67008ea2
feat: add a runtime config shim to the dashboard so a single built image reads its API url, app url, tracking domain, and turnstile key from container env via /config.js
Matthew Meszaros
2026-07-22 18:40:06 +02:00
666ea44256
fix: type the conditional token build/parse helpers with a uid-less subset so the web typecheck passes on the transient uid attribute
Matthew Meszaros
2026-07-22 18:40:06 +02:00
443dcbf4b5
Merge pull request #83 from warmbly/ai-content-blocks
Matthew Meszaros
2026-07-22 17:59:36 +02:00
cca3bd5ba0
fix: keep the marketing site on sharp 0.34.5 since 0.35 breaks its Cloudflare build, bump svgo to 4.0.2, and suppress the remaining build-only sharp HIGH in the documented .trivyignore
Matthew Meszaros
2026-07-22 17:53:13 +02:00
00e12fab8f
fix: upgrade grpc to 1.82.1 (plus its otel/oauth2/genproto bumps) to clear the HIGH GHSA-hrxh-6v49-42gf xDS RBAC and HTTP/2 advisories
Matthew Meszaros
2026-07-22 17:53:13 +02:00
66e9856ea8
fix: bump sharp to 0.35 in docs and site and linkify-it to 5.0.2 in web via pnpm overrides to clear the HIGH-severity Trivy CVEs blocking the security scan
Matthew Meszaros
2026-07-22 17:33:52 +02:00
55dfa56c33
docs: wrap the company merge token in backticks in the AI variables guide so MDX does not parse it as a JS expression and the docs build succeeds
Matthew Meszaros
2026-07-22 17:24:07 +02:00
a7d06cbfbe
docs: italicize the support and enterprise note callout to match the repo aside style
Matthew Meszaros
2026-07-22 17:17:14 +02:00
f2f06b10ed
docs: add a Runs on Docker Compose badge to the self-hosting section and restyle the support and enterprise section as a blue-bordered note callout
Matthew Meszaros
2026-07-22 17:14:06 +02:00
225a9c7472
docs: add a support and enterprise section to the README with managed-infrastructure and enterprise-support offers plus WarmblyHQ X follow buttons
Matthew Meszaros
2026-07-22 17:05:38 +02:00
8032167837
docs: document the expanded agent tool surface in the MCP reference and rewrite the AI assistant guide for real sending with approval and full workspace parity
Matthew Meszaros
2026-07-22 17:05:38 +02:00
fe127c9d8d
feat: add automation agent tools to list, read, update, enable or disable, and delete automations while preserving the node graph
Matthew Meszaros
2026-07-22 17:05:37 +02:00
67c4dbfdfb
feat: add API key and webhook management agent tools gated on manage-api-keys and manage-settings
Matthew Meszaros
2026-07-22 17:05:37 +02:00
1efc722458
feat: add JWT-only team, org-settings, voice-profile, and read-only billing agent tools that API keys and MCP cannot reach
Matthew Meszaros
2026-07-22 17:05:37 +02:00
49507b0244
feat: add mailbox management agent tools for reading, updating sending limits and warmup, tracking domains, warmup appeals, and disconnecting a mailbox
Matthew Meszaros
2026-07-22 17:05:37 +02:00
64ccd1b273
feat: add send_reply and compose_email agent tools as RiskSend with a recipient-suppression gate, so the assistant can send with per-action approval and never over MCP
Matthew Meszaros
2026-07-22 17:05:37 +02:00
ae99322f29
feat: add unified-inbox agent tools for mark-seen, thread labels, snooze, and scheduled-send review and cancel
Matthew Meszaros
2026-07-22 17:05:24 +02:00
6d8385c89e
feat: add campaign sequence-step agent tools to list, add, update, and delete steps
Matthew Meszaros
2026-07-22 17:05:24 +02:00
2efffda0f6
feat: add campaign get, update, delete, sender-set, tracking-domain verify, and logs agent tools
Matthew Meszaros
2026-07-22 17:05:24 +02:00
66c49bddc0
feat: add full CRM agent tools covering deals, tasks, task types, notes, pipelines, and stages end to end
Matthew Meszaros
2026-07-22 17:05:24 +02:00
2fba098313
feat: add contact create, delete, bulk-edit, timeline, and sent-email agent tools, each gated by the caller's contact permission and org
Matthew Meszaros
2026-07-22 17:05:24 +02:00
2da507c673
feat: wire the new agent tool service dependencies into the tool registry and hoist advancedService construction so the compose and reply suppression gate is available
Matthew Meszaros
2026-07-22 17:05:24 +02:00
97461cd0cb
feat: expand aitools Deps with mailbox, send, compose, warmup, sequence, org, api-key, webhook, subscription, and suppression services and register the new tool groups
Matthew Meszaros
2026-07-22 17:05:24 +02:00
0f86917880
feat: add a JWTOnly gate to the aitools registry so tools whose HTTP route has no API-key scope are hidden from API-key and MCP callers
Matthew Meszaros
2026-07-22 17:05:24 +02:00
58c166c073
docs: add the AI variables guide and update the campaigns API reference, endpoints scope map, AI credits, and guides nav for per-recipient AI blocks
Matthew Meszaros
2026-07-22 17:05:08 +02:00
14587b9cd1
feat: swap the OG and Twitter social image for the new Social.jpg and update its width and height to 1280x640
Matthew Meszaros
2026-07-22 17:05:08 +02:00
887106c3d9
feat: add BuildInlineSnippetRules humanization prompt for AI variables so inline fragments read plainly and fit the surrounding sentence instead of copywriting rhythm
Matthew Meszaros
2026-07-22 17:05:08 +02:00