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