]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/emailer.ts
do not crash if SMTP server is down
[github/Chocobozzz/PeerTube.git] / server / lib / emailer.ts
CommitLineData
d95d1559
C
1import { readFileSync } from 'fs-extra'
2import { merge } from 'lodash'
ecb4e35f 3import { createTransport, Transporter } from 'nodemailer'
d95d1559
C
4import { join } from 'path'
5import { VideoChannelModel } from '@server/models/video/video-channel'
6import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
9ff36c2d 8import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
594d3e48 9import { AbuseState, EmailPayload, UserAbuse } from '@shared/models'
d95d1559 10import { SendEmailOptions } from '../../shared/models/server/emailer.model'
df4c603d 11import { isTestInstance, root } from '../helpers/core-utils'
05e67d62 12import { bunyanLogger, logger } from '../helpers/logger'
4c1c1709 13import { CONFIG, isEmailEnabled } from '../initializers/config'
6dd9de95 14import { WEBSERVER } from '../initializers/constants'
d573926e 15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
d95d1559
C
16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
17import { JobQueue } from './job-queue'
18
98b94643
K
19const sanitizeHtml = require('sanitize-html')
20const markdownItEmoji = require('markdown-it-emoji/light')
21const MarkdownItClass = require('markdown-it')
22const markdownIt = new MarkdownItClass('default', { linkify: true, breaks: true, html: true })
23
9ff36c2d 24markdownIt.enable(TEXT_WITH_HTML_RULES)
98b94643
K
25
26markdownIt.use(markdownItEmoji)
27
28const toSafeHtml = text => {
29 // Restore line feed
30 const textWithLineFeed = text.replace(/<br.?\/?>/g, '\r\n')
31
32 // Convert possible markdown (emojis, emphasis and lists) to html
33 const html = markdownIt.render(textWithLineFeed)
34
35 // Convert to safe Html
9ff36c2d 36 return sanitizeHtml(html, SANITIZE_OPTIONS)
98b94643
K
37}
38
df4c603d 39const Email = require('email-templates')
dee77e76 40
ecb4e35f
C
41class Emailer {
42
43 private static instance: Emailer
44 private initialized = false
45 private transporter: Transporter
46
a1587156
C
47 private constructor () {
48 }
ecb4e35f
C
49
50 init () {
51 // Already initialized
52 if (this.initialized === true) return
53 this.initialized = true
54
887e1a03 55 if (isEmailEnabled()) {
ed3f089c
IB
56 if (CONFIG.SMTP.TRANSPORT === 'smtp') {
57 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
58
59 let tls
60 if (CONFIG.SMTP.CA_FILE) {
61 tls = {
62 ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
63 }
ecb4e35f 64 }
ecb4e35f 65
ed3f089c
IB
66 let auth
67 if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) {
68 auth = {
69 user: CONFIG.SMTP.USERNAME,
70 pass: CONFIG.SMTP.PASSWORD
71 }
f076daa7 72 }
f076daa7 73
ed3f089c
IB
74 this.transporter = createTransport({
75 host: CONFIG.SMTP.HOSTNAME,
76 port: CONFIG.SMTP.PORT,
77 secure: CONFIG.SMTP.TLS,
78 debug: CONFIG.LOG.LEVEL === 'debug',
79 logger: bunyanLogger as any,
80 ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS,
81 tls,
82 auth
83 })
84 } else { // sendmail
85 logger.info('Using sendmail to send emails')
86
87 this.transporter = createTransport({
88 sendmail: true,
89 newline: 'unix',
9afa0901 90 path: CONFIG.SMTP.SENDMAIL
ed3f089c
IB
91 })
92 }
ecb4e35f
C
93 } else {
94 if (!isTestInstance()) {
95 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
96 }
97 }
98 }
99
d3e56c0c 100 static isEnabled () {
ed3f089c
IB
101 if (CONFIG.SMTP.TRANSPORT === 'sendmail') {
102 return !!CONFIG.SMTP.SENDMAIL
103 } else if (CONFIG.SMTP.TRANSPORT === 'smtp') {
104 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
105 } else {
106 return false
107 }
3b3b1820
C
108 }
109
75594f47 110 async checkConnection () {
ed3f089c 111 if (!this.transporter || CONFIG.SMTP.TRANSPORT !== 'smtp') return
ecb4e35f 112
3d3441d6
C
113 logger.info('Testing SMTP server...')
114
ecb4e35f
C
115 try {
116 const success = await this.transporter.verify()
75594f47 117 if (success !== true) this.warnOnConnectionFailure()
ecb4e35f
C
118
119 logger.info('Successfully connected to SMTP server.')
120 } catch (err) {
75594f47 121 this.warnOnConnectionFailure(err)
ecb4e35f
C
122 }
123 }
124
453e83ea 125 addNewVideoFromSubscriberNotification (to: string[], video: MVideoAccountLight) {
cef534ed 126 const channelName = video.VideoChannel.getDisplayName()
6dd9de95 127 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
cef534ed 128
ecb4e35f 129 const emailPayload: EmailPayload = {
cef534ed 130 to,
df4c603d
RK
131 subject: channelName + ' just published a new video',
132 text: `Your subscription ${channelName} just published a new video: "${video.name}".`,
133 locals: {
134 title: 'New content ',
135 action: {
136 text: 'View video',
137 url: videoUrl
138 }
139 }
ecb4e35f
C
140 }
141
142 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
143 }
144
8424c402 145 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
f7cc67b4
C
146 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
147
f7cc67b4 148 const emailPayload: EmailPayload = {
df4c603d 149 template: 'follower-on-channel',
f7cc67b4 150 to,
df4c603d
RK
151 subject: `New follower on your channel ${followingName}`,
152 locals: {
153 followerName: actorFollow.ActorFollower.Account.getDisplayName(),
154 followerUrl: actorFollow.ActorFollower.url,
155 followingName,
156 followingUrl: actorFollow.ActorFollowing.url,
157 followType
158 }
f7cc67b4
C
159 }
160
161 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
162 }
163
453e83ea 164 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
883993c8
C
165 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
166
883993c8
C
167 const emailPayload: EmailPayload = {
168 to,
df4c603d
RK
169 subject: 'New instance follower',
170 text: `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}.`,
171 locals: {
172 title: 'New instance follower',
173 action: {
174 text: 'Review followers',
175 url: WEBSERVER.URL + '/admin/follows/followers-list'
176 }
177 }
883993c8
C
178 }
179
180 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
181 }
182
8424c402 183 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
df4c603d 184 const instanceUrl = actorFollow.ActorFollowing.url
8424c402
C
185 const emailPayload: EmailPayload = {
186 to,
df4c603d
RK
187 subject: 'Auto instance following',
188 text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.`
8424c402
C
189 }
190
191 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
192 }
193
453e83ea 194 myVideoPublishedNotification (to: string[], video: MVideo) {
6dd9de95 195 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
dc133480 196
dc133480
C
197 const emailPayload: EmailPayload = {
198 to,
df4c603d
RK
199 subject: `Your video ${video.name} has been published`,
200 text: `Your video "${video.name}" has been published.`,
201 locals: {
202 title: 'You video is live',
203 action: {
204 text: 'View video',
205 url: videoUrl
206 }
207 }
dc133480
C
208 }
209
210 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
211 }
212
453e83ea 213 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
6dd9de95 214 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
dc133480 215
dc133480
C
216 const emailPayload: EmailPayload = {
217 to,
df4c603d
RK
218 subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`,
219 text: `Your video "${videoImport.getTargetIdentifier()}" just finished importing.`,
220 locals: {
221 title: 'Import complete',
222 action: {
223 text: 'View video',
224 url: videoUrl
225 }
226 }
dc133480
C
227 }
228
229 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
230 }
231
453e83ea 232 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
17119e4a 233 const importUrl = WEBSERVER.URL + '/my-library/video-imports'
dc133480 234
df4c603d
RK
235 const text =
236 `Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` +
a1587156 237 '\n\n' +
df4c603d 238 `See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.`
dc133480
C
239
240 const emailPayload: EmailPayload = {
241 to,
df4c603d
RK
242 subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`,
243 text,
244 locals: {
245 title: 'Import failed',
246 action: {
247 text: 'Review imports',
248 url: importUrl
249 }
250 }
dc133480
C
251 }
252
253 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
254 }
255
453e83ea 256 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
cef534ed 257 const video = comment.Video
df4c603d 258 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
6dd9de95 259 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
98b94643 260 const commentHtml = toSafeHtml(comment.text)
cef534ed 261
d9eaee39 262 const emailPayload: EmailPayload = {
df4c603d 263 template: 'video-comment-new',
cef534ed 264 to,
df4c603d
RK
265 subject: 'New comment on your video ' + video.name,
266 locals: {
267 accountName: comment.Account.getDisplayName(),
268 accountUrl: comment.Account.Actor.url,
269 comment,
98b94643 270 commentHtml,
df4c603d
RK
271 video,
272 videoUrl,
273 action: {
274 text: 'View comment',
275 url: commentUrl
276 }
277 }
d9eaee39
JM
278 }
279
280 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
281 }
282
453e83ea 283 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
f7cc67b4
C
284 const accountName = comment.Account.getDisplayName()
285 const video = comment.Video
df4c603d 286 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
6dd9de95 287 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
98b94643 288 const commentHtml = toSafeHtml(comment.text)
f7cc67b4 289
f7cc67b4 290 const emailPayload: EmailPayload = {
df4c603d 291 template: 'video-comment-mention',
f7cc67b4 292 to,
df4c603d
RK
293 subject: 'Mention on video ' + video.name,
294 locals: {
295 comment,
98b94643 296 commentHtml,
df4c603d
RK
297 video,
298 videoUrl,
299 accountName,
300 action: {
301 text: 'View comment',
302 url: commentUrl
303 }
304 }
f7cc67b4
C
305 }
306
307 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
308 }
309
d95d1559 310 addAbuseModeratorsNotification (to: string[], parameters: {
edbc9325 311 abuse: UserAbuse
d95d1559 312 abuseInstance: MAbuseFull
df4c603d
RK
313 reporter: string
314 }) {
d95d1559 315 const { abuse, abuseInstance, reporter } = parameters
ba75d268 316
d95d1559
C
317 const action = {
318 text: 'View report #' + abuse.id,
319 url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
320 }
321
322 let emailPayload: EmailPayload
323
324 if (abuseInstance.VideoAbuse) {
325 const video = abuseInstance.VideoAbuse.Video
326 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
327
328 emailPayload = {
329 template: 'video-abuse-new',
330 to,
331 subject: `New video abuse report from ${reporter}`,
332 locals: {
333 videoUrl,
334 isLocal: video.remote === false,
335 videoCreatedAt: new Date(video.createdAt).toLocaleString(),
336 videoPublishedAt: new Date(video.publishedAt).toLocaleString(),
337 videoName: video.name,
338 reason: abuse.reason,
cfde28ba
C
339 videoChannel: abuse.video.channel,
340 reporter,
d95d1559
C
341 action
342 }
343 }
344 } else if (abuseInstance.VideoCommentAbuse) {
345 const comment = abuseInstance.VideoCommentAbuse.VideoComment
346 const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
347
348 emailPayload = {
4f32032f 349 template: 'video-comment-abuse-new',
d95d1559
C
350 to,
351 subject: `New comment abuse report from ${reporter}`,
352 locals: {
353 commentUrl,
310b5219 354 videoName: comment.Video.name,
d95d1559
C
355 isLocal: comment.isOwned(),
356 commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
357 reason: abuse.reason,
358 flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(),
cfde28ba 359 reporter,
d95d1559
C
360 action
361 }
362 }
363 } else {
364 const account = abuseInstance.FlaggedAccount
365 const accountUrl = account.getClientUrl()
366
367 emailPayload = {
368 template: 'account-abuse-new',
369 to,
370 subject: `New account abuse report from ${reporter}`,
371 locals: {
372 accountUrl,
373 accountDisplayName: account.getDisplayName(),
374 isLocal: account.isOwned(),
375 reason: abuse.reason,
cfde28ba 376 reporter,
d95d1559 377 action
df4c603d
RK
378 }
379 }
ba75d268
C
380 }
381
382 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
383 }
384
594d3e48
C
385 addAbuseStateChangeNotification (to: string[], abuse: MAbuseFull) {
386 const text = abuse.state === AbuseState.ACCEPTED
387 ? 'Report #' + abuse.id + ' has been accepted'
388 : 'Report #' + abuse.id + ' has been rejected'
389
d573926e
C
390 const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
391
594d3e48
C
392 const action = {
393 text,
d573926e 394 url: abuseUrl
594d3e48
C
395 }
396
397 const emailPayload: EmailPayload = {
398 template: 'abuse-state-change',
399 to,
400 subject: text,
401 locals: {
402 action,
403 abuseId: abuse.id,
d573926e 404 abuseUrl,
594d3e48
C
405 isAccepted: abuse.state === AbuseState.ACCEPTED
406 }
407 }
408
409 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
410 }
411
d573926e
C
412 addAbuseNewMessageNotification (
413 to: string[],
414 options: {
415 target: 'moderator' | 'reporter'
416 abuse: MAbuseFull
417 message: MAbuseMessage
418 accountMessage: MAccountDefault
419 }) {
420 const { abuse, target, message, accountMessage } = options
421
422 const text = 'New message on report #' + abuse.id
423 const abuseUrl = target === 'moderator'
424 ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
425 : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
594d3e48 426
594d3e48
C
427 const action = {
428 text,
d573926e 429 url: abuseUrl
594d3e48
C
430 }
431
432 const emailPayload: EmailPayload = {
433 template: 'abuse-new-message',
434 to,
435 subject: text,
436 locals: {
d573926e 437 abuseId: abuse.id,
594d3e48 438 abuseUrl: action.url,
d573926e 439 messageAccountName: accountMessage.getDisplayName(),
594d3e48
C
440 messageText: message.message,
441 action
442 }
443 }
444
445 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
446 }
447
df4c603d 448 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
6dd9de95 449 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
8424c402 450 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
df4c603d 451 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
7ccddd7b
JM
452
453 const emailPayload: EmailPayload = {
df4c603d 454 template: 'video-auto-blacklist-new',
7ccddd7b 455 to,
df4c603d
RK
456 subject: 'A new video is pending moderation',
457 locals: {
458 channel,
459 videoUrl,
460 videoName: videoBlacklist.Video.name,
461 action: {
462 text: 'Review autoblacklist',
463 url: VIDEO_AUTO_BLACKLIST_URL
464 }
465 }
7ccddd7b
JM
466 }
467
468 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
469 }
470
453e83ea 471 addNewUserRegistrationNotification (to: string[], user: MUser) {
f7cc67b4 472 const emailPayload: EmailPayload = {
df4c603d 473 template: 'user-registered',
f7cc67b4 474 to,
df4c603d
RK
475 subject: `a new user registered on ${WEBSERVER.HOST}: ${user.username}`,
476 locals: {
477 user
478 }
f7cc67b4
C
479 }
480
481 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
482 }
483
453e83ea 484 addVideoBlacklistNotification (to: string[], videoBlacklist: MVideoBlacklistVideo) {
cef534ed 485 const videoName = videoBlacklist.Video.name
6dd9de95 486 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
26b7305a 487
cef534ed 488 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
6dd9de95 489 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.`
26b7305a 490
26b7305a 491 const emailPayload: EmailPayload = {
cef534ed 492 to,
df4c603d
RK
493 subject: `Video ${videoName} blacklisted`,
494 text: blockedString,
495 locals: {
496 title: 'Your video was blacklisted'
497 }
26b7305a
C
498 }
499
500 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
501 }
502
453e83ea 503 addVideoUnblacklistNotification (to: string[], video: MVideo) {
6dd9de95 504 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
26b7305a 505
26b7305a 506 const emailPayload: EmailPayload = {
cef534ed 507 to,
df4c603d
RK
508 subject: `Video ${video.name} unblacklisted`,
509 text: `Your video "${video.name}" (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.`,
510 locals: {
511 title: 'Your video was unblacklisted'
512 }
26b7305a
C
513 }
514
515 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
516 }
517
963023ab 518 addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
cef534ed 519 const emailPayload: EmailPayload = {
df4c603d 520 template: 'password-reset',
cef534ed 521 to: [ to ],
df4c603d
RK
522 subject: 'Reset your account password',
523 locals: {
963023ab 524 username,
df4c603d
RK
525 resetPasswordUrl
526 }
cef534ed
C
527 }
528
529 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
530 }
531
df4c603d 532 addPasswordCreateEmailJob (username: string, to: string, createPasswordUrl: string) {
45f1bd72 533 const emailPayload: EmailPayload = {
df4c603d 534 template: 'password-create',
45f1bd72 535 to: [ to ],
df4c603d
RK
536 subject: 'Create your account password',
537 locals: {
538 username,
539 createPasswordUrl
540 }
45f1bd72
JL
541 }
542
543 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
544 }
545
963023ab 546 addVerifyEmailJob (username: string, to: string, verifyEmailUrl: string) {
cef534ed 547 const emailPayload: EmailPayload = {
df4c603d 548 template: 'verify-email',
cef534ed 549 to: [ to ],
df4c603d
RK
550 subject: `Verify your email on ${WEBSERVER.HOST}`,
551 locals: {
963023ab 552 username,
df4c603d
RK
553 verifyEmailUrl
554 }
cef534ed
C
555 }
556
557 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
558 }
559
453e83ea 560 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
eacb25c4
C
561 const reasonString = reason ? ` for the following reason: ${reason}` : ''
562 const blockedWord = blocked ? 'blocked' : 'unblocked'
eacb25c4
C
563
564 const to = user.email
565 const emailPayload: EmailPayload = {
566 to: [ to ],
df4c603d
RK
567 subject: 'Account ' + blockedWord,
568 text: `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
eacb25c4
C
569 }
570
571 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
572 }
573
4e9fa5b7 574 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) {
a4101923 575 const emailPayload: EmailPayload = {
df4c603d 576 template: 'contact-form',
a4101923 577 to: [ CONFIG.ADMIN.EMAIL ],
df4c603d
RK
578 replyTo: `"${fromName}" <${fromEmail}>`,
579 subject: `(contact form) ${subject}`,
580 locals: {
581 fromName,
582 fromEmail,
b9cf3fb6
C
583 body,
584
585 // There are not notification preferences for the contact form
586 hideNotificationPreferences: true
df4c603d 587 }
a4101923
C
588 }
589
590 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
591 }
592
47f6cb31 593 async sendMail (options: EmailPayload) {
4c1c1709 594 if (!isEmailEnabled()) {
ecb4e35f
C
595 throw new Error('Cannot send mail because SMTP is not configured.')
596 }
597
df4c603d
RK
598 const fromDisplayName = options.from
599 ? options.from
6dd9de95 600 : WEBSERVER.HOST
4759fedc 601
df4c603d
RK
602 const email = new Email({
603 send: true,
604 message: {
605 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`
606 },
607 transport: this.transporter,
608 views: {
03fc1928 609 root: join(root(), 'dist', 'server', 'lib', 'emails')
df4c603d
RK
610 },
611 subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX
612 })
613
47f6cb31 614 for (const to of options.to) {
df4c603d
RK
615 await email
616 .send(merge(
617 {
618 template: 'common',
619 message: {
620 to,
621 from: options.from,
622 subject: options.subject,
623 replyTo: options.replyTo
624 },
625 locals: { // default variables available in all templates
626 WEBSERVER,
627 EMAIL: CONFIG.EMAIL,
628 text: options.text,
629 subject: options.subject
630 }
631 },
632 options // overriden/new variables given for a specific template in the payload
633 ) as SendEmailOptions)
03fc1928
C
634 .then(res => logger.debug('Sent email.', { res }))
635 .catch(err => logger.error('Error in email sender.', { err }))
47f6cb31 636 }
ecb4e35f
C
637 }
638
75594f47 639 private warnOnConnectionFailure (err?: Error) {
d5b7d911 640 logger.error('Failed to connect to SMTP %s:%d.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT, { err })
ecb4e35f
C
641 }
642
643 static get Instance () {
644 return this.instance || (this.instance = new this())
645 }
646}
647
648// ---------------------------------------------------------------------------
649
650export {
8dc8a34e 651 Emailer
ecb4e35f 652}