-
-
Notifications
You must be signed in to change notification settings - Fork 11.7k
Email adapter #28247
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?
Email adapter #28247
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /** | ||
| * Base class for email provider adapters | ||
| * | ||
| * All email providers must implement the required methods defined below. | ||
| * This ensures consistent interface across different email providers (Postmark, SendGrid, AWS SES, etc.) | ||
| * | ||
| * This base class handles both email sending and analytics fetching in a unified interface. | ||
| */ | ||
| class EmailProviderBase { | ||
| /** | ||
| * Required methods that all email providers must implement | ||
| */ | ||
| static requiredFns = ['send', 'getMaximumRecipients', 'getTargetDeliveryWindow', 'fetchLatest']; | ||
|
|
||
| constructor(config) { | ||
| this.config = config; | ||
| } | ||
|
|
||
| /** | ||
| * Send an email | ||
| * | ||
| * @param {Object} data - Email data | ||
| * @param {string} data.subject - Email subject | ||
| * @param {string} data.html - HTML content | ||
| * @param {string} data.plaintext - Plain text content | ||
| * @param {string} data.from - Sender email address | ||
| * @param {string} data.emailId - Email ID for tracking | ||
| * @param {string} [data.replyTo] - Reply-to address | ||
| * @param {string} [data.domainOverride] - Override domain | ||
| * @param {Array<Object>} data.recipients - Array of recipients | ||
| * @param {Array<Object>} data.replacementDefinitions - Replacement variable definitions | ||
| * | ||
| * @param {Object} options - Sending options | ||
| * @param {boolean} options.clickTrackingEnabled - Enable click tracking | ||
| * @param {boolean} options.openTrackingEnabled - Enable open tracking | ||
| * @param {Date} [options.deliveryTime] - Scheduled delivery time | ||
| * | ||
| * @returns {Promise<{id: string}>} Provider message ID | ||
| */ | ||
| async send(data, options) { | ||
| throw new Error('EmailProviderBase.send must be implemented by the email adapter'); | ||
| } | ||
|
|
||
| /** | ||
| * Get maximum number of recipients per batch | ||
| * | ||
| * @returns {number} Maximum recipients | ||
| */ | ||
| getMaximumRecipients() { | ||
| throw new Error('EmailProviderBase.getMaximumRecipients must be implemented by the email adapter'); | ||
| } | ||
|
|
||
| /** | ||
| * Get target delivery window in milliseconds | ||
| * | ||
| * @returns {number} Delivery window in milliseconds | ||
| */ | ||
| getTargetDeliveryWindow() { | ||
| throw new Error('EmailProviderBase.getTargetDeliveryWindow must be implemented by the email adapter'); | ||
| } | ||
|
|
||
| /** | ||
| * Fetch latest email events for analytics | ||
| * | ||
| * @param {Function} batchHandler - Handler for processing event batches | ||
| * @param {Object} [options] - Fetch options | ||
| * @param {number} [options.maxEvents] - Maximum events to fetch (not strict) | ||
| * @param {Date} [options.begin] - Start date for events | ||
| * @param {Date} [options.end] - End date for events | ||
| * @param {String[]} [options.events] - Event types to fetch | ||
| * @returns {Promise<void>} | ||
| */ | ||
| async fetchLatest(batchHandler, options) { | ||
| throw new Error('EmailProviderBase.fetchLatest must be implemented by the email adapter'); | ||
| } | ||
| } | ||
|
|
||
| module.exports = EmailProviderBase; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| const EmailProviderBase = require('./EmailProviderBase'); | ||
| const MailgunEmailProvider = require('../../services/email-service/mailgun-email-provider'); | ||
| const EmailAnalyticsProviderMailgun = require('../../services/email-analytics/email-analytics-provider-mailgun'); | ||
| const MailgunClient = require('../../services/lib/mailgun-client'); | ||
|
|
||
| /** | ||
| * Mailgun Email Adapter | ||
| * | ||
| * Thin wrapper around existing MailgunEmailProvider and EmailAnalyticsProviderMailgun | ||
| * to conform to the unified adapter pattern. | ||
| * | ||
| * @extends EmailProviderBase | ||
| */ | ||
| class Mailgun extends EmailProviderBase { | ||
| #emailProvider; | ||
| #analyticsProvider; | ||
|
|
||
| /** | ||
| * @param {Object} config - Adapter configuration | ||
| * @param {Object} config.configService - Ghost config service | ||
| * @param {Object} config.settingsCache - Ghost settings cache | ||
| * @param {Object} config.labs - Ghost labs service | ||
| * @param {Function} [config.errorHandler] - Custom error handler | ||
| */ | ||
| constructor(config) { | ||
| super(config); | ||
|
|
||
| const {configService, settingsCache, labs, errorHandler} = config; | ||
|
|
||
| // Initialize Mailgun client (shared between email and analytics) | ||
| const mailgunClient = new MailgunClient({ | ||
| config: configService, | ||
| settings: settingsCache, | ||
| labs | ||
| }); | ||
|
|
||
| // Initialize the existing email provider | ||
| this.#emailProvider = new MailgunEmailProvider({ | ||
| mailgunClient, | ||
| errorHandler | ||
| }); | ||
|
|
||
| // Initialize the existing analytics provider | ||
| this.#analyticsProvider = new EmailAnalyticsProviderMailgun({ | ||
| config: configService, | ||
| settings: settingsCache, | ||
| labs | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Send an email (delegates to existing MailgunEmailProvider) | ||
| */ | ||
| async send(data, options) { | ||
| return await this.#emailProvider.send(data, options); | ||
| } | ||
|
|
||
| /** | ||
| * Get maximum recipients per batch (delegates to existing MailgunEmailProvider) | ||
| */ | ||
| getMaximumRecipients() { | ||
| return this.#emailProvider.getMaximumRecipients(); | ||
| } | ||
|
|
||
| /** | ||
| * Get target delivery window (delegates to existing MailgunEmailProvider) | ||
| */ | ||
| getTargetDeliveryWindow() { | ||
| return this.#emailProvider.getTargetDeliveryWindow(); | ||
| } | ||
|
|
||
| /** | ||
| * Fetch latest email events for analytics (delegates to existing EmailAnalyticsProviderMailgun) | ||
| */ | ||
| async fetchLatest(batchHandler, options) { | ||
| return await this.#analyticsProvider.fetchLatest(batchHandler, options); | ||
| } | ||
| } | ||
|
|
||
| module.exports = Mailgun; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| const adapterManager = require('../../services/adapter-manager'); | ||
|
|
||
| /** | ||
| * Get an email adapter instance | ||
| * | ||
| * @param {string} [feature] - Optional feature name for feature-specific adapter (e.g., 'transactional', 'bulk') | ||
| * @returns {Object} Email adapter instance | ||
| */ | ||
| function getEmailAdapter(feature) { | ||
| let adapterName = 'email'; | ||
|
|
||
| if (feature) { | ||
| adapterName += `:${feature}`; | ||
| } | ||
|
|
||
| return adapterManager.getAdapter(adapterName); | ||
| } | ||
|
|
||
| module.exports = { | ||
| getEmailAdapter | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,6 @@ class EmailAnalyticsServiceWrapper { | |
| const EmailAnalyticsService = require('./email-analytics-service'); | ||
| const EmailEventStorage = require('../email-service/email-event-storage'); | ||
| const EmailEventProcessor = require('../email-service/email-event-processor'); | ||
| const MailgunProvider = require('./email-analytics-provider-mailgun'); | ||
| const {EmailRecipientFailure, EmailSpamComplaintEvent, Email} = require('../../models'); | ||
| const StartEmailAnalyticsJobEvent = require('./events/start-email-analytics-job-event'); | ||
| const domainEvents = require('@tryghost/domain-events'); | ||
|
|
@@ -47,13 +46,44 @@ class EmailAnalyticsServiceWrapper { | |
| prometheusClient | ||
| }); | ||
|
|
||
| // Use unified email adapter (handles both sending and analytics) | ||
| const bulkEmailConfig = config.get('bulkEmail'); | ||
| const emailProvider = bulkEmailConfig?.provider || 'mailgun'; | ||
|
|
||
| logging.info(`[EmailAnalytics] Initializing ${emailProvider} analytics via unified adapter`); | ||
|
|
||
| const emailAdapter = require('../../adapters/email'); | ||
| const providers = []; | ||
|
|
||
| try { | ||
| // Get unified email adapter instance (same one used for email sending) | ||
| const adapterInstance = emailAdapter.getEmailAdapter(); | ||
|
|
||
| // Inject dependencies needed by the adapter | ||
| const AdapterClass = adapterInstance.constructor; | ||
| const adapterConfig = { | ||
| configService: config, | ||
| settingsCache: settings | ||
| }; | ||
|
|
||
| // Add labs for Mailgun | ||
| if (emailProvider === 'mailgun') { | ||
| adapterConfig.labs = labs; | ||
| } | ||
|
|
||
| // Create a new instance for analytics (the email service has its own instance) | ||
| providers.push(new AdapterClass(adapterConfig)); | ||
| } catch (error) { | ||
| logging.error(`[EmailAnalytics] Failed to load ${emailProvider} adapter: ${error.message}`); | ||
| logging.error(error.stack); | ||
| throw error; | ||
| } | ||
|
Comment on lines
+58
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# 1) Inspect the email adapter factory (getEmailAdapter)
fd -t f 'index.js' | rg -n 'adapters/email/index.js$'
rg -nP -C4 'getEmailAdapter'
# 2) Inspect the EmailProviderBase + Mailgun adapter constructors / destructured config keys
fd -t f 'EmailProviderBase.js' --exec cat {}
fd -t f 'Mailgun.js' -p 'adapters/email' --exec cat {}Repository: TryGhost/Ghost Length of output: 6440 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the Mailgun adapter file(s)
fd -t f -i 'mailgun\.js$' . || true
fd -t f -i '*mailgun*' ghost/core/core/server/adapters || true
# Inspect the actual Mailgun adapter implementation (whatever file exists)
# If multiple matches exist, print the first few.
matches=( $(fd -t f -i '*mailgun*' ghost/core/core/server/adapters -a 2>/dev/null | head -n 20) )
echo "MAILGUN_MATCHES=${`#matches`[@]}"
for f in "${matches[@]}"; do
echo "---- $f (head) ----"
sed -n '1,160p' "$f"
echo "---- $f (tail) ----"
tail -n 80 "$f"
done
# Inspect email adapter factory / adapterManager usage
sed -n '1,120p' ghost/core/core/server/adapters/email/index.js
# Inspect email-service wrapper's adapter loading block for comparison
sed -n '1,140p' ghost/core/core/server/services/email-service/email-service-wrapper.js
# Inspect email-analytics wrapper around the relevant lines (for exact config construction)
sed -n '1,140p' ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.js
# Search for "labs" destructuring in adapters/providers
rg -n "labs" ghost/core/core/server/adapters/email ghost/core/core/server/services/email-analytics ghost/core/core/server/adapters -S || true
rg -nP "constructor\((\{)?\s*[^)]*labs" ghost/core/core/server/adapters -S || true
# Search for where getEmailAdapter is called with/without args
rg -n "getEmailAdapter\(" ghost/core/core/server/services/email-analytics ghost/core/core/server/services/email-service ghost/core/core/server/adapters -S || true
# Inspect adapterManager.getAdapter contract (if present)
fd -t f -i 'adapterManager\.js$' ghost/core/core/server || true
fd -t f -i '*adapter*manager*' ghost/core/core/server | head -n 20Repository: TryGhost/Ghost Length of output: 541 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) Locate Mailgun adapter(s) deterministically
fd -t f -i '*mailgun*.js' ghost/core/core/server/adapters/email -a
fd -t f -i '*mailgun*.js' ghost/core/core/server/adapters -a
# 2) Print adapter factory + adapterManager usage
sed -n '1,140p' ghost/core/core/server/adapters/email/index.js || true
fd -t f -i '*adapter*manager*.js' ghost/core/core/server | head -n 50
# 3) Inspect Mailgun adapter implementation
MAILGUN_FILE="$(fd -t f -i 'Mailgun\.js$' ghost/core/core/server/adapters/email | head -n 1)"
echo "MAILGUN_FILE=$MAILGUN_FILE"
sed -n '1,240p' "$MAILGUN_FILE"
tail -n 120 "$MAILGUN_FILE"
# 4) Inspect EmailProviderBase (to see constructor shape and config expectations)
EMAIL_BASE_FILE="$(fd -t f -i 'EmailProviderBase.*\.js$' ghost/core/core/server/adapters/email ghost/core/core/server/adapters -a | head -n 1)"
echo "EMAIL_BASE_FILE=$EMAIL_BASE_FILE"
if [[ -n "${EMAIL_BASE_FILE:-}" ]]; then
sed -n '1,220p' "$EMAIL_BASE_FILE"
fi
# 5) Inspect email-analytics wrapper and the mailgun-specific analytics provider
sed -n '1,220p' ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.js
sed -n '1,220p' ghost/core/core/server/services/email-analytics/email-analytics-provider-mailgun.js
# 6) Inspect email-service wrapper for comparison of how adapters are instantiated/configured
sed -n '1,220p' ghost/core/core/server/services/email-service/email-service-wrapper.js
# 7) Search for exact usage of adapterConfig keys inside Mailgun adapter
rg -n --fixed-strings "configService" ghost/core/core/server/adapters/email/Mailgun.js || true
rg -n --fixed-strings "settingsCache" ghost/core/core/server/adapters/email/Mailgun.js || true
rg -n --fixed-strings "labs" ghost/core/core/server/adapters/email/Mailgun.js || true
rg -n --fixed-strings "constructor" ghost/core/core/server/adapters/email/Mailgun.js || true
# 8) Search for destructuring patterns in Email adapter constructor calls
rg -nP "getEmailAdapter\\(([^)]*)\\)" ghost/core/core/server/services/email-analytics ghost/core/core/server/services/email-service ghost/core/core/server/adapters/email/index.js || true
rg -nP "new .*Adapter\\((\\{)?[^}]*\\}" ghost/core/core/server/services/email-analytics ghost/core/core/server/services/email-service ghost/core/core/server/adapters/email -S || trueRepository: TryGhost/Ghost Length of output: 350 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Cleanly locate Mailgun adapter(s) via safe glob
MAILGUN_FILES=( )
while IFS= read -r f; do MAILGUN_FILES+=("$f"); done < <(fd -t f --glob '*mailgun*.js' ghost/core/core/server/adapters/email)
echo "MAILGUN_FILES_COUNT=${`#MAILGUN_FILES`[@]}"
printf '%s\n' "${MAILGUN_FILES[@]}"
# Email adapter factory
echo "---- adapters/email/index.js ----"
sed -n '1,220p' ghost/core/core/server/adapters/email/index.js
# Find EmailProviderBase (exact file by name)
EMAIL_BASE_FILE="$(fd -t f --glob '*EmailProviderBase*.js' ghost/core/core/server/adapters/email ghost/core/core/server/adapters -a | head -n 1 || true)"
echo "EMAIL_BASE_FILE=$EMAIL_BASE_FILE"
if [[ -n "${EMAIL_BASE_FILE:-}" ]]; then
sed -n '1,260p' "$EMAIL_BASE_FILE"
fi
# Inspect Mailgun adapter(s) + search for config keys
for f in "${MAILGUN_FILES[@]}"; do
echo "---- $f (head) ----"
sed -n '1,260p' "$f"
echo "---- $f (search config keys) ----"
rg -n --fixed-strings 'configService' "$f" || true
rg -n --fixed-strings 'settingsCache' "$f" || true
rg -n --fixed-strings 'labs' "$f" || true
rg -n 'constructor\s*\(' "$f" || true
done
# Inspect analytics wrapper + email-analytics-provider-mailgun
echo "---- email-analytics-service-wrapper.js (relevant) ----"
sed -n '1,220p' ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.js
echo "---- email-analytics-provider-mailgun.js (relevant) ----"
sed -n '1,260p' ghost/core/core/server/services/email-analytics/email-analytics-provider-mailgun.js
# Inspect email-service wrapper for how it passes config
echo "---- email-service-wrapper.js (relevant) ----"
sed -n '1,220p' ghost/core/core/server/services/email-service/email-service-wrapper.js
# Search for getEmailAdapter invocation patterns in wrappers
echo "---- getEmailAdapter usage in wrappers ----"
rg -n "getEmailAdapter\s*\(" ghost/core/core/server/services/email-analytics ghost/core/core/server/services/email-service ghost/core/core/server/adapters/email -S || trueRepository: TryGhost/Ghost Length of output: 115 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) List Mailgun adapter files (use glob, not regex)
TMP1=/tmp/mailgun_files.txt
rm -f "$TMP1"
fd -t f --glob '*mailgun*.js' ghost/core/core/server/adapters/email > "$TMP1" || true
echo "MAILGUN_FILES_COUNT=$(wc -l < "$TMP1" 2>/dev/null || echo 0)"
echo "---- mailgun files ----"
cat "$TMP1" 2>/dev/null || true
# 2) Print adapter factory
echo "---- adapters/email/index.js ----"
sed -n '1,220p' ghost/core/core/server/adapters/email/index.js
# 3) Locate EmailProviderBase
TMP2=/tmp/email_base_files.txt
rm -f "$TMP2"
fd -t f --glob '*EmailProviderBase*.js' ghost/core/core/server/adapters > "$TMP2" || true
EMAIL_BASE_FILE="$(head -n 1 "$TMP2" 2>/dev/null || true)"
echo "EMAIL_BASE_FILE=${EMAIL_BASE_FILE:-<none>}"
if [[ -n "${EMAIL_BASE_FILE:-}" ]]; then
sed -n '1,260p' "$EMAIL_BASE_FILE"
fi
# 4) Inspect Mailgun adapter(s)
while IFS= read -r f; do
[[ -z "$f" ]] && continue
echo "==================== $f ===================="
sed -n '1,260p' "$f"
echo "---- search config keys in $f ----"
rg -n --fixed-strings 'configService' "$f" || true
rg -n --fixed-strings 'settingsCache' "$f" || true
rg -n --fixed-strings 'labs' "$f" || true
echo "---- search constructor/config destructuring in $f ----"
rg -nP "constructor\s*\(\s*\{\s*[^}]*\}\s*\)" "$f" || true
rg -nP "constructor\s*\(\s*[^)]*\)" "$f" || true
done < "$TMP1"
# 5) Inspect wrappers
echo "---- email-analytics-service-wrapper.js ----"
sed -n '1,240p' ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.js
echo "---- email-analytics-provider-mailgun.js ----"
sed -n '1,260p' ghost/core/core/server/services/email-analytics/email-analytics-provider-mailgun.js
echo "---- email-service-wrapper.js ----"
sed -n '1,240p' ghost/core/core/server/services/email-service/email-service-wrapper.js
# 6) Search getEmailAdapter usage patterns
echo "---- getEmailAdapter usage in wrappers ----"
rg -n "getEmailAdapter\s*\(" ghost/core/core/server/services/email-analytics ghost/core/core/server/services/email-service ghost/core/core/server/adapters/email/index.js -S || trueRepository: TryGhost/Ghost Length of output: 27507 🏁 Script executed: #!/bin/bash
set -euo pipefail
# List all email adapters under adapters/email
echo "---- adapters/email/*.js ----"
fd -t f ghost/core/core/server/adapters/email | sort
# Inspect adapter-manager implementation for getAdapter contract
ADAPTER_MANAGER_FILE="$(fd -t f -i 'adapter-manager\.js$' ghost/core/core/server/services | head -n 1 || true)"
if [[ -n "${ADAPTER_MANAGER_FILE:-}" ]]; then
echo "---- $ADAPTER_MANAGER_FILE (head) ----"
sed -n '1,220p' "$ADAPTER_MANAGER_FILE"
echo "---- $ADAPTER_MANAGER_FILE (search getAdapter) ----"
rg -n "getAdapter" "$ADAPTER_MANAGER_FILE" || true
else
echo "adapter-manager.js not found under services; searching broader..."
ADAPTER_MANAGER_FILE="$(fd -t f -i '*adapter*manager*.js' ghost/core/core/server | head -n 1 || true)"
echo "---- $ADAPTER_MANAGER_FILE (head) ----"
sed -n '1,240p' "$ADAPTER_MANAGER_FILE"
rg -n "getAdapter" "$ADAPTER_MANAGER_FILE" || true
fi
# Inspect analytics provider wrapper usage of errorHandler/labs (if any)
rg -n "errorHandler" ghost/core/core/server/services/email-analytics -S || true
rg -n "new AdapterClass" ghost/core/core/server/services/email-analytics -S || trueRepository: TryGhost/Ghost Length of output: 560 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) List adapter files in the adapters/email directory
echo "---- ls ghost/core/core/server/adapters/email ----"
ls -la ghost/core/core/server/adapters/email || true
echo "---- fd files in ghost/core/core/server/adapters/email ----"
fd -t f . ghost/core/core/server/adapters/email | sort
# 2) Locate adapter-manager
echo "---- locate adapter-manager ----"
ADAPTER_MANAGER_FILES=$(fd -t f --full-path -i '*adapter*manager*.js' ghost/core/core/server || true)
echo "$ADAPTER_MANAGER_FILES" | head -n 20
ADAPTER_MANAGER_FILE=$(echo "$ADAPTER_MANAGER_FILES" | head -n 1 || true)
if [[ -n "${ADAPTER_MANAGER_FILE:-}" ]]; then
echo "---- $ADAPTER_MANAGER_FILE (head) ----"
sed -n '1,240p' "$ADAPTER_MANAGER_FILE"
echo "---- $ADAPTER_MANAGER_FILE (getAdapter search) ----"
rg -n "getAdapter" "$ADAPTER_MANAGER_FILE" || true
fi
# 3) Also inspect any nearby adapter-manager imports
echo "---- references to '../../services/adapter-manager' ----"
rg -n --fixed-strings "services/adapter-manager" ghost/core/core/server/adapters/email ghost/core/core/server/services | head -n 50
# 4) Inspect the email-service adapter factory (already known) for getEmailAdapter(feature) contract
echo "---- adapters/email/index.js (full) ----"
sed -n '1,120p' ghost/core/core/server/adapters/email/index.jsRepository: TryGhost/Ghost Length of output: 1686 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate adapter-manager files safely (glob, not regex)
fd -t f --glob '*adapter*manager*.js' ghost/core/core/server/services -a | sort | head -n 20
fd -t f --glob '*adapter*manager*.js' ghost/core/core/server -a | sort | head -n 50
ADAPTER_MANAGER_FILE="$(fd -t f --glob '*adapter*manager*.js' ghost/core/core/server/services -a | sort | head -n 1 || true)"
echo "ADAPTER_MANAGER_FILE=${ADAPTER_MANAGER_FILE:-<none>}"
if [[ -n "${ADAPTER_MANAGER_FILE:-}" ]]; then
sed -n '1,260p' "$ADAPTER_MANAGER_FILE"
rg -n "getAdapter" "$ADAPTER_MANAGER_FILE" || true
rg -n "email" "$ADAPTER_MANAGER_FILE" || true
fi
# Search for any other adapters extending EmailProviderBase (beyond adapters/email/Mailgun.js)
rg -n --glob '**/*.js' --glob '**/*.ts' "extends EmailProviderBase" ghost/core/core/server/adapters/email ghost/core/core/server | head -n 50 || true
# Search for adapterName usage like 'email:' or adapter registry keys
rg -n --fixed-strings "email:" ghost/core/core/server | head -n 50 || true
rg -n --fixed-strings "getAdapter('email" ghost/core/core/server | head -n 50 || true
rg -n --fixed-strings "getAdapter(\"email" ghost/core/core/server | head -n 50 || trueRepository: TryGhost/Ghost Length of output: 16463 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect adapter-manager module export from services/adapter-manager
ls -la ghost/core/core/server/services/adapter-manager || true
fd -t f --glob 'ghost/core/core/server/services/adapter-manager/*' ghost/core/core/server/services/adapter-manager -a | sort
# Print entrypoints (index.js or main file)
for f in \
ghost/core/core/server/services/adapter-manager/index.js \
ghost/core/core/server/services/adapter-manager.js \
ghost/core/core/server/services/adapter-manager/*.js
do
if [[ -f "$f" ]]; then
echo "---- $f ----"
sed -n '1,260p' "$f"
fi
done
# Search for adapterManager.getAdapter wrappers or adapters registration
rg -n "registerAdapter\\(" ghost/core/core/server | head -n 50
rg -n "new AdapterManager" ghost/core/core/server | head -n 50
rg -n "adapterManager\\.getAdapter" ghost/core/core/server | head -n 50
# Specifically find where adapter type 'email' is registered
rg -n "registerAdapter\\(['\"]email['\"]" ghost/core/core/server | head -n 50 || true
rg -n "registerAdapter\\(\"email\"" ghost/core/core/server | head -n 50 || trueRepository: TryGhost/Ghost Length of output: 16749 Align email analytics adapter instantiation/config with email sending The However, the wrapper still recreates the adapter via 🤖 Prompt for AI Agents |
||
|
|
||
| this.service = new EmailAnalyticsService({ | ||
| config, | ||
| settings, | ||
| eventProcessor, | ||
| providers: [ | ||
| new MailgunProvider({config, settings, labs}) | ||
| ], | ||
| providers, | ||
| queries, | ||
| domainEvents, | ||
| prometheusClient | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,11 +21,9 @@ class EmailServiceWrapper { | |
| const SendingService = require('./sending-service'); | ||
| const BatchSendingService = require('./batch-sending-service'); | ||
| const EmailSegmenter = require('./email-segmenter'); | ||
| const MailgunEmailProvider = require('./mailgun-email-provider'); | ||
| const {DomainWarmingService} = require('./domain-warming-service'); | ||
|
|
||
| const {Post, Newsletter, Email, EmailBatch, EmailRecipient, Member} = require('../../models'); | ||
| const MailgunClient = require('../lib/mailgun-client'); | ||
| const configService = require('../../../shared/config'); | ||
| const settingsCache = require('../../../shared/settings-cache'); | ||
| const settingsHelpers = require('../settings-helpers'); | ||
|
|
@@ -49,16 +47,46 @@ class EmailServiceWrapper { | |
| const emailAnalyticsJobs = require('../email-analytics/jobs'); | ||
| const {cachedImageSizeFromUrl} = require('../../lib/image'); | ||
|
|
||
| // capture errors from mailgun client and log them in sentry | ||
| // Determine which email provider to use based on configuration | ||
| const bulkEmailConfig = configService.get('bulkEmail'); | ||
| const emailProvider = bulkEmailConfig?.provider || 'mailgun'; | ||
|
|
||
| // capture errors from email provider and log them in sentry | ||
| const errorHandler = (error) => { | ||
| logging.info(`Capturing error for mailgun email provider service`); | ||
| logging.info(`Capturing error for ${emailProvider} email provider service`); | ||
| sentry.captureException(error); | ||
| }; | ||
|
|
||
| // Mailgun client instance for email provider | ||
| const mailgunClient = new MailgunClient({ | ||
| config: configService, settings: settingsCache, labs | ||
| }); | ||
| let emailProviderInstance; | ||
|
|
||
| // Use adapter pattern for all email providers | ||
| logging.info(`Initializing ${emailProvider} email provider via adapter`); | ||
|
|
||
| const emailAdapter = require('../../adapters/email'); | ||
|
|
||
| // Get adapter instance with injected dependencies | ||
| emailProviderInstance = emailAdapter.getEmailAdapter(); | ||
|
|
||
| // Inject dependencies needed by the adapter | ||
| const AdapterClass = emailProviderInstance.constructor; | ||
| const adapterConfig = { | ||
| configService, | ||
| settingsCache, | ||
| errorHandler | ||
| }; | ||
|
|
||
| // Add labs for Mailgun | ||
| if (emailProvider === 'mailgun') { | ||
| adapterConfig.labs = labs; | ||
| } | ||
|
|
||
| // Merge with provider-specific config | ||
| if (bulkEmailConfig[emailProvider]) { | ||
| Object.assign(adapterConfig, bulkEmailConfig[emailProvider]); | ||
| } | ||
|
Comment on lines
+84
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard Line 52 reads 🛡️ Proposed guard- if (bulkEmailConfig[emailProvider]) {
+ if (bulkEmailConfig?.[emailProvider]) {
Object.assign(adapterConfig, bulkEmailConfig[emailProvider]);
}🤖 Prompt for AI Agents |
||
|
|
||
| emailProviderInstance = new AdapterClass(adapterConfig); | ||
|
|
||
| const i18nLanguage = settingsCache.get('locale') || 'en'; | ||
| const i18n = i18nLib(i18nLanguage, 'ghost'); | ||
|
|
||
|
|
@@ -67,11 +95,6 @@ class EmailServiceWrapper { | |
| i18n.changeLanguage(model.get('value')); | ||
| }); | ||
|
|
||
| const mailgunEmailProvider = new MailgunEmailProvider({ | ||
| mailgunClient, | ||
| errorHandler | ||
| }); | ||
|
|
||
| const emailRenderer = new EmailRenderer({ | ||
| settingsCache, | ||
| settingsHelpers, | ||
|
|
@@ -96,7 +119,7 @@ class EmailServiceWrapper { | |
| }); | ||
|
|
||
| const sendingService = new SendingService({ | ||
| emailProvider: mailgunEmailProvider, | ||
| emailProvider: emailProviderInstance, | ||
| emailRenderer, | ||
| emailAddressService: emailAddressService.service | ||
| }); | ||
|
|
||
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.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
getEmailAdapterprovides no way to inject runtime dependencies.The factory returns a fully-constructed instance from the adapter manager using only the static config from
defaults.json(Mailgun: {}). Runtime deps likeconfigService,settingsCache,labs, anderrorHandlercannot be passed in, which forces consumers (the email-service wrapper) to construct the adapter a second time. Consider accepting a config/deps argument and threading it through to the adapter manager. See the related comment inemail-service-wrapper.js.🤖 Prompt for AI Agents