-
-
Notifications
You must be signed in to change notification settings - Fork 11.3k
Postmark integration #22771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Postmark integration #22771
Conversation
Copy pasted from Mailgun client
We still need to batch the batches
ref TryGhost#22771 This establishes the foundation for multi-provider email support using Ghost's existing AdapterManager pattern. The EmailProviderBase class defines the contract that all email provider adapters must follow, enabling community-developed providers to be published as npm packages. - Created EmailProviderBase with required send() method - Registered email adapter type in AdapterManager - Added comprehensive tests validating base class contract
Hi @ErisDS I’ve submitted a PR series to add the adapter. Hoping to align on next steps for it with you and begin a plan for the first third-party Amazon SES adapter and admin UX work. Happy to also collaborate with @andreascreten @peterpacket on migrating their Postmark work too if useful. Thanks for any guidance and support you can offer me to help make this happen! |
no issue - `tokenProvider.verifyOTC` has an internal dependency on the `otcRef` being correct meaning we'd never reach the `INVALID_OTC_REF` condition - nothing changes for user-visible behaviour but this re-order does mean that logging can be more explicit about the type of error that has occurred ----- Co-authored-by: Steve Larson <9larsons@gmail.com>
ref https://linear.app/ghost/issue/BER-2977/ - during a refactor of `SingleUseTokenProvider` the time-since-first-usage check was extracted to a method but in doing so the pre-condition check for the token having been used was lost meaning all magic links had an effective lifetime of `validityAfterFirstUsage` (10 minutes) - restored pre-condition check and added missing test that would have caught this during the refactor
ref TryGhost#22771 This establishes the foundation for multi-provider email support using Ghost's existing AdapterManager pattern. The EmailProviderBase class defines the contract that all email provider adapters must follow, enabling community-developed providers to be published as npm packages. - Created EmailProviderBase with required send() method - Registered email adapter type in AdapterManager - Added comprehensive tests validating base class contract
fixes TryGhost#22771 PR6 (SES Analytics): - Fixed partial SQS message deletion: When maxEvents limit is reached mid-message, don't delete the message so next run can process remaining recipients - Added fullyProcessed flag to track whether all events from a message were processed PR7 (Email Personalization): - Fixed hasPersonalization detection: Now ignores required system tokens (list_unsubscribe) that are always present - Bulk BCC path now actually runs for non-personalized newsletters - Only sends personalized emails when actual member data is used (name, email, etc) Technical Details: 1. SQS Message Handling: - Only marks messages for deletion if all events processed - Partially processed messages remain in queue for next analytics run - Prevents losing recipients in bulk sends when hitting maxEvents limit 2. Personalization Detection: - Checks for replacements beyond REQUIRED_TOKEN_IDS array - list_unsubscribe is always present (required: true in EmailRenderer) - Bulk path triggers when only system tokens present - Personalized path triggers when name, email, first_name, etc used Example: - Newsletter with no %%{name}%%: Uses bulk BCC (1 API call per 50 recipients) - Newsletter with %%{name}%%: Uses personalized (1 API call per recipient)
Includes admin build orchestration improvements, Portal fixes, i18n updates, and e2e test infrastructure enhancements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is being reviewed by Cursor Bugbot
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
| id: event.MessageID, | ||
| type: 'opened', | ||
| severity: 'permanent', | ||
| recipientEmail: event.Recipient, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Potential null reference error in normalizeEvent
The normalizeEvent method calls event.Tag.split('|')[1] without checking if event.Tag exists or is a string. If Postmark returns an event without a Tag field or with a null/undefined Tag, this will throw a runtime error. The nullish coalescing operator only handles the case where the split result doesn't have a second element, not when Tag itself is missing.
Sync with upstream Ghost v6.5.1
Sync with upstream Ghost v6.5.3
Sync with upstream Ghost v6.6.1
Sync with upstream Ghost v6.7.0
| onEditingChange={handleEditingChange} | ||
| onSave={async () => { | ||
| handleSave(); | ||
| }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Missing Mailgun region default value handling
The new BulkEmail component removes the special handling that existed in the old Mailgun component for setting the default Mailgun region. When a user saves Mailgun settings without explicitly selecting a region, the mailgun_base_url remains null because the Select component doesn't trigger updateSetting if the user doesn't interact with it. This breaks Mailgun configuration for new setups where users don't explicitly select a region.
Sync with upstream Ghost v6.8.0
Sync with upstream Ghost v6.8.1
Sync with upstream Ghost v6.9.1
| logging.error(error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Postmark analytics ignores event filter, only tracks opens
The fetchEvents method receives postmarkOptions (which includes the event filter for 'delivered OR opened OR failed OR unsubscribed OR complained') but completely ignores it. The method only calls getMessageOpens() which fetches only open events. Additionally, normalizeEvent hardcodes type: 'opened' for all events. This means delivery tracking, bounce tracking, unsubscribe tracking, and spam complaint tracking will not work with Postmark, causing Ghost to have incomplete email analytics data.
Additional Locations (1)
Sync with upstream Ghost v6.9.3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is being reviewed by Cursor Bugbot
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
| logging.error(error); | ||
| metrics.metric('postmark-send-mail', { | ||
| value: Date.now() - startTime, | ||
| statusCode: error.code |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
startTime may be undefined in error metrics
In the send method's catch block, Date.now() - startTime is used to calculate the metric value. However, startTime is only assigned at line 95, after building emailMessages. If an error occurs during the message building phase (lines 51-93), startTime will be undefined and the metric value will be NaN.
| Object.keys(recipientData[recipient]).forEach((key) => { | ||
| messageData.HtmlBody = messageData.HtmlBody.replaceAll(`%recipient.${key}%`, recipientData[recipient][key]); | ||
| messageData.TextBody = messageData.TextBody.replaceAll(`%recipient.${key}%`, recipientData[recipient][key]); | ||
| messageData.Subject = messageData.Subject.replaceAll(`%recipient.${key}%`, recipientData[recipient][key]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
replaceAll on potentially null HtmlBody or TextBody crashes
The send method calls .replaceAll() on messageData.HtmlBody, messageData.TextBody, and messageData.Subject without null checks. These values come from messageContent.html and messageContent.plaintext. If the message content is null or undefined (e.g., if an email has no HTML or plaintext version), calling .replaceAll() will throw a TypeError, crashing email sending.
| const section = page.getByTestId('bulk-email'); | ||
|
|
||
| await expect(section.getByText('Mailgun is not set up')).toHaveCount(1); | ||
| await expect(section.getByText('Email provider is not set up')).toHaveCount(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (e.error && e.messageData) { | ||
| const {error, messageData} = e; | ||
|
|
||
| // REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#status-codes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BulkEmailProvider crashes when PostmarkClient returns null
When PostmarkClient.send() is called but Postmark is not configured (no API token), it returns null at line 38-39 of PostmarkClient.js. The BulkEmailProvider.send() method then attempts to access response.id.trim() on this null value, causing a TypeError: Cannot read properties of null. This can occur if a user selects Postmark as provider but doesn't configure the API token.
Why are you making it?
We use postmark to send our mails, but we are missing a postmark integration for Ghost.
What does it do?
It add postmark as a second mail service provider
Why is this something Ghost users or developers need?
For Ghost users where mailgun is not an option.
Please check your PR against these items:
This a copy of #20530, submitting it again because our main branch got corrupted due to a merge conflict.
Note
Introduces Postmark as an alternative bulk email provider and refactors email sending/analytics to a provider-agnostic setup.
BulkEmail.tsxreplaces Mailgun-only UI; users selectbulk_email_provider(postmarkormailgun) and configurepostmark_api_tokenor Mailgun keysbulk_email_provider,postmark_api_token; exposespostmarkIsConfiguredin public configBulkEmailProviderusing either@tryghost/postmark-clientorMailgunClientbased onbulk_email_provider; updates suppression list to use selected client@tryghost/postmark-clientpackage (send, events, suppressions) and@tryghost/email-analytics-provider-postmark; wires intoEmailAnalyticsServiceWrapperbulkEmailIsConfiguredinstead of Mailgun-only; adjusts acceptance/unit tests accordinglyWritten by Cursor Bugbot for commit 5209ffa. This will update automatically on new commits. Configure here.