2022-11-21 12:29:53 +03:00
/* eslint-disable no-unused-vars */
2022-11-23 13:33:44 +03:00
/ * *
* @ typedef { object } Post
* @ typedef { object } Email
* @ typedef { object } LimitService
2023-01-04 13:22:12 +03:00
* @ typedef { { checkVerificationRequired ( ) : Promise < boolean > } } VerificationTrigger
2022-11-23 13:33:44 +03:00
* /
const BatchSendingService = require ( './batch-sending-service' ) ;
const errors = require ( '@tryghost/errors' ) ;
const tpl = require ( '@tryghost/tpl' ) ;
const EmailRenderer = require ( './email-renderer' ) ;
const EmailSegmenter = require ( './email-segmenter' ) ;
2022-11-30 15:56:28 +03:00
const SendingService = require ( './sending-service' ) ;
2023-01-26 19:32:34 +03:00
const logging = require ( '@tryghost/logging' ) ;
2022-11-23 13:33:44 +03:00
const messages = {
archivedNewsletterError : 'Cannot send email to archived newsletters' ,
2023-01-04 13:22:12 +03:00
missingNewsletterError : 'The post does not have a newsletter relation' ,
emailSendingDisabled : ` Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org `
2022-11-23 13:33:44 +03:00
} ;
2022-11-21 12:29:53 +03:00
class EmailService {
2022-11-23 13:33:44 +03:00
# batchSendingService ;
2022-11-30 15:56:28 +03:00
# sendingService ;
2022-11-23 13:33:44 +03:00
# models ;
# settingsCache ;
# emailRenderer ;
# emailSegmenter ;
# limitService ;
2022-11-30 15:56:28 +03:00
# membersRepository ;
2023-01-04 13:22:12 +03:00
# verificationTrigger ;
2023-01-26 19:32:34 +03:00
# emailAnalyticsJobs ;
2022-11-23 13:33:44 +03:00
/ * *
2022-11-30 13:51:58 +03:00
*
* @ param { object } dependencies
2022-11-23 13:33:44 +03:00
* @ param { BatchSendingService } dependencies . batchSendingService
2022-11-30 15:56:28 +03:00
* @ param { SendingService } dependencies . sendingService
2022-11-23 13:33:44 +03:00
* @ param { object } dependencies . models
* @ param { object } dependencies . models . Email
* @ param { object } dependencies . settingsCache
* @ param { EmailRenderer } dependencies . emailRenderer
* @ param { EmailSegmenter } dependencies . emailSegmenter
* @ param { LimitService } dependencies . limitService
2022-11-30 15:56:28 +03:00
* @ param { object } dependencies . membersRepository
2023-01-04 13:22:12 +03:00
* @ param { VerificationTrigger } dependencies . verificationTrigger
2023-01-26 19:32:34 +03:00
* @ param { object } dependencies . emailAnalyticsJobs
2022-11-23 13:33:44 +03:00
* /
constructor ( {
batchSendingService ,
2022-11-30 15:56:28 +03:00
sendingService ,
2022-11-23 13:33:44 +03:00
models ,
settingsCache ,
emailRenderer ,
emailSegmenter ,
2022-11-30 15:56:28 +03:00
limitService ,
2023-01-04 13:22:12 +03:00
membersRepository ,
2023-01-26 19:32:34 +03:00
verificationTrigger ,
emailAnalyticsJobs
2022-11-23 13:33:44 +03:00
} ) {
this . # batchSendingService = batchSendingService ;
this . # models = models ;
this . # settingsCache = settingsCache ;
this . # emailRenderer = emailRenderer ;
this . # emailSegmenter = emailSegmenter ;
this . # limitService = limitService ;
2022-11-30 15:56:28 +03:00
this . # membersRepository = membersRepository ;
this . # sendingService = sendingService ;
2023-01-04 13:22:12 +03:00
this . # verificationTrigger = verificationTrigger ;
2023-01-26 19:32:34 +03:00
this . # emailAnalyticsJobs = emailAnalyticsJobs ;
2022-11-23 13:33:44 +03:00
}
/ * *
* @ private
* /
2023-01-04 13:22:12 +03:00
async checkLimits ( addedCount = 0 ) {
2022-11-23 13:33:44 +03:00
// Check host limit for allowed member count and throw error if over limit
// - do this even if it's a retry so that there's no way around the limit
if ( this . # limitService . isLimited ( 'members' ) ) {
await this . # limitService . errorIfIsOverLimit ( 'members' ) ;
}
// Check host limit for disabled emails or going over emails limit
if ( this . # limitService . isLimited ( 'emails' ) ) {
2023-01-04 13:22:12 +03:00
await this . # limitService . errorIfWouldGoOverLimit ( 'emails' , { addedCount } ) ;
}
// Check if email verification is required
if ( await this . # verificationTrigger . checkVerificationRequired ( ) ) {
throw new errors . HostLimitError ( {
message : tpl ( messages . emailSendingDisabled )
} ) ;
2022-11-23 13:33:44 +03:00
}
2022-11-21 12:29:53 +03:00
}
2022-11-23 13:33:44 +03:00
/ * *
2022-11-30 13:51:58 +03:00
*
* @ param { Post } post
2022-11-23 13:33:44 +03:00
* @ returns { Promise < Email > }
* /
2022-11-21 12:29:53 +03:00
async createEmail ( post ) {
2022-11-23 13:33:44 +03:00
let newsletter = await post . getLazyRelation ( 'newsletter' ) ;
if ( ! newsletter ) {
throw new errors . EmailError ( {
message : tpl ( messages . missingNewsletterError )
} ) ;
}
if ( newsletter . get ( 'status' ) !== 'active' ) {
// A post might have been scheduled to an archived newsletter.
// Don't send it (people can't unsubscribe any longer).
throw new errors . EmailError ( {
message : tpl ( messages . archivedNewsletterError )
} ) ;
}
const emailRecipientFilter = post . get ( 'email_recipient_filter' ) ;
2023-01-04 13:22:12 +03:00
const emailCount = await this . # emailSegmenter . getMembersCount ( newsletter , emailRecipientFilter ) ;
await this . checkLimits ( emailCount ) ;
2022-11-23 13:33:44 +03:00
const email = await this . # models . Email . add ( {
post _id : post . id ,
newsletter _id : newsletter . id ,
status : 'pending' ,
submitted _at : new Date ( ) ,
track _opens : ! ! this . # settingsCache . get ( 'email_track_opens' ) ,
track _clicks : ! ! this . # settingsCache . get ( 'email_track_clicks' ) ,
feedback _enabled : ! ! newsletter . get ( 'feedback_enabled' ) ,
recipient _filter : emailRecipientFilter ,
2022-11-29 13:27:17 +03:00
subject : this . # emailRenderer . getSubject ( post ) ,
2022-11-23 13:33:44 +03:00
from : this . # emailRenderer . getFromAddress ( post , newsletter ) ,
replyTo : this . # emailRenderer . getReplyToAddress ( post , newsletter ) ,
2023-01-04 13:22:12 +03:00
email _count : emailCount ,
2022-11-29 13:27:17 +03:00
source : post . get ( 'lexical' ) || post . get ( 'mobiledoc' ) ,
source _type : post . get ( 'lexical' ) ? 'lexical' : 'mobiledoc'
2022-11-23 13:33:44 +03:00
} ) ;
try {
this . # batchSendingService . scheduleEmail ( email ) ;
} catch ( e ) {
await email . save ( {
status : 'failed' ,
error : e . message || 'Something went wrong while scheduling the email'
} , { patch : true } ) ;
}
2023-01-26 19:32:34 +03:00
// make sure recurring background analytics jobs are running once we have emails
try {
await this . # emailAnalyticsJobs . scheduleRecurringJobs ( true ) ;
} catch ( e ) {
logging . error ( e ) ;
}
2022-11-23 13:33:44 +03:00
return email ;
2022-11-21 12:29:53 +03:00
}
async retryEmail ( email ) {
2022-11-23 13:33:44 +03:00
await this . checkLimits ( ) ;
this . # batchSendingService . scheduleEmail ( email ) ;
return email ;
2022-11-21 12:29:53 +03:00
}
2022-11-30 15:56:28 +03:00
/ * *
* @ private
* @ param { string } [ email ] ( optional ) Search for a member with this email address and use it as the example . If not found , defaults to the default but still uses the provided email address .
* @ return { Promise < import ( './email-renderer' ) . MemberLike > }
* /
async getExampleMember ( email ) {
/ * *
* @ type { import ( './email-renderer' ) . MemberLike }
* /
const exampleMember = {
id : 'example-id' ,
uuid : 'example-uuid' ,
email : 'jamie@example.com' ,
name : 'Jamie Larson'
} ;
// fetch any matching members so that replacements use expected values
if ( email ) {
const member = await this . # membersRepository . get ( { email } ) ;
if ( member ) {
exampleMember . id = member . id ;
exampleMember . uuid = member . get ( 'uuid' ) ;
exampleMember . email = member . get ( 'email' ) ;
exampleMember . name = member . get ( 'name' ) ;
} else {
exampleMember . name = '' ; // Force empty name to simulate name fallbacks
exampleMember . email = email ;
}
}
return exampleMember ;
}
/ * *
2022-12-14 13:17:45 +03:00
*
* @ param { * } post
* @ param { * } newsletter
* @ param { import ( './email-renderer' ) . Segment } segment
2022-11-30 15:56:28 +03:00
* @ returns { Promise < { subject : string , html : string , plaintext : string } > } Email preview
* /
2022-11-21 12:29:53 +03:00
async previewEmail ( post , newsletter , segment ) {
2022-11-30 15:56:28 +03:00
const exampleMember = await this . getExampleMember ( ) ;
const subject = this . # emailRenderer . getSubject ( post ) ;
let { html , plaintext , replacements } = await this . # emailRenderer . renderBody ( post , newsletter , segment , { clickTrackingEnabled : false } ) ;
// Do manual replacements with an example member
for ( const replacement of replacements ) {
html = html . replace ( replacement . token , replacement . getValue ( exampleMember ) ) ;
plaintext = plaintext . replace ( replacement . token , replacement . getValue ( exampleMember ) ) ;
}
return {
subject ,
html ,
plaintext
} ;
2022-11-21 12:29:53 +03:00
}
2022-11-30 15:56:28 +03:00
/ * *
2022-12-14 13:17:45 +03:00
*
* @ param { * } post
* @ param { * } newsletter
* @ param { import ( './email-renderer' ) . Segment } segment
2022-11-30 15:56:28 +03:00
* @ param { string [ ] } emails
* /
2022-11-21 12:29:53 +03:00
async sendTestEmail ( post , newsletter , segment , emails ) {
2022-11-30 15:56:28 +03:00
const members = [ ] ;
for ( const email of emails ) {
members . push ( await this . getExampleMember ( email ) ) ;
}
await this . # sendingService . send ( {
post ,
newsletter ,
segment ,
members ,
emailId : null
} , {
clickTrackingEnabled : false ,
openTrackingEnabled : false
} ) ;
2022-11-21 12:29:53 +03:00
}
}
module . exports = EmailService ;