Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions ghost/core/core/server/adapters/email/EmailProviderBase.js
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;
80 changes: 80 additions & 0 deletions ghost/core/core/server/adapters/email/Mailgun.js
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;
21 changes: 21 additions & 0 deletions ghost/core/core/server/adapters/email/index.js
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);
}
Comment on lines +9 to +17
Copy link
Copy Markdown
Contributor

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

getEmailAdapter provides 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 like configService, settingsCache, labs, and errorHandler cannot 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 in email-service-wrapper.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/core/server/adapters/email/index.js` around lines 9 - 17,
getEmailAdapter currently builds an adapterName and calls
adapterManager.getAdapter(adapterName) with no way to pass runtime dependencies;
modify getEmailAdapter to accept a second parameter (e.g., deps or options) and
thread that through to adapterManager.getAdapter so runtime deps like
configService, settingsCache, labs and errorHandler can be injected by callers
(see email-service-wrapper.js); update the function signature
(getEmailAdapter(feature, deps)) and ensure adapterManager.getAdapter is invoked
with those deps/options so callers can avoid reconstructing adapters.


module.exports = {
getEmailAdapter
};
1 change: 1 addition & 0 deletions ghost/core/core/server/services/adapter-manager/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ adapterManager.registerAdapter('scheduling', require('../../adapters/scheduling/
adapterManager.registerAdapter('sso', require('../../adapters/sso/SSOBase'));
adapterManager.registerAdapter('cache', require('@tryghost/adapter-base-cache'));
adapterManager.registerAdapter('redirects', require('../../adapters/redirects/RedirectsStoreBase'));
adapterManager.registerAdapter('email', require('../../adapters/email/EmailProviderBase'));

module.exports = {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 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 20

Repository: 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 || true

Repository: 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 || true

Repository: 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 || true

Repository: 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 || true

Repository: 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.js

Repository: 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 || true

Repository: 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 || true

Repository: TryGhost/Ghost

Length of output: 16749


Align email analytics adapter instantiation/config with email sending

The adapters/email/Mailgun.js constructor expects {configService, settingsCache, labs, errorHandler}, and this analytics wrapper passes configService/settingsCache and conditionally labs for mailgun—so the key names match for Mailgun.

However, the wrapper still recreates the adapter via emailAdapter.getEmailAdapter().constructor and (unlike email-service-wrapper.js) does not provide an errorHandler and does not merge bulkEmailConfig[emailProvider] into adapterConfig. For any provider/adaptor that relies on provider-specific options (or error handling) this can leave the analytics adapter misconfigured even while sending works.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.js`
around lines 58 - 80, The analytics wrapper currently builds adapterConfig from
config/settings and labs but omits provider-specific options and the error
handler used by the sending adapter; fix this by merging
bulkEmailConfig[emailProvider] into adapterConfig (so provider-specific keys are
preserved) and include the same errorHandler that the sending path provides
(mirror what email-service-wrapper.js does) before instantiating AdapterClass
(from emailAdapter.getEmailAdapter().constructor), keeping the existing
conditional labs injection and then creating the new instance with the merged
adapterConfig.


this.service = new EmailAnalyticsService({
config,
settings,
eventProcessor,
providers: [
new MailgunProvider({config, settings, labs})
],
providers,
queries,
domainEvents,
prometheusClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard bulkEmailConfig access for consistency.

Line 52 reads bulkEmailConfig?.provider (treating an absent bulkEmail config as valid), but line 84 dereferences bulkEmailConfig[emailProvider] unguarded, which would throw a TypeError if bulkEmail is undefined. Although defaults.json always defines bulkEmail, the inconsistency is a latent footgun.

🛡️ Proposed guard
-        if (bulkEmailConfig[emailProvider]) {
+        if (bulkEmailConfig?.[emailProvider]) {
             Object.assign(adapterConfig, bulkEmailConfig[emailProvider]);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/core/server/services/email-service/email-service-wrapper.js`
around lines 84 - 86, The code dereferences bulkEmailConfig[emailProvider]
without guarding against bulkEmailConfig being undefined; update the block
around emailProvider and adapterConfig so you only access
bulkEmailConfig[emailProvider] when bulkEmailConfig is present — e.g., change
the if to check bulkEmailConfig && bulkEmailConfig[emailProvider] (or use
optional chaining bulkEmailConfig?.[emailProvider]) before calling
Object.assign(adapterConfig, ...), ensuring you reference the existing symbols
bulkEmailConfig, emailProvider and adapterConfig.


emailProviderInstance = new AdapterClass(adapterConfig);

const i18nLanguage = settingsCache.get('locale') || 'en';
const i18n = i18nLib(i18nLanguage, 'ghost');

Expand All @@ -67,11 +95,6 @@ class EmailServiceWrapper {
i18n.changeLanguage(model.get('value'));
});

const mailgunEmailProvider = new MailgunEmailProvider({
mailgunClient,
errorHandler
});

const emailRenderer = new EmailRenderer({
settingsCache,
settingsHelpers,
Expand All @@ -96,7 +119,7 @@ class EmailServiceWrapper {
});

const sendingService = new SendingService({
emailProvider: mailgunEmailProvider,
emailProvider: emailProviderInstance,
emailRenderer,
emailAddressService: emailAddressService.service
});
Expand Down
4 changes: 4 additions & 0 deletions ghost/core/core/shared/config/defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"active": "FileStore",
"FileStore": {},
"S3RedirectsStore": {}
},
"email": {
"active": "Mailgun",
"Mailgun": {}
}
},
"storage": {
Expand Down
Loading
Loading