]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/emailer.ts
Merge branch 'release/v1.3.0' into develop
[github/Chocobozzz/PeerTube.git] / server / lib / emailer.ts
1 import { createTransport, Transporter } from 'nodemailer'
2 import { isTestInstance } from '../helpers/core-utils'
3 import { bunyanLogger, logger } from '../helpers/logger'
4 import { CONFIG } from '../initializers/config'
5 import { UserModel } from '../models/account/user'
6 import { VideoModel } from '../models/video/video'
7 import { JobQueue } from './job-queue'
8 import { EmailPayload } from './job-queue/handlers/email'
9 import { readFileSync } from 'fs-extra'
10 import { VideoCommentModel } from '../models/video/video-comment'
11 import { VideoAbuseModel } from '../models/video/video-abuse'
12 import { VideoBlacklistModel } from '../models/video/video-blacklist'
13 import { VideoImportModel } from '../models/video/video-import'
14 import { ActorFollowModel } from '../models/activitypub/actor-follow'
15 import { WEBSERVER } from '../initializers/constants'
16
17 type SendEmailOptions = {
18 to: string[]
19 subject: string
20 text: string
21
22 fromDisplayName?: string
23 replyTo?: string
24 }
25
26 class 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
39 if (Emailer.isEnabled()) {
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
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
57 this.transporter = createTransport({
58 host: CONFIG.SMTP.HOSTNAME,
59 port: CONFIG.SMTP.PORT,
60 secure: CONFIG.SMTP.TLS,
61 debug: CONFIG.LOG.LEVEL === 'debug',
62 logger: bunyanLogger as any,
63 ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS,
64 tls,
65 auth
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
74 static isEnabled () {
75 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
76 }
77
78 async checkConnectionOrDie () {
79 if (!this.transporter) return
80
81 logger.info('Testing SMTP server...')
82
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
93 addNewVideoFromSubscriberNotification (to: string[], video: VideoModel) {
94 const channelName = video.VideoChannel.getDisplayName()
95 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
96
97 const text = `Hi dear user,\n\n` +
98 `Your subscription ${channelName} just published a new video: ${video.name}` +
99 `\n\n` +
100 `You can view it on ${videoUrl} ` +
101 `\n\n` +
102 `Cheers,\n` +
103 `${CONFIG.EMAIL.BODY.SIGNATURE}`
104
105 const emailPayload: EmailPayload = {
106 to,
107 subject: CONFIG.EMAIL.OBJECT.PREFIX + channelName + ' just published a new video',
108 text
109 }
110
111 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
112 }
113
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` +
122 `${CONFIG.EMAIL.BODY.SIGNATURE}`
123
124 const emailPayload: EmailPayload = {
125 to,
126 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New follower on your channel ' + followingName,
127 text
128 }
129
130 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
131 }
132
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` +
140 `${CONFIG.EMAIL.BODY.SIGNATURE}`
141
142 const emailPayload: EmailPayload = {
143 to,
144 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New instance follower',
145 text
146 }
147
148 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
149 }
150
151 myVideoPublishedNotification (to: string[], video: VideoModel) {
152 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
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` +
160 `${CONFIG.EMAIL.BODY.SIGNATURE}`
161
162 const emailPayload: EmailPayload = {
163 to,
164 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Your video ${video.name} is published`,
165 text
166 }
167
168 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
169 }
170
171 myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) {
172 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
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` +
180 `${CONFIG.EMAIL.BODY.SIGNATURE}`
181
182 const emailPayload: EmailPayload = {
183 to,
184 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`,
185 text
186 }
187
188 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
189 }
190
191 myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) {
192 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
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` +
200 `${CONFIG.EMAIL.BODY.SIGNATURE}`
201
202 const emailPayload: EmailPayload = {
203 to,
204 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`,
205 text
206 }
207
208 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
209 }
210
211 addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) {
212 const accountName = comment.Account.getDisplayName()
213 const video = comment.Video
214 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
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` +
221 `Cheers,\n` +
222 `${CONFIG.EMAIL.BODY.SIGNATURE}`
223
224 const emailPayload: EmailPayload = {
225 to,
226 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New comment on your video ' + video.name,
227 text
228 }
229
230 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
231 }
232
233 addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) {
234 const accountName = comment.Account.getDisplayName()
235 const video = comment.Video
236 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
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` +
244 `${CONFIG.EMAIL.BODY.SIGNATURE}`
245
246 const emailPayload: EmailPayload = {
247 to,
248 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Mention on video ' + video.name,
249 text
250 }
251
252 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
253 }
254
255 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) {
256 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
257
258 const text = `Hi,\n\n` +
259 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` +
260 `Cheers,\n` +
261 `${CONFIG.EMAIL.BODY.SIGNATURE}`
262
263 const emailPayload: EmailPayload = {
264 to,
265 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Received a video abuse',
266 text
267 }
268
269 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
270 }
271
272 addVideoAutoBlacklistModeratorsNotification (to: string[], video: VideoModel) {
273 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
274 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
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` +
284 `${CONFIG.EMAIL.BODY.SIGNATURE}`
285
286 const emailPayload: EmailPayload = {
287 to,
288 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'An auto-blacklisted video is awaiting review',
289 text
290 }
291
292 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
293 }
294
295 addNewUserRegistrationNotification (to: string[], user: UserModel) {
296 const text = `Hi,\n\n` +
297 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
298 `Cheers,\n` +
299 `${CONFIG.EMAIL.BODY.SIGNATURE}`
300
301 const emailPayload: EmailPayload = {
302 to,
303 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST,
304 text
305 }
306
307 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
308 }
309
310 addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) {
311 const videoName = videoBlacklist.Video.name
312 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
313
314 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
315 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.`
316
317 const text = 'Hi,\n\n' +
318 blockedString +
319 '\n\n' +
320 'Cheers,\n' +
321 `${CONFIG.EMAIL.BODY.SIGNATURE}`
322
323 const emailPayload: EmailPayload = {
324 to,
325 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Video ${videoName} blacklisted`,
326 text
327 }
328
329 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
330 }
331
332 addVideoUnblacklistNotification (to: string[], video: VideoModel) {
333 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
334
335 const text = 'Hi,\n\n' +
336 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
337 '\n\n' +
338 'Cheers,\n' +
339 `${CONFIG.EMAIL.BODY.SIGNATURE}`
340
341 const emailPayload: EmailPayload = {
342 to,
343 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Video ${video.name} unblacklisted`,
344 text
345 }
346
347 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
348 }
349
350 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
351 const text = `Hi dear user,\n\n` +
352 `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` +
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` +
356 `${CONFIG.EMAIL.BODY.SIGNATURE}`
357
358 const emailPayload: EmailPayload = {
359 to: [ to ],
360 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Reset your password',
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` +
369 `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` +
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` +
373 `${CONFIG.EMAIL.BODY.SIGNATURE}`
374
375 const emailPayload: EmailPayload = {
376 to: [ to ],
377 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Verify your email',
378 text
379 }
380
381 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
382 }
383
384 addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) {
385 const reasonString = reason ? ` for the following reason: ${reason}` : ''
386 const blockedWord = blocked ? 'blocked' : 'unblocked'
387 const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
388
389 const text = 'Hi,\n\n' +
390 blockedString +
391 '\n\n' +
392 'Cheers,\n' +
393 `${CONFIG.EMAIL.BODY.SIGNATURE}`
394
395 const to = user.email
396 const emailPayload: EmailPayload = {
397 to: [ to ],
398 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Account ' + blockedWord,
399 text
400 }
401
402 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
403 }
404
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 = {
415 fromDisplayName: fromEmail,
416 replyTo: fromEmail,
417 to: [ CONFIG.ADMIN.EMAIL ],
418 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Contact form submitted',
419 text
420 }
421
422 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
423 }
424
425 sendMail (options: EmailPayload) {
426 if (!Emailer.isEnabled()) {
427 throw new Error('Cannot send mail because SMTP is not configured.')
428 }
429
430 const fromDisplayName = options.fromDisplayName
431 ? options.fromDisplayName
432 : WEBSERVER.HOST
433
434 return this.transporter.sendMail({
435 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`,
436 replyTo: options.replyTo,
437 to: options.to.join(','),
438 subject: options.subject,
439 text: options.text
440 })
441 }
442
443 private dieOnConnectionFailure (err?: Error) {
444 logger.error('Failed to connect to SMTP %s:%d.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT, { err })
445 process.exit(-1)
446 }
447
448 static get Instance () {
449 return this.instance || (this.instance = new this())
450 }
451 }
452
453 // ---------------------------------------------------------------------------
454
455 export {
456 Emailer,
457 SendEmailOptions
458 }