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