Repository navigation
feat(contacts): import contacts from CSV exports and export to CSV - #88
Merged
Merged
Conversation
POST /sequences/:id/emails/:emailId/test (and the MCP tool send_sequence_email_test) renders the email exactly like a real send, through the delivery source the send would use, with a [Test] subject. It defaults to the signed-in user, takes up to five addresses, skips suppressed ones and is capped at 50 recipients per team per hour. Tracking tokens carry a contact id that matches nobody and the unsubscribe link uses a test token whose page explains it was a test. The email records lastTestSentAt. The broadcast editor and the full-screen email editor get a Send test dialog. The per-recipient render pipeline moves to mail/campaign.ts so real and test sends share it. Refs #34, #79
Every send now carries a text/plain alternative generated from the final
HTML (links as "text (url)", bulleted lists, no images, styles or
preheader), or the caller's own text for transactional sends (new
optional `text`, stored on the row).
Marketing emails link a hosted web version from the managed footer, also
available as the {{ view_in_browser_url }} merge tag. GET /view/:token
serves it for the signed recipient with their merge data and no
tracking, as a sandboxed document. The editor default no longer starts
with a plain "View in browser" line that linked nowhere.
Refs #35, #79
sendMail had no replyTo, so a transactional send's stored Reply-To (and the organization grant's) never reached the recipient. It is now sent, and team ESPs get an optional reply-to (REST, MCP, the ESP dialog) used for broadcasts, sequences, tests and transactional sends without their own. Organization grants fall back to their ESP's reply-to. Refs #47, #79
Each ESP gets an optional maxSendRate (messages per second); when unset, Amazon SES ESPs default to 14/s and Resend to 2/s. sendMail reserves a slot per ESP with a Redis GCRA script, so sends through one ESP are spaced evenly across every worker and process. A send that would wait more than 30s, or that the provider throttles (421/45x, SES "Throttling"), is deferred: sequences reschedule the row without counting a failed attempt and transactional jobs are moved to delayed without using an attempt. REST, MCP and the ESP dialog expose the rate. CI gets a Redis service for the limiter test. Refs #51, #79
POST /contacts/import (MCP import_contacts) upserts up to 1000 rows per request by email. Each row's source status is interpreted with a shared vocabulary (beehiiv Active/Paused/Inactive/Bounced/Pending, Mailchimp cleaned, ...) and a status only gets stricter: nobody is re-subscribed, suppressed addresses stay unsubscribed, bounced and complained rows are suppressed, paused contacts are tagged. Tags and custom fields merge, the earliest signup date wins, imports don't trigger sequences, and subscribed contacts over the plan limit are skipped. The response counts created/updated/unchanged/suppressed rows and lists skipped rows with reasons. GET /contacts/export (MCP export_contacts) returns CSV with status, tags, created_at, suppression reason and custom field columns; it round-trips through the importer. Cells are formula-injection safe. The Contacts page gets an Import dialog (CSV parsed in the browser, column mapping pre-filled from common export headers, chunked upload, report) and an Export button. Refs #38, #79
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #38. Part of #79.
POST /contacts/import(MCPimport_contacts): up to 1000 rows per request, upserted by email. A shared vocabulary in@sendlit/api-contractinterprets source statuses (beehiiv Active/Paused/Inactive/Bounced/Pending/Needs approval, Mailchimp cleaned, …) and recognises common export headers. Statuses only get stricter: nobody is re-subscribed, suppressed addresses stay unsubscribed, bounced/complained rows are suppressed, paused contacts are taggedpaused, pending ones skipped (or imported unsubscribed). Tags/custom fields merge, the earliest signup date wins, imports don't trigger sequences, subscribed contacts over the plan limit are skipped. The response lists created/updated/unchanged/suppressed counts and each skipped row with its reason.GET /contacts/export(MCPexport_contacts): CSV with status, tags, created_at, suppression reason and custom field columns; round-trips through the importer; formula-injection safe.Testing