]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/emailer.ts
Begin auth plugin support
[github/Chocobozzz/PeerTube.git] / server / lib / emailer.ts
CommitLineData
ecb4e35f
C
1import { createTransport, Transporter } from 'nodemailer'
2import { isTestInstance } from '../helpers/core-utils'
05e67d62 3import { bunyanLogger, logger } from '../helpers/logger'
4c1c1709 4import { CONFIG, isEmailEnabled } from '../initializers/config'
ecb4e35f
C
5import { JobQueue } from './job-queue'
6import { EmailPayload } from './job-queue/handlers/email'
c9d5c64f 7import { readFileSync } from 'fs-extra'
6dd9de95 8import { WEBSERVER } from '../initializers/constants'
8424c402
C
9import {
10 MCommentOwnerVideo,
11 MVideo,
12 MVideoAbuseVideo,
13 MVideoAccountLight,
14 MVideoBlacklistLightVideo,
15 MVideoBlacklistVideo
16} from '../typings/models/video'
17import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
453e83ea 18import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
ecb4e35f 19
dee77e76
C
20type SendEmailOptions = {
21 to: string[]
22 subject: string
23 text: string
24
25 fromDisplayName?: string
26 replyTo?: string
27}
28
ecb4e35f
C
29class Emailer {
30
31 private static instance: Emailer
32 private initialized = false
33 private transporter: Transporter
34
a1587156
C
35 private constructor () {
36 }
ecb4e35f
C
37
38 init () {
39 // Already initialized
40 if (this.initialized === true) return
41 this.initialized = true
42
887e1a03 43 if (isEmailEnabled()) {
ed3f089c
IB
44 if (CONFIG.SMTP.TRANSPORT === 'smtp') {
45 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
46
47 let tls
48 if (CONFIG.SMTP.CA_FILE) {
49 tls = {
50 ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
51 }
ecb4e35f 52 }
ecb4e35f 53
ed3f089c
IB
54 let auth
55 if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) {
56 auth = {
57 user: CONFIG.SMTP.USERNAME,
58 pass: CONFIG.SMTP.PASSWORD
59 }
f076daa7 60 }
f076daa7 61
ed3f089c
IB
62 this.transporter = createTransport({
63 host: CONFIG.SMTP.HOSTNAME,
64 port: CONFIG.SMTP.PORT,
65 secure: CONFIG.SMTP.TLS,
66 debug: CONFIG.LOG.LEVEL === 'debug',
67 logger: bunyanLogger as any,
68 ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS,
69 tls,
70 auth
71 })
72 } else { // sendmail
73 logger.info('Using sendmail to send emails')
74
75 this.transporter = createTransport({
76 sendmail: true,
77 newline: 'unix',
9afa0901 78 path: CONFIG.SMTP.SENDMAIL
ed3f089c
IB
79 })
80 }
ecb4e35f
C
81 } else {
82 if (!isTestInstance()) {
83 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
84 }
85 }
86 }
87
d3e56c0c 88 static isEnabled () {
ed3f089c
IB
89 if (CONFIG.SMTP.TRANSPORT === 'sendmail') {
90 return !!CONFIG.SMTP.SENDMAIL
91 } else if (CONFIG.SMTP.TRANSPORT === 'smtp') {
92 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
93 } else {
94 return false
95 }
3b3b1820
C
96 }
97
ecb4e35f 98 async checkConnectionOrDie () {
ed3f089c 99 if (!this.transporter || CONFIG.SMTP.TRANSPORT !== 'smtp') return
ecb4e35f 100
3d3441d6
C
101 logger.info('Testing SMTP server...')
102
ecb4e35f
C
103 try {
104 const success = await this.transporter.verify()
105 if (success !== true) this.dieOnConnectionFailure()
106
107 logger.info('Successfully connected to SMTP server.')
108 } catch (err) {
109 this.dieOnConnectionFailure(err)
110 }
111 }
112
453e83ea 113 addNewVideoFromSubscriberNotification (to: string[], video: MVideoAccountLight) {
cef534ed 114 const channelName = video.VideoChannel.getDisplayName()
6dd9de95 115 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
cef534ed 116
a1587156 117 const text = 'Hi dear user,\n\n' +
cef534ed 118 `Your subscription ${channelName} just published a new video: ${video.name}` +
a1587156 119 '\n\n' +
cef534ed 120 `You can view it on ${videoUrl} ` +
a1587156
C
121 '\n\n' +
122 'Cheers,\n' +
b5bfadf0 123 `${CONFIG.EMAIL.BODY.SIGNATURE}`
ecb4e35f
C
124
125 const emailPayload: EmailPayload = {
cef534ed 126 to,
916937d7 127 subject: CONFIG.EMAIL.SUBJECT.PREFIX + channelName + ' just published a new video',
ecb4e35f
C
128 text
129 }
130
131 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
132 }
133
8424c402 134 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
f7cc67b4
C
135 const followerName = actorFollow.ActorFollower.Account.getDisplayName()
136 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
137
a1587156 138 const text = 'Hi dear user,\n\n' +
f7cc67b4 139 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` +
a1587156
C
140 '\n\n' +
141 'Cheers,\n' +
b5bfadf0 142 `${CONFIG.EMAIL.BODY.SIGNATURE}`
f7cc67b4
C
143
144 const emailPayload: EmailPayload = {
145 to,
916937d7 146 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New follower on your channel ' + followingName,
f7cc67b4
C
147 text
148 }
149
150 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
151 }
152
453e83ea 153 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
883993c8
C
154 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
155
a1587156 156 const text = 'Hi dear admin,\n\n' +
883993c8 157 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
a1587156
C
158 '\n\n' +
159 'Cheers,\n' +
b5bfadf0 160 `${CONFIG.EMAIL.BODY.SIGNATURE}`
883993c8
C
161
162 const emailPayload: EmailPayload = {
163 to,
916937d7 164 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New instance follower',
883993c8
C
165 text
166 }
167
168 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
169 }
170
8424c402 171 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
a1587156 172 const text = 'Hi dear admin,\n\n' +
8424c402 173 `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` +
a1587156
C
174 '\n\n' +
175 'Cheers,\n' +
8424c402
C
176 `${CONFIG.EMAIL.BODY.SIGNATURE}`
177
178 const emailPayload: EmailPayload = {
179 to,
180 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following',
181 text
182 }
183
184 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
185 }
186
453e83ea 187 myVideoPublishedNotification (to: string[], video: MVideo) {
6dd9de95 188 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
dc133480 189
a1587156 190 const text = 'Hi dear user,\n\n' +
dc133480 191 `Your video ${video.name} has been published.` +
a1587156 192 '\n\n' +
dc133480 193 `You can view it on ${videoUrl} ` +
a1587156
C
194 '\n\n' +
195 'Cheers,\n' +
b5bfadf0 196 `${CONFIG.EMAIL.BODY.SIGNATURE}`
dc133480
C
197
198 const emailPayload: EmailPayload = {
199 to,
916937d7 200 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video ${video.name} is published`,
dc133480
C
201 text
202 }
203
204 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
205 }
206
453e83ea 207 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
6dd9de95 208 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
dc133480 209
a1587156 210 const text = 'Hi dear user,\n\n' +
dc133480 211 `Your video import ${videoImport.getTargetIdentifier()} is finished.` +
a1587156 212 '\n\n' +
dc133480 213 `You can view the imported video on ${videoUrl} ` +
a1587156
C
214 '\n\n' +
215 'Cheers,\n' +
b5bfadf0 216 `${CONFIG.EMAIL.BODY.SIGNATURE}`
dc133480
C
217
218 const emailPayload: EmailPayload = {
219 to,
916937d7 220 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`,
dc133480
C
221 text
222 }
223
224 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
225 }
226
453e83ea 227 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
6dd9de95 228 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
dc133480 229
a1587156 230 const text = 'Hi dear user,\n\n' +
dc133480 231 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` +
a1587156 232 '\n\n' +
dc133480 233 `See your videos import dashboard for more information: ${importUrl}` +
a1587156
C
234 '\n\n' +
235 'Cheers,\n' +
b5bfadf0 236 `${CONFIG.EMAIL.BODY.SIGNATURE}`
dc133480
C
237
238 const emailPayload: EmailPayload = {
239 to,
916937d7 240 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`,
dc133480
C
241 text
242 }
243
244 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
245 }
246
453e83ea 247 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
cef534ed
C
248 const accountName = comment.Account.getDisplayName()
249 const video = comment.Video
6dd9de95 250 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
cef534ed 251
a1587156 252 const text = 'Hi dear user,\n\n' +
cef534ed 253 `A new comment has been posted by ${accountName} on your video ${video.name}` +
a1587156 254 '\n\n' +
cef534ed 255 `You can view it on ${commentUrl} ` +
a1587156
C
256 '\n\n' +
257 'Cheers,\n' +
b5bfadf0 258 `${CONFIG.EMAIL.BODY.SIGNATURE}`
d9eaee39
JM
259
260 const emailPayload: EmailPayload = {
cef534ed 261 to,
916937d7 262 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New comment on your video ' + video.name,
d9eaee39
JM
263 text
264 }
265
266 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
267 }
268
453e83ea 269 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
f7cc67b4
C
270 const accountName = comment.Account.getDisplayName()
271 const video = comment.Video
6dd9de95 272 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
f7cc67b4 273
a1587156 274 const text = 'Hi dear user,\n\n' +
f7cc67b4 275 `${accountName} mentioned you on video ${video.name}` +
a1587156 276 '\n\n' +
f7cc67b4 277 `You can view the comment on ${commentUrl} ` +
a1587156
C
278 '\n\n' +
279 'Cheers,\n' +
b5bfadf0 280 `${CONFIG.EMAIL.BODY.SIGNATURE}`
f7cc67b4
C
281
282 const emailPayload: EmailPayload = {
283 to,
916937d7 284 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Mention on video ' + video.name,
f7cc67b4
C
285 text
286 }
287
288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
289 }
290
453e83ea 291 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) {
6dd9de95 292 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
ba75d268 293
a1587156 294 const text = 'Hi,\n\n' +
86521a67 295 `${WEBSERVER.HOST} received an abuse for the following video: ${videoUrl}\n\n` +
a1587156 296 'Cheers,\n' +
b5bfadf0 297 `${CONFIG.EMAIL.BODY.SIGNATURE}`
ba75d268 298
ba75d268
C
299 const emailPayload: EmailPayload = {
300 to,
916937d7 301 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Received a video abuse',
ba75d268
C
302 text
303 }
304
305 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
306 }
307
8424c402 308 addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
6dd9de95 309 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
8424c402 310 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
7ccddd7b 311
a1587156
C
312 const text = 'Hi,\n\n' +
313 'A recently added video was auto-blacklisted and requires moderator review before publishing.' +
314 '\n\n' +
7ccddd7b 315 `You can view it and take appropriate action on ${videoUrl}` +
a1587156 316 '\n\n' +
7ccddd7b 317 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
a1587156
C
318 '\n\n' +
319 'Cheers,\n' +
b5bfadf0 320 `${CONFIG.EMAIL.BODY.SIGNATURE}`
7ccddd7b
JM
321
322 const emailPayload: EmailPayload = {
323 to,
916937d7 324 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'An auto-blacklisted video is awaiting review',
7ccddd7b
JM
325 text
326 }
327
328 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
329 }
330
453e83ea 331 addNewUserRegistrationNotification (to: string[], user: MUser) {
a1587156 332 const text = 'Hi,\n\n' +
6dd9de95 333 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
a1587156 334 'Cheers,\n' +
b5bfadf0 335 `${CONFIG.EMAIL.BODY.SIGNATURE}`
f7cc67b4
C
336
337 const emailPayload: EmailPayload = {
338 to,
916937d7 339 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST,
f7cc67b4
C
340 text
341 }
342
343 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
344 }
345
453e83ea 346 addVideoBlacklistNotification (to: string[], videoBlacklist: MVideoBlacklistVideo) {
cef534ed 347 const videoName = videoBlacklist.Video.name
6dd9de95 348 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
26b7305a 349
cef534ed 350 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
6dd9de95 351 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.`
26b7305a
C
352
353 const text = 'Hi,\n\n' +
354 blockedString +
355 '\n\n' +
356 'Cheers,\n' +
b5bfadf0 357 `${CONFIG.EMAIL.BODY.SIGNATURE}`
26b7305a 358
26b7305a 359 const emailPayload: EmailPayload = {
cef534ed 360 to,
916937d7 361 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${videoName} blacklisted`,
26b7305a
C
362 text
363 }
364
365 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
366 }
367
453e83ea 368 addVideoUnblacklistNotification (to: string[], video: MVideo) {
6dd9de95 369 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
26b7305a
C
370
371 const text = 'Hi,\n\n' +
6dd9de95 372 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
26b7305a
C
373 '\n\n' +
374 'Cheers,\n' +
b5bfadf0 375 `${CONFIG.EMAIL.BODY.SIGNATURE}`
26b7305a 376
26b7305a 377 const emailPayload: EmailPayload = {
cef534ed 378 to,
916937d7 379 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${video.name} unblacklisted`,
26b7305a
C
380 text
381 }
382
383 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
384 }
385
b426edd4 386 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
a1587156 387 const text = 'Hi dear user,\n\n' +
6dd9de95 388 `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` +
f88ee4a9 389 `Please follow this link to reset it: ${resetPasswordUrl} (the link will expire within 1 hour)\n\n` +
a1587156
C
390 'If you are not the person who initiated this request, please ignore this email.\n\n' +
391 'Cheers,\n' +
b5bfadf0 392 `${CONFIG.EMAIL.BODY.SIGNATURE}`
cef534ed
C
393
394 const emailPayload: EmailPayload = {
395 to: [ to ],
916937d7 396 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Reset your password',
cef534ed
C
397 text
398 }
399
400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
401 }
402
45f1bd72
JL
403 addPasswordCreateEmailJob (username: string, to: string, resetPasswordUrl: string) {
404 const text = 'Hi,\n\n' +
405 `Welcome to your ${WEBSERVER.HOST} PeerTube instance. Your username is: ${username}.\n\n` +
406 `Please set your password by following this link: ${resetPasswordUrl} (this link will expire within seven days).\n\n` +
407 'Cheers,\n' +
408 `${CONFIG.EMAIL.BODY.SIGNATURE}`
409
410 const emailPayload: EmailPayload = {
411 to: [ to ],
412 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New PeerTube account password',
413 text
414 }
415
416 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
417 }
418
cef534ed 419 addVerifyEmailJob (to: string, verifyEmailUrl: string) {
a1587156 420 const text = 'Welcome to PeerTube,\n\n' +
6dd9de95 421 `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` +
cef534ed 422 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
a1587156
C
423 'If you are not the person who initiated this request, please ignore this email.\n\n' +
424 'Cheers,\n' +
b5bfadf0 425 `${CONFIG.EMAIL.BODY.SIGNATURE}`
cef534ed
C
426
427 const emailPayload: EmailPayload = {
428 to: [ to ],
916937d7 429 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Verify your email',
cef534ed
C
430 text
431 }
432
433 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
434 }
435
453e83ea 436 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
eacb25c4
C
437 const reasonString = reason ? ` for the following reason: ${reason}` : ''
438 const blockedWord = blocked ? 'blocked' : 'unblocked'
6dd9de95 439 const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
eacb25c4
C
440
441 const text = 'Hi,\n\n' +
442 blockedString +
443 '\n\n' +
444 'Cheers,\n' +
b5bfadf0 445 `${CONFIG.EMAIL.BODY.SIGNATURE}`
eacb25c4
C
446
447 const to = user.email
448 const emailPayload: EmailPayload = {
449 to: [ to ],
916937d7 450 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Account ' + blockedWord,
eacb25c4
C
451 text
452 }
453
454 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
455 }
456
4e9fa5b7 457 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) {
a4101923
C
458 const text = 'Hello dear admin,\n\n' +
459 fromName + ' sent you a message' +
460 '\n\n---------------------------------------\n\n' +
461 body +
462 '\n\n---------------------------------------\n\n' +
463 'Cheers,\n' +
464 'PeerTube.'
465
466 const emailPayload: EmailPayload = {
4759fedc
C
467 fromDisplayName: fromEmail,
468 replyTo: fromEmail,
a4101923 469 to: [ CONFIG.ADMIN.EMAIL ],
916937d7 470 subject: CONFIG.EMAIL.SUBJECT.PREFIX + subject,
a4101923
C
471 text
472 }
473
474 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
475 }
476
47f6cb31 477 async sendMail (options: EmailPayload) {
4c1c1709 478 if (!isEmailEnabled()) {
ecb4e35f
C
479 throw new Error('Cannot send mail because SMTP is not configured.')
480 }
481
4759fedc
C
482 const fromDisplayName = options.fromDisplayName
483 ? options.fromDisplayName
6dd9de95 484 : WEBSERVER.HOST
4759fedc 485
47f6cb31
C
486 for (const to of options.to) {
487 await this.transporter.sendMail({
488 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`,
489 replyTo: options.replyTo,
490 to,
491 subject: options.subject,
492 text: options.text
493 })
494 }
ecb4e35f
C
495 }
496
497 private dieOnConnectionFailure (err?: Error) {
d5b7d911 498 logger.error('Failed to connect to SMTP %s:%d.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT, { err })
ecb4e35f
C
499 process.exit(-1)
500 }
501
502 static get Instance () {
503 return this.instance || (this.instance = new this())
504 }
505}
506
507// ---------------------------------------------------------------------------
508
509export {
dee77e76
C
510 Emailer,
511 SendEmailOptions
ecb4e35f 512}