ghost 4.13.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/content/themes/casper/assets/built/screen.css +1 -1
  2. package/content/themes/casper/assets/built/screen.css.map +1 -1
  3. package/content/themes/casper/assets/css/screen.css +1 -1
  4. package/content/themes/casper/default.hbs +2 -2
  5. package/content/themes/casper/package.json +1 -1
  6. package/content/themes/casper/page.hbs +28 -26
  7. package/content/themes/casper/partials/post-card.hbs +2 -2
  8. package/content/themes/casper/post.hbs +67 -65
  9. package/content/themes/casper/tag.hbs +2 -2
  10. package/core/built/assets/{chunk.3.f80c7fbb7573ce508a05.js → chunk.3.4b1d9e20e57164ac9c29.js} +31 -29
  11. package/core/built/assets/ghost-dark-bb2831fc27fcb02893ed0a761207dc63.css +1 -0
  12. package/core/built/assets/{ghost.min-ba7f03a78d7d98444af386b8ae9347a7.js → ghost.min-d1d99f3ed6e0f427874b2a11e7078475.js} +1777 -1685
  13. package/core/built/assets/ghost.min-e7612edfa72b0fe2c201b387923e6fc7.css +1 -0
  14. package/core/built/assets/icons/check-2.svg +1 -0
  15. package/core/built/assets/icons/discount-bubble.svg +1 -0
  16. package/core/built/assets/icons/no-data-line-chart.svg +1 -0
  17. package/core/built/assets/icons/no-data-list.svg +9 -8
  18. package/core/built/assets/icons/no-data-subscription.svg +1 -0
  19. package/core/built/assets/{vendor.min-29784d514390cb5abc74ae660cb2fbc7.js → vendor.min-3660ec7864887f1496fe7a27fd23ab76.js} +1570 -1289
  20. package/core/frontend/helpers/ghost_head.js +7 -1
  21. package/core/frontend/helpers/match.js +19 -4
  22. package/core/frontend/helpers/products.js +68 -0
  23. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  24. package/core/frontend/services/routing/controllers/email-post.js +3 -2
  25. package/core/frontend/services/settings/loader.js +2 -2
  26. package/core/frontend/services/sitemap/base-generator.js +12 -8
  27. package/core/frontend/services/sitemap/manager.js +1 -1
  28. package/core/frontend/services/theme-engine/handlebars/helpers.js +1 -0
  29. package/core/frontend/services/theme-engine/middleware.js +4 -1
  30. package/core/server/api/canary/email-preview.js +15 -33
  31. package/core/server/api/canary/integrations.js +7 -30
  32. package/core/server/api/canary/labels.js +8 -9
  33. package/core/server/api/canary/members.js +13 -9
  34. package/core/server/api/canary/schedules.js +9 -57
  35. package/core/server/api/canary/settings.js +20 -158
  36. package/core/server/api/canary/themes.js +5 -59
  37. package/core/server/api/canary/utils/serializers/output/members.js +2 -14
  38. package/core/server/api/canary/utils/validators/input/settings.js +23 -1
  39. package/core/server/api/canary/webhooks.js +6 -24
  40. package/core/server/api/v2/schedules.js +9 -57
  41. package/core/server/api/v3/email-preview.js +15 -28
  42. package/core/server/api/v3/integrations.js +7 -30
  43. package/core/server/api/v3/labels.js +8 -9
  44. package/core/server/api/v3/members.js +4 -1
  45. package/core/server/api/v3/schedules.js +9 -57
  46. package/core/server/api/v3/settings.js +13 -132
  47. package/core/server/api/v3/utils/validators/input/settings.js +23 -1
  48. package/core/server/api/v3/webhooks.js +6 -28
  49. package/core/server/data/exporter/table-lists.js +1 -0
  50. package/core/server/data/importer/import-manager.js +398 -0
  51. package/core/server/data/importer/importers/data/data-importer.js +162 -0
  52. package/core/server/data/importer/importers/data/index.js +1 -162
  53. package/core/server/data/importer/index.js +1 -379
  54. package/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js +70 -0
  55. package/core/server/data/migrations/versions/4.14/02-fix-free-members-status-events.js +60 -0
  56. package/core/server/data/migrations/versions/4.15/01-add-temp-members-analytic-events-table.js +12 -0
  57. package/core/server/data/migrations/versions/4.16/01-add-custom-theme-settings-table.js +9 -0
  58. package/core/server/data/schema/fixtures/utils.js +6 -1
  59. package/core/server/data/schema/schema.js +26 -0
  60. package/core/server/lib/request-external.js +3 -2
  61. package/core/server/models/action.js +1 -1
  62. package/core/server/models/api-key.js +1 -1
  63. package/core/server/models/base/bookshelf.js +0 -3
  64. package/core/server/models/base/index.js +2 -0
  65. package/core/server/models/base/plugins/events.js +2 -2
  66. package/core/server/models/base/plugins/raw-knex.js +10 -10
  67. package/core/server/models/custom-theme-setting.js +9 -0
  68. package/core/server/models/email.js +2 -2
  69. package/core/server/models/index.js +2 -0
  70. package/core/server/models/integration.js +1 -1
  71. package/core/server/models/label.js +2 -2
  72. package/core/server/models/member-analytic-event.js +9 -0
  73. package/core/server/models/member.js +2 -2
  74. package/core/server/models/post.js +2 -2
  75. package/core/server/models/settings.js +2 -2
  76. package/core/server/models/tag.js +2 -2
  77. package/core/server/models/user.js +2 -2
  78. package/core/server/models/webhook.js +2 -2
  79. package/core/server/services/bulk-email/bulk-email-processor.js +1 -4
  80. package/core/server/services/custom-theme-settings.js +8 -0
  81. package/core/server/services/integrations/integrations-service.js +61 -0
  82. package/core/server/services/mail/GhostMailer.js +29 -37
  83. package/core/server/services/mega/email-preview.js +41 -0
  84. package/core/server/services/mega/index.js +4 -0
  85. package/core/server/services/mega/mega.js +27 -23
  86. package/core/server/services/mega/post-email-serializer.js +28 -21
  87. package/core/server/services/mega/template.js +11 -0
  88. package/core/server/services/members/api.js +1 -0
  89. package/core/server/services/members/emails/signup.js +1 -1
  90. package/core/server/services/oembed.js +7 -2
  91. package/core/server/services/posts/post-scheduling-service.js +100 -0
  92. package/core/server/services/settings/index.js +13 -16
  93. package/core/server/services/settings/settings-bread-service.js +188 -0
  94. package/core/server/services/settings/settings-utils.js +32 -0
  95. package/core/server/services/themes/ThemeStorage.js +5 -4
  96. package/core/server/services/themes/activation-bridge.js +14 -0
  97. package/core/server/services/themes/index.js +2 -0
  98. package/core/server/services/themes/installer.js +72 -0
  99. package/core/server/services/themes/validate.js +5 -2
  100. package/core/server/services/webhooks/webhooks-service.js +55 -0
  101. package/core/server/web/admin/views/default-prod.html +4 -4
  102. package/core/server/web/admin/views/default.html +4 -4
  103. package/core/server/web/members/app.js +3 -0
  104. package/core/shared/config/defaults.json +2 -2
  105. package/core/shared/custom-theme-settings-cache.js +3 -0
  106. package/core/shared/i18n/translations/en.json +1 -6
  107. package/core/shared/labs.js +5 -7
  108. package/package.json +64 -62
  109. package/yarn.lock +1490 -1055
  110. package/core/built/assets/ghost-dark-98d56e4973a502750748090f9dbc8280.css +0 -1
  111. package/core/built/assets/ghost.min-6932a664a1cb92a8e4a15f540cae3ad8.css +0 -1
  112. package/core/server/services/mega/template-labs.js +0 -1024
@@ -0,0 +1,9 @@
1
+ const utils = require('../../utils');
2
+
3
+ module.exports = utils.addTable('custom_theme_settings', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ theme: {type: 'string', maxlength: 191, nullable: false},
6
+ key: {type: 'string', maxlength: 191, nullable: false},
7
+ type: {type: 'string', maxlength: 50, nullable: false},
8
+ value: {type: 'text', maxlength: 65535, nullable: true}
9
+ });
@@ -158,7 +158,12 @@ const addFixturesForRelation = function addFixturesForRelation(relationFixture,
158
158
  toItems = _.reject(toItems, function (item) {
159
159
  return fromItem
160
160
  .related(relationFixture.from.relation)
161
- .findWhere(matchObj(relationFixture.to.match, item));
161
+ .find((model) => {
162
+ const objectToMatch = matchObj(relationFixture.to.match, item);
163
+ return Object.keys(objectToMatch).every(function (keyToCheck) {
164
+ return model.get(keyToCheck) === objectToMatch[keyToCheck];
165
+ });
166
+ });
162
167
  });
163
168
 
164
169
  if (toItems && toItems.length > 0) {
@@ -633,5 +633,31 @@ module.exports = {
633
633
  created_by: {type: 'string', maxlength: 24, nullable: false},
634
634
  updated_at: {type: 'dateTime', nullable: true},
635
635
  updated_by: {type: 'string', maxlength: 24, nullable: true}
636
+ },
637
+ temp_member_analytic_events: {
638
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
639
+ event_name: {type: 'string', maxlength: 50, nullable: false},
640
+ created_at: {type: 'dateTime', nullable: false},
641
+ member_id: {type: 'string', maxlength: 24, nullable: false},
642
+ member_status: {type: 'string', maxlength: 50, nullable: false},
643
+ entry_id: {type: 'string', maxlength: 24, nullable: true},
644
+ source_url: {type: 'string', maxlength: 2000, nullable: true},
645
+ metadata: {type: 'string', maxlength: 191, nullable: true}
646
+ },
647
+ custom_theme_settings: {
648
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
649
+ theme: {type: 'string', maxlength: 191, nullable: false},
650
+ key: {type: 'string', maxlength: 191, nullable: false},
651
+ type: {
652
+ type: 'string',
653
+ maxlength: 50,
654
+ nullable: false,
655
+ validations: {
656
+ isIn: [[
657
+ 'select'
658
+ ]]
659
+ }
660
+ },
661
+ value: {type: 'text', maxlength: 65535, nullable: true}
636
662
  }
637
663
  };
@@ -12,8 +12,9 @@ function isPrivateIp(addr) {
12
12
  /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) ||
13
13
  /^f[cd][0-9a-f]{2}:/i.test(addr) ||
14
14
  /^fe80:/i.test(addr) ||
15
- /^::1$/.test(addr) ||
16
- /^::$/.test(addr);
15
+ /^::[10]$/.test(addr) ||
16
+ /^::$/.test(addr) ||
17
+ /^0/.test(addr);
17
18
  }
18
19
 
19
20
  async function errorIfHostnameResolvesToPrivateIp(options) {
@@ -3,7 +3,7 @@ const ghostBookshelf = require('./base');
3
3
 
4
4
  const candidates = [];
5
5
 
6
- _.each(ghostBookshelf._models, (model) => {
6
+ _.each(ghostBookshelf.registry.models, (model) => {
7
7
  candidates.push([model, model.prototype.tableName.replace(/s$/, '')]);
8
8
  });
9
9
 
@@ -72,7 +72,7 @@ const ApiKey = ghostBookshelf.Model.extend({
72
72
  }
73
73
  }
74
74
  },
75
- onUpdated(model, attrs, options) {
75
+ onUpdated(model, options) {
76
76
  if (this.previous('secret') !== this.get('secret')) {
77
77
  this.addAction(model, 'refreshed', options);
78
78
  }
@@ -10,9 +10,6 @@ const db = require('../../data/db');
10
10
  // Initializes a new Bookshelf instance called ghostBookshelf, for reference elsewhere in Ghost.
11
11
  const ghostBookshelf = bookshelf(db.knex);
12
12
 
13
- // Load the Bookshelf registry plugin, which helps us avoid circular dependencies
14
- ghostBookshelf.plugin('registry');
15
-
16
13
  ghostBookshelf.plugin(plugins.eagerLoad);
17
14
 
18
15
  // Add committed/rollback events.
@@ -21,6 +21,8 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
21
21
  // Bookshelf `hasTimestamps` - handles created_at and updated_at properties
22
22
  hasTimestamps: true,
23
23
 
24
+ requireFetch: false,
25
+
24
26
  // https://github.com/bookshelf/bookshelf/commit/a55db61feb8ad5911adb4f8c3b3d2a97a45bd6db
25
27
  parsedIdAttribute: function () {
26
28
  return false;
@@ -128,7 +128,7 @@ module.exports = function (Bookshelf) {
128
128
  }
129
129
  },
130
130
 
131
- onCreated(model, attrs, options) {
131
+ onCreated(model, options) {
132
132
  this.addAction(model, 'added', options);
133
133
  },
134
134
 
@@ -189,7 +189,7 @@ module.exports = function (Bookshelf) {
189
189
  });
190
190
  },
191
191
 
192
- onUpdated(model, attrs, options) {
192
+ onUpdated(model, options) {
193
193
  this.addAction(model, 'edited', options);
194
194
  },
195
195
 
@@ -106,17 +106,17 @@ module.exports = function (Bookshelf) {
106
106
 
107
107
  if (!withRelated) {
108
108
  return _.map(objects, (object) => {
109
- object = Bookshelf._models[modelName].prototype.toJSON.bind({
109
+ object = Bookshelf.registry.models[modelName].prototype.toJSON.bind({
110
110
  attributes: object,
111
111
  related: function (key) {
112
112
  return object[key];
113
113
  },
114
- serialize: Bookshelf._models[modelName].prototype.serialize,
115
- formatsToJSON: Bookshelf._models[modelName].prototype.formatsToJSON
114
+ serialize: Bookshelf.registry.models[modelName].prototype.serialize,
115
+ formatsToJSON: Bookshelf.registry.models[modelName].prototype.formatsToJSON
116
116
  })();
117
117
 
118
- object = Bookshelf._models[modelName].prototype.fixBools(object);
119
- object = Bookshelf._models[modelName].prototype.fixDatesWhenFetch(object);
118
+ object = Bookshelf.registry.models[modelName].prototype.fixBools(object);
119
+ object = Bookshelf.registry.models[modelName].prototype.fixDatesWhenFetch(object);
120
120
  return object;
121
121
  });
122
122
  }
@@ -180,7 +180,7 @@ module.exports = function (Bookshelf) {
180
180
  object[relation] = relationsToAttach[relation][object.id];
181
181
  });
182
182
 
183
- object = Bookshelf._models[modelName].prototype.toJSON.bind({
183
+ object = Bookshelf.registry.models[modelName].prototype.toJSON.bind({
184
184
  attributes: object,
185
185
  _originalOptions: {
186
186
  withRelated: Object.keys(relationsToAttach)
@@ -188,12 +188,12 @@ module.exports = function (Bookshelf) {
188
188
  related: function (key) {
189
189
  return object[key];
190
190
  },
191
- serialize: Bookshelf._models[modelName].prototype.serialize,
192
- formatsToJSON: Bookshelf._models[modelName].prototype.formatsToJSON
191
+ serialize: Bookshelf.registry.models[modelName].prototype.serialize,
192
+ formatsToJSON: Bookshelf.registry.models[modelName].prototype.formatsToJSON
193
193
  })();
194
194
 
195
- object = Bookshelf._models[modelName].prototype.fixBools(object);
196
- object = Bookshelf._models[modelName].prototype.fixDatesWhenFetch(object);
195
+ object = Bookshelf.registry.models[modelName].prototype.fixBools(object);
196
+ object = Bookshelf.registry.models[modelName].prototype.fixDatesWhenFetch(object);
197
197
  return object;
198
198
  });
199
199
 
@@ -0,0 +1,9 @@
1
+ const ghostBookshelf = require('./base');
2
+
3
+ const CustomThemeSetting = ghostBookshelf.Model.extend({
4
+ tableName: 'custom_theme_settings'
5
+ });
6
+
7
+ module.exports = {
8
+ CustomThemeSetting: ghostBookshelf.model('CustomThemeSetting', CustomThemeSetting)
9
+ };
@@ -59,13 +59,13 @@ const Email = ghostBookshelf.Model.extend({
59
59
  ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
60
60
  },
61
61
 
62
- onCreated: function onCreated(model, attrs, options) {
62
+ onCreated: function onCreated(model, options) {
63
63
  ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
64
64
 
65
65
  model.emitChange('added', options);
66
66
  },
67
67
 
68
- onUpdated: function onUpdated(model, attrs, options) {
68
+ onUpdated: function onUpdated(model, options) {
69
69
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
70
70
 
71
71
  model.emitChange('edited', options);
@@ -17,6 +17,7 @@ const models = [
17
17
  'post',
18
18
  'role',
19
19
  'settings',
20
+ 'custom-theme-setting',
20
21
  'session',
21
22
  'tag',
22
23
  'tag-public',
@@ -39,6 +40,7 @@ const models = [
39
40
  'member-payment-event',
40
41
  'member-status-event',
41
42
  'member-product-event',
43
+ 'member-analytic-event',
42
44
  'posts-meta',
43
45
  'member-stripe-customer',
44
46
  'stripe-customer-subscription',
@@ -36,7 +36,7 @@ const Integration = ghostBookshelf.Model.extend({
36
36
  }
37
37
  },
38
38
 
39
- onCreated: function onCreated(model, response, options) {
39
+ onCreated: function onCreated(model, options) {
40
40
  ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
41
41
 
42
42
  model.emitChange('added', options);
@@ -14,13 +14,13 @@ Label = ghostBookshelf.Model.extend({
14
14
  ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
15
15
  },
16
16
 
17
- onCreated: function onCreated(model, attrs, options) {
17
+ onCreated: function onCreated(model, options) {
18
18
  ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
19
19
 
20
20
  model.emitChange('added', options);
21
21
  },
22
22
 
23
- onUpdated: function onUpdated(model, attrs, options) {
23
+ onUpdated: function onUpdated(model, options) {
24
24
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
25
25
 
26
26
  model.emitChange('edited', options);
@@ -0,0 +1,9 @@
1
+ const ghostBookshelf = require('./base');
2
+
3
+ const MemberAnalyticEvent = ghostBookshelf.Model.extend({
4
+ tableName: 'temp_member_analytic_events'
5
+ });
6
+
7
+ module.exports = {
8
+ MemberAnalyticEvent: ghostBookshelf.model('MemberAnalyticEvent', MemberAnalyticEvent)
9
+ };
@@ -140,13 +140,13 @@ const Member = ghostBookshelf.Model.extend({
140
140
  ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
141
141
  },
142
142
 
143
- onCreated: function onCreated(model, attrs, options) {
143
+ onCreated: function onCreated(model, options) {
144
144
  ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
145
145
 
146
146
  model.emitChange('added', options);
147
147
  },
148
148
 
149
- onUpdated: function onUpdated(model, attrs, options) {
149
+ onUpdated: function onUpdated(model, options) {
150
150
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
151
151
 
152
152
  model.emitChange('edited', options);
@@ -301,7 +301,7 @@ Post = ghostBookshelf.Model.extend({
301
301
  * bookshelf-relations listens on `created` + `updated`.
302
302
  * We ensure that we are catching the event after bookshelf relations.
303
303
  */
304
- onSaved: function onSaved(model, response, options) {
304
+ onSaved: function onSaved(model, options) {
305
305
  ghostBookshelf.Model.prototype.onSaved.apply(this, arguments);
306
306
 
307
307
  if (options.method !== 'insert') {
@@ -317,7 +317,7 @@ Post = ghostBookshelf.Model.extend({
317
317
  }
318
318
  },
319
319
 
320
- onUpdated: function onUpdated(model, attrs, options) {
320
+ onUpdated: function onUpdated(model, options) {
321
321
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
322
322
 
323
323
  model.statusChanging = model.get('status') !== model.previous('status');
@@ -104,14 +104,14 @@ Settings = ghostBookshelf.Model.extend({
104
104
  model.emitChange(model._previousAttributes.key + '.' + 'deleted', options);
105
105
  },
106
106
 
107
- onCreated: function onCreated(model, response, options) {
107
+ onCreated: function onCreated(model, options) {
108
108
  ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
109
109
 
110
110
  model.emitChange('added', options);
111
111
  model.emitChange(model.attributes.key + '.' + 'added', options);
112
112
  },
113
113
 
114
- onUpdated: function onUpdated(model, response, options) {
114
+ onUpdated: function onUpdated(model, options) {
115
115
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
116
116
 
117
117
  model.emitChange('edited', options);
@@ -73,13 +73,13 @@ Tag = ghostBookshelf.Model.extend({
73
73
  ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
74
74
  },
75
75
 
76
- onCreated: function onCreated(model, attrs, options) {
76
+ onCreated: function onCreated(model, options) {
77
77
  ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
78
78
 
79
79
  model.emitChange('added', options);
80
80
  },
81
81
 
82
- onUpdated: function onUpdated(model, attrs, options) {
82
+ onUpdated: function onUpdated(model, options) {
83
83
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
84
84
 
85
85
  model.emitChange('edited', options);
@@ -98,7 +98,7 @@ User = ghostBookshelf.Model.extend({
98
98
  model.emitChange('deleted', options);
99
99
  },
100
100
 
101
- onCreated: function onCreated(model, attrs, options) {
101
+ onCreated: function onCreated(model, options) {
102
102
  ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
103
103
 
104
104
  model.emitChange('added', options);
@@ -109,7 +109,7 @@ User = ghostBookshelf.Model.extend({
109
109
  }
110
110
  },
111
111
 
112
- onUpdated: function onUpdated(model, response, options) {
112
+ onUpdated: function onUpdated(model, options) {
113
113
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
114
114
 
115
115
  model.statusChanging = model.get('status') !== model.previous('status');
@@ -22,13 +22,13 @@ Webhook = ghostBookshelf.Model.extend({
22
22
  ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
23
23
  },
24
24
 
25
- onCreated: function onCreated(model, response, options) {
25
+ onCreated: function onCreated(model, options) {
26
26
  ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
27
27
 
28
28
  model.emitChange('added', options);
29
29
  },
30
30
 
31
- onUpdated: function onUpdated(model, response, options) {
31
+ onUpdated: function onUpdated(model, options) {
32
32
  ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
33
33
 
34
34
  model.emitChange('edited', options);
@@ -9,7 +9,6 @@ const mailgunProvider = require('./mailgun');
9
9
  const sentry = require('../../../shared/sentry');
10
10
  const debug = require('@tryghost/debug')('mega');
11
11
  const postEmailSerializer = require('../mega/post-email-serializer');
12
- const labs = require('../../../shared/labs');
13
12
 
14
13
  const BATCH_SIZE = mailgunProvider.BATCH_SIZE;
15
14
 
@@ -231,9 +230,7 @@ module.exports = {
231
230
  recipientData[recipient.member_email] = data;
232
231
  });
233
232
 
234
- if (labs.isSet('emailCardSegments')) {
235
- emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);
236
- }
233
+ emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);
237
234
 
238
235
  return mailgunProvider.send(emailData, recipientData, replacements).then((response) => {
239
236
  debug(`sent message (${Date.now() - startTime}ms)`);
@@ -0,0 +1,8 @@
1
+ const {Service: CustomThemeSettingsService} = require('@tryghost/custom-theme-settings-service');
2
+ const customThemeSettingsCache = require('../../shared/custom-theme-settings-cache');
3
+ const models = require('../models');
4
+
5
+ module.exports = new CustomThemeSettingsService({
6
+ model: models.CustomThemeSetting,
7
+ cache: customThemeSettingsCache
8
+ });
@@ -0,0 +1,61 @@
1
+ const {NotFoundError, GhostError} = require('@tryghost/errors');
2
+ const tpl = require('@tryghost/tpl');
3
+
4
+ const messages = {
5
+ notFound: '{resource} not found.'
6
+ };
7
+
8
+ class IntegrationsService {
9
+ constructor({IntegrationModel, ApiKeyModel}) {
10
+ this.IntegrationModel = IntegrationModel;
11
+ this.ApiKeyModel = ApiKeyModel;
12
+ }
13
+
14
+ async edit(data, options) {
15
+ if (options.keyid) {
16
+ const model = await this.ApiKeyModel.findOne({id: options.keyid});
17
+
18
+ if (!model) {
19
+ throw new NotFoundError({
20
+ message: tpl(messages.notFound, {
21
+ resource: 'ApiKey'
22
+ })
23
+ });
24
+ }
25
+ try {
26
+ await this.ApiKeyModel.refreshSecret(model.toJSON(), Object.assign({}, options, {id: options.keyid}));
27
+
28
+ return await this.IntegrationModel.findOne({id: options.id}, {
29
+ withRelated: ['api_keys', 'webhooks']
30
+ });
31
+ } catch (err) {
32
+ throw new GhostError({
33
+ err: err
34
+ });
35
+ }
36
+ }
37
+
38
+ try {
39
+ return await this.IntegrationModel.edit(data, Object.assign(options, {require: true}));
40
+ } catch (error) {
41
+ if (error.message === 'NotFound' || error.message === 'EmptyResponse') {
42
+ throw new NotFoundError({
43
+ message: tpl(messages.notFound, {
44
+ resource: 'Integration'
45
+ })
46
+ });
47
+ }
48
+
49
+ throw error;
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @returns {IntegrationsService} instance of the PostsService
56
+ */
57
+ const getIntegrationsServiceInstance = ({IntegrationModel, ApiKeyModel}) => {
58
+ return new IntegrationsService({IntegrationModel, ApiKeyModel});
59
+ };
60
+
61
+ module.exports = getIntegrationsServiceInstance;
@@ -1,7 +1,6 @@
1
1
  // # Mail
2
2
  // Handles sending email for Ghost
3
3
  const _ = require('lodash');
4
- const Promise = require('bluebird');
5
4
  const validator = require('@tryghost/validator');
6
5
  const config = require('../../../shared/config');
7
6
  const errors = require('@tryghost/errors');
@@ -67,15 +66,18 @@ function createMailError({message, err, ignoreDefaultMessage} = {message: ''}) {
67
66
 
68
67
  module.exports = class GhostMailer {
69
68
  constructor() {
70
- const nodemailer = require('nodemailer');
71
- const transport = config.get('mail') && config.get('mail').transport || 'direct';
69
+ const nodemailer = require('@tryghost/nodemailer');
70
+
71
+ let transport = config.get('mail') && config.get('mail').transport || 'direct';
72
+ transport = transport.toLowerCase();
73
+
72
74
  // nodemailer mutates the options passed to createTransport
73
75
  const options = config.get('mail') && _.clone(config.get('mail').options) || {};
74
76
 
75
77
  this.state = {
76
78
  usingDirect: transport === 'direct'
77
79
  };
78
- this.transport = nodemailer.createTransport(transport, options);
80
+ this.transport = nodemailer(transport, options);
79
81
  }
80
82
 
81
83
  /**
@@ -102,52 +104,42 @@ module.exports = class GhostMailer {
102
104
 
103
105
  const response = await this.sendMail(messageToSend);
104
106
 
105
- if (this.transport.transportType === 'DIRECT') {
107
+ if (this.state.usingDirect) {
106
108
  return this.handleDirectTransportResponse(response);
107
109
  }
108
110
 
109
111
  return response;
110
112
  }
111
113
 
112
- sendMail(message) {
113
- return new Promise((resolve, reject) => {
114
- this.transport.sendMail(message, (err, response) => {
115
- if (err) {
116
- reject(createMailError({
117
- message: i18n.t('errors.mail.reason', {reason: err.message || err}),
118
- err
119
- }));
120
- }
121
- resolve(response);
114
+ async sendMail(message) {
115
+ try {
116
+ const response = await this.transport.sendMail(message);
117
+ return response;
118
+ } catch (err) {
119
+ throw createMailError({
120
+ message: i18n.t('errors.mail.reason', {reason: err.message || err}),
121
+ err
122
122
  });
123
- });
123
+ }
124
124
  }
125
125
 
126
126
  handleDirectTransportResponse(response) {
127
- return new Promise((resolve, reject) => {
128
- response.statusHandler.once('failed', function (data) {
129
- if (data.error && data.error.code === 'ENOTFOUND') {
130
- reject(createMailError({
131
- message: i18n.t('errors.mail.noMailServerAtAddress.error', {domain: data.domain})
132
- }));
133
- }
134
-
135
- reject(createMailError());
136
- });
137
-
138
- response.statusHandler.once('requeue', function (data) {
139
- if (data.error && data.error.message) {
140
- reject(createMailError({
141
- message: i18n.t('errors.mail.reason', {reason: data.error.message})
142
- }));
143
- }
127
+ if (!response) {
128
+ return i18n.t('notices.mail.messageSent');
129
+ }
144
130
 
145
- reject(createMailError());
131
+ if (response.pending.length > 0) {
132
+ throw createMailError({
133
+ message: i18n.t('errors.mail.reason', {reason: 'Email has been temporarily rejected'})
146
134
  });
135
+ }
147
136
 
148
- response.statusHandler.once('sent', function () {
149
- resolve(i18n.t('notices.mail.messageSent'));
137
+ if (response.errors.length > 0) {
138
+ throw createMailError({
139
+ message: i18n.t('errors.mail.reason', {reason: response.errors[0].message})
150
140
  });
151
- });
141
+ }
142
+
143
+ return i18n.t('notices.mail.messageSent');
152
144
  }
153
145
  };
@@ -0,0 +1,41 @@
1
+ const postEmailSerializer = require('./post-email-serializer');
2
+
3
+ class EmailPreview {
4
+ /**
5
+ * @constructor
6
+ * @param {Object} options
7
+ * @param {String} options.apiVersion
8
+ */
9
+ constructor({apiVersion}) {
10
+ this.apiVersion = apiVersion;
11
+ }
12
+
13
+ /**
14
+ * @param {Object} post - Post model object instance
15
+ * @param {String} memberSegment - member segment filter
16
+ * @returns {Promise<Object>}
17
+ */
18
+ async generateEmailContent(post, memberSegment) {
19
+ let emailContent = await postEmailSerializer.serialize(post, {
20
+ isBrowserPreview: true,
21
+ apiVersion: this.apiVersion
22
+ });
23
+
24
+ if (memberSegment) {
25
+ emailContent = postEmailSerializer.renderEmailForSegment(emailContent, memberSegment);
26
+ }
27
+
28
+ const replacements = postEmailSerializer.parseReplacements(emailContent);
29
+
30
+ replacements.forEach((replacement) => {
31
+ emailContent[replacement.format] = emailContent[replacement.format].replace(
32
+ replacement.match,
33
+ replacement.fallback || ''
34
+ );
35
+ });
36
+
37
+ return emailContent;
38
+ }
39
+ }
40
+
41
+ module.exports = EmailPreview;
@@ -5,6 +5,10 @@ module.exports = {
5
5
 
6
6
  get postEmailSerializer() {
7
7
  return require('./post-email-serializer');
8
+ },
9
+
10
+ get EmailPreview() {
11
+ return require('./email-preview');
8
12
  }
9
13
  };
10
14