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