Skip to content
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

Add support for Mailjet #21954

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
8 changes: 8 additions & 0 deletions apps/admin-x-framework/src/test/responses/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@
"key": "mailgun_base_url",
"value": null
},
{
"key": "mailjet_api_key",
"value": null
},
{
"key": "mailjet_secret_key",
"value": null
},
{
"key": "email_track_opens",
"value": true
Expand Down
1 change: 1 addition & 0 deletions apps/admin-x-settings/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const Sidebar: React.FC = () => {
<NavItem icon='recepients' keywords={emailSearchKeywords.defaultRecipients} navid='default-recipients' title="Default recipients" onClick={handleSectionClick} />
<NavItem icon='email' keywords={emailSearchKeywords.newsletters} navid='newsletters' title="Newsletters" onClick={handleSectionClick} />
{!config.mailgunIsConfigured && <NavItem icon='at-sign' keywords={emailSearchKeywords.mailgun} navid='mailgun' title="Mailgun settings" onClick={handleSectionClick} />}
{!config.mailjetIsConfigured && <NavItem icon='email' keywords={emailSearchKeywords.mailjet} navid='mailjet' title="Mailjet settings" onClick={handleSectionClick} />}
</>
)}
</SettingNavSection>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import DefaultRecipients from './DefaultRecipients';
import EnableNewsletters from './EnableNewsletters';
import MailGun from './Mailgun';
import Mailjet from './Mailjet';
import Newsletters from './Newsletters';
import React from 'react';
import SearchableSection from '../../SearchableSection';
Expand All @@ -11,7 +12,8 @@ export const searchKeywords = {
enableNewsletters: ['emails', 'newsletters', 'newsletter sending', 'enable', 'disable', 'turn on', 'turn off'],
newsletters: ['newsletters', 'emails'],
defaultRecipients: ['newsletters', 'default recipients', 'emails'],
mailgun: ['mailgun', 'emails', 'newsletters']
mailgun: ['mailgun', 'emails', 'newsletters'],
mailjet: ['mailjet', 'emails', 'newsletters']
};

const EmailSettings: React.FC = () => {
Expand All @@ -26,6 +28,7 @@ const EmailSettings: React.FC = () => {
<DefaultRecipients keywords={searchKeywords.defaultRecipients} />
<Newsletters keywords={searchKeywords.newsletters} />
{!config.mailgunIsConfigured && <MailGun keywords={searchKeywords.mailgun} />}
<Mailjet keywords={searchKeywords.mailjet}/>
</>
)}
</SearchableSection>
Expand Down
90 changes: 90 additions & 0 deletions apps/admin-x-settings/src/components/settings/email/Mailjet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {Icon, Link, SettingGroupContent, TextField, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';

const Mailjet: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {
localSettings,
isEditing,
saveState,
handleSave,
handleCancel,
updateSetting,
handleEditingChange
} = useSettingGroup();

const [mailjetApiKey, mailjetSecretKey] = getSettingValues(localSettings, [
'mailjet_api_key',
'mailjet_secret_key'
]) as string[];

const isMailjetSetup = mailjetApiKey && mailjetSecretKey;

const data = isMailjetSetup ? [
{
key: 'status',
value: (<div className='flex items-center gap-2'>
<Icon colorClass='text-green' name='check' size='sm' />
<span>Enabled</span>
</div>)
}
] : [
{
heading: 'Status',
key: 'status',
value: 'Mailjet is not set up'
}
];

const values = (
<SettingGroupContent
columns={1}
values={data}
/>
);

const inputs = (
<SettingGroupContent>
<TextField
error={!mailjetApiKey && isEditing}
title="Mailjet API Key"
value={mailjetApiKey}
hint="Find your API Key in Mailjet's account settings"
onChange={e => updateSetting('mailjet_api_key', e.target.value)}
/>
<TextField
error={!mailjetSecretKey && isEditing}
title="Mailjet Secret Key"
value={mailjetSecretKey}
hint="Find your Secret Key in Mailjet's account settings"
onChange={e => {console.log(updateSetting, e.target.value); updateSetting('mailjet_secret_key', e.target.value)}}
/>
<div className="mt-1">
<Link href="https://app.mailjet.com/account/apikeys" target="_blank">
Get your Mailjet API keys here →
</Link>
</div>
</SettingGroupContent>
);

return (
<TopLevelGroup
description="Configure Mailjet to send your newsletter emails"
isEditing={isEditing}
keywords={keywords}
navid="mailjet"
saveState={saveState}
testId="mailjet"
title="Mailjet"
onCancel={handleCancel}
onEditingChange={handleEditingChange}
onSave={handleSave}
>
{isEditing ? inputs: values}
</TopLevelGroup>
);
};

export default withErrorBoundary(Mailjet, 'Mailjet');
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const NewsletterPreviewContent: React.FC<{

let emailHeader;

if ({hasNewEmailAddresses} || isManagedEmail(config)) {
if (hasNewEmailAddresses || isManagedEmail(config)) {
emailHeader = <><p className="leading-normal"><span className="font-semibold text-grey-900">From: </span><span>{senderName} ({senderEmail})</span></p>
<p className="leading-normal">
<span className="font-semibold text-grey-900">Reply-to: </span>{senderReplyTo ? senderReplyTo : senderEmail}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ const AddOfferModal = () => {
const handleAmountTypeChange = (amountType: string) => {
updateForm(state => ({
...state,
type: amountType === 'percent' ? 'percent' : 'fixed' || state.type
type: amountType === 'percent' ? 'percent' : 'fixed'
}));
};

Expand Down
4 changes: 2 additions & 2 deletions apps/admin-x-settings/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export function getOptionLabel(
}

export function getInitials(name: string = '') {
let rgx = new RegExp(/(\p{L}{1})\p{L}+/, 'gu');
let rgxInitials = [...name.matchAll(rgx)] || [];
let rgx = /(\p{L}{1})\p{L}+/u;
let rgxInitials = [...name.matchAll(rgx)];

const initials = (
(rgxInitials.shift()?.[1] || '') + (rgxInitials.pop()?.[1] || '')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
<LinkTo @route="members">Add members</LinkTo>
to start sending newsletters!
</p>
{{else if (not @publishOptions.mailgunIsConfigured)}}
{{else if (not @publishOptions.bulkMailIsConfigured)}}
<p class="gh-box gh-content-box" data-test-publish-type-error="no-mailgun">
Set up <a href="https://ghost.org/docs/newsletters/#bulk-email-configuration" target="_blank" rel="noreferrer noopener">Mailgun</a> to start sending newsletters!
Set up <a href="https://ghost.org/docs/newsletters/#bulk-email-configuration" target="_blank" rel="noreferrer noopener">bulk mail</a> to start sending newsletters!
</p>
{{/if}}
2 changes: 2 additions & 0 deletions ghost/admin/app/models/setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export default Model.extend(ValidationEngine, {
mailgunApiKey: attr('string'),
mailgunDomain: attr('string'),
mailgunBaseUrl: attr('string'),
mailjetApiKey: attr('string', {defaultValue: null}),
mailjetSecretKey: attr('string', {defaultValue: null}),
portalButton: attr('boolean'),
portalName: attr('boolean'),
portalPlans: attr('json-string'),
Expand Down
5 changes: 5 additions & 0 deletions ghost/admin/app/services/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export default class SettingsService extends Service.extend(ValidationEngine) {
return this.mailgunApiKey && this.mailgunDomain && this.mailgunBaseUrl;
}

get mailjetIsConfigured() {
console.log("mailjetIsConfigured", this, this.mailjetApiKey, this.mailjetSecretKey);
return this.mailjetApiKey && this.mailjetSecretKey;
}

// the settings API endpoint is a little weird as it's singular and we have
// to pass in all types - if we ever fetch settings without all types then
// save we have problems with the missing settings being removed or reset
Expand Down
6 changes: 5 additions & 1 deletion ghost/admin/app/utils/publish-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,18 @@ export default class PublishOptions {
get emailDisabled() {
const hasNoMembers = this.totalMemberCount === 0;

return !this.mailgunIsConfigured || hasNoMembers || this.emailDisabledError;
return !this.bulkMailIsConfigured || hasNoMembers || this.emailDisabledError;
}

get mailgunIsConfigured() {
return this.settings.mailgunIsConfigured
|| this.config.mailgunIsConfigured;
}

get bulkMailIsConfigured() {
return this.mailgunIsConfigured || this.settings.mailjetIsConfigured;
}

@action
setPublishType(newValue) {
// TODO: validate option is allowed when setting?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const EDITABLE_SETTINGS = [
'mailgun_api_key',
'mailgun_domain',
'mailgun_base_url',
'mailjet_api_key',
'mailjet_secret_key',
'email_track_opens',
'email_track_clicks',
'members_track_sources',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const {combineTransactionalMigrations, addSetting} = require('../../utils');

module.exports = combineTransactionalMigrations(
addSetting({
key: 'mailjet_api_key',
value: '',
type: 'string',
group: 'email'
}),
addSetting({
key: 'mailjet_secret_key',
value: '',
type: 'string',
group: 'email'
}),
)
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,14 @@
"defaultValue": null,
"type": "string"
},
"mailjet_api_key": {
"defaultValue": null,
"type": "string"
},
"mailjet_secret_key": {
"defaultValue": null,
"type": "string"
},
"email_track_opens": {
"defaultValue": "true",
"validations": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ class EmailServiceWrapper {
return;
}

const {EmailService, EmailController, EmailRenderer, SendingService, BatchSendingService, EmailSegmenter, MailgunEmailProvider} = require('@tryghost/email-service');
const {EmailService, EmailController, EmailRenderer, SendingService, BatchSendingService, EmailSegmenter, MailgunEmailProvider, MailjetEmailProvider} = require('@tryghost/email-service');
const {Post, Newsletter, Email, EmailBatch, EmailRecipient, Member} = require('../../models');
const MailgunClient = require('@tryghost/mailgun-client');
const Mailjet = require('node-mailjet');
const configService = require('../../../shared/config');
const settingsCache = require('../../../shared/settings-cache');
const settingsHelpers = require('../settings-helpers');
Expand Down Expand Up @@ -51,6 +52,8 @@ class EmailServiceWrapper {
const mailgunClient = new MailgunClient({
config: configService, settings: settingsCache
});


const i18nLanguage = labs.isSet('i18n') ? settingsCache.get('locale') || 'en' : 'en';
const i18n = i18nLib(i18nLanguage, 'newsletter');

Expand All @@ -76,6 +79,12 @@ class EmailServiceWrapper {
errorHandler
});

logging.error(`settingsCache ${settingsCache}`);
const mailjetEmailProvider = new MailjetEmailProvider({
settings: settingsCache,
errorHandler
});

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

const sendingService = new SendingService({
emailProvider: mailgunEmailProvider,
emailProvider: mailjetEmailProvider,
emailRenderer
});

Expand Down
1 change: 1 addition & 0 deletions ghost/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
"mysql2": "3.11.5",
"nconf": "0.12.1",
"node-jose": "2.2.0",
"node-mailjet": "^6.0.6",
"path-match": "1.2.4",
"probe-image-size": "7.2.3",
"rss": "1.2.2",
Expand Down
5 changes: 4 additions & 1 deletion ghost/email-service/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const lib = require('@tryghost/members-events-service/lib');

module.exports = {
EmailService: require('./lib/EmailService'),
EmailController: require('./lib/EmailController'),
Expand All @@ -7,5 +9,6 @@ module.exports = {
BatchSendingService: require('./lib/BatchSendingService'),
EmailEventProcessor: require('./lib/EmailEventProcessor'),
EmailEventStorage: require('./lib/EmailEventStorage'),
MailgunEmailProvider: require('./lib/MailgunEmailProvider')
MailgunEmailProvider: require('./lib/MailgunEmailProvider'),
MailjetEmailProvider: require('./lib/MailjetEmailProvider')
};
2 changes: 2 additions & 0 deletions ghost/email-service/lib/BatchSendingService.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ class BatchSendingService {
email._retryCutOffTime = retryCutOffTime;

try {
logging.info(`Sending email ${emailId}`);
await this.sendEmail(email);
await this.retryDb(async () => {
await email.save({
Expand Down Expand Up @@ -210,6 +211,7 @@ class BatchSendingService {
if (batches.length === 0) {
batches = await this.createBatches({email, newsletter, post});
}
logging.info(`Sending batches for email ${email.id}`);
await this.sendBatches({email, batches, post, newsletter});
}

Expand Down
Loading