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