aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/emailer.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/emailer.ts')
-rw-r--r--server/lib/emailer.ts477
1 files changed, 259 insertions, 218 deletions
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 7484524a4..935c9e882 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,9 +1,8 @@
1import { createTransport, Transporter } from 'nodemailer' 1import { createTransport, Transporter } from 'nodemailer'
2import { isTestInstance } from '../helpers/core-utils' 2import { isTestInstance, root } from '../helpers/core-utils'
3import { bunyanLogger, logger } from '../helpers/logger' 3import { bunyanLogger, logger } from '../helpers/logger'
4import { CONFIG } from '../initializers/config' 4import { CONFIG, isEmailEnabled } from '../initializers/config'
5import { JobQueue } from './job-queue' 5import { JobQueue } from './job-queue'
6import { EmailPayload } from './job-queue/handlers/email'
7import { readFileSync } from 'fs-extra' 6import { readFileSync } from 'fs-extra'
8import { WEBSERVER } from '../initializers/constants' 7import { WEBSERVER } from '../initializers/constants'
9import { 8import {
@@ -16,15 +15,13 @@ import {
16} from '../typings/models/video' 15} from '../typings/models/video'
17import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' 16import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
18import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' 17import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
19 18import { EmailPayload } from '@shared/models'
20type SendEmailOptions = { 19import { join } from 'path'
21 to: string[] 20import { VideoAbuse } from '../../shared/models/videos'
22 subject: string 21import { SendEmailOptions } from '../../shared/models/server/emailer.model'
23 text: string 22import { merge } from 'lodash'
24 23import { VideoChannelModel } from '@server/models/video/video-channel'
25 fromDisplayName?: string 24const Email = require('email-templates')
26 replyTo?: string
27}
28 25
29class Emailer { 26class Emailer {
30 27
@@ -32,41 +29,52 @@ class Emailer {
32 private initialized = false 29 private initialized = false
33 private transporter: Transporter 30 private transporter: Transporter
34 31
35 private constructor () {} 32 private constructor () {
33 }
36 34
37 init () { 35 init () {
38 // Already initialized 36 // Already initialized
39 if (this.initialized === true) return 37 if (this.initialized === true) return
40 this.initialized = true 38 this.initialized = true
41 39
42 if (Emailer.isEnabled()) { 40 if (isEmailEnabled()) {
43 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) 41 if (CONFIG.SMTP.TRANSPORT === 'smtp') {
42 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
44 43
45 let tls 44 let tls
46 if (CONFIG.SMTP.CA_FILE) { 45 if (CONFIG.SMTP.CA_FILE) {
47 tls = { 46 tls = {
48 ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ] 47 ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
48 }
49 } 49 }
50 }
51 50
52 let auth 51 let auth
53 if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) { 52 if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) {
54 auth = { 53 auth = {
55 user: CONFIG.SMTP.USERNAME, 54 user: CONFIG.SMTP.USERNAME,
56 pass: CONFIG.SMTP.PASSWORD 55 pass: CONFIG.SMTP.PASSWORD
56 }
57 } 57 }
58 }
59 58
60 this.transporter = createTransport({ 59 this.transporter = createTransport({
61 host: CONFIG.SMTP.HOSTNAME, 60 host: CONFIG.SMTP.HOSTNAME,
62 port: CONFIG.SMTP.PORT, 61 port: CONFIG.SMTP.PORT,
63 secure: CONFIG.SMTP.TLS, 62 secure: CONFIG.SMTP.TLS,
64 debug: CONFIG.LOG.LEVEL === 'debug', 63 debug: CONFIG.LOG.LEVEL === 'debug',
65 logger: bunyanLogger as any, 64 logger: bunyanLogger as any,
66 ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS, 65 ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS,
67 tls, 66 tls,
68 auth 67 auth
69 }) 68 })
69 } else { // sendmail
70 logger.info('Using sendmail to send emails')
71
72 this.transporter = createTransport({
73 sendmail: true,
74 newline: 'unix',
75 path: CONFIG.SMTP.SENDMAIL
76 })
77 }
70 } else { 78 } else {
71 if (!isTestInstance()) { 79 if (!isTestInstance()) {
72 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') 80 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
@@ -75,11 +83,17 @@ class Emailer {
75 } 83 }
76 84
77 static isEnabled () { 85 static isEnabled () {
78 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT 86 if (CONFIG.SMTP.TRANSPORT === 'sendmail') {
87 return !!CONFIG.SMTP.SENDMAIL
88 } else if (CONFIG.SMTP.TRANSPORT === 'smtp') {
89 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
90 } else {
91 return false
92 }
79 } 93 }
80 94
81 async checkConnectionOrDie () { 95 async checkConnectionOrDie () {
82 if (!this.transporter) return 96 if (!this.transporter || CONFIG.SMTP.TRANSPORT !== 'smtp') return
83 97
84 logger.info('Testing SMTP server...') 98 logger.info('Testing SMTP server...')
85 99
@@ -97,37 +111,36 @@ class Emailer {
97 const channelName = video.VideoChannel.getDisplayName() 111 const channelName = video.VideoChannel.getDisplayName()
98 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 112 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
99 113
100 const text = `Hi dear user,\n\n` +
101 `Your subscription ${channelName} just published a new video: ${video.name}` +
102 `\n\n` +
103 `You can view it on ${videoUrl} ` +
104 `\n\n` +
105 `Cheers,\n` +
106 `${CONFIG.EMAIL.BODY.SIGNATURE}`
107
108 const emailPayload: EmailPayload = { 114 const emailPayload: EmailPayload = {
109 to, 115 to,
110 subject: CONFIG.EMAIL.SUBJECT.PREFIX + channelName + ' just published a new video', 116 subject: channelName + ' just published a new video',
111 text 117 text: `Your subscription ${channelName} just published a new video: "${video.name}".`,
118 locals: {
119 title: 'New content ',
120 action: {
121 text: 'View video',
122 url: videoUrl
123 }
124 }
112 } 125 }
113 126
114 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 127 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
115 } 128 }
116 129
117 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') { 130 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
118 const followerName = actorFollow.ActorFollower.Account.getDisplayName()
119 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() 131 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
120 132
121 const text = `Hi dear user,\n\n` +
122 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` +
123 `\n\n` +
124 `Cheers,\n` +
125 `${CONFIG.EMAIL.BODY.SIGNATURE}`
126
127 const emailPayload: EmailPayload = { 133 const emailPayload: EmailPayload = {
134 template: 'follower-on-channel',
128 to, 135 to,
129 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New follower on your channel ' + followingName, 136 subject: `New follower on your channel ${followingName}`,
130 text 137 locals: {
138 followerName: actorFollow.ActorFollower.Account.getDisplayName(),
139 followerUrl: actorFollow.ActorFollower.url,
140 followingName,
141 followingUrl: actorFollow.ActorFollowing.url,
142 followType
143 }
131 } 144 }
132 145
133 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 146 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -136,32 +149,28 @@ class Emailer {
136 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) { 149 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
137 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' 150 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
138 151
139 const text = `Hi dear admin,\n\n` +
140 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
141 `\n\n` +
142 `Cheers,\n` +
143 `${CONFIG.EMAIL.BODY.SIGNATURE}`
144
145 const emailPayload: EmailPayload = { 152 const emailPayload: EmailPayload = {
146 to, 153 to,
147 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New instance follower', 154 subject: 'New instance follower',
148 text 155 text: `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}.`,
156 locals: {
157 title: 'New instance follower',
158 action: {
159 text: 'Review followers',
160 url: WEBSERVER.URL + '/admin/follows/followers-list'
161 }
162 }
149 } 163 }
150 164
151 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 165 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
152 } 166 }
153 167
154 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { 168 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
155 const text = `Hi dear admin,\n\n` + 169 const instanceUrl = actorFollow.ActorFollowing.url
156 `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` +
157 `\n\n` +
158 `Cheers,\n` +
159 `${CONFIG.EMAIL.BODY.SIGNATURE}`
160
161 const emailPayload: EmailPayload = { 170 const emailPayload: EmailPayload = {
162 to, 171 to,
163 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following', 172 subject: 'Auto instance following',
164 text 173 text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.`
165 } 174 }
166 175
167 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 176 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -170,18 +179,17 @@ class Emailer {
170 myVideoPublishedNotification (to: string[], video: MVideo) { 179 myVideoPublishedNotification (to: string[], video: MVideo) {
171 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 180 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
172 181
173 const text = `Hi dear user,\n\n` +
174 `Your video ${video.name} has been published.` +
175 `\n\n` +
176 `You can view it on ${videoUrl} ` +
177 `\n\n` +
178 `Cheers,\n` +
179 `${CONFIG.EMAIL.BODY.SIGNATURE}`
180
181 const emailPayload: EmailPayload = { 182 const emailPayload: EmailPayload = {
182 to, 183 to,
183 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video ${video.name} is published`, 184 subject: `Your video ${video.name} has been published`,
184 text 185 text: `Your video "${video.name}" has been published.`,
186 locals: {
187 title: 'You video is live',
188 action: {
189 text: 'View video',
190 url: videoUrl
191 }
192 }
185 } 193 }
186 194
187 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 195 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -190,18 +198,17 @@ class Emailer {
190 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) { 198 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
191 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() 199 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
192 200
193 const text = `Hi dear user,\n\n` +
194 `Your video import ${videoImport.getTargetIdentifier()} is finished.` +
195 `\n\n` +
196 `You can view the imported video on ${videoUrl} ` +
197 `\n\n` +
198 `Cheers,\n` +
199 `${CONFIG.EMAIL.BODY.SIGNATURE}`
200
201 const emailPayload: EmailPayload = { 201 const emailPayload: EmailPayload = {
202 to, 202 to,
203 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`, 203 subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`,
204 text 204 text: `Your video "${videoImport.getTargetIdentifier()}" just finished importing.`,
205 locals: {
206 title: 'Import complete',
207 action: {
208 text: 'View video',
209 url: videoUrl
210 }
211 }
205 } 212 }
206 213
207 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 214 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -210,40 +217,47 @@ class Emailer {
210 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) { 217 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
211 const importUrl = WEBSERVER.URL + '/my-account/video-imports' 218 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
212 219
213 const text = `Hi dear user,\n\n` + 220 const text =
214 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + 221 `Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` +
215 `\n\n` + 222 '\n\n' +
216 `See your videos import dashboard for more information: ${importUrl}` + 223 `See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.`
217 `\n\n` +
218 `Cheers,\n` +
219 `${CONFIG.EMAIL.BODY.SIGNATURE}`
220 224
221 const emailPayload: EmailPayload = { 225 const emailPayload: EmailPayload = {
222 to, 226 to,
223 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`, 227 subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`,
224 text 228 text,
229 locals: {
230 title: 'Import failed',
231 action: {
232 text: 'Review imports',
233 url: importUrl
234 }
235 }
225 } 236 }
226 237
227 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 238 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
228 } 239 }
229 240
230 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) { 241 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
231 const accountName = comment.Account.getDisplayName()
232 const video = comment.Video 242 const video = comment.Video
243 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
233 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 244 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
234 245
235 const text = `Hi dear user,\n\n` +
236 `A new comment has been posted by ${accountName} on your video ${video.name}` +
237 `\n\n` +
238 `You can view it on ${commentUrl} ` +
239 `\n\n` +
240 `Cheers,\n` +
241 `${CONFIG.EMAIL.BODY.SIGNATURE}`
242
243 const emailPayload: EmailPayload = { 246 const emailPayload: EmailPayload = {
247 template: 'video-comment-new',
244 to, 248 to,
245 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New comment on your video ' + video.name, 249 subject: 'New comment on your video ' + video.name,
246 text 250 locals: {
251 accountName: comment.Account.getDisplayName(),
252 accountUrl: comment.Account.Actor.url,
253 comment,
254 video,
255 videoUrl,
256 action: {
257 text: 'View comment',
258 url: commentUrl
259 }
260 }
247 } 261 }
248 262
249 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 263 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -252,75 +266,88 @@ class Emailer {
252 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) { 266 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
253 const accountName = comment.Account.getDisplayName() 267 const accountName = comment.Account.getDisplayName()
254 const video = comment.Video 268 const video = comment.Video
269 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
255 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 270 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
256 271
257 const text = `Hi dear user,\n\n` +
258 `${accountName} mentioned you on video ${video.name}` +
259 `\n\n` +
260 `You can view the comment on ${commentUrl} ` +
261 `\n\n` +
262 `Cheers,\n` +
263 `${CONFIG.EMAIL.BODY.SIGNATURE}`
264
265 const emailPayload: EmailPayload = { 272 const emailPayload: EmailPayload = {
273 template: 'video-comment-mention',
266 to, 274 to,
267 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Mention on video ' + video.name, 275 subject: 'Mention on video ' + video.name,
268 text 276 locals: {
277 comment,
278 video,
279 videoUrl,
280 accountName,
281 action: {
282 text: 'View comment',
283 url: commentUrl
284 }
285 }
269 } 286 }
270 287
271 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
272 } 289 }
273 290
274 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) { 291 addVideoAbuseModeratorsNotification (to: string[], parameters: {
275 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() 292 videoAbuse: VideoAbuse
276 293 videoAbuseInstance: MVideoAbuseVideo
277 const text = `Hi,\n\n` + 294 reporter: string
278 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + 295 }) {
279 `Cheers,\n` + 296 const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id
280 `${CONFIG.EMAIL.BODY.SIGNATURE}` 297 const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()
281 298
282 const emailPayload: EmailPayload = { 299 const emailPayload: EmailPayload = {
300 template: 'video-abuse-new',
283 to, 301 to,
284 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Received a video abuse', 302 subject: `New video abuse report from ${parameters.reporter}`,
285 text 303 locals: {
304 videoUrl,
305 videoAbuseUrl,
306 videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(),
307 videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(),
308 videoAbuse: parameters.videoAbuse,
309 reporter: parameters.reporter,
310 action: {
311 text: 'View report #' + parameters.videoAbuse.id,
312 url: videoAbuseUrl
313 }
314 }
286 } 315 }
287 316
288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 317 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
289 } 318 }
290 319
291 addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 320 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
292 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 321 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
293 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 322 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
294 323 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
295 const text = `Hi,\n\n` +
296 `A recently added video was auto-blacklisted and requires moderator review before publishing.` +
297 `\n\n` +
298 `You can view it and take appropriate action on ${videoUrl}` +
299 `\n\n` +
300 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
301 `\n\n` +
302 `Cheers,\n` +
303 `${CONFIG.EMAIL.BODY.SIGNATURE}`
304 324
305 const emailPayload: EmailPayload = { 325 const emailPayload: EmailPayload = {
326 template: 'video-auto-blacklist-new',
306 to, 327 to,
307 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'An auto-blacklisted video is awaiting review', 328 subject: 'A new video is pending moderation',
308 text 329 locals: {
330 channel,
331 videoUrl,
332 videoName: videoBlacklist.Video.name,
333 action: {
334 text: 'Review autoblacklist',
335 url: VIDEO_AUTO_BLACKLIST_URL
336 }
337 }
309 } 338 }
310 339
311 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 340 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
312 } 341 }
313 342
314 addNewUserRegistrationNotification (to: string[], user: MUser) { 343 addNewUserRegistrationNotification (to: string[], user: MUser) {
315 const text = `Hi,\n\n` +
316 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
317 `Cheers,\n` +
318 `${CONFIG.EMAIL.BODY.SIGNATURE}`
319
320 const emailPayload: EmailPayload = { 344 const emailPayload: EmailPayload = {
345 template: 'user-registered',
321 to, 346 to,
322 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST, 347 subject: `a new user registered on ${WEBSERVER.HOST}: ${user.username}`,
323 text 348 locals: {
349 user
350 }
324 } 351 }
325 352
326 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 353 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -333,16 +360,13 @@ class Emailer {
333 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' 360 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
334 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.` 361 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.`
335 362
336 const text = 'Hi,\n\n' +
337 blockedString +
338 '\n\n' +
339 'Cheers,\n' +
340 `${CONFIG.EMAIL.BODY.SIGNATURE}`
341
342 const emailPayload: EmailPayload = { 363 const emailPayload: EmailPayload = {
343 to, 364 to,
344 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${videoName} blacklisted`, 365 subject: `Video ${videoName} blacklisted`,
345 text 366 text: blockedString,
367 locals: {
368 title: 'Your video was blacklisted'
369 }
346 } 370 }
347 371
348 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 372 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -351,50 +375,53 @@ class Emailer {
351 addVideoUnblacklistNotification (to: string[], video: MVideo) { 375 addVideoUnblacklistNotification (to: string[], video: MVideo) {
352 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 376 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
353 377
354 const text = 'Hi,\n\n' +
355 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
356 '\n\n' +
357 'Cheers,\n' +
358 `${CONFIG.EMAIL.BODY.SIGNATURE}`
359
360 const emailPayload: EmailPayload = { 378 const emailPayload: EmailPayload = {
361 to, 379 to,
362 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${video.name} unblacklisted`, 380 subject: `Video ${video.name} unblacklisted`,
363 text 381 text: `Your video "${video.name}" (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.`,
382 locals: {
383 title: 'Your video was unblacklisted'
384 }
364 } 385 }
365 386
366 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 387 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
367 } 388 }
368 389
369 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { 390 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
370 const text = `Hi dear user,\n\n` + 391 const emailPayload: EmailPayload = {
371 `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` + 392 template: 'password-reset',
372 `Please follow this link to reset it: ${resetPasswordUrl} (the link will expire within 1 hour)\n\n` + 393 to: [ to ],
373 `If you are not the person who initiated this request, please ignore this email.\n\n` + 394 subject: 'Reset your account password',
374 `Cheers,\n` + 395 locals: {
375 `${CONFIG.EMAIL.BODY.SIGNATURE}` 396 resetPasswordUrl
397 }
398 }
376 399
400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
401 }
402
403 addPasswordCreateEmailJob (username: string, to: string, createPasswordUrl: string) {
377 const emailPayload: EmailPayload = { 404 const emailPayload: EmailPayload = {
405 template: 'password-create',
378 to: [ to ], 406 to: [ to ],
379 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Reset your password', 407 subject: 'Create your account password',
380 text 408 locals: {
409 username,
410 createPasswordUrl
411 }
381 } 412 }
382 413
383 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 414 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
384 } 415 }
385 416
386 addVerifyEmailJob (to: string, verifyEmailUrl: string) { 417 addVerifyEmailJob (to: string, verifyEmailUrl: string) {
387 const text = `Welcome to PeerTube,\n\n` +
388 `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` +
389 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
390 `If you are not the person who initiated this request, please ignore this email.\n\n` +
391 `Cheers,\n` +
392 `${CONFIG.EMAIL.BODY.SIGNATURE}`
393
394 const emailPayload: EmailPayload = { 418 const emailPayload: EmailPayload = {
419 template: 'verify-email',
395 to: [ to ], 420 to: [ to ],
396 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Verify your email', 421 subject: `Verify your email on ${WEBSERVER.HOST}`,
397 text 422 locals: {
423 verifyEmailUrl
424 }
398 } 425 }
399 426
400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 427 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -403,61 +430,76 @@ class Emailer {
403 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) { 430 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
404 const reasonString = reason ? ` for the following reason: ${reason}` : '' 431 const reasonString = reason ? ` for the following reason: ${reason}` : ''
405 const blockedWord = blocked ? 'blocked' : 'unblocked' 432 const blockedWord = blocked ? 'blocked' : 'unblocked'
406 const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
407
408 const text = 'Hi,\n\n' +
409 blockedString +
410 '\n\n' +
411 'Cheers,\n' +
412 `${CONFIG.EMAIL.BODY.SIGNATURE}`
413 433
414 const to = user.email 434 const to = user.email
415 const emailPayload: EmailPayload = { 435 const emailPayload: EmailPayload = {
416 to: [ to ], 436 to: [ to ],
417 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Account ' + blockedWord, 437 subject: 'Account ' + blockedWord,
418 text 438 text: `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
419 } 439 }
420 440
421 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 441 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
422 } 442 }
423 443
424 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) { 444 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) {
425 const text = 'Hello dear admin,\n\n' +
426 fromName + ' sent you a message' +
427 '\n\n---------------------------------------\n\n' +
428 body +
429 '\n\n---------------------------------------\n\n' +
430 'Cheers,\n' +
431 'PeerTube.'
432
433 const emailPayload: EmailPayload = { 445 const emailPayload: EmailPayload = {
434 fromDisplayName: fromEmail, 446 template: 'contact-form',
435 replyTo: fromEmail,
436 to: [ CONFIG.ADMIN.EMAIL ], 447 to: [ CONFIG.ADMIN.EMAIL ],
437 subject: CONFIG.EMAIL.SUBJECT.PREFIX + subject, 448 replyTo: `"${fromName}" <${fromEmail}>`,
438 text 449 subject: `(contact form) ${subject}`,
450 locals: {
451 fromName,
452 fromEmail,
453 body
454 }
439 } 455 }
440 456
441 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 457 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
442 } 458 }
443 459
444 async sendMail (options: EmailPayload) { 460 async sendMail (options: EmailPayload) {
445 if (!Emailer.isEnabled()) { 461 if (!isEmailEnabled()) {
446 throw new Error('Cannot send mail because SMTP is not configured.') 462 throw new Error('Cannot send mail because SMTP is not configured.')
447 } 463 }
448 464
449 const fromDisplayName = options.fromDisplayName 465 const fromDisplayName = options.from
450 ? options.fromDisplayName 466 ? options.from
451 : WEBSERVER.HOST 467 : WEBSERVER.HOST
452 468
469 const email = new Email({
470 send: true,
471 message: {
472 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`
473 },
474 transport: this.transporter,
475 views: {
476 root: join(root(), 'server', 'lib', 'emails')
477 },
478 subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX
479 })
480
453 for (const to of options.to) { 481 for (const to of options.to) {
454 await this.transporter.sendMail({ 482 await email
455 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`, 483 .send(merge(
456 replyTo: options.replyTo, 484 {
457 to, 485 template: 'common',
458 subject: options.subject, 486 message: {
459 text: options.text 487 to,
460 }) 488 from: options.from,
489 subject: options.subject,
490 replyTo: options.replyTo
491 },
492 locals: { // default variables available in all templates
493 WEBSERVER,
494 EMAIL: CONFIG.EMAIL,
495 text: options.text,
496 subject: options.subject
497 }
498 },
499 options // overriden/new variables given for a specific template in the payload
500 ) as SendEmailOptions)
501 .then(logger.info)
502 .catch(logger.error)
461 } 503 }
462 } 504 }
463 505
@@ -474,6 +516,5 @@ class Emailer {
474// --------------------------------------------------------------------------- 516// ---------------------------------------------------------------------------
475 517
476export { 518export {
477 Emailer, 519 Emailer
478 SendEmailOptions
479} 520}