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'
17 type SendEmailOptions = {
22 fromDisplayName?: string
28 private static instance: Emailer
29 private initialized = false
30 private transporter: Transporter
32 private constructor () {}
35 // Already initialized
36 if (this.initialized === true) return
37 this.initialized = true
39 if (Emailer.isEnabled()) {
40 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
43 if (CONFIG.SMTP.CA_FILE) {
45 ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
50 if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) {
52 user: CONFIG.SMTP.USERNAME,
53 pass: CONFIG.SMTP.PASSWORD
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,
68 if (!isTestInstance()) {
69 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
75 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
78 async checkConnectionOrDie () {
79 if (!this.transporter) return
81 logger.info('Testing SMTP server...')
84 const success = await this.transporter.verify()
85 if (success !== true) this.dieOnConnectionFailure()
87 logger.info('Successfully connected to SMTP server.')
89 this.dieOnConnectionFailure(err)
93 addNewVideoFromSubscriberNotification (to: string[], video: VideoModel) {
94 const channelName = video.VideoChannel.getDisplayName()
95 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
97 const text = `Hi dear user,\n\n` +
98 `Your subscription ${channelName} just published a new video: ${video.name}` +
100 `You can view it on ${videoUrl} ` +
103 `${CONFIG.EMAIL.BODY.SIGNATURE}`
105 const emailPayload: EmailPayload = {
107 subject: CONFIG.EMAIL.OBJECT.PREFIX + channelName + ' just published a new video',
111 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
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()
118 const text = `Hi dear user,\n\n` +
119 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` +
122 `${CONFIG.EMAIL.BODY.SIGNATURE}`
124 const emailPayload: EmailPayload = {
126 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New follower on your channel ' + followingName,
130 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
133 addNewInstanceFollowerNotification (to: string[], actorFollow: ActorFollowModel) {
134 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
136 const text = `Hi dear admin,\n\n` +
137 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
140 `${CONFIG.EMAIL.BODY.SIGNATURE}`
142 const emailPayload: EmailPayload = {
144 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New instance follower',
148 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
151 myVideoPublishedNotification (to: string[], video: VideoModel) {
152 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
154 const text = `Hi dear user,\n\n` +
155 `Your video ${video.name} has been published.` +
157 `You can view it on ${videoUrl} ` +
160 `${CONFIG.EMAIL.BODY.SIGNATURE}`
162 const emailPayload: EmailPayload = {
164 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Your video ${video.name} is published`,
168 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
171 myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) {
172 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
174 const text = `Hi dear user,\n\n` +
175 `Your video import ${videoImport.getTargetIdentifier()} is finished.` +
177 `You can view the imported video on ${videoUrl} ` +
180 `${CONFIG.EMAIL.BODY.SIGNATURE}`
182 const emailPayload: EmailPayload = {
184 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`,
188 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
191 myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) {
192 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
194 const text = `Hi dear user,\n\n` +
195 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` +
197 `See your videos import dashboard for more information: ${importUrl}` +
200 `${CONFIG.EMAIL.BODY.SIGNATURE}`
202 const emailPayload: EmailPayload = {
204 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`,
208 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
211 addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) {
212 const accountName = comment.Account.getDisplayName()
213 const video = comment.Video
214 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
216 const text = `Hi dear user,\n\n` +
217 `A new comment has been posted by ${accountName} on your video ${video.name}` +
219 `You can view it on ${commentUrl} ` +
222 `${CONFIG.EMAIL.BODY.SIGNATURE}`
224 const emailPayload: EmailPayload = {
226 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New comment on your video ' + video.name,
230 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
233 addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) {
234 const accountName = comment.Account.getDisplayName()
235 const video = comment.Video
236 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
238 const text = `Hi dear user,\n\n` +
239 `${accountName} mentioned you on video ${video.name}` +
241 `You can view the comment on ${commentUrl} ` +
244 `${CONFIG.EMAIL.BODY.SIGNATURE}`
246 const emailPayload: EmailPayload = {
248 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Mention on video ' + video.name,
252 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
255 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) {
256 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
258 const text = `Hi,\n\n` +
259 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` +
261 `${CONFIG.EMAIL.BODY.SIGNATURE}`
263 const emailPayload: EmailPayload = {
265 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Received a video abuse',
269 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
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()
276 const text = `Hi,\n\n` +
277 `A recently added video was auto-blacklisted and requires moderator review before publishing.` +
279 `You can view it and take appropriate action on ${videoUrl}` +
281 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
284 `${CONFIG.EMAIL.BODY.SIGNATURE}`
286 const emailPayload: EmailPayload = {
288 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'An auto-blacklisted video is awaiting review',
292 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
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` +
299 `${CONFIG.EMAIL.BODY.SIGNATURE}`
301 const emailPayload: EmailPayload = {
303 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST,
307 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
310 addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) {
311 const videoName = videoBlacklist.Video.name
312 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
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}.`
317 const text = 'Hi,\n\n' +
321 `${CONFIG.EMAIL.BODY.SIGNATURE}`
323 const emailPayload: EmailPayload = {
325 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Video ${videoName} blacklisted`,
329 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
332 addVideoUnblacklistNotification (to: string[], video: VideoModel) {
333 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
335 const text = 'Hi,\n\n' +
336 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
339 `${CONFIG.EMAIL.BODY.SIGNATURE}`
341 const emailPayload: EmailPayload = {
343 subject: CONFIG.EMAIL.OBJECT.PREFIX + `Video ${video.name} unblacklisted`,
347 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
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` +
356 `${CONFIG.EMAIL.BODY.SIGNATURE}`
358 const emailPayload: EmailPayload = {
360 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Reset your password',
364 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
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` +
373 `${CONFIG.EMAIL.BODY.SIGNATURE}`
375 const emailPayload: EmailPayload = {
377 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Verify your email',
381 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
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}.`
389 const text = 'Hi,\n\n' +
393 `${CONFIG.EMAIL.BODY.SIGNATURE}`
395 const to = user.email
396 const emailPayload: EmailPayload = {
398 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Account ' + blockedWord,
402 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
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' +
410 '\n\n---------------------------------------\n\n' +
414 const emailPayload: EmailPayload = {
415 fromDisplayName: fromEmail,
417 to: [ CONFIG.ADMIN.EMAIL ],
418 subject: CONFIG.EMAIL.OBJECT.PREFIX + 'Contact form submitted',
422 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
425 sendMail (options: EmailPayload) {
426 if (!Emailer.isEnabled()) {
427 throw new Error('Cannot send mail because SMTP is not configured.')
430 const fromDisplayName = options.fromDisplayName
431 ? options.fromDisplayName
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,
443 private dieOnConnectionFailure (err?: Error) {
444 logger.error('Failed to connect to SMTP %s:%d.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT, { err })
448 static get Instance () {
449 return this.instance || (this.instance = new this())
453 // ---------------------------------------------------------------------------