Commit Graph
412 Commits
Author SHA1 Message Date
Dries Augustyns 80beb2bb99 feat(EmailService): add worker concurrency settings and improve email queue prioritization 2026-05-27 17:53:30 +02:00
Dries Augustyns 6ab4d77ca9 feat(SecurityService): enhance phishing detection by verifying sender domains and institutional TLDs 2026-05-25 18:16:07 +02:00
Dries Augustyns edfc399061 feat(SecurityService): enhance phishing detection by verifying sender domains and institutional TLDs 2026-05-25 18:04:52 +02:00
Dries AugustynsandGitHub 4a145f3488 Merge pull request #391 from andygrunwald/andygrunwald/csv-import-coerce-custom-field-types
fix: coerce boolean and numeric values in custom CSV columns
2026-05-25 07:30:50 +02:00
Andy Grunwald 868bdee615 import-processor: Reworked comments for coerceCustomValue 2026-05-24 16:15:57 +02:00
Andy GrunwaldandClaude Opus 4.7 971b98a4cc docs(wiki): document MAIL_FROM_SUBDOMAIN and NGINX_PORT env vars
Both variables exist in .env.self-host.example but were missing from the
user-facing env vars reference, so self-hosters had to read the example file's
inline comments to discover them.

- MAIL_FROM_SUBDOMAIN: added a row to the AWS SES section explaining how the
  prefix combines with a verified domain to construct the MAIL FROM hostname,
  and when to override the default.
- NGINX_PORT: added a new "Advanced" section for variables that almost never
  need tuning, with the host-port override use-case spelled out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:56:00 +02:00
Andy GrunwaldandClaude Opus 4.7 81315eac8e docs(env): add wiki-documented vars to apps/api/.env.example
Brings the dev template into parity with the env vars wiki for variables a
developer might toggle locally:

- PORT (in Environment & Security)
- PLUNK_FROM_ADDRESS (in Plunk API)
- New sections: Notifications (NTFY_URL), Attachments, User Management
  (DISABLE_SIGNUPS, VERIFY_EMAIL_ON_SIGNUP), Phishing Detection

All additions are commented so default local behaviour is unchanged. Vars that
only make sense in the bundled Docker stack (SMTP relay ports, Minio internals,
NEXT_PUBLIC_*, NGINX_PORT) are intentionally omitted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:55:52 +02:00
Andy GrunwaldandClaude Opus 4.7 844be42151 Document boolean and numeric CSV value typing
The preceding commits taught the import worker to coerce custom CSV
column values into JSON booleans and numbers via `coerceCustomValue`
in `apps/api/src/jobs/import-processor.ts`. Without a corresponding
docs update, users can't predict whether a cell like `01234` lands as
a string or as the number `1234`, or which segment-filter operators a
field will expose after import.

Add two bullets to the existing "Rules and limits" list in the
contact-import guide, adjacent to the **Date columns** bullet that
already documents value typing for ISO 8601 dates. The bullets mirror
that style and brevity: one names the boolean keyword set and the
toggle it unlocks in segment filters, the other names the numeric
pattern, the `gt`/`lt` operators it unlocks, and the deliberately
preserved-as-string forms (leading zeros, `+`-prefixed, scientific
notation) so users keep their IDs, zip codes, and phone numbers
intact.

No other content is touched. Closes the documentation gap for
useplunk/plunk#390.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 11:26:52 +02:00
Andy GrunwaldandClaude Opus 4.7 d59f4a10cd Treat "0" and "1" as numbers, not booleans, in custom CSV columns
The previous coercion treated "1"/"0" as booleans for parity with the
reserved `subscribed` column parser, but in custom columns those values
are far more often counts, quantities, or ids than true/false flags.
Coercing them to booleans hid the numeric segment-filter operators
(`gt`, `lt`) for fields that semantically are numbers, and miscategorised
them in `ContactService.getAvailableFields()` via `jsonb_typeof()`.

Drop "1"/"0" from the truthy/falsy keyword sets in `coerceCustomValue`.
With those entries gone the existing `NUMERIC_RE` branch picks both up
unchanged (the pattern already matches single-digit `0` and `1`), so
they land in `Contact.data` as JSON numbers. Boolean keyword coverage
remains for `true`/`false`/`yes`/`no` (case-insensitive, trimmed).

The reserved `subscribed` column parser at the top of the worker keeps
its own inline keyword set and continues to accept "1"/"0" — that
column is explicitly boolean by contract, so the asymmetry is
intentional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:16:34 +02:00
Andy GrunwaldandClaude Opus 4.7 51ac88b9f5 Coerce custom CSV column values to number during import
Extends the helper introduced for boolean coercion so numeric values
(`42`, `3.14`) become JSON numbers in `Contact.data` instead of strings.
With the right JSON primitive in place, post-import inference via
`jsonb_typeof()` classifies the field as `number`, the dashboard's
segment-filter UI renders a numeric input, and comparison operators
(`gt`, `lt`) become available.

Use a strict integer-or-decimal regex rather than `Number()` to
preserve string-shaped numerics that users intend as identifiers:
leading-zero IDs (`"01234"`), zip codes, phone numbers, and signed or
scientific-notation forms (`"+42"`, `"1e10"`, `".5"`, `"42."`) are left
as strings. Boolean coercion still takes precedence, so `"1"` and `"0"`
remain booleans for consistency with how `subscribed` is parsed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:16:34 +02:00
Andy GrunwaldandClaude Opus 4.7 523bcad602 Coerce custom CSV column values to boolean during import
Custom CSV columns with values like "true"/"false" were stored as
strings, which made `ContactService.getAvailableFields()` report them
as string fields and lost the boolean toggle in the dashboard's
segment-filter UI. Only the reserved `subscribed` column was coerced.

Mirror the truthy keyword set already used for `subscribed`
(`true`/`1`/`yes`) and add its symmetric falsy counterpart
(`false`/`0`/`no`) for custom columns. Values outside that keyword set
are returned unchanged so names, IDs, or arbitrary strings are not
corrupted into `false`. Coercion runs over `Object.entries(customData)`
right before the `upsert()` call; no other code path needs to change
because `ContactService.upsert()` already accepts
`Record<string, unknown>` and `mergeContactData()` accepts mixed JSON
primitives. Post-import inference via `jsonb_typeof()` then classifies
the stored JSON boolean as type `boolean` automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:16:33 +02:00
Dries Augustyns 71e2277643 refactor(database): increase Prisma connection pool limits for improved test performance 2026-05-22 20:43:49 +02:00
Dries Augustyns 079e1879b3 test(SecurityService): update test case for complaint count thresholds to reflect new ceiling values 2026-05-22 20:20:45 +02:00
Dries Augustyns 4de40f40fa refactor(SecurityService): update absolute count ceilings for new projects to improve spam detection 2026-05-22 20:12:25 +02:00
Dries Augustyns 94ceadbbe4 feat: add disabledReason field to projects for better tracking of disable reasons 2026-05-22 13:03:32 +02:00
Dries AugustynsandGitHub 9797aed47f Merge pull request #384 from jaschaio/tiptap-aware-html-detection
feat: make detectCustomHtmlPatterns aware of TipTap's actual capabilities
2026-05-17 20:24:49 +02:00
Dries AugustynsandGitHub 8b3657d056 Merge pull request #383 from taniasanz7/patch-28-uniform-filter-row-heights
fix: make email templates, campaigns and workflow search inputs same height as the rest of the app
2026-05-17 19:15:31 +02:00
Dries Augustyns 01ec34a8cb docs: add new recipe pages for waitlist and sync unsubscribes 2026-05-17 18:08:26 +02:00
jaschaio ba3813e242 feat: make detectCustomHtmlPatterns aware of TipTap's actual capabilities
The previous detection tripped on ANY inline `style=` attribute and on
`<span>` elements specifically, which forced templates into HTML-only
editing mode whenever the user had used Visual mode features like text
color. TipTap's TextStyle + Color + Link extensions (configured in
EmailEditor.tsx) natively round-trip exactly that markup -- TipTap emits
`<span style="color: rgb(...)">…</span>` itself when you change a text
color, then the detection rejected it as "custom HTML" on the very next
load. Empirically about 21% of a 156-template corpus tripped this purely
on TipTap-export artifacts (`background-color: initial`, color spans).

This rewrite permits what TipTap can represent and rejects only what it
can't:

* Drop the broad inline-style check entirely (TextStyle/Color/Link
  preserve inline styles on spans and links).
* Remove `<span>` from the custom-elements list (TextStyle handles it).
* Expand the custom-elements list to explicitly cover everything TipTap
  has no extension for: `<div>`, `<section>`, `<article>`, `<header>`,
  `<footer>`, `<nav>`, `<aside>`, `<main>`, full table family
  (`<table>`, `<tr>`, `<td>`, `<th>`, `<tbody>`, `<thead>`, `<tfoot>`,
  `<colgroup>`, `<col>` -- no Table extension is loaded), form/embed/
  media/interactive (`<form>`, `<input>`, `<button>`, `<select>`,
  `<textarea>`, `<iframe>`, `<video>`, `<audio>`, `<svg>`, `<object>`,
  `<embed>`, `<details>`, `<summary>`, `<dialog>`). Single-table is now
  enough to opt out (previously needed nested tables -- harmless
  tightening, single `<table>` already isn't TipTap content).
* Tighten the custom-attributes regex with a leading `[\s"']` boundary so
  query strings like `<a href="…?id=…">` no longer false-match as an
  HTML `id=` attribute.
* `<style>` tags, `@media` queries, and the class allowlist (`prose`,
  `variable-`, `email-image`, `ProseMirror`, `resizable-image`,
  `selected`, `resize-handle`) are unchanged.

Mirrors the same logic in apps/api/src/services/EmailService.ts so the
server-side wrap decision in `EmailService.compile()` stays in lockstep
with the client-side editor-mode decision.

Side-effect on `wrapEmailWithStyles` / `EmailService.compile`: templates
that previously kept their own (unwrapped) shell because they contained
a colored `<span>` or an inline-styled `<a>` will now flow through the
prose wrapper. This is the correct behavior -- those templates ARE
visual-editor output and SHOULD get the same wrapper the preview modal
applies.

Tests: new vitest suite at apps/web/src/lib/__tests__/emailStyles.test.ts
covers 24 cases including the TipTap-export artifacts above, the
href-URL-with-id false-match, and the rejected-element set.
2026-05-17 15:57:37 +02:00
Tania Sanz 283f40239d fix(filters): land templates/workflows/campaigns search inputs at 32px to match filter buttons
The Input atom defaults to h-9 (36px) while the size="sm" filter
Buttons in the same row are h-8 (32px). On /workflows the search bar
is alone in the row so the mismatch isn't visible. On /templates and
/campaigns the type/status filter buttons sit next to the search bar
and the row stretches to the Input's 36px, visibly offsetting the
buttons. Shrink the search Input to h-8 text-xs on all three pages
and add sm:items-center to the filter rows so the controls vertically
center.
2026-05-17 15:57:14 +02:00
Dries Augustyns 53b631e6c6 seo: add data-nosnippet attribute and improve markdown type negotiation 2026-05-17 11:09:54 +02:00
Dries AugustynsandGitHub 6759bffd2a Merge pull request #381 from taniasanz7/patch-29-contact-email-link
feat(contacts): make email cell a link to the contact detail page
2026-05-16 21:45:25 +02:00
Dries AugustynsandGitHub d2496bc51d Merge pull request #368 from ReylanLugo/feat/domains-api-key-auth
feat(api): allow API key authentication for domain endpoints
2026-05-16 21:42:43 +02:00
Tania Sanz 6d98d51222 feat(contacts): make email cell a link to the contact detail page
Currently the only entry point from the /contacts list to a contact's
detail page is the small Edit icon-button in the actions column. Make
the email itself a Link to /contacts/:id so the obvious affordance
("click the thing that identifies the row") works too. Applied to both
the desktop table cell and the mobile card variant.
2026-05-16 21:26:18 +02:00
taniasanz7andTania Sanz c484da88ab feat: render template variables in WEBHOOK step url, headers and body 2026-05-16 08:11:35 +02:00
Dries Augustyns 9e4aa9443b fix: improve iframe height adjustment logic in EmailEditor component 2026-05-13 19:22:17 +02:00
Dries Augustyns a27d564e1a fix: implement mergeContactData method for efficient contact data updates 2026-05-13 19:19:11 +02:00
Dries Augustyns 4e95c5a4e4 seo: implement OpenAPI operation rendering and add llms documentation 2026-05-13 17:37:02 +02:00
Dries AugustynsandGitHub 463301b5db Merge pull request #373 from taniasanz7/patch-18-configurable-mail-from-subdomain
feat: make MAIL FROM subdomain configurable via MAIL_FROM_SUBDOMAIN env
2026-05-12 22:00:21 +02:00
Dries Augustyns 6195b39f3d feat: add SwitchOffer component to promote switching from competitors for enhanced user engagement 2026-05-12 21:48:35 +02:00
Tania Sanz e0bf0f628a feat: make MAIL FROM subdomain configurable via MAIL_FROM_SUBDOMAIN env var 2026-05-12 20:35:53 +02:00
Dries Augustyns 715961c007 fix: handle undefined path in footer component for improved stability 2026-05-12 08:35:12 +02:00
Dries Augustyns ccd516e4e8 feat: add Markdown cut link to footer and page for improved accessibility 2026-05-12 08:07:57 +02:00
ReylanLugo 6aee5db588 refactor(api): rely on auth middleware for domain endpoint permissions
Replace per-route apiKey/jwt branching with auth.projectId from middleware,
matching the contacts controller pattern. Preserve JWT admin gating on
POST/DELETE; API keys are project-scoped by design and skip the role check.
Cross-project domain access by ID now returns 404 instead of 403 to avoid
leaking existence.
2026-05-10 22:44:28 -04:00
Dries Augustyns 649bbf6d6b fix: enhance Quick Start card layout for improved responsiveness and usability 2026-05-10 13:57:46 +02:00
Dries Augustyns e43f70d8a1 fix: update activity item colors and backgrounds for improved visual distinction 2026-05-10 11:59:54 +02:00
Dries Augustyns f22da4add1 feat: implement caching for recent activity count to optimize performance and reduce database load 2026-05-10 11:37:31 +02:00
Dries Augustyns de6335e999 feat: implement bulk contact action selector for improved flexibility in bulk operations 2026-05-10 10:40:40 +02:00
Dries Augustyns 7658a59b5d feat: enhance campaign scheduling and audience settings UI for better clarity and usability 2026-05-10 09:52:25 +02:00
Dries Augustyns aaf5ac6530 feat: refactor template editing layout for improved usability and clarity 2026-05-10 09:24:21 +02:00
ReylanLugo 3f30a48c40 feat(api): allow API key authentication for domain endpoints
Switch /domains controller from `isAuthenticated` (cookie-only) to
`requireAuth` (cookie OR API key), matching the pattern used by other
project-scoped API endpoints (/v1/send, /contacts, etc.).

API keys are project-scoped credentials with full access; for write
operations the projectId in the request must equal the API key's
projectId. JWT (dashboard) auth retains role-based checks
(requireAdminAccess for POST/DELETE).

Also: improve UX when a domain is already linked to the same project
by returning a clear error instead of the generic "linked to another
project" message.

Refactor DomainService.checkDomainOwnership to make `userId` optional
(needed for API key path) while preserving its existing return shape
and adding `projectId` to the result.
2026-05-09 22:25:17 -04:00
Dries Augustyns c6340a1dc7 feat: add workflow duplication functionality with API endpoint and UI button 2026-05-09 16:51:36 +02:00
Dries Augustyns 4ba43dd3b6 feat: add external link to edit email templates in SendEmailStepDialog and WorkflowBuilder 2026-05-09 16:36:47 +02:00
Dries Augustyns d11495061d feat: add search functionality to campaigns list with debounce effect 2026-05-09 09:10:17 +02:00
Dries Augustyns fadc19d139 feat: Ability to change subscription status in workflows 2026-05-09 08:43:05 +02:00
Dries Augustyns 2ea1802290 fix: update TemplateSearchPicker to maintain selected template name on change 2026-05-08 08:03:15 +02:00
Dries Augustyns 15bdcf6e49 seo: Enhance copy of landing page 2026-05-07 09:21:30 +02:00
Dries Augustyns 2529c9b428 fix: enhance campaign finalization process to handle pending emails and ensure accurate status updates 2026-05-06 21:48:54 +02:00
Dries Augustyns 4ddafdc041 docs: expand documentation with new sections on importing contacts, unsubscribe pages, and API key management 2026-05-06 21:37:00 +02:00
Dries Augustyns 88be252a29 seo: add comparison pages for Mailjet, Buttondown, and Amazon SES 2026-05-06 20:21:49 +02:00